修复商品库存与网络摘要并校验订购编号

This commit is contained in:
cieanyau committed 2026-11-06 04:48:38 +08:00
1 parent 28894e0813
commit d8224ec5b7
15 files changed
+529 -68

No files matched your search

+4 -10
View File
@@ -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 { t as storage_capacity_t } from "~/lib/storage-capacity.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 = {
timeoutMs: 5e3,
},
@@ -142,14 +144,6 @@ function O(e) {
: ``);
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 = ``) {
return {
label: e,
@@ -158,7 +152,7 @@ function A(e, t, n = ``) {
}
// 按真实订单模块读取配置;含级联或高级限制且无法安全推导默认值时不生成摘要。
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);
try {
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`));
else if (e.bill_type === `flow`) {
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;
n &&
s.push(
+1 -8
View File
@@ -60,14 +60,7 @@ import ConfigResourceOption from "~/components/config/ConfigResourceOption.vue";
import { t as password_t } from "~/lib/password-2.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";
function I(e) {
if (e)
return Number(
e.type === `step` || e.type === `total`
? (e.min_value ?? e.value)
: e.value,
);
}
import { resourceOptionValue as I } from "~/lib/resource-option-value.js";
function L(e, t) {
// 区间内的值还必须落在步长网格上,不能仅凭最小/最大值认定配置可购买。
if (t == null || t === ``) return !1;
+5 -17
View File
@@ -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 ConfigDiskCard from "~/components/config/ConfigDiskCard.vue";
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 = {}) {
// 商品库存与型号同步库存可能同时生效;可购数量取所有有效限制中的最小值。
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),
),
ne = computed(() => A.value.find((e) => e.id === D.value.line_id)),
re = computed(() => {
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;
}),
re = computed(() => productStock(m.value, j.value)),
ie = computed(() => re.value === null || C.value <= re.value),
F = computed(() =>
JSON.stringify({
@@ -188,12 +180,8 @@ function z(e, t = {}, n = `mf_dcim`, r = {}) {
}));
let q = (e) => !V && e === z,
le = (e) => {
let t = e?.[0];
return t
? String(
t.type && t.type !== `radio` ? (t.min_value ?? t.value) : t.value,
)
: ``;
const value = resourceOptionValue(e?.[0]);
return value === undefined ? `` : String(value);
};
function ue() {
// 报价和提交共用前置校验,型号/线路/镜像与正整数数量缺一不可。
+3 -2
View File
@@ -1,8 +1,9 @@
/** 优先使用商品已有描述;缺失时按商品 id 补查,最多等待 5 秒,失败不阻塞配置页。 */
import { productId } from "~/lib/product-id.js";
async function e(e, t) {
if (typeof t.description == `string`) return t.description;
let n = Number(t.id);
if (!Number.isSafeInteger(n) || n <= 0) return ``;
let n = productId(t.id);
if (n === null) return ``;
try {
let t = await e.get(
`/product`,
+8
View File
@@ -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;
}
+14
View File
@@ -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;
}
+2
View File
@@ -1,4 +1,5 @@
/** 下单前确认真实配置模块;本地 DCIM 与转售 DCIM 使用不同配置组件,不能仅看商品名。 */
import { productId } from "~/lib/product-id.js";
function e(e) {
let t = typeof e.module == `string` ? e.module.trim() : ``,
n = typeof e.res_module == `string` ? e.res_module.trim() : ``;
@@ -7,6 +8,7 @@ function e(e) {
}
// 已知非 mf_cloud 模块可直接使用;缺失/含糊时读取配置接口,取不到模块就明确失败。
async function t(t, n) {
if (productId(n.id) === null) throw Error(`商品编号无效`);
let r = typeof n.module == `string` ? n.module.trim() : ``,
i = e(n);
if (i) return i;
+11
View File
@@ -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;
}
+3 -2
View File
@@ -58,6 +58,7 @@ import { r as order_image_continuation_r } from "~/lib/order-image-continuation.
import MfDcimConfig from "~/components/config/MfDcimConfig.vue";
import { t as composables_t } from "~/lib/composables-2.js";
import { t as product_description_data_t } from "~/lib/product-description-data.js";
import { productId } from "~/lib/product-id.js";
var j = {
class: `page product-order-page`,
},
@@ -83,7 +84,7 @@ var j = {
let b = useRoute(),
I = useRouter(),
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),
B = b.fullPath;
composables_t({
@@ -132,7 +133,7 @@ var j = {
let e = ++Z;
((W.value = !0), (G.value = ``), (H.value = null), (U.value = null));
try {
if (!/^[1-9]\d*$/.test(R)) throw Error(`商品编号无效`);
if (productId(R) === null) throw Error(`商品编号无效`);
let t = await L.get(`/product/${R}`);
if (e !== Z) return;
let n = t?.product ?? t;
+7 -1
View File
@@ -60,6 +60,7 @@ import KkyEcsConfig from "~/components/config/KkyEcsConfig.vue";
import MfCloudConfig from "~/components/config/MfCloudConfig.vue";
import { t as composables_t } from "~/lib/composables-2.js";
import { t as product_description_data_t } from "~/lib/product-description-data.js";
import { productId } from "~/lib/product-id.js";
var ge = {
class: `cloud-order-page`,
},
@@ -135,6 +136,7 @@ var ge = {
? `库存待确认`
: `暂时缺货`,
G = (e) =>
productId(e.id) !== null &&
Number(e.hidden ?? 0) !== 1 &&
(e.product_group_id_second == null ||
Number(e.product_group_id_second) === T.value);
@@ -208,7 +210,7 @@ var ge = {
((E.value = (t.list ?? []).filter(G)),
(I.value = Number(t.count) || 0),
(A.value = !1));
let n = Number(C.query.id ?? C.query.product_id),
let n = productId(C.query.id ?? C.query.product_id),
r =
E.value.find((e) => e.id === n && !U(e)) ??
E.value.find((e) => !U(e));
@@ -238,6 +240,10 @@ var ge = {
(j.value = !1),
(A.value = !1));
try {
// 显式指定的商品编号无效时就地提示,不改选首件商品,更不把超限值交给后端。
const requestedId = C.query.id ?? C.query.product_id;
if (requestedId !== undefined && productId(requestedId) === null)
throw Error(`商品编号无效`);
let [t, n] = await Promise.all([
S.get(`/product/group/first`),
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 product_description_data_t } from "~/lib/product-description-data.js";
import index from "~/pages/products/dedicated-server/index.vue";
import { productId } from "~/lib/product-id.js";
var A = {
key: 1,
class: `dedicated-customize`,
@@ -65,7 +66,7 @@ var A = {
let x = useRoute(),
P = useRouter(),
F = useApi(),
I = computed(() => String(x.query.id ?? x.query.product_id ?? ``));
I = computed(() => x.query.id ?? x.query.product_id ?? ``);
composables_t({
key: (e) => e.fullPath,
});
@@ -85,7 +86,7 @@ var A = {
t = I.value;
((R.value = !0), (z.value = ``), (L.value = null));
try {
if (!/^[1-9]\d*$/.test(t)) throw Error(`商品编号无效`);
if (productId(t) === null) throw Error(`商品编号无效`);
let n = await F.get(`/product/${t}`);
if (e !== U) return;
let r = n?.product ?? n;
+16 -14
View File
@@ -32,6 +32,9 @@ import { n as product_name_n } from "~/lib/product-name.js";
import inputnumber from "primevue/inputnumber";
import tag from "primevue/tag";
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 的物理商品;未知模块不强行套用配置器。
function O(e) {
let t = e.module?.trim() || ``;
@@ -122,14 +125,8 @@ function k() {
}
// 商品库存与自动同步的机型库存都满足时才允许选购。
function w(e) {
return (
!(e.product.stock_control === 1 && Number(e.product.qty) <= 0) &&
!(
e.model.auto_sync_dcim_stock === 1 &&
e.model.qty !== void 0 &&
Number(e.model.qty) <= 0
)
);
const stock = productStock(e.product, e.model);
return stock === null || stock > 0;
}
// 分页收集商品后并行加载物理模块配置;单个商品失败保留其余可用项并提示部分加载失败。
async function T() {
@@ -158,7 +155,7 @@ function k() {
}
let o = t.flatMap((e) => {
let t = O(e);
return e.hidden !== 1 && t
return e.hidden !== 1 && productId(e.id) !== null && t
? [
{
product: {
@@ -851,15 +848,20 @@ var te = {
),
t.lines.length
? unref(s).inStock(t)
? t.model.qty !== void 0 &&
t.model.qty >= 0
? productStock(
t.product,
t.model,
) !== null
? (openBlock(),
createElementBlock(
`span`,
B,
`剩余 ` +
toDisplayString(
t.model.qty,
productStock(
t.product,
t.model,
),
) +
` 台`,
1,
@@ -1100,8 +1102,8 @@ var te = {
toDisplayString(
unref(H)[t.id]
?.bill_type === `flow`
? `${unref(H)[t.id]?.flow?.[0]?.value ?? `—`} GB`
: `${unref(H)[t.id]?.bw?.[0]?.value ?? `—`} Mbps`,
? `${resourceOptionValue(unref(H)[t.id]?.flow?.[0]) ?? `—`} GB`
: `${resourceOptionValue(unref(H)[t.id]?.bw?.[0]) ?? `—`} Mbps`,
),
1,
),
+14 -11
View File
@@ -51,6 +51,7 @@ import { n as ProductConfigurationSummary_n } from "~/components/ProductConfigur
import ProductConfigurationSummary from "~/components/ProductConfigurationSummary.vue";
import ProductDescription from "~/components/ProductDescription.vue";
import ProductGroupLabel from "~/components/ProductGroupLabel.vue";
import { productId } from "~/lib/product-id.js";
var j = rolldown_runtime_i(getCrypto(), 1),
M = 2592e6,
N = 100,
@@ -613,17 +614,19 @@ var me = {
a || ((T.value = []), (E.value = 0), (A.value = !1));
let s = await t.get(`/product`, r);
if (n !== W) return;
((T.value = (s.list ?? []).map((t) => ({
...t,
startingPrice: q(t)
? (o.get(t.id)?.startingPrice ?? null)
: void 0,
configurationSummary: product_name_t(t.name)
? e
? o.get(t.id)?.configurationSummary
: y.peek(t)
: [],
}))),
((T.value = (s.list ?? [])
.filter((t) => productId(t.id) !== null)
.map((t) => ({
...t,
startingPrice: q(t)
? (o.get(t.id)?.startingPrice ?? null)
: void 0,
configurationSummary: product_name_t(t.name)
? e
? o.get(t.id)?.configurationSummary
: y.peek(t)
: [],
}))),
(E.value = Number(s.count) || 0),
(K = i),
(A.value = !0));
+2 -1
View File
@@ -21,7 +21,8 @@ const shopPages = new Set([
export function isPublicShop(route) {
if (Object.hasOwn(route.query || {}, "edit_position")) return false;
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) {
return authPages.has(path.replace(/\/+$/, "") || "/");