UNPKG

@pisell/pisellos

Version:

一个可扩展的前端模块化SDK框架,支持插件系统

894 lines (892 loc) 32.9 kB
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 });