@pisell/pisellos
Version:
一个可扩展的前端模块化SDK框架,支持插件系统
894 lines (892 loc) • 32.9 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/model/strategy/adapter/promotion/evaluator.ts
var evaluator_exports = {};
__export(evaluator_exports, {
PromotionEvaluator: () => PromotionEvaluator
});
module.exports = __toCommonJS(evaluator_exports);
var import_decimal = __toESM(require("decimal.js"));
var import_engine = require("../../engine");
var import_adapter = require("./adapter");
var import_type = require("./type");
var defaultLocales = {
en: {
no_applicable_promotion: "No applicable promotion",
promotion_not_in_time_range: "Promotion not in valid time range",
product_not_in_promotion: "Product not in promotion scope"
},
"zh-CN": {
no_applicable_promotion: "无适用的促销活动",
promotion_not_in_time_range: "促销活动不在有效时间范围内",
product_not_in_promotion: "商品不在促销范围内"
},
"zh-HK": {
no_applicable_promotion: "無適用的促銷活動",
promotion_not_in_time_range: "促銷活動不在有效時間範圍內",
product_not_in_promotion: "商品不在促銷範圍內"
}
};
var PromotionEvaluator = class {
constructor() {
this.strategyConfigs = [];
this.locale = "en";
this.locales = defaultLocales;
this.engine = new import_engine.StrategyEngine({
debug: false,
enableTrace: false
});
this.adapter = new import_adapter.PromotionAdapter();
}
// ============================================
// 配置管理
// ============================================
/**
* 设置策略配置列表
*/
setStrategyConfigs(strategyConfigs) {
const newStrategyConfigs = strategyConfigs.filter((item) => item.metadata.type === "promotion");
this.strategyConfigs = newStrategyConfigs;
}
/**
* 获取策略配置列表
*/
getStrategyConfigs() {
return this.strategyConfigs;
}
/**
* 添加策略配置
*/
addStrategyConfig(strategyConfig) {
this.strategyConfigs.push(strategyConfig);
}
/**
* 设置语言
*/
setLocale(locale) {
this.locale = locale;
}
/**
* 设置自定义多语言文案
*/
setLocales(locales) {
this.locales = { ...defaultLocales, ...locales };
}
/**
* 获取多语言文案
*/
getText(key) {
var _a, _b;
return ((_b = (_a = this.locales) == null ? void 0 : _a[this.locale]) == null ? void 0 : _b[key]) || key;
}
/**
* 获取多语言名称
*/
getLocalizedName(name) {
if (typeof name === "string") {
return name;
}
return name[this.locale] || name["en"] || Object.values(name)[0] || "";
}
// ============================================
// 核心评估方法
// ============================================
/**
* 评估商品列表
*
* 判断每个商品适用哪些促销活动
* 支持主商品和 bundle 子商品的匹配
*
* @param input 评估输入
* @returns 每个商品的促销评估结果
*/
evaluateProducts(input) {
const { products, strategyConfigs, channel } = input;
const configs = strategyConfigs || this.strategyConfigs;
const results = [];
for (const product of products) {
const applicablePromotions = [];
for (const config of configs) {
const matchInfo = this.getProductMatchInfo(product, config);
if (!matchInfo.isMatch) {
continue;
}
let contextProduct = product;
if (matchInfo.matchedBundleIndex !== void 0 && product.bundle) {
const bundleItem = product.bundle[matchInfo.matchedBundleIndex];
contextProduct = {
...product,
product_id: bundleItem._bundle_product_id,
product_variant_id: bundleItem.product_variant_id
};
}
const context = this.adapter.prepareContext({
products: [contextProduct],
currentProduct: contextProduct,
channel
});
const result = this.engine.evaluate(config, context);
if (result.applicable && result.matchedActions.length > 0) {
const action = result.matchedActions[0];
const transformedResult = this.adapter.transformResult(result, {
products: [product],
currentProduct: product,
channel
});
if (transformedResult.actionDetail) {
applicablePromotions.push({
strategyId: config.metadata.id,
strategyName: config.metadata.name,
actionType: action.type,
actionDetail: transformedResult.actionDetail,
display: this.getDisplayConfig(config),
strategyConfig: config,
// 记录匹配的是哪个 bundle 子商品
matchedBundleIndex: matchInfo.matchedBundleIndex
});
}
}
}
results.push({
product,
applicablePromotions,
hasPromotion: applicablePromotions.length > 0
});
}
return results;
}
/**
* 评估购物车
*
* 返回所有适用的促销及按促销分组的商品
* 支持 bundle 子商品的数量计算(主商品数量 × 子商品数量)
*
* @param input 评估输入
* @returns 购物车评估结果
*/
evaluateCart(input) {
const { products, strategyConfigs, channel } = input;
const configs = strategyConfigs || this.strategyConfigs;
const productResults = this.evaluateProducts(input);
const promotionMap = /* @__PURE__ */ new Map();
for (const productResult of productResults) {
for (const promo of productResult.applicablePromotions) {
const existing = promotionMap.get(promo.strategyId);
const product = productResult.product;
let promoQuantity = product.quantity;
if (promo.matchedBundleIndex !== void 0 && product.bundle) {
const bundleItem = product.bundle[promo.matchedBundleIndex];
promoQuantity = product.quantity * (bundleItem.quantity || 1);
}
if (existing) {
existing.applicableProducts.push(product);
existing.totalQuantity += promoQuantity;
existing.totalAmount += product.price * product.quantity;
if (!existing.productMatchedBundleIndexMap) {
existing.productMatchedBundleIndexMap = /* @__PURE__ */ new Map();
}
existing.productMatchedBundleIndexMap.set(product.id, promo.matchedBundleIndex);
} else {
const productMatchedBundleIndexMap = /* @__PURE__ */ new Map();
productMatchedBundleIndexMap.set(product.id, promo.matchedBundleIndex);
promotionMap.set(promo.strategyId, {
...promo,
applicableProducts: [product],
totalQuantity: promoQuantity,
totalAmount: product.price * product.quantity,
productMatchedBundleIndexMap
});
}
}
}
const applicablePromotions = Array.from(promotionMap.values());
const promotionGroups = applicablePromotions.map(
(promo) => {
var _a;
return {
strategyId: promo.strategyId,
strategyName: promo.strategyName,
strategyMetadata: (_a = promo.strategyConfig) == null ? void 0 : _a.metadata,
strategyConfig: promo.strategyConfig,
actionType: promo.actionType,
actionDetail: promo.actionDetail,
products: promo.applicableProducts,
totalQuantity: promo.totalQuantity,
totalAmount: promo.totalAmount,
// 传递 productMatchedBundleIndexMap 到 group
productMatchedBundleIndexMap: promo.productMatchedBundleIndexMap
};
}
);
return {
productResults,
applicablePromotions,
promotionGroups
};
}
/**
* 评估购物车并计算定价
*
* 返回处理后的商品数组(包含拆分、finalPrice)和赠品信息
* - 对于 X_ITEMS_FOR_Y_PRICE:按原价比例均摊价格,优先使用高价商品
* - 对于 BUY_X_GET_Y_FREE:计算赠品数量和可选赠品列表
*
* @param input 评估输入
* @returns 带定价的购物车评估结果
*/
evaluateCartWithPricing(input) {
const { products } = input;
const cartResult = this.evaluateCart(input);
const sortedGroups = this.sortPromotionGroupsByPriority(
cartResult.promotionGroups,
cartResult.applicablePromotions
);
const processedQuantityMap = /* @__PURE__ */ new Map();
const pricedProducts = [];
const gifts = [];
let totalOriginalAmount = new import_decimal.default(0);
let totalFinalAmount = new import_decimal.default(0);
for (const group of sortedGroups) {
const { actionType } = group;
const matchedBundleIndexMap = group.productMatchedBundleIndexMap || /* @__PURE__ */ new Map();
if (actionType === import_type.PROMOTION_ACTION_TYPES.X_ITEMS_FOR_Y_PRICE) {
const result = this.processXItemsForYPrice(
group,
processedQuantityMap,
matchedBundleIndexMap
);
pricedProducts.push(...result.products);
totalOriginalAmount = totalOriginalAmount.plus(result.originalAmount);
totalFinalAmount = totalFinalAmount.plus(result.finalAmount);
} else if (actionType === import_type.PROMOTION_ACTION_TYPES.BUY_X_GET_Y_FREE) {
const result = this.processBuyXGetYFree(
group,
processedQuantityMap,
matchedBundleIndexMap
);
pricedProducts.push(...result.products);
totalOriginalAmount = totalOriginalAmount.plus(result.originalAmount);
totalFinalAmount = totalFinalAmount.plus(result.finalAmount);
if (result.giftInfo) {
gifts.push(result.giftInfo);
}
}
}
const remainingProducts = this.getRemainingProducts(
products,
processedQuantityMap
);
for (const product of remainingProducts) {
const amount = new import_decimal.default(product.price).mul(product.quantity);
totalOriginalAmount = totalOriginalAmount.plus(amount);
totalFinalAmount = totalFinalAmount.plus(amount);
}
pricedProducts.push(...remainingProducts);
const totalDiscount = totalOriginalAmount.minus(totalFinalAmount);
return {
products: pricedProducts,
gifts,
totalDiscount: import_decimal.default.max(0, totalDiscount).toNumber()
};
}
/**
* 获取商品适用的促销列表
*
* 简化方法,用于商品卡片展示
*
* @param product 商品
* @param strategyConfigs 策略配置(可选)
* @returns 适用的促销列表
*/
getProductPromotions(product, strategyConfigs) {
var _a;
const results = this.evaluateProducts({
products: [product],
strategyConfigs
});
return ((_a = results[0]) == null ? void 0 : _a.applicablePromotions) || [];
}
/**
* 获取商品的促销标签
*
* 用于商品卡片展示
*
* @param product 商品
* @param strategyConfigs 策略配置(可选)
* @returns 促销标签列表
*/
getProductPromotionTags(product, strategyConfigs) {
const promotions = this.getProductPromotions(product, strategyConfigs);
return promotions.filter((promo) => promo.display).map((promo) => ({
text: this.getLocalizedName(promo.display.text),
type: promo.display.type,
strategyId: promo.strategyId
}));
}
/**
* 查找商品适用的策略配置
*
* @param product 商品
* @param strategyConfigs 策略配置列表(可选)
* @returns 适用的策略配置列表
*/
findApplicableStrategies(product, strategyConfigs) {
const configs = strategyConfigs || this.strategyConfigs;
return configs.filter(
(config) => this.isProductInStrategy(product, config)
);
}
/**
* 批量获取商品列表的适用策略信息
*
* 用于给商品列表追加策略标签信息
* 只检查商品 ID 匹配和策略时间范围
*
* @param input 评估输入(商品列表)
* @param matchVariant 是否需要匹配 product_variant_id,默认 true(严格匹配),false 时只匹配 product_id
* @returns 每个商品的适用策略完整数据
*/
getProductsApplicableStrategies(input, matchVariant = true) {
const { products, strategyConfigs, channel } = input;
const configs = strategyConfigs || this.strategyConfigs;
const results = [];
for (const product of products) {
const applicableStrategies = [];
for (const config of configs) {
let contextProduct = product;
if (!matchVariant) {
const productMatchRule = this.findProductMatchRule(config);
if (productMatchRule) {
const configProducts = productMatchRule.value;
const matchedConfig = configProducts.find(
(item) => item.product_id === product.product_id
);
if (!matchedConfig) {
continue;
}
if (matchedConfig.product_variant_id !== 0 && matchedConfig.product_variant_id !== product.product_variant_id) {
contextProduct = {
...product,
product_variant_id: matchedConfig.product_variant_id
};
}
}
}
const context = this.adapter.prepareContext({
products: [contextProduct],
currentProduct: contextProduct,
channel
});
const result = this.engine.evaluate(config, context);
if (result.applicable && result.matchedActions.length > 0) {
const action = result.matchedActions[0];
const transformedResult = this.adapter.transformResult(result, {
products: [product],
currentProduct: product,
channel
});
if (transformedResult.actionDetail) {
const { requiredQuantity, eligibleProducts } = this.getPromotionRequirements(
action.type,
transformedResult.actionDetail,
config
);
applicableStrategies.push({
strategyId: config.metadata.id,
strategyName: config.metadata.name,
strategyMetadata: config.metadata,
actionType: action.type,
actionDetail: transformedResult.actionDetail,
display: this.getDisplayConfig(config),
strategyConfig: config,
requiredQuantity,
eligibleProducts
});
}
}
}
results.push({
product,
applicableStrategies,
hasApplicableStrategy: applicableStrategies.length > 0
});
}
return results;
}
/**
* 获取促销所需数量和可参与商品列表
*
* @param actionType 促销类型
* @param actionDetail 促销详情
* @param config 策略配置
* @returns { requiredQuantity, eligibleProducts }
*/
getPromotionRequirements(actionType, actionDetail, config) {
let requiredQuantity = 0;
if (actionType === "BUY_X_GET_Y_FREE") {
const detail = actionDetail;
requiredQuantity = detail.buyQuantity || 0;
} else if (actionType === "X_ITEMS_FOR_Y_PRICE") {
const detail = actionDetail;
requiredQuantity = detail.x || 0;
}
const productMatchRule = this.findProductMatchRule(config);
const eligibleProducts = (productMatchRule == null ? void 0 : productMatchRule.value) || [];
return {
requiredQuantity,
eligibleProducts
};
}
// ============================================
// 私有辅助方法
// ============================================
/**
* 按优先级排序促销组
* 优先级从 strategyConfig.actions[0].priority 获取,数值越小优先级越高
*/
sortPromotionGroupsByPriority(groups, promotions) {
var _a, _b;
const priorityMap = /* @__PURE__ */ new Map();
for (const promo of promotions) {
const actions = ((_a = promo.strategyConfig) == null ? void 0 : _a.actions) || [];
const priority = ((_b = actions[0]) == null ? void 0 : _b.priority) ?? 999;
priorityMap.set(promo.strategyId, priority);
}
return [...groups].sort((a, b) => {
const priorityA = priorityMap.get(a.strategyId) ?? 999;
const priorityB = priorityMap.get(b.strategyId) ?? 999;
return priorityA - priorityB;
});
}
/**
* 处理 X件Y元 促销
*
* 1. 展开商品为单件列表,按价格从高到低排序
* 2. 计算可凑成的组数
* 3. 按原价比例均摊价格
* 4. 拆分商品(部分参与促销、部分原价)
*
* 注意:每个商品按 id 独立处理,不会合并不同 id 的商品
*/
processXItemsForYPrice(group, processedQuantityMap, matchedBundleIndexMap) {
const { strategyId, strategyMetadata, actionDetail, products: groupProducts } = group;
const detail = actionDetail;
const { x, price: groupPrice, cumulative } = detail;
const result = [];
let originalAmount = new import_decimal.default(0);
let finalAmount = new import_decimal.default(0);
const unitProducts = [];
for (const product of groupProducts) {
const key = this.getProductKey(product);
const processedQty = processedQuantityMap.get(key) || 0;
const availableQty = product.quantity - processedQty;
if (availableQty <= 0)
continue;
for (let i = 0; i < availableQty; i++) {
unitProducts.push({
product,
price: product.price,
productId: product.id,
sourceAvailableQty: availableQty
});
}
}
if (unitProducts.length === 0) {
return { products: [], originalAmount: 0, finalAmount: 0 };
}
unitProducts.sort((a, b) => {
const aIsMultiple = a.sourceAvailableQty > 0 && a.sourceAvailableQty % x === 0 ? 1 : 0;
const bIsMultiple = b.sourceAvailableQty > 0 && b.sourceAvailableQty % x === 0 ? 1 : 0;
if (aIsMultiple !== bIsMultiple)
return bIsMultiple - aIsMultiple;
return b.price - a.price;
});
const totalUnits = unitProducts.length;
const groupCount = cumulative ? Math.floor(totalUnits / x) : totalUnits >= x ? 1 : 0;
const promotionUnits = groupCount * x;
const inPromotionUnits = unitProducts.slice(0, promotionUnits);
const notInPromotionUnits = unitProducts.slice(promotionUnits);
const promotionQtyByProductId = /* @__PURE__ */ new Map();
for (const unit of inPromotionUnits) {
const id = unit.productId;
promotionQtyByProductId.set(id, (promotionQtyByProductId.get(id) || 0) + 1);
}
const remainingQtyByProductId = /* @__PURE__ */ new Map();
for (const unit of notInPromotionUnits) {
const id = unit.productId;
remainingQtyByProductId.set(id, (remainingQtyByProductId.get(id) || 0) + 1);
}
const allocatedPricesByProductId = /* @__PURE__ */ new Map();
for (let g = 0; g < groupCount; g++) {
const startIdx = g * x;
const endIdx = startIdx + x;
const groupUnits = inPromotionUnits.slice(startIdx, endIdx);
const groupOriginalTotal = groupUnits.reduce(
(sum, u) => sum.plus(u.price),
new import_decimal.default(0)
);
const groupPriceDecimal = new import_decimal.default(groupPrice);
let allocatedSum = new import_decimal.default(0);
for (let i = 0; i < groupUnits.length; i++) {
const unit = groupUnits[i];
const isLastUnit = i === groupUnits.length - 1;
const ratio = groupOriginalTotal.eq(0) ? new import_decimal.default(0) : new import_decimal.default(unit.price).div(groupOriginalTotal);
const allocatedDecimal = isLastUnit ? groupPriceDecimal.minus(allocatedSum) : groupPriceDecimal.mul(ratio);
const productId = unit.productId;
const prices = allocatedPricesByProductId.get(productId) || [];
prices.push(allocatedDecimal);
allocatedPricesByProductId.set(productId, prices);
allocatedSum = allocatedSum.plus(allocatedDecimal);
}
}
for (const product of groupProducts) {
const key = this.getProductKey(product);
const processedQty = processedQuantityMap.get(key) || 0;
const availableQty = product.quantity - processedQty;
if (availableQty <= 0)
continue;
const promoQty = promotionQtyByProductId.get(product.id) || 0;
const remainingQty = remainingQtyByProductId.get(product.id) || 0;
const allocatedPrices = allocatedPricesByProductId.get(product.id) || [];
const matchedBundleIndex = matchedBundleIndexMap == null ? void 0 : matchedBundleIndexMap.get(product.id);
if (promoQty > 0 && remainingQty === 0) {
const totalAllocated = allocatedPrices.reduce(
(sum, p) => sum.plus(p),
new import_decimal.default(0)
);
const productOriginalAmount = new import_decimal.default(product.price).mul(promoQty);
const actualTotal = import_decimal.default.min(totalAllocated, productOriginalAmount);
const finalPricePerUnit = this.formatPrice(actualTotal.div(promoQty));
result.push({
...product,
quantity: promoQty,
finalPrice: finalPricePerUnit,
strategyId,
strategyMetadata,
inPromotion: true,
isSplit: false,
matchedBundleIndex
});
originalAmount = originalAmount.plus(productOriginalAmount);
finalAmount = finalAmount.plus(actualTotal);
} else if (promoQty === 0 && remainingQty > 0) {
result.push({
...product,
quantity: remainingQty,
finalPrice: this.formatPrice(product.price),
strategyId: void 0,
strategyMetadata: void 0,
inPromotion: false,
isSplit: false
});
const amount = new import_decimal.default(product.price).mul(remainingQty);
originalAmount = originalAmount.plus(amount);
finalAmount = finalAmount.plus(amount);
} else if (promoQty > 0 && remainingQty > 0) {
const totalAllocated = allocatedPrices.reduce(
(sum, p) => sum.plus(p),
new import_decimal.default(0)
);
const promoOriginalAmount = new import_decimal.default(product.price).mul(promoQty);
const actualTotal = import_decimal.default.min(totalAllocated, promoOriginalAmount);
const finalPricePerUnit = this.formatPrice(actualTotal.div(promoQty));
result.push({
...product,
id: this.generateRandomId(),
originalId: product.id,
quantity: promoQty,
finalPrice: finalPricePerUnit,
strategyId,
strategyMetadata,
inPromotion: true,
matchedBundleIndex,
isSplit: true
});
originalAmount = originalAmount.plus(promoOriginalAmount);
finalAmount = finalAmount.plus(actualTotal);
result.push({
...product,
quantity: remainingQty,
finalPrice: this.formatPrice(product.price),
strategyId: void 0,
strategyMetadata: void 0,
inPromotion: false,
isSplit: true
});
const remainingAmount = new import_decimal.default(product.price).mul(remainingQty);
originalAmount = originalAmount.plus(remainingAmount);
finalAmount = finalAmount.plus(remainingAmount);
}
processedQuantityMap.set(key, processedQty + availableQty);
}
if (groupCount > 0) {
const expectedPromoTotal = new import_decimal.default(groupPrice).mul(groupCount);
const promoItems = result.filter((p) => p.inPromotion);
let actualPromoTotal = new import_decimal.default(0);
for (const p of promoItems) {
actualPromoTotal = actualPromoTotal.plus(
new import_decimal.default(p.finalPrice).mul(p.quantity)
);
}
const roundingDiff = actualPromoTotal.minus(expectedPromoTotal);
if (!roundingDiff.eq(0)) {
const adjustTarget = promoItems.find((p) => p.quantity === 1);
if (adjustTarget) {
adjustTarget.finalPrice = this.formatPrice(
new import_decimal.default(adjustTarget.finalPrice).minus(roundingDiff)
);
} else if (promoItems.length > 0) {
const lastPromo = promoItems[promoItems.length - 1];
const adjustedUnitPrice = this.formatPrice(
new import_decimal.default(lastPromo.finalPrice).minus(roundingDiff)
);
lastPromo.quantity -= 1;
lastPromo.isSplit = true;
result.push({
...lastPromo,
id: this.generateRandomId(),
originalId: lastPromo.originalId || lastPromo.id,
quantity: 1,
finalPrice: adjustedUnitPrice,
isSplit: true
});
}
finalAmount = new import_decimal.default(0);
for (const p of result) {
finalAmount = finalAmount.plus(
new import_decimal.default(p.finalPrice).mul(p.quantity)
);
}
}
}
return {
products: result,
originalAmount: originalAmount.toNumber(),
finalAmount: finalAmount.toNumber()
};
}
/**
* 处理 买X送Y 促销
*
* 计算赠品数量,商品本身价格不变
* 支持 bundle 子商品的数量计算(主商品数量 × 子商品数量)
*/
processBuyXGetYFree(group, processedQuantityMap, matchedBundleIndexMap) {
const {
strategyId,
strategyName,
strategyMetadata,
actionDetail,
products: groupProducts
} = group;
const detail = actionDetail;
const { buyQuantity, freeQuantity, cumulative, giftProducts } = detail;
const result = [];
let originalAmount = new import_decimal.default(0);
let finalAmount = new import_decimal.default(0);
let totalPromoQty = 0;
const productInfoMap = /* @__PURE__ */ new Map();
for (const product of groupProducts) {
const key = this.getProductKey(product);
const processedQty = processedQuantityMap.get(key) || 0;
const availableQty = product.quantity - processedQty;
if (availableQty <= 0)
continue;
const matchedBundleIndex = matchedBundleIndexMap == null ? void 0 : matchedBundleIndexMap.get(product.id);
let promoQty = availableQty;
if (matchedBundleIndex !== void 0 && product.bundle) {
const bundleItem = product.bundle[matchedBundleIndex];
promoQty = availableQty * (bundleItem.quantity || 1);
}
totalPromoQty += promoQty;
productInfoMap.set(key, { availableQty, promoQty, matchedBundleIndex });
}
const isFulfilled = totalPromoQty >= buyQuantity;
for (const product of groupProducts) {
const key = this.getProductKey(product);
const info = productInfoMap.get(key);
if (!info || info.availableQty <= 0)
continue;
const processedQty = processedQuantityMap.get(key) || 0;
result.push({
...product,
quantity: info.availableQty,
// 返回主商品数量
finalPrice: this.formatPrice(product.price),
strategyId,
strategyMetadata,
inPromotion: isFulfilled,
isSplit: info.availableQty < product.quantity,
matchedBundleIndex: info.matchedBundleIndex
});
const amount = new import_decimal.default(product.price).mul(info.availableQty);
originalAmount = originalAmount.plus(amount);
finalAmount = finalAmount.plus(amount);
processedQuantityMap.set(key, processedQty + info.availableQty);
}
let giftCount = 0;
if (isFulfilled) {
giftCount = cumulative ? Math.floor(totalPromoQty / buyQuantity) * freeQuantity : freeQuantity;
}
const giftInfo = giftCount > 0 ? {
strategyId,
strategyName,
giftCount,
giftOptions: giftProducts || []
} : null;
return {
products: result,
originalAmount: originalAmount.toNumber(),
finalAmount: finalAmount.toNumber(),
giftInfo
};
}
/**
* 获取未参与任何促销的商品
*/
getRemainingProducts(allProducts, processedQuantityMap) {
const result = [];
for (const product of allProducts) {
const key = this.getProductKey(product);
const processedQty = processedQuantityMap.get(key) || 0;
const remainingQty = product.quantity - processedQty;
if (remainingQty > 0) {
result.push({
...product,
quantity: remainingQty,
finalPrice: this.formatPrice(product.price),
strategyId: void 0,
inPromotion: false,
isSplit: remainingQty < product.quantity
});
}
}
return result;
}
/**
* 价格统一保留两位小数
*/
formatPrice(value) {
return new import_decimal.default(value).toDecimalPlaces(2, import_decimal.default.ROUND_HALF_UP).toNumber();
}
/**
* 获取商品唯一标识 key
* 使用商品的 id 作为唯一标识,不同 id 的商品不会被合并
*/
getProductKey(product) {
return String(product.id);
}
/**
* 生成随机ID
*/
generateRandomId() {
return Math.floor(Math.random() * 1e9) + Date.now();
}
/**
* 商品匹配结果信息
*/
getProductMatchInfo(product, config) {
const productMatchRule = this.findProductMatchRule(config);
if (!productMatchRule) {
return { isMatch: true };
}
const configProducts = productMatchRule.value;
if (this.isProductIdMatch(product.product_id, product.product_variant_id, configProducts)) {
return { isMatch: true };
}
if (product.bundle && product.bundle.length > 0) {
for (let i = 0; i < product.bundle.length; i++) {
const bundleItem = product.bundle[i];
if (this.isProductIdMatch(
bundleItem._bundle_product_id,
bundleItem.product_variant_id,
configProducts
)) {
return { isMatch: true, matchedBundleIndex: i };
}
}
}
return { isMatch: false };
}
/**
* 检查商品是否在策略的适用范围内
*/
isProductInStrategy(product, config) {
return this.getProductMatchInfo(product, config).isMatch;
}
/**
* 查找商品匹配规则
*/
findProductMatchRule(config) {
var _a;
if (!((_a = config == null ? void 0 : config.conditions) == null ? void 0 : _a.rules)) {
return null;
}
return config.conditions.rules.find(
(rule) => rule.field === "productIdAndVariantId" && (rule.operator === "product_match" || rule.operator === "object_in")
);
}
/**
* 检查商品ID是否匹配配置列表
*/
isProductIdMatch(productId, productVariantId, configProducts) {
return configProducts.some((config) => {
if (config.product_id !== productId) {
return false;
}
if (config.product_variant_id === 0) {
return true;
}
return config.product_variant_id === productVariantId;
});
}
/**
* 检查商品是否匹配配置(兼容旧方法)
*/
isProductMatch(product, configProducts) {
return this.isProductIdMatch(product.product_id, product.product_variant_id, configProducts);
}
/**
* 获取展示配置
*/
getDisplayConfig(config) {
var _a;
const custom = config.metadata.custom;
if ((_a = custom == null ? void 0 : custom.display) == null ? void 0 : _a.product_card) {
return {
text: custom.display.product_card.text,
type: custom.display.product_card.type
};
}
return void 0;
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
PromotionEvaluator
});