add product stock preview

This commit is contained in:
Timi
2026-09-16 14:53:36 +08:00
parent 443d0a6190
commit f8a9c0e649
12 changed files with 456 additions and 86 deletions
+86
View File
@@ -0,0 +1,86 @@
import type { ProductStock } from "@/types/Product";
import { addPackageQuantity, comparePackageQuantity } from "@/utils/package";
export type ProductStockRequirement = {
skuId: string;
baseQuantity: string;
productName: string;
unitName: string;
stockEnabled: boolean;
};
export type ProductStockPreviewItem = ProductStockRequirement & {
availableQuantity: string;
shortageQuantity?: string;
};
/** 合并同一 SKU 的基础库存需求,避免多单位订单拆开校验造成误判。 */
export function mergeProductStockRequirements(requirementList: ProductStockRequirement[]): ProductStockRequirement[] {
const requirementMap = new Map<string, ProductStockRequirement>();
for (const requirement of requirementList) {
const existing = requirementMap.get(requirement.skuId);
if (!existing) {
requirementMap.set(requirement.skuId, { ...requirement });
continue;
}
const baseQuantity = addPackageQuantity(existing.baseQuantity, requirement.baseQuantity);
if (baseQuantity) {
existing.baseQuantity = baseQuantity;
}
}
return [...requirementMap.values()];
}
/** 返回库存不足提示;库存单位统一为 SKU 基础单位。 */
export function getProductStockShortageMessage(stockList: ProductStock[], requirementList: ProductStockRequirement[]): string | undefined {
const stockMap = new Map(stockList.map(stock => [stock.skuId, stock]));
const shortageList: string[] = [];
for (const requirement of requirementList) {
if (!requirement.stockEnabled) {
continue;
}
const availableQuantity = stockMap.get(requirement.skuId)?.availableQuantity ?? "0";
if (!(0 < comparePackageQuantity(requirement.baseQuantity, availableQuantity))) {
continue;
}
const shortageQuantity = subtractPackageQuantity(requirement.baseQuantity, availableQuantity);
shortageList.push(`${requirement.productName} 可用 ${availableQuantity} ${requirement.unitName},缺少 ${shortageQuantity} ${requirement.unitName}`);
}
return shortageList.length ? `库存不足:${shortageList.join(";")}` : undefined;
}
/** 生成启用库存管理 SKU 的库存预检明细。 */
export function buildProductStockPreview(stockList: ProductStock[], requirementList: ProductStockRequirement[]): ProductStockPreviewItem[] {
const stockMap = new Map(stockList.map(stock => [stock.skuId, stock]));
return requirementList.filter(requirement => requirement.stockEnabled).map(requirement => {
const availableQuantity = String(stockMap.get(requirement.skuId)?.availableQuantity ?? "0");
const shortageQuantity = 0 < comparePackageQuantity(requirement.baseQuantity, availableQuantity)
? subtractPackageQuantity(requirement.baseQuantity, availableQuantity)
: undefined;
return { ...requirement, availableQuantity, shortageQuantity };
});
}
function subtractPackageQuantity(left: string, right: number | string): string {
const leftParts = toQuantityParts(left);
const rightParts = toQuantityParts(right);
if (!leftParts || !rightParts) {
return left;
}
const scale = Math.max(leftParts.scale, rightParts.scale);
const result = leftParts.value * 10n ** BigInt(scale - leftParts.scale)
- rightParts.value * 10n ** BigInt(scale - rightParts.scale);
const text = result.toString().padStart(scale + 1, "0");
const integerText = scale ? text.slice(0, -scale) : text;
const fractionText = scale ? text.slice(-scale).replace(/0+$/, "") : "";
return fractionText ? `${integerText}.${fractionText}` : integerText;
}
function toQuantityParts(value: number | string): { value: bigint; scale: number } | undefined {
const text = String(value).trim();
if (!/^\d+(?:\.\d{1,3})?$/.test(text)) {
return undefined;
}
const [integerText, fractionText = ""] = text.split(".");
return { value: BigInt(`${integerText}${fractionText}`), scale: fractionText.length };
}