修复商品库存与网络摘要并校验订购编号
This commit is contained in:
1 parent
28894e0813
commit
d8224ec5b7
15 files changed
+529
-68
No files matched your search
@@ -20,6 +20,8 @@ import { t as product_name_t } from "~/lib/product-name.js";
|
|||||||
import { a as storage_capacity_a } from "~/lib/storage-capacity.js";
|
import { a as storage_capacity_a } from "~/lib/storage-capacity.js";
|
||||||
import { t as storage_capacity_t } from "~/lib/storage-capacity.js";
|
import { t as storage_capacity_t } from "~/lib/storage-capacity.js";
|
||||||
import { t as product_order_module_t } from "~/lib/product-order-module.js";
|
import { t as product_order_module_t } from "~/lib/product-order-module.js";
|
||||||
|
import { resourceOptionValue as k } from "~/lib/resource-option-value.js";
|
||||||
|
import { productId } from "~/lib/product-id.js";
|
||||||
var y = {
|
var y = {
|
||||||
timeoutMs: 5e3,
|
timeoutMs: 5e3,
|
||||||
},
|
},
|
||||||
@@ -142,14 +144,6 @@ function O(e) {
|
|||||||
: ``);
|
: ``);
|
||||||
return `${e.min}${t}`;
|
return `${e.min}${t}`;
|
||||||
}
|
}
|
||||||
function k(e) {
|
|
||||||
return (
|
|
||||||
e &&
|
|
||||||
(e.type === `step` || e.type === `total`
|
|
||||||
? (e.min_value ?? e.value)
|
|
||||||
: e.value)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
function A(e, t, n = ``) {
|
function A(e, t, n = ``) {
|
||||||
return {
|
return {
|
||||||
label: e,
|
label: e,
|
||||||
@@ -158,7 +152,7 @@ function A(e, t, n = ``) {
|
|||||||
}
|
}
|
||||||
// 按真实订单模块读取配置;含级联或高级限制且无法安全推导默认值时不生成摘要。
|
// 按真实订单模块读取配置;含级联或高级限制且无法安全推导默认值时不生成摘要。
|
||||||
async function j(e, t) {
|
async function j(e, t) {
|
||||||
if (!product_name_t(t.name)) return [];
|
if (!product_name_t(t.name) || productId(t.id) === null) return [];
|
||||||
let n = (t, n) => e.get(t, n, !1, y);
|
let n = (t, n) => e.get(t, n, !1, y);
|
||||||
try {
|
try {
|
||||||
let r = await product_order_module_t(
|
let r = await product_order_module_t(
|
||||||
@@ -252,7 +246,7 @@ async function j(e, t) {
|
|||||||
if (e.bill_type === `bw`) s.push(A(`带宽`, k(e.bw?.[0]), `Mbps`));
|
if (e.bill_type === `bw`) s.push(A(`带宽`, k(e.bw?.[0]), `Mbps`));
|
||||||
else if (e.bill_type === `flow`) {
|
else if (e.bill_type === `flow`) {
|
||||||
let t = e.flow?.[0];
|
let t = e.flow?.[0];
|
||||||
s.push(A(`流量`, t?.value, `GB`));
|
s.push(A(`流量`, k(t), `GB`));
|
||||||
let n = t?.bw?.[0] ?? t?.other_config;
|
let n = t?.bw?.[0] ?? t?.other_config;
|
||||||
n &&
|
n &&
|
||||||
s.push(
|
s.push(
|
||||||
|
|||||||
@@ -60,14 +60,7 @@ import ConfigResourceOption from "~/components/config/ConfigResourceOption.vue";
|
|||||||
import { t as password_t } from "~/lib/password-2.js";
|
import { t as password_t } from "~/lib/password-2.js";
|
||||||
import { n as order_image_continuation_n } from "~/lib/order-image-continuation.js";
|
import { n as order_image_continuation_n } from "~/lib/order-image-continuation.js";
|
||||||
import { t as order_image_continuation_t } from "~/lib/order-image-continuation.js";
|
import { t as order_image_continuation_t } from "~/lib/order-image-continuation.js";
|
||||||
function I(e) {
|
import { resourceOptionValue as I } from "~/lib/resource-option-value.js";
|
||||||
if (e)
|
|
||||||
return Number(
|
|
||||||
e.type === `step` || e.type === `total`
|
|
||||||
? (e.min_value ?? e.value)
|
|
||||||
: e.value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
function L(e, t) {
|
function L(e, t) {
|
||||||
// 区间内的值还必须落在步长网格上,不能仅凭最小/最大值认定配置可购买。
|
// 区间内的值还必须落在步长网格上,不能仅凭最小/最大值认定配置可购买。
|
||||||
if (t == null || t === ``) return !1;
|
if (t == null || t === ``) return !1;
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ import { o as cart_edit_o } from "~/lib/cart-edit.js";
|
|||||||
import { r as cart_edit_r } from "~/lib/cart-edit.js";
|
import { r as cart_edit_r } from "~/lib/cart-edit.js";
|
||||||
import ConfigDiskCard from "~/components/config/ConfigDiskCard.vue";
|
import ConfigDiskCard from "~/components/config/ConfigDiskCard.vue";
|
||||||
import slider from "primevue/slider";
|
import slider from "primevue/slider";
|
||||||
|
import { productStock } from "~/lib/product-inventory.js";
|
||||||
|
import { resourceOptionValue } from "~/lib/resource-option-value.js";
|
||||||
function z(e, t = {}, n = `mf_dcim`, r = {}) {
|
function z(e, t = {}, n = `mf_dcim`, r = {}) {
|
||||||
// 商品库存与型号同步库存可能同时生效;可购数量取所有有效限制中的最小值。
|
// 商品库存与型号同步库存可能同时生效;可购数量取所有有效限制中的最小值。
|
||||||
let i = useApi(),
|
let i = useApi(),
|
||||||
@@ -108,17 +110,7 @@ function z(e, t = {}, n = `mf_dcim`, r = {}) {
|
|||||||
h.value?.model_config.find((e) => e.id === D.value.model_config_id),
|
h.value?.model_config.find((e) => e.id === D.value.model_config_id),
|
||||||
),
|
),
|
||||||
ne = computed(() => A.value.find((e) => e.id === D.value.line_id)),
|
ne = computed(() => A.value.find((e) => e.id === D.value.line_id)),
|
||||||
re = computed(() => {
|
re = computed(() => productStock(m.value, j.value)),
|
||||||
let e = [
|
|
||||||
m.value?.stock_control === 1 ? m.value.qty : void 0,
|
|
||||||
j.value?.auto_sync_dcim_stock === 1 ? j.value.qty : void 0,
|
|
||||||
]
|
|
||||||
.filter(
|
|
||||||
(e) => e !== void 0 && Number.isFinite(Number(e)) && Number(e) >= 0,
|
|
||||||
)
|
|
||||||
.map(Number);
|
|
||||||
return e.length ? Math.min(...e) : null;
|
|
||||||
}),
|
|
||||||
ie = computed(() => re.value === null || C.value <= re.value),
|
ie = computed(() => re.value === null || C.value <= re.value),
|
||||||
F = computed(() =>
|
F = computed(() =>
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -188,12 +180,8 @@ function z(e, t = {}, n = `mf_dcim`, r = {}) {
|
|||||||
}));
|
}));
|
||||||
let q = (e) => !V && e === z,
|
let q = (e) => !V && e === z,
|
||||||
le = (e) => {
|
le = (e) => {
|
||||||
let t = e?.[0];
|
const value = resourceOptionValue(e?.[0]);
|
||||||
return t
|
return value === undefined ? `` : String(value);
|
||||||
? String(
|
|
||||||
t.type && t.type !== `radio` ? (t.min_value ?? t.value) : t.value,
|
|
||||||
)
|
|
||||||
: ``;
|
|
||||||
};
|
};
|
||||||
function ue() {
|
function ue() {
|
||||||
// 报价和提交共用前置校验,型号/线路/镜像与正整数数量缺一不可。
|
// 报价和提交共用前置校验,型号/线路/镜像与正整数数量缺一不可。
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
/** 优先使用商品已有描述;缺失时按商品 id 补查,最多等待 5 秒,失败不阻塞配置页。 */
|
/** 优先使用商品已有描述;缺失时按商品 id 补查,最多等待 5 秒,失败不阻塞配置页。 */
|
||||||
|
import { productId } from "~/lib/product-id.js";
|
||||||
async function e(e, t) {
|
async function e(e, t) {
|
||||||
if (typeof t.description == `string`) return t.description;
|
if (typeof t.description == `string`) return t.description;
|
||||||
let n = Number(t.id);
|
let n = productId(t.id);
|
||||||
if (!Number.isSafeInteger(n) || n <= 0) return ``;
|
if (n === null) return ``;
|
||||||
try {
|
try {
|
||||||
let t = await e.get(
|
let t = await e.get(
|
||||||
`/product`,
|
`/product`,
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/** 商品接口使用有符号 32 位正整数;在拼接后端路径之前拒绝数组、精度丢失和超限编号。 */
|
||||||
|
export function productId(value) {
|
||||||
|
if (typeof value !== "string" && typeof value !== "number") return null;
|
||||||
|
const text = String(value);
|
||||||
|
if (!/^[1-9]\d{0,9}$/.test(text)) return null;
|
||||||
|
const id = Number(text);
|
||||||
|
return id <= 2147483647 ? id : null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/** 只有已开启控制且数量有效的库存才约束购买;null 表示没有可展示的确定数量,不代表无限库存。 */
|
||||||
|
export function productStock(product, model) {
|
||||||
|
const quantities = [
|
||||||
|
[product?.stock_control, product?.qty],
|
||||||
|
[model?.auto_sync_dcim_stock, model?.qty],
|
||||||
|
]
|
||||||
|
.filter(
|
||||||
|
([enabled, quantity]) =>
|
||||||
|
Number(enabled) === 1 && quantity != null && quantity !== "",
|
||||||
|
)
|
||||||
|
.map(([, quantity]) => Number(quantity))
|
||||||
|
.filter((quantity) => Number.isFinite(quantity) && quantity >= 0);
|
||||||
|
return quantities.length ? Math.min(...quantities) : null;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
/** 下单前确认真实配置模块;本地 DCIM 与转售 DCIM 使用不同配置组件,不能仅看商品名。 */
|
/** 下单前确认真实配置模块;本地 DCIM 与转售 DCIM 使用不同配置组件,不能仅看商品名。 */
|
||||||
|
import { productId } from "~/lib/product-id.js";
|
||||||
function e(e) {
|
function e(e) {
|
||||||
let t = typeof e.module == `string` ? e.module.trim() : ``,
|
let t = typeof e.module == `string` ? e.module.trim() : ``,
|
||||||
n = typeof e.res_module == `string` ? e.res_module.trim() : ``;
|
n = typeof e.res_module == `string` ? e.res_module.trim() : ``;
|
||||||
@@ -7,6 +8,7 @@ function e(e) {
|
|||||||
}
|
}
|
||||||
// 已知非 mf_cloud 模块可直接使用;缺失/含糊时读取配置接口,取不到模块就明确失败。
|
// 已知非 mf_cloud 模块可直接使用;缺失/含糊时读取配置接口,取不到模块就明确失败。
|
||||||
async function t(t, n) {
|
async function t(t, n) {
|
||||||
|
if (productId(n.id) === null) throw Error(`商品编号无效`);
|
||||||
let r = typeof n.module == `string` ? n.module.trim() : ``,
|
let r = typeof n.module == `string` ? n.module.trim() : ``,
|
||||||
i = e(n);
|
i = e(n);
|
||||||
if (i) return i;
|
if (i) return i;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/** radio 的 value 是可选值;step/total 的 value 可能只是占位,默认值应从区间下限读取。 */
|
||||||
|
export function resourceOptionValue(option) {
|
||||||
|
if (!option) return;
|
||||||
|
const value =
|
||||||
|
option.type === "step" || option.type === "total"
|
||||||
|
? (option.min_value ?? option.value)
|
||||||
|
: option.value;
|
||||||
|
if (value == null || value === "") return;
|
||||||
|
const number = Number(value);
|
||||||
|
return Number.isFinite(number) ? number : undefined;
|
||||||
|
}
|
||||||
@@ -58,6 +58,7 @@ import { r as order_image_continuation_r } from "~/lib/order-image-continuation.
|
|||||||
import MfDcimConfig from "~/components/config/MfDcimConfig.vue";
|
import MfDcimConfig from "~/components/config/MfDcimConfig.vue";
|
||||||
import { t as composables_t } from "~/lib/composables-2.js";
|
import { t as composables_t } from "~/lib/composables-2.js";
|
||||||
import { t as product_description_data_t } from "~/lib/product-description-data.js";
|
import { t as product_description_data_t } from "~/lib/product-description-data.js";
|
||||||
|
import { productId } from "~/lib/product-id.js";
|
||||||
var j = {
|
var j = {
|
||||||
class: `page product-order-page`,
|
class: `page product-order-page`,
|
||||||
},
|
},
|
||||||
@@ -83,7 +84,7 @@ var j = {
|
|||||||
let b = useRoute(),
|
let b = useRoute(),
|
||||||
I = useRouter(),
|
I = useRouter(),
|
||||||
L = useApi(),
|
L = useApi(),
|
||||||
R = String(b.params.id ?? b.query.id ?? b.query.product_id ?? ``),
|
R = b.params.id ?? b.query.id ?? b.query.product_id ?? ``,
|
||||||
z = order_image_continuation_r(b.query.order_image),
|
z = order_image_continuation_r(b.query.order_image),
|
||||||
B = b.fullPath;
|
B = b.fullPath;
|
||||||
composables_t({
|
composables_t({
|
||||||
@@ -132,7 +133,7 @@ var j = {
|
|||||||
let e = ++Z;
|
let e = ++Z;
|
||||||
((W.value = !0), (G.value = ``), (H.value = null), (U.value = null));
|
((W.value = !0), (G.value = ``), (H.value = null), (U.value = null));
|
||||||
try {
|
try {
|
||||||
if (!/^[1-9]\d*$/.test(R)) throw Error(`商品编号无效`);
|
if (productId(R) === null) throw Error(`商品编号无效`);
|
||||||
let t = await L.get(`/product/${R}`);
|
let t = await L.get(`/product/${R}`);
|
||||||
if (e !== Z) return;
|
if (e !== Z) return;
|
||||||
let n = t?.product ?? t;
|
let n = t?.product ?? t;
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import KkyEcsConfig from "~/components/config/KkyEcsConfig.vue";
|
|||||||
import MfCloudConfig from "~/components/config/MfCloudConfig.vue";
|
import MfCloudConfig from "~/components/config/MfCloudConfig.vue";
|
||||||
import { t as composables_t } from "~/lib/composables-2.js";
|
import { t as composables_t } from "~/lib/composables-2.js";
|
||||||
import { t as product_description_data_t } from "~/lib/product-description-data.js";
|
import { t as product_description_data_t } from "~/lib/product-description-data.js";
|
||||||
|
import { productId } from "~/lib/product-id.js";
|
||||||
var ge = {
|
var ge = {
|
||||||
class: `cloud-order-page`,
|
class: `cloud-order-page`,
|
||||||
},
|
},
|
||||||
@@ -135,6 +136,7 @@ var ge = {
|
|||||||
? `库存待确认`
|
? `库存待确认`
|
||||||
: `暂时缺货`,
|
: `暂时缺货`,
|
||||||
G = (e) =>
|
G = (e) =>
|
||||||
|
productId(e.id) !== null &&
|
||||||
Number(e.hidden ?? 0) !== 1 &&
|
Number(e.hidden ?? 0) !== 1 &&
|
||||||
(e.product_group_id_second == null ||
|
(e.product_group_id_second == null ||
|
||||||
Number(e.product_group_id_second) === T.value);
|
Number(e.product_group_id_second) === T.value);
|
||||||
@@ -208,7 +210,7 @@ var ge = {
|
|||||||
((E.value = (t.list ?? []).filter(G)),
|
((E.value = (t.list ?? []).filter(G)),
|
||||||
(I.value = Number(t.count) || 0),
|
(I.value = Number(t.count) || 0),
|
||||||
(A.value = !1));
|
(A.value = !1));
|
||||||
let n = Number(C.query.id ?? C.query.product_id),
|
let n = productId(C.query.id ?? C.query.product_id),
|
||||||
r =
|
r =
|
||||||
E.value.find((e) => e.id === n && !U(e)) ??
|
E.value.find((e) => e.id === n && !U(e)) ??
|
||||||
E.value.find((e) => !U(e));
|
E.value.find((e) => !U(e));
|
||||||
@@ -238,6 +240,10 @@ var ge = {
|
|||||||
(j.value = !1),
|
(j.value = !1),
|
||||||
(A.value = !1));
|
(A.value = !1));
|
||||||
try {
|
try {
|
||||||
|
// 显式指定的商品编号无效时就地提示,不改选首件商品,更不把超限值交给后端。
|
||||||
|
const requestedId = C.query.id ?? C.query.product_id;
|
||||||
|
if (requestedId !== undefined && productId(requestedId) === null)
|
||||||
|
throw Error(`商品编号无效`);
|
||||||
let [t, n] = await Promise.all([
|
let [t, n] = await Promise.all([
|
||||||
S.get(`/product/group/first`),
|
S.get(`/product/group/first`),
|
||||||
S.get(`/product/group/second`),
|
S.get(`/product/group/second`),
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import MfDcimConfig from "~/components/config/MfDcimConfig.vue";
|
|||||||
import { t as composables_t } from "~/lib/composables-2.js";
|
import { t as composables_t } from "~/lib/composables-2.js";
|
||||||
import { t as product_description_data_t } from "~/lib/product-description-data.js";
|
import { t as product_description_data_t } from "~/lib/product-description-data.js";
|
||||||
import index from "~/pages/products/dedicated-server/index.vue";
|
import index from "~/pages/products/dedicated-server/index.vue";
|
||||||
|
import { productId } from "~/lib/product-id.js";
|
||||||
var A = {
|
var A = {
|
||||||
key: 1,
|
key: 1,
|
||||||
class: `dedicated-customize`,
|
class: `dedicated-customize`,
|
||||||
@@ -65,7 +66,7 @@ var A = {
|
|||||||
let x = useRoute(),
|
let x = useRoute(),
|
||||||
P = useRouter(),
|
P = useRouter(),
|
||||||
F = useApi(),
|
F = useApi(),
|
||||||
I = computed(() => String(x.query.id ?? x.query.product_id ?? ``));
|
I = computed(() => x.query.id ?? x.query.product_id ?? ``);
|
||||||
composables_t({
|
composables_t({
|
||||||
key: (e) => e.fullPath,
|
key: (e) => e.fullPath,
|
||||||
});
|
});
|
||||||
@@ -85,7 +86,7 @@ var A = {
|
|||||||
t = I.value;
|
t = I.value;
|
||||||
((R.value = !0), (z.value = ``), (L.value = null));
|
((R.value = !0), (z.value = ``), (L.value = null));
|
||||||
try {
|
try {
|
||||||
if (!/^[1-9]\d*$/.test(t)) throw Error(`商品编号无效`);
|
if (productId(t) === null) throw Error(`商品编号无效`);
|
||||||
let n = await F.get(`/product/${t}`);
|
let n = await F.get(`/product/${t}`);
|
||||||
if (e !== U) return;
|
if (e !== U) return;
|
||||||
let r = n?.product ?? n;
|
let r = n?.product ?? n;
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ import { n as product_name_n } from "~/lib/product-name.js";
|
|||||||
import inputnumber from "primevue/inputnumber";
|
import inputnumber from "primevue/inputnumber";
|
||||||
import tag from "primevue/tag";
|
import tag from "primevue/tag";
|
||||||
import skeleton from "primevue/skeleton";
|
import skeleton from "primevue/skeleton";
|
||||||
|
import { productStock } from "~/lib/product-inventory.js";
|
||||||
|
import { resourceOptionValue } from "~/lib/resource-option-value.js";
|
||||||
|
import { productId } from "~/lib/product-id.js";
|
||||||
// 只识别本地或资源模块映射到 mf_dcim 的物理商品;未知模块不强行套用配置器。
|
// 只识别本地或资源模块映射到 mf_dcim 的物理商品;未知模块不强行套用配置器。
|
||||||
function O(e) {
|
function O(e) {
|
||||||
let t = e.module?.trim() || ``;
|
let t = e.module?.trim() || ``;
|
||||||
@@ -122,14 +125,8 @@ function k() {
|
|||||||
}
|
}
|
||||||
// 商品库存与自动同步的机型库存都满足时才允许选购。
|
// 商品库存与自动同步的机型库存都满足时才允许选购。
|
||||||
function w(e) {
|
function w(e) {
|
||||||
return (
|
const stock = productStock(e.product, e.model);
|
||||||
!(e.product.stock_control === 1 && Number(e.product.qty) <= 0) &&
|
return stock === null || stock > 0;
|
||||||
!(
|
|
||||||
e.model.auto_sync_dcim_stock === 1 &&
|
|
||||||
e.model.qty !== void 0 &&
|
|
||||||
Number(e.model.qty) <= 0
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// 分页收集商品后并行加载物理模块配置;单个商品失败保留其余可用项并提示部分加载失败。
|
// 分页收集商品后并行加载物理模块配置;单个商品失败保留其余可用项并提示部分加载失败。
|
||||||
async function T() {
|
async function T() {
|
||||||
@@ -158,7 +155,7 @@ function k() {
|
|||||||
}
|
}
|
||||||
let o = t.flatMap((e) => {
|
let o = t.flatMap((e) => {
|
||||||
let t = O(e);
|
let t = O(e);
|
||||||
return e.hidden !== 1 && t
|
return e.hidden !== 1 && productId(e.id) !== null && t
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
product: {
|
product: {
|
||||||
@@ -851,15 +848,20 @@ var te = {
|
|||||||
),
|
),
|
||||||
t.lines.length
|
t.lines.length
|
||||||
? unref(s).inStock(t)
|
? unref(s).inStock(t)
|
||||||
? t.model.qty !== void 0 &&
|
? productStock(
|
||||||
t.model.qty >= 0
|
t.product,
|
||||||
|
t.model,
|
||||||
|
) !== null
|
||||||
? (openBlock(),
|
? (openBlock(),
|
||||||
createElementBlock(
|
createElementBlock(
|
||||||
`span`,
|
`span`,
|
||||||
B,
|
B,
|
||||||
`剩余 ` +
|
`剩余 ` +
|
||||||
toDisplayString(
|
toDisplayString(
|
||||||
t.model.qty,
|
productStock(
|
||||||
|
t.product,
|
||||||
|
t.model,
|
||||||
|
),
|
||||||
) +
|
) +
|
||||||
` 台`,
|
` 台`,
|
||||||
1,
|
1,
|
||||||
@@ -1100,8 +1102,8 @@ var te = {
|
|||||||
toDisplayString(
|
toDisplayString(
|
||||||
unref(H)[t.id]
|
unref(H)[t.id]
|
||||||
?.bill_type === `flow`
|
?.bill_type === `flow`
|
||||||
? `${unref(H)[t.id]?.flow?.[0]?.value ?? `—`} GB`
|
? `${resourceOptionValue(unref(H)[t.id]?.flow?.[0]) ?? `—`} GB`
|
||||||
: `${unref(H)[t.id]?.bw?.[0]?.value ?? `—`} Mbps`,
|
: `${resourceOptionValue(unref(H)[t.id]?.bw?.[0]) ?? `—`} Mbps`,
|
||||||
),
|
),
|
||||||
1,
|
1,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import { n as ProductConfigurationSummary_n } from "~/components/ProductConfigur
|
|||||||
import ProductConfigurationSummary from "~/components/ProductConfigurationSummary.vue";
|
import ProductConfigurationSummary from "~/components/ProductConfigurationSummary.vue";
|
||||||
import ProductDescription from "~/components/ProductDescription.vue";
|
import ProductDescription from "~/components/ProductDescription.vue";
|
||||||
import ProductGroupLabel from "~/components/ProductGroupLabel.vue";
|
import ProductGroupLabel from "~/components/ProductGroupLabel.vue";
|
||||||
|
import { productId } from "~/lib/product-id.js";
|
||||||
var j = rolldown_runtime_i(getCrypto(), 1),
|
var j = rolldown_runtime_i(getCrypto(), 1),
|
||||||
M = 2592e6,
|
M = 2592e6,
|
||||||
N = 100,
|
N = 100,
|
||||||
@@ -613,17 +614,19 @@ var me = {
|
|||||||
a || ((T.value = []), (E.value = 0), (A.value = !1));
|
a || ((T.value = []), (E.value = 0), (A.value = !1));
|
||||||
let s = await t.get(`/product`, r);
|
let s = await t.get(`/product`, r);
|
||||||
if (n !== W) return;
|
if (n !== W) return;
|
||||||
((T.value = (s.list ?? []).map((t) => ({
|
((T.value = (s.list ?? [])
|
||||||
...t,
|
.filter((t) => productId(t.id) !== null)
|
||||||
startingPrice: q(t)
|
.map((t) => ({
|
||||||
? (o.get(t.id)?.startingPrice ?? null)
|
...t,
|
||||||
: void 0,
|
startingPrice: q(t)
|
||||||
configurationSummary: product_name_t(t.name)
|
? (o.get(t.id)?.startingPrice ?? null)
|
||||||
? e
|
: void 0,
|
||||||
? o.get(t.id)?.configurationSummary
|
configurationSummary: product_name_t(t.name)
|
||||||
: y.peek(t)
|
? e
|
||||||
: [],
|
? o.get(t.id)?.configurationSummary
|
||||||
}))),
|
: y.peek(t)
|
||||||
|
: [],
|
||||||
|
}))),
|
||||||
(E.value = Number(s.count) || 0),
|
(E.value = Number(s.count) || 0),
|
||||||
(K = i),
|
(K = i),
|
||||||
(A.value = !0));
|
(A.value = !0));
|
||||||
|
|||||||
+2
-1
@@ -21,7 +21,8 @@ const shopPages = new Set([
|
|||||||
export function isPublicShop(route) {
|
export function isPublicShop(route) {
|
||||||
if (Object.hasOwn(route.query || {}, "edit_position")) return false;
|
if (Object.hasOwn(route.query || {}, "edit_position")) return false;
|
||||||
const path = route.path.replace(/\/+$/, "") || "/";
|
const path = route.path.replace(/\/+$/, "") || "/";
|
||||||
return shopPages.has(path) || /^\/products\/[1-9]\d*$/.test(path);
|
// 动态商品入口由页面统一校验编号;非法编号也应显示公开错误,而不是被误送到登录页。
|
||||||
|
return shopPages.has(path) || /^\/products\/[^/]+$/.test(path);
|
||||||
}
|
}
|
||||||
export function isAuthPage(path) {
|
export function isAuthPage(path) {
|
||||||
return authPages.has(path.replace(/\/+$/, "") || "/");
|
return authPages.has(path.replace(/\/+$/, "") || "/");
|
||||||
|
|||||||
@@ -0,0 +1,436 @@
|
|||||||
|
/** 商品目录回归走公开页面及真实配置器,只在既有后端 HTTP 边界提供生产形状数据。 */
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import {
|
||||||
|
appOrigin,
|
||||||
|
backendOrigin,
|
||||||
|
eventually,
|
||||||
|
scenario,
|
||||||
|
} from "./helpers/console-browser.mjs";
|
||||||
|
|
||||||
|
async function catalogBoundary(page) {
|
||||||
|
const state = { productReads: [], quotes: [] };
|
||||||
|
const variants = [
|
||||||
|
{
|
||||||
|
id: 201,
|
||||||
|
name: "未控库存",
|
||||||
|
stock_control: 0,
|
||||||
|
qty: 0,
|
||||||
|
sync: 0,
|
||||||
|
modelQty: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 202,
|
||||||
|
name: "商品售罄",
|
||||||
|
stock_control: 1,
|
||||||
|
qty: 0,
|
||||||
|
sync: 0,
|
||||||
|
modelQty: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 203,
|
||||||
|
name: "共同约束",
|
||||||
|
stock_control: 1,
|
||||||
|
qty: 8,
|
||||||
|
sync: 1,
|
||||||
|
modelQty: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 204,
|
||||||
|
name: "型号约束",
|
||||||
|
stock_control: 0,
|
||||||
|
qty: 0,
|
||||||
|
sync: 1,
|
||||||
|
modelQty: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 205,
|
||||||
|
name: "型号售罄",
|
||||||
|
stock_control: 1,
|
||||||
|
qty: 8,
|
||||||
|
sync: 1,
|
||||||
|
modelQty: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 206,
|
||||||
|
name: "库存待确认",
|
||||||
|
stock_control: 1,
|
||||||
|
qty: null,
|
||||||
|
sync: 0,
|
||||||
|
modelQty: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 207,
|
||||||
|
name: "商品限制更小",
|
||||||
|
stock_control: 1,
|
||||||
|
qty: 2,
|
||||||
|
sync: 1,
|
||||||
|
modelQty: 9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 208,
|
||||||
|
name: "仅商品限制",
|
||||||
|
stock_control: 1,
|
||||||
|
qty: 5,
|
||||||
|
sync: 0,
|
||||||
|
modelQty: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const product = (entry) => ({
|
||||||
|
...entry,
|
||||||
|
module: "mf_dcim",
|
||||||
|
description: "",
|
||||||
|
price: "350.00",
|
||||||
|
cycle: "月",
|
||||||
|
product_group_id_second: 7,
|
||||||
|
});
|
||||||
|
const lineOptions = [
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
name: "阶梯带宽",
|
||||||
|
bill_type: "bw",
|
||||||
|
bw: [
|
||||||
|
{ type: "step", value: "0", min_value: 30, max_value: 30, step: 1 },
|
||||||
|
{ type: "step", value: "0", min_value: 31, max_value: 1000, step: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
name: "固定带宽",
|
||||||
|
bill_type: "bw",
|
||||||
|
bw: [{ type: "radio", value: "100", min_value: 999, max_value: 999 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
name: "区间流量",
|
||||||
|
bill_type: "flow",
|
||||||
|
flow: [
|
||||||
|
{
|
||||||
|
type: "total",
|
||||||
|
value: "0",
|
||||||
|
min_value: 500,
|
||||||
|
max_value: 2000,
|
||||||
|
step: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
// 每个请求仍经过 useApi 的真实配置/解析链,未替换任何项目内部模块。
|
||||||
|
await page
|
||||||
|
.context()
|
||||||
|
.route(`${backendOrigin}/console/v1/**`, async (route) => {
|
||||||
|
const request = route.request();
|
||||||
|
const url = new URL(request.url());
|
||||||
|
const path = url.pathname.replace(/^\/console\/v1/, "");
|
||||||
|
const reply = (data) =>
|
||||||
|
route.fulfill({
|
||||||
|
json: { status: 200, msg: "", data },
|
||||||
|
headers: { "Access-Control-Allow-Origin": "*" },
|
||||||
|
});
|
||||||
|
if (path === "/product/group/first")
|
||||||
|
return reply({ list: [{ id: 6, name: "国内云服务器" }] });
|
||||||
|
if (path === "/product/group/second")
|
||||||
|
return reply({ list: [{ id: 7, parent_id: 6, name: "测试线路" }] });
|
||||||
|
if (path === "/product")
|
||||||
|
return reply({ list: variants.map(product), count: variants.length });
|
||||||
|
const match = /^\/product\/([^/]+)(.*)$/.exec(path);
|
||||||
|
if (!match) return route.fallback();
|
||||||
|
const id = Number(match[1]);
|
||||||
|
const suffix = match[2];
|
||||||
|
if (request.method() === "GET") state.productReads.push(path);
|
||||||
|
if (!suffix) {
|
||||||
|
const entry = variants.find((entry) => entry.id === id);
|
||||||
|
return reply({
|
||||||
|
product: entry
|
||||||
|
? product(entry)
|
||||||
|
: {
|
||||||
|
id,
|
||||||
|
module: "mf_dcim",
|
||||||
|
order_allowed: false,
|
||||||
|
order_block_reason: "边界商品已停止订购",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const entry = variants.find((entry) => entry.id === id) ?? variants[0];
|
||||||
|
if (suffix === "/mf_dcim/order_page")
|
||||||
|
return reply({
|
||||||
|
model_config: [
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
name: `${entry.name}机型`,
|
||||||
|
cpu: "8核",
|
||||||
|
memory: "16G",
|
||||||
|
disk: "480G SSD",
|
||||||
|
qty: entry.modelQty,
|
||||||
|
auto_sync_dcim_stock: entry.sync,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
data_center: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "中国",
|
||||||
|
city: [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "测试地区",
|
||||||
|
area: [
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "测试机房",
|
||||||
|
line: lineOptions.map(({ id, name, bill_type }) => ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
bill_type,
|
||||||
|
data_center_id: 1,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const line = /^\/mf_dcim\/line\/(\d+)$/.exec(suffix);
|
||||||
|
if (line)
|
||||||
|
return reply({
|
||||||
|
...lineOptions.find((entry) => entry.id === Number(line[1])),
|
||||||
|
ip: [{ type: "radio", value: "1", desc: "1个" }],
|
||||||
|
defence: [{ type: "radio", value: "0", desc: "基础防护" }],
|
||||||
|
});
|
||||||
|
if (suffix === "/mf_dcim/image")
|
||||||
|
return reply({
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "Ubuntu",
|
||||||
|
image: [
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
name: "Ubuntu 24.04",
|
||||||
|
image_group_id: 1,
|
||||||
|
price: "0.00",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (suffix === "/self_defined_field/order_page")
|
||||||
|
return reply({ data: [] });
|
||||||
|
if (suffix === "/mf_dcim/duration")
|
||||||
|
return reply([{ id: 1, name: "月付", price: "350.00", is_default: 1 }]);
|
||||||
|
if (suffix === "/config_option") {
|
||||||
|
state.quotes.push(request.postDataJSON());
|
||||||
|
return reply({ price_total: "350.00" });
|
||||||
|
}
|
||||||
|
return route.fallback();
|
||||||
|
});
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
test(
|
||||||
|
"catalog stock labels and eligibility share effective product and model limits",
|
||||||
|
{ timeout: 60000 },
|
||||||
|
async (t) => {
|
||||||
|
await scenario(t, {}, async (page) => {
|
||||||
|
const state = await catalogBoundary(page);
|
||||||
|
await page.goto(`${appOrigin}/products/dedicated-server`);
|
||||||
|
const card = (name) =>
|
||||||
|
page.locator(".hardware-card").filter({
|
||||||
|
has: page.getByRole("heading", {
|
||||||
|
name: `${name}机型`,
|
||||||
|
exact: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
await card("未控库存").waitFor();
|
||||||
|
assert.equal(
|
||||||
|
await card("未控库存").getByText("剩余 0 台", { exact: true }).count(),
|
||||||
|
0,
|
||||||
|
"Uncontrolled placeholder zero is not an available-stock count",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
await card("未控库存")
|
||||||
|
.getByRole("button", { name: "选择线路", exact: true })
|
||||||
|
.isEnabled(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
for (const name of ["商品售罄", "型号售罄"])
|
||||||
|
assert.equal(
|
||||||
|
await card(name)
|
||||||
|
.getByRole("button", { name: "库存不足", exact: true })
|
||||||
|
.isDisabled(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
await card("共同约束").getByText("剩余 3 台", { exact: true }).waitFor();
|
||||||
|
await card("型号约束").getByText("剩余 4 台", { exact: true }).waitFor();
|
||||||
|
await card("商品限制更小")
|
||||||
|
.getByText("剩余 2 台", { exact: true })
|
||||||
|
.waitFor();
|
||||||
|
await card("仅商品限制")
|
||||||
|
.getByText("剩余 5 台", { exact: true })
|
||||||
|
.waitFor();
|
||||||
|
assert.equal(
|
||||||
|
await card("库存待确认")
|
||||||
|
.getByText("剩余 0 台", { exact: true })
|
||||||
|
.count(),
|
||||||
|
0,
|
||||||
|
"Missing quantity must not be coerced into advertised zero stock",
|
||||||
|
);
|
||||||
|
await page.goto(
|
||||||
|
`${appOrigin}/products/dedicated-server/customize?id=203&model_id=203&line_id=11`,
|
||||||
|
);
|
||||||
|
await page.getByText("当前库存 3 台", { exact: true }).waitFor();
|
||||||
|
assert.equal(
|
||||||
|
await page
|
||||||
|
.getByLabel("购买数量", { exact: true })
|
||||||
|
.getAttribute("aria-valuemax"),
|
||||||
|
"3",
|
||||||
|
);
|
||||||
|
const beforeSoldOut = state.quotes.length;
|
||||||
|
await page.goto(
|
||||||
|
`${appOrigin}/products/dedicated-server/customize?id=202&model_id=202&line_id=11`,
|
||||||
|
);
|
||||||
|
await page
|
||||||
|
.getByText("所选型号库存不足,请减少数量或选择其他型号")
|
||||||
|
.first()
|
||||||
|
.waitFor();
|
||||||
|
assert.equal(
|
||||||
|
state.quotes.length,
|
||||||
|
beforeSoldOut,
|
||||||
|
"Direct configuration of controlled zero stock must not request a quote",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
uncontrolledZeroNotAdvertised: true,
|
||||||
|
controlledZeroBlocked: true,
|
||||||
|
jointMinimumStock: 3,
|
||||||
|
modelOnlyStock: 4,
|
||||||
|
productSmallerStock: 2,
|
||||||
|
productOnlyStock: 5,
|
||||||
|
unknownNotTreatedAsZero: true,
|
||||||
|
configurationMatchesCatalog: true,
|
||||||
|
directSoldOutConfigurationDoesNotQuote: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"catalog radio and ranged network summaries agree with configurator defaults",
|
||||||
|
{ timeout: 60000 },
|
||||||
|
async (t) => {
|
||||||
|
await scenario(t, {}, async (page) => {
|
||||||
|
const state = await catalogBoundary(page);
|
||||||
|
const observations = [];
|
||||||
|
for (const [lineId, name, expected, field, value] of [
|
||||||
|
[11, "阶梯带宽", "30 Mbps", "bw", "30"],
|
||||||
|
[12, "固定带宽", "100 Mbps", "bw", "100"],
|
||||||
|
[13, "区间流量", "500 GB", "flow", "500"],
|
||||||
|
]) {
|
||||||
|
await page.goto(`${appOrigin}/products/dedicated-server`);
|
||||||
|
await page
|
||||||
|
.locator(".hardware-card")
|
||||||
|
.filter({
|
||||||
|
has: page.getByRole("heading", {
|
||||||
|
name: "未控库存机型",
|
||||||
|
exact: true,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.getByRole("button", { name: "选择线路", exact: true })
|
||||||
|
.click();
|
||||||
|
const line = page.locator(".line-card").filter({
|
||||||
|
has: page.locator(".line-tags").getByText(name, { exact: true }),
|
||||||
|
});
|
||||||
|
await line.locator(".line-specs").waitFor();
|
||||||
|
assert.equal(
|
||||||
|
(await line.innerText()).includes(expected),
|
||||||
|
true,
|
||||||
|
"Network summary must show a selectable value, not the range placeholder",
|
||||||
|
);
|
||||||
|
const before = state.quotes.length;
|
||||||
|
await line.getByRole("link", { name: "立即订购", exact: true }).click();
|
||||||
|
await eventually(
|
||||||
|
() => state.quotes.slice(before),
|
||||||
|
(quotes) =>
|
||||||
|
quotes.some(
|
||||||
|
(quote) =>
|
||||||
|
quote.config_options.line_id === lineId &&
|
||||||
|
quote.config_options[field] === value,
|
||||||
|
),
|
||||||
|
"Configurator quote must use the same valid network value as the catalog",
|
||||||
|
);
|
||||||
|
observations.push({
|
||||||
|
option: name,
|
||||||
|
summary: expected,
|
||||||
|
configuredValue: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { networkOptions: observations };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"invalid product identifiers show public errors before product API reads",
|
||||||
|
{ timeout: 90000 },
|
||||||
|
async (t) => {
|
||||||
|
await scenario(t, {}, async (page) => {
|
||||||
|
const state = await catalogBoundary(page);
|
||||||
|
const invalid = [
|
||||||
|
"2147483648",
|
||||||
|
"999999999999999999999999999999999",
|
||||||
|
"0",
|
||||||
|
"-1",
|
||||||
|
"01",
|
||||||
|
"not-a-product",
|
||||||
|
];
|
||||||
|
const observed = [];
|
||||||
|
for (const id of invalid) {
|
||||||
|
for (const path of [
|
||||||
|
`/products/${id}`,
|
||||||
|
`/products/dedicated-server/customize?id=${id}`,
|
||||||
|
`/products/cloud-server?id=${id}`,
|
||||||
|
`/products/cvm?product_id=${id}`,
|
||||||
|
]) {
|
||||||
|
const before = state.productReads.length;
|
||||||
|
await page.goto(appOrigin + path);
|
||||||
|
await eventually(
|
||||||
|
async () =>
|
||||||
|
state.productReads.length > before ||
|
||||||
|
(await page.getByText("商品编号无效").first().isVisible()),
|
||||||
|
Boolean,
|
||||||
|
"Identifier must either be rejected visibly or reach the observed external boundary",
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
state.productReads.slice(before),
|
||||||
|
[],
|
||||||
|
"Invalid identifier must not enter a product endpoint",
|
||||||
|
);
|
||||||
|
await page.getByText("商品编号无效").first().waitFor();
|
||||||
|
assert.equal(
|
||||||
|
new URL(page.url()).pathname,
|
||||||
|
path.split("?")[0],
|
||||||
|
"Invalid public product identifiers must not redirect to login",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
state.productReads.length,
|
||||||
|
before,
|
||||||
|
"Invalid identifier must not enter a product endpoint",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
observed.push(id);
|
||||||
|
}
|
||||||
|
await page.goto(`${appOrigin}/products/2147483647`);
|
||||||
|
await page.getByText("边界商品已停止订购", { exact: true }).waitFor();
|
||||||
|
assert.equal(
|
||||||
|
state.productReads.includes("/product/2147483647"),
|
||||||
|
true,
|
||||||
|
"Largest supported signed 32-bit product identifier remains valid",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
invalidIdentifiersRejected: observed,
|
||||||
|
publicErrorNotLoginLoop: true,
|
||||||
|
invalidProductApiReads: 0,
|
||||||
|
largestValidIdentifierAccepted: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
Reference in new issue
Block a user