@pisell/pisellos 0.0.567 → 0.0.568

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/dist/core/index.js +6 -9
  2. package/dist/model/strategy/adapter/walletPass/evaluator.js +0 -2
  3. package/dist/model/strategy/adapter/walletPass/example.js +3 -26
  4. package/dist/model/strategy/adapter/walletPass/utils.js +0 -3
  5. package/dist/model/strategy/strategy-example.js +5 -19
  6. package/dist/modules/Cart/index.js +0 -3
  7. package/dist/modules/Cart/utils/cartProduct.js +0 -1
  8. package/dist/modules/Date/index.js +1 -5
  9. package/dist/modules/Discount/index.js +1 -4
  10. package/dist/modules/OpenData/index.js +0 -2
  11. package/dist/modules/Order/index.d.ts +1 -1
  12. package/dist/modules/Order/index.js +14 -41
  13. package/dist/modules/Payment/index.js +142 -270
  14. package/dist/modules/Payment/walletpass.js +4 -5
  15. package/dist/modules/Rules/index.js +0 -7
  16. package/dist/modules/Schedule/utils.js +0 -1
  17. package/dist/modules/Step/index.js +0 -1
  18. package/dist/plugins/request.js +1 -4
  19. package/dist/plugins/window.js +2 -6
  20. package/dist/server/index.js +88 -121
  21. package/dist/server/modules/menu/index.js +32 -41
  22. package/dist/server/modules/products/index.js +29 -49
  23. package/dist/server/modules/quotation/index.js +29 -38
  24. package/dist/server/modules/resource/index.js +3 -4
  25. package/dist/server/modules/schedule/index.js +32 -40
  26. package/dist/server/utils/product.js +0 -1
  27. package/dist/solution/BookingByStep/index.d.ts +1 -1
  28. package/dist/solution/BookingByStep/index.js +7 -59
  29. package/dist/solution/BookingByStep/utils/capacity.js +1 -11
  30. package/dist/solution/BookingByStep/utils/resources.js +1 -3
  31. package/dist/solution/BookingByStep/utils/timeslots.js +2 -43
  32. package/dist/solution/BookingTicket/index.d.ts +1 -1
  33. package/dist/solution/BookingTicket/index.js +5 -7
  34. package/dist/solution/BookingTicket/utils/scan/handleScan.js +25 -31
  35. package/dist/solution/BookingTicket/utils/scan/index.js +6 -10
  36. package/dist/solution/BuyTickets/index.js +9 -12
  37. package/dist/solution/Checkout/index.js +177 -246
  38. package/dist/solution/Checkout/utils/index.js +4 -16
  39. package/dist/solution/RegisterAndLogin/index.js +30 -76
  40. package/dist/solution/ScanOrder/index.js +50 -60
  41. package/dist/solution/ShopDiscount/index.js +2 -7
  42. package/dist/solution/VenueBooking/index.d.ts +5 -0
  43. package/dist/solution/VenueBooking/index.js +364 -327
  44. package/dist/utils/task.js +15 -18
  45. package/dist/utils/watch.js +0 -1
  46. package/lib/apis/picoding.js +2 -0
  47. package/lib/core/index.js +130 -131
  48. package/lib/effects/index.js +46 -57
  49. package/lib/index.js +92 -53
  50. package/lib/model/index.js +14 -21
  51. package/lib/model/strategy/adapter/dataVariant/adapter.js +38 -50
  52. package/lib/model/strategy/adapter/dataVariant/evaluator.js +162 -236
  53. package/lib/model/strategy/adapter/dataVariant/examples.js +229 -237
  54. package/lib/model/strategy/adapter/dataVariant/index.js +39 -34
  55. package/lib/model/strategy/adapter/dataVariant/type.js +57 -28
  56. package/lib/model/strategy/adapter/index.js +100 -64
  57. package/lib/model/strategy/adapter/itemRule/adapter.js +156 -144
  58. package/lib/model/strategy/adapter/itemRule/evaluator.js +36 -48
  59. package/lib/model/strategy/adapter/itemRule/examples.js +295 -225
  60. package/lib/model/strategy/adapter/itemRule/index.js +83 -57
  61. package/lib/model/strategy/adapter/itemRule/type.js +97 -36
  62. package/lib/model/strategy/adapter/promotion/adapter.js +84 -63
  63. package/lib/model/strategy/adapter/promotion/evaluator.js +381 -231
  64. package/lib/model/strategy/adapter/promotion/examples.js +139 -159
  65. package/lib/model/strategy/adapter/promotion/index.js +1 -49
  66. package/lib/model/strategy/adapter/promotion/type.js +199 -35
  67. package/lib/model/strategy/adapter/type.js +4 -16
  68. package/lib/model/strategy/adapter/walletPass/evaluator.js +180 -152
  69. package/lib/model/strategy/adapter/walletPass/example.js +217 -190
  70. package/lib/model/strategy/adapter/walletPass/index.js +75 -51
  71. package/lib/model/strategy/adapter/walletPass/locales.js +36 -58
  72. package/lib/model/strategy/adapter/walletPass/type.js +4 -16
  73. package/lib/model/strategy/adapter/walletPass/utils.js +652 -402
  74. package/lib/model/strategy/engine.js +270 -168
  75. package/lib/model/strategy/index.js +49 -34
  76. package/lib/model/strategy/strategy-example.js +243 -239
  77. package/lib/model/strategy/type.js +100 -40
  78. package/lib/modules/Account/index.js +39 -53
  79. package/lib/modules/Account/types.js +20 -33
  80. package/lib/modules/AccountList/index.js +116 -154
  81. package/lib/modules/AccountList/types.js +30 -31
  82. package/lib/modules/AccountList/utils.js +18 -30
  83. package/lib/modules/BaseModule.js +22 -45
  84. package/lib/modules/Cart/index.js +170 -124
  85. package/lib/modules/Cart/types.js +46 -51
  86. package/lib/modules/Cart/utils/cartAccount.js +40 -39
  87. package/lib/modules/Cart/utils/cartDate.js +56 -61
  88. package/lib/modules/Cart/utils/cartDiscount.js +28 -33
  89. package/lib/modules/Cart/utils/cartNote.js +27 -32
  90. package/lib/modules/Cart/utils/cartProduct.js +306 -241
  91. package/lib/modules/Cart/utils/cartRelationForms.js +29 -35
  92. package/lib/modules/Cart/utils/cartResource.js +72 -78
  93. package/lib/modules/Cart/utils/changePrice.js +26 -62
  94. package/lib/modules/Cart/utils/index.js +106 -48
  95. package/lib/modules/Customer/constants.js +13 -34
  96. package/lib/modules/Customer/index.js +216 -153
  97. package/lib/modules/Customer/types.js +38 -35
  98. package/lib/modules/Date/index.js +116 -115
  99. package/lib/modules/Date/types.js +16 -28
  100. package/lib/modules/Date/utils.js +174 -123
  101. package/lib/modules/Discount/index.js +117 -113
  102. package/lib/modules/Discount/types.js +9 -31
  103. package/lib/modules/Guests/index.js +30 -56
  104. package/lib/modules/Guests/types.js +23 -33
  105. package/lib/modules/Holder/index.js +209 -189
  106. package/lib/modules/Holder/types.js +4 -16
  107. package/lib/modules/Holder/utils.js +20 -49
  108. package/lib/modules/OpenData/index.js +63 -74
  109. package/lib/modules/OpenData/types.js +4 -16
  110. package/lib/modules/OpenData/utils.js +44 -78
  111. package/lib/modules/Order/index.d.ts +1 -1
  112. package/lib/modules/Order/index.js +332 -320
  113. package/lib/modules/Order/types.js +24 -33
  114. package/lib/modules/Order/utils.js +350 -284
  115. package/lib/modules/Payment/cash.js +20 -33
  116. package/lib/modules/Payment/cashRecommendationAlgorithm.js +145 -76
  117. package/lib/modules/Payment/eftpos.js +16 -30
  118. package/lib/modules/Payment/index.js +566 -432
  119. package/lib/modules/Payment/mx51.js +1 -0
  120. package/lib/modules/Payment/types.js +216 -107
  121. package/lib/modules/Payment/utils.js +50 -47
  122. package/lib/modules/Payment/walletpass.js +221 -203
  123. package/lib/modules/Product/index.js +51 -46
  124. package/lib/modules/Product/types.js +4 -16
  125. package/lib/modules/Product/utils.js +32 -33
  126. package/lib/modules/ProductList/index.js +111 -110
  127. package/lib/modules/ProductList/types.js +9 -31
  128. package/lib/modules/Quotation/index.js +60 -82
  129. package/lib/modules/Quotation/types.js +4 -16
  130. package/lib/modules/Resource/index.js +27 -59
  131. package/lib/modules/Resource/types.js +22 -32
  132. package/lib/modules/Resource/utils.js +30 -38
  133. package/lib/modules/Rules/index.js +1172 -872
  134. package/lib/modules/Rules/types.js +17 -40
  135. package/lib/modules/SalesSummary/index.js +59 -68
  136. package/lib/modules/SalesSummary/types.js +4 -16
  137. package/lib/modules/SalesSummary/utils.js +176 -193
  138. package/lib/modules/ScanOrderLogger/index.js +59 -79
  139. package/lib/modules/ScanOrderLogger/providers/feishu.js +54 -78
  140. package/lib/modules/ScanOrderLogger/providers/grafana.js +13 -36
  141. package/lib/modules/ScanOrderLogger/types.js +4 -16
  142. package/lib/modules/Schedule/getDateIsInSchedule.js +334 -167
  143. package/lib/modules/Schedule/index.js +97 -107
  144. package/lib/modules/Schedule/type.js +4 -16
  145. package/lib/modules/Schedule/types.js +4 -16
  146. package/lib/modules/Schedule/utils.js +433 -291
  147. package/lib/modules/Step/index.js +40 -52
  148. package/lib/modules/Step/tyeps.js +4 -16
  149. package/lib/modules/Summary/index.js +66 -70
  150. package/lib/modules/Summary/types.js +4 -16
  151. package/lib/modules/Summary/utils.js +782 -429
  152. package/lib/modules/index.js +212 -57
  153. package/lib/plugins/app.js +4 -16
  154. package/lib/plugins/index.js +36 -25
  155. package/lib/plugins/request.js +137 -126
  156. package/lib/plugins/shopStore.js +4 -16
  157. package/lib/plugins/user.js +4 -16
  158. package/lib/plugins/window.js +171 -179
  159. package/lib/server/index.js +882 -623
  160. package/lib/server/modules/index.js +85 -59
  161. package/lib/server/modules/menu/index.js +146 -122
  162. package/lib/server/modules/menu/types.js +18 -31
  163. package/lib/server/modules/order/index.js +195 -231
  164. package/lib/server/modules/order/types.js +107 -32
  165. package/lib/server/modules/order/utils/filterBookings.js +174 -140
  166. package/lib/server/modules/order/utils/filterOrders.js +89 -60
  167. package/lib/server/modules/products/index.js +430 -335
  168. package/lib/server/modules/products/types.js +41 -34
  169. package/lib/server/modules/quotation/index.js +76 -77
  170. package/lib/server/modules/quotation/types.js +21 -31
  171. package/lib/server/modules/resource/index.js +216 -187
  172. package/lib/server/modules/resource/types.js +42 -33
  173. package/lib/server/modules/schedule/index.js +134 -119
  174. package/lib/server/modules/schedule/types.js +14 -21
  175. package/lib/server/modules/schedule/utils.js +334 -163
  176. package/lib/server/types.js +4 -16
  177. package/lib/server/utils/index.js +25 -23
  178. package/lib/server/utils/product.js +196 -139
  179. package/lib/server/utils/schedule.js +34 -41
  180. package/lib/server/utils/time.js +40 -49
  181. package/lib/solution/BookingByStep/index.d.ts +1 -1
  182. package/lib/solution/BookingByStep/index.js +1530 -1343
  183. package/lib/solution/BookingByStep/types.js +40 -99
  184. package/lib/solution/BookingByStep/utils/capacity.js +192 -160
  185. package/lib/solution/BookingByStep/utils/products.js +42 -52
  186. package/lib/solution/BookingByStep/utils/resources.js +527 -330
  187. package/lib/solution/BookingByStep/utils/stock.js +65 -44
  188. package/lib/solution/BookingByStep/utils/timeslots.js +129 -131
  189. package/lib/solution/BookingTicket/index.d.ts +1 -1
  190. package/lib/solution/BookingTicket/index.js +147 -122
  191. package/lib/solution/BookingTicket/types.js +38 -64
  192. package/lib/solution/BookingTicket/utils/scan/cloudSearch.js +64 -80
  193. package/lib/solution/BookingTicket/utils/scan/handleScan.js +82 -80
  194. package/lib/solution/BookingTicket/utils/scan/index.js +90 -94
  195. package/lib/solution/BookingTicket/utils/scan/scanCache.js +90 -43
  196. package/lib/solution/BuyTickets/index.js +67 -70
  197. package/lib/solution/BuyTickets/types.js +22 -38
  198. package/lib/solution/Checkout/index.js +1451 -1158
  199. package/lib/solution/Checkout/types.js +91 -61
  200. package/lib/solution/Checkout/utils/index.js +227 -158
  201. package/lib/solution/PlaceOrder/index.js +55 -0
  202. package/lib/solution/RegisterAndLogin/config.js +493 -460
  203. package/lib/solution/RegisterAndLogin/index.js +633 -630
  204. package/lib/solution/RegisterAndLogin/types.js +189 -76
  205. package/lib/solution/RegisterAndLogin/utils.js +183 -125
  206. package/lib/solution/Sales/index.js +218 -200
  207. package/lib/solution/Sales/types.js +27 -33
  208. package/lib/solution/ScanOrder/index.js +803 -840
  209. package/lib/solution/ScanOrder/types.js +34 -34
  210. package/lib/solution/ScanOrder/utils.js +372 -331
  211. package/lib/solution/ShopDiscount/index.js +230 -236
  212. package/lib/solution/ShopDiscount/types.js +15 -37
  213. package/lib/solution/ShopDiscount/utils.js +309 -172
  214. package/lib/solution/VenueBooking/index.d.ts +5 -0
  215. package/lib/solution/VenueBooking/index.js +886 -929
  216. package/lib/solution/VenueBooking/types.js +20 -40
  217. package/lib/solution/VenueBooking/utils/dateSummary.js +22 -54
  218. package/lib/solution/VenueBooking/utils/resource.js +31 -54
  219. package/lib/solution/VenueBooking/utils/slotMerge.js +88 -106
  220. package/lib/solution/VenueBooking/utils/smartPricing.js +112 -100
  221. package/lib/solution/VenueBooking/utils/timeSlot.js +133 -132
  222. package/lib/solution/VenueBooking/utils.js +130 -67
  223. package/lib/solution/index.js +102 -37
  224. package/lib/store/createStore.js +32 -29
  225. package/lib/types/index.js +4 -16
  226. package/lib/utils/task.js +36 -36
  227. package/lib/utils/watch.js +81 -47
  228. package/package.json +2 -1
@@ -1,122 +1,128 @@
1
- var __create = Object.create;
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __getProtoOf = Object.getPrototypeOf;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, { get: all[name], enumerable: true });
10
- };
11
- var __copyProps = (to, from, except, desc) => {
12
- if (from && typeof from === "object" || typeof from === "function") {
13
- for (let key of __getOwnPropNames(from))
14
- if (!__hasOwnProp.call(to, key) && key !== except)
15
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
- }
17
- return to;
18
- };
19
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
- // If the importer is in node compatibility mode or this is not an ESM
21
- // file that has been converted to a CommonJS file using a Babel-
22
- // compatible transform (i.e. "__esModule" has not been set), then set
23
- // "default" to the CommonJS "module.exports" for node compatibility.
24
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
- mod
26
- ));
27
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
-
29
- // src/model/strategy/adapter/promotion/evaluator.ts
30
- var evaluator_exports = {};
31
- __export(evaluator_exports, {
32
- PromotionEvaluator: () => PromotionEvaluator
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
33
5
  });
34
- module.exports = __toCommonJS(evaluator_exports);
35
- var import_decimal = __toESM(require("decimal.js"));
36
- var import_engine = require("../../engine");
37
- var import_adapter = require("./adapter");
38
- var import_type = require("./type");
39
- var defaultLocales = {
6
+ exports.PromotionEvaluator = void 0;
7
+ var _decimal = _interopRequireDefault(require("decimal.js"));
8
+ var _engine = require("../../engine");
9
+ var _adapter = require("./adapter");
10
+ var _type = require("./type");
11
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
+ // ============================================
13
+ // 类型定义
14
+ // ============================================
15
+
16
+ // ============================================
17
+ // 多语言定义
18
+ // ============================================
19
+
20
+ const defaultLocales = {
40
21
  en: {
41
- no_applicable_promotion: "No applicable promotion",
42
- promotion_not_in_time_range: "Promotion not in valid time range",
43
- product_not_in_promotion: "Product not in promotion scope"
22
+ no_applicable_promotion: 'No applicable promotion',
23
+ promotion_not_in_time_range: 'Promotion not in valid time range',
24
+ product_not_in_promotion: 'Product not in promotion scope'
44
25
  },
45
- "zh-CN": {
46
- no_applicable_promotion: "无适用的促销活动",
47
- promotion_not_in_time_range: "促销活动不在有效时间范围内",
48
- product_not_in_promotion: "商品不在促销范围内"
26
+ 'zh-CN': {
27
+ no_applicable_promotion: '无适用的促销活动',
28
+ promotion_not_in_time_range: '促销活动不在有效时间范围内',
29
+ product_not_in_promotion: '商品不在促销范围内'
49
30
  },
50
- "zh-HK": {
51
- no_applicable_promotion: "無適用的促銷活動",
52
- promotion_not_in_time_range: "促銷活動不在有效時間範圍內",
53
- product_not_in_promotion: "商品不在促銷範圍內"
31
+ 'zh-HK': {
32
+ no_applicable_promotion: '無適用的促銷活動',
33
+ promotion_not_in_time_range: '促銷活動不在有效時間範圍內',
34
+ product_not_in_promotion: '商品不在促銷範圍內'
54
35
  }
55
36
  };
56
- var PromotionEvaluator = class {
37
+
38
+ // ============================================
39
+ // PromotionEvaluator 实现
40
+ // ============================================
41
+
42
+ /**
43
+ * Promotion 评估器
44
+ *
45
+ * 用于评估商品适用的促销活动
46
+ * 封装了 StrategyEngine 和 PromotionAdapter 的调用逻辑
47
+ */
48
+ class PromotionEvaluator {
49
+ engine;
50
+ adapter;
51
+ strategyConfigs = [];
52
+ locale = 'en';
53
+ locales = defaultLocales;
57
54
  constructor() {
58
- this.strategyConfigs = [];
59
- this.locale = "en";
60
- this.locales = defaultLocales;
61
- this.engine = new import_engine.StrategyEngine({
55
+ this.engine = new _engine.StrategyEngine({
62
56
  debug: false,
63
57
  enableTrace: false
64
58
  });
65
- this.adapter = new import_adapter.PromotionAdapter();
59
+ this.adapter = new _adapter.PromotionAdapter();
66
60
  }
61
+
67
62
  // ============================================
68
63
  // 配置管理
69
64
  // ============================================
65
+
70
66
  /**
71
67
  * 设置策略配置列表
72
68
  */
73
69
  setStrategyConfigs(strategyConfigs) {
74
- const newStrategyConfigs = strategyConfigs.filter((item) => item.metadata.type === "promotion");
70
+ const newStrategyConfigs = strategyConfigs.filter(item => item.metadata.type === 'promotion');
75
71
  this.strategyConfigs = newStrategyConfigs;
76
72
  }
73
+
77
74
  /**
78
75
  * 获取策略配置列表
79
76
  */
80
77
  getStrategyConfigs() {
81
78
  return this.strategyConfigs;
82
79
  }
80
+
83
81
  /**
84
82
  * 添加策略配置
85
83
  */
86
84
  addStrategyConfig(strategyConfig) {
87
85
  this.strategyConfigs.push(strategyConfig);
88
86
  }
87
+
89
88
  /**
90
89
  * 设置语言
91
90
  */
92
91
  setLocale(locale) {
93
92
  this.locale = locale;
94
93
  }
94
+
95
95
  /**
96
96
  * 设置自定义多语言文案
97
97
  */
98
98
  setLocales(locales) {
99
- this.locales = { ...defaultLocales, ...locales };
99
+ this.locales = {
100
+ ...defaultLocales,
101
+ ...locales
102
+ };
100
103
  }
104
+
101
105
  /**
102
106
  * 获取多语言文案
103
107
  */
104
108
  getText(key) {
105
- var _a, _b;
106
- return ((_b = (_a = this.locales) == null ? void 0 : _a[this.locale]) == null ? void 0 : _b[key]) || key;
109
+ return this.locales?.[this.locale]?.[key] || key;
107
110
  }
111
+
108
112
  /**
109
113
  * 获取多语言名称
110
114
  */
111
115
  getLocalizedName(name) {
112
- if (typeof name === "string") {
116
+ if (typeof name === 'string') {
113
117
  return name;
114
118
  }
115
- return name[this.locale] || name["en"] || Object.values(name)[0] || "";
119
+ return name[this.locale] || name['en'] || Object.values(name)[0] || '';
116
120
  }
121
+
117
122
  // ============================================
118
123
  // 核心评估方法
119
124
  // ============================================
125
+
120
126
  /**
121
127
  * 评估商品列表
122
128
  *
@@ -127,19 +133,31 @@ var PromotionEvaluator = class {
127
133
  * @returns 每个商品的促销评估结果
128
134
  */
129
135
  evaluateProducts(input) {
130
- const { products, strategyConfigs, channel } = input;
136
+ const {
137
+ products,
138
+ strategyConfigs,
139
+ channel
140
+ } = input;
131
141
  const configs = strategyConfigs || this.strategyConfigs;
132
142
  const results = [];
133
143
  for (const product of products) {
134
144
  const applicablePromotions = [];
145
+
146
+ // 遍历所有策略,检查此商品是否适用
135
147
  for (const config of configs) {
148
+ // 1. 先用 getProductMatchInfo 检查商品是否在策略的适用范围内(包含 bundle 子商品检查)
136
149
  const matchInfo = this.getProductMatchInfo(product, config);
137
150
  if (!matchInfo.isMatch) {
151
+ // 商品不在策略适用范围内,跳过
138
152
  continue;
139
153
  }
154
+
155
+ // 2. 准备上下文(如果是 bundle 子商品匹配,需要用 bundle 子商品的 ID)
140
156
  let contextProduct = product;
141
- if (matchInfo.matchedBundleIndex !== void 0 && product.bundle) {
157
+ if (matchInfo.matchedBundleIndex !== undefined && product.bundle) {
142
158
  const bundleItem = product.bundle[matchInfo.matchedBundleIndex];
159
+ // 创建一个临时的"虚拟商品"用于策略引擎评估
160
+ // 这样策略引擎的 product_match 条件能正确通过
143
161
  contextProduct = {
144
162
  ...product,
145
163
  product_id: bundleItem._bundle_product_id,
@@ -151,6 +169,8 @@ var PromotionEvaluator = class {
151
169
  currentProduct: contextProduct,
152
170
  channel
153
171
  });
172
+
173
+ // 3. 评估策略(检查其他条件如时间范围等)
154
174
  const result = this.engine.evaluate(config, context);
155
175
  if (result.applicable && result.matchedActions.length > 0) {
156
176
  const action = result.matchedActions[0];
@@ -181,6 +201,7 @@ var PromotionEvaluator = class {
181
201
  }
182
202
  return results;
183
203
  }
204
+
184
205
  /**
185
206
  * 评估购物车
186
207
  *
@@ -191,29 +212,42 @@ var PromotionEvaluator = class {
191
212
  * @returns 购物车评估结果
192
213
  */
193
214
  evaluateCart(input) {
194
- const { products, strategyConfigs, channel } = input;
215
+ const {
216
+ products,
217
+ strategyConfigs,
218
+ channel
219
+ } = input;
195
220
  const configs = strategyConfigs || this.strategyConfigs;
221
+
222
+ // 1. 先获取每个商品的促销结果
196
223
  const productResults = this.evaluateProducts(input);
197
- const promotionMap = /* @__PURE__ */ new Map();
224
+
225
+ // 2. 收集所有适用的促销(去重)
226
+ const promotionMap = new Map();
198
227
  for (const productResult of productResults) {
199
228
  for (const promo of productResult.applicablePromotions) {
200
229
  const existing = promotionMap.get(promo.strategyId);
201
230
  const product = productResult.product;
231
+
232
+ // 计算促销数量:如果是 bundle 子商品匹配,数量 = 主商品数量 × 子商品数量
202
233
  let promoQuantity = product.quantity;
203
- if (promo.matchedBundleIndex !== void 0 && product.bundle) {
234
+ if (promo.matchedBundleIndex !== undefined && product.bundle) {
204
235
  const bundleItem = product.bundle[promo.matchedBundleIndex];
205
236
  promoQuantity = product.quantity * (bundleItem.quantity || 1);
206
237
  }
207
238
  if (existing) {
239
+ // 已存在,添加商品到列表
208
240
  existing.applicableProducts.push(product);
209
241
  existing.totalQuantity += promoQuantity;
210
242
  existing.totalAmount += product.price * product.quantity;
243
+ // 记录每个商品的 matchedBundleIndex
211
244
  if (!existing.productMatchedBundleIndexMap) {
212
- existing.productMatchedBundleIndexMap = /* @__PURE__ */ new Map();
245
+ existing.productMatchedBundleIndexMap = new Map();
213
246
  }
214
247
  existing.productMatchedBundleIndexMap.set(product.id, promo.matchedBundleIndex);
215
248
  } else {
216
- const productMatchedBundleIndexMap = /* @__PURE__ */ new Map();
249
+ // 新促销
250
+ const productMatchedBundleIndexMap = new Map();
217
251
  productMatchedBundleIndexMap.set(product.id, promo.matchedBundleIndex);
218
252
  promotionMap.set(promo.strategyId, {
219
253
  ...promo,
@@ -226,30 +260,28 @@ var PromotionEvaluator = class {
226
260
  }
227
261
  }
228
262
  const applicablePromotions = Array.from(promotionMap.values());
229
- const promotionGroups = applicablePromotions.map(
230
- (promo) => {
231
- var _a;
232
- return {
233
- strategyId: promo.strategyId,
234
- strategyName: promo.strategyName,
235
- strategyMetadata: (_a = promo.strategyConfig) == null ? void 0 : _a.metadata,
236
- strategyConfig: promo.strategyConfig,
237
- actionType: promo.actionType,
238
- actionDetail: promo.actionDetail,
239
- products: promo.applicableProducts,
240
- totalQuantity: promo.totalQuantity,
241
- totalAmount: promo.totalAmount,
242
- // 传递 productMatchedBundleIndexMap 到 group
243
- productMatchedBundleIndexMap: promo.productMatchedBundleIndexMap
244
- };
245
- }
246
- );
263
+
264
+ // 3. 按促销分组
265
+ const promotionGroups = applicablePromotions.map(promo => ({
266
+ strategyId: promo.strategyId,
267
+ strategyName: promo.strategyName,
268
+ strategyMetadata: promo.strategyConfig?.metadata,
269
+ strategyConfig: promo.strategyConfig,
270
+ actionType: promo.actionType,
271
+ actionDetail: promo.actionDetail,
272
+ products: promo.applicableProducts,
273
+ totalQuantity: promo.totalQuantity,
274
+ totalAmount: promo.totalAmount,
275
+ // 传递 productMatchedBundleIndexMap 到 group
276
+ productMatchedBundleIndexMap: promo.productMatchedBundleIndexMap
277
+ }));
247
278
  return {
248
279
  productResults,
249
280
  applicablePromotions,
250
281
  promotionGroups
251
282
  };
252
283
  }
284
+
253
285
  /**
254
286
  * 评估购物车并计算定价
255
287
  *
@@ -261,60 +293,70 @@ var PromotionEvaluator = class {
261
293
  * @returns 带定价的购物车评估结果
262
294
  */
263
295
  evaluateCartWithPricing(input) {
264
- const { products } = input;
296
+ const {
297
+ products
298
+ } = input;
299
+
300
+ // 1. 先调用 evaluateCart 获取分组结果
265
301
  const cartResult = this.evaluateCart(input);
266
- const sortedGroups = this.sortPromotionGroupsByPriority(
267
- cartResult.promotionGroups,
268
- cartResult.applicablePromotions
269
- );
270
- const processedQuantityMap = /* @__PURE__ */ new Map();
302
+
303
+ // 2. 按 action priority 排序促销组(从 strategyConfig 获取 priority)
304
+ const sortedGroups = this.sortPromotionGroupsByPriority(cartResult.promotionGroups, cartResult.applicablePromotions);
305
+
306
+ // 3. 追踪已处理的商品(避免重复处理)
307
+ // key: `${product_id}_${product_variant_id}`
308
+ const processedQuantityMap = new Map();
309
+
310
+ // 4. 处理每个促销组,收集结果
271
311
  const pricedProducts = [];
272
312
  const gifts = [];
273
- let totalOriginalAmount = new import_decimal.default(0);
274
- let totalFinalAmount = new import_decimal.default(0);
313
+ let totalOriginalAmount = new _decimal.default(0);
314
+ let totalFinalAmount = new _decimal.default(0);
275
315
  for (const group of sortedGroups) {
276
- const { actionType } = group;
277
- const matchedBundleIndexMap = group.productMatchedBundleIndexMap || /* @__PURE__ */ new Map();
278
- if (actionType === import_type.PROMOTION_ACTION_TYPES.X_ITEMS_FOR_Y_PRICE) {
279
- const result = this.processXItemsForYPrice(
280
- group,
281
- processedQuantityMap,
282
- matchedBundleIndexMap
283
- );
316
+ const {
317
+ actionType
318
+ } = group;
319
+
320
+ // 直接使用 evaluateCart 阶段计算好的 matchedBundleIndexMap
321
+ const matchedBundleIndexMap = group.productMatchedBundleIndexMap || new Map();
322
+ if (actionType === _type.PROMOTION_ACTION_TYPES.X_ITEMS_FOR_Y_PRICE) {
323
+ // X件Y元处理
324
+ const result = this.processXItemsForYPrice(group, processedQuantityMap, matchedBundleIndexMap);
284
325
  pricedProducts.push(...result.products);
285
326
  totalOriginalAmount = totalOriginalAmount.plus(result.originalAmount);
286
327
  totalFinalAmount = totalFinalAmount.plus(result.finalAmount);
287
- } else if (actionType === import_type.PROMOTION_ACTION_TYPES.BUY_X_GET_Y_FREE) {
288
- const result = this.processBuyXGetYFree(
289
- group,
290
- processedQuantityMap,
291
- matchedBundleIndexMap
292
- );
328
+ } else if (actionType === _type.PROMOTION_ACTION_TYPES.BUY_X_GET_Y_FREE) {
329
+ // 买X送Y处理
330
+ const result = this.processBuyXGetYFree(group, processedQuantityMap, matchedBundleIndexMap);
293
331
  pricedProducts.push(...result.products);
294
332
  totalOriginalAmount = totalOriginalAmount.plus(result.originalAmount);
295
333
  totalFinalAmount = totalFinalAmount.plus(result.finalAmount);
334
+
335
+ // 添加赠品信息
296
336
  if (result.giftInfo) {
297
337
  gifts.push(result.giftInfo);
298
338
  }
299
339
  }
300
340
  }
301
- const remainingProducts = this.getRemainingProducts(
302
- products,
303
- processedQuantityMap
304
- );
341
+
342
+ // 5. 处理未参与任何促销的商品
343
+ const remainingProducts = this.getRemainingProducts(products, processedQuantityMap);
305
344
  for (const product of remainingProducts) {
306
- const amount = new import_decimal.default(product.price).mul(product.quantity);
345
+ const amount = new _decimal.default(product.price).mul(product.quantity);
307
346
  totalOriginalAmount = totalOriginalAmount.plus(amount);
308
347
  totalFinalAmount = totalFinalAmount.plus(amount);
309
348
  }
310
349
  pricedProducts.push(...remainingProducts);
350
+
351
+ // 6. 计算总优惠金额
311
352
  const totalDiscount = totalOriginalAmount.minus(totalFinalAmount);
312
353
  return {
313
354
  products: pricedProducts,
314
355
  gifts,
315
- totalDiscount: import_decimal.default.max(0, totalDiscount).toNumber()
356
+ totalDiscount: _decimal.default.max(0, totalDiscount).toNumber()
316
357
  };
317
358
  }
359
+
318
360
  /**
319
361
  * 获取商品适用的促销列表
320
362
  *
@@ -325,13 +367,13 @@ var PromotionEvaluator = class {
325
367
  * @returns 适用的促销列表
326
368
  */
327
369
  getProductPromotions(product, strategyConfigs) {
328
- var _a;
329
370
  const results = this.evaluateProducts({
330
371
  products: [product],
331
372
  strategyConfigs
332
373
  });
333
- return ((_a = results[0]) == null ? void 0 : _a.applicablePromotions) || [];
374
+ return results[0]?.applicablePromotions || [];
334
375
  }
376
+
335
377
  /**
336
378
  * 获取商品的促销标签
337
379
  *
@@ -343,12 +385,13 @@ var PromotionEvaluator = class {
343
385
  */
344
386
  getProductPromotionTags(product, strategyConfigs) {
345
387
  const promotions = this.getProductPromotions(product, strategyConfigs);
346
- return promotions.filter((promo) => promo.display).map((promo) => ({
388
+ return promotions.filter(promo => promo.display).map(promo => ({
347
389
  text: this.getLocalizedName(promo.display.text),
348
390
  type: promo.display.type,
349
391
  strategyId: promo.strategyId
350
392
  }));
351
393
  }
394
+
352
395
  /**
353
396
  * 查找商品适用的策略配置
354
397
  *
@@ -358,10 +401,9 @@ var PromotionEvaluator = class {
358
401
  */
359
402
  findApplicableStrategies(product, strategyConfigs) {
360
403
  const configs = strategyConfigs || this.strategyConfigs;
361
- return configs.filter(
362
- (config) => this.isProductInStrategy(product, config)
363
- );
404
+ return configs.filter(config => this.isProductInStrategy(product, config));
364
405
  }
406
+
365
407
  /**
366
408
  * 批量获取商品列表的适用策略信息
367
409
  *
@@ -373,23 +415,30 @@ var PromotionEvaluator = class {
373
415
  * @returns 每个商品的适用策略完整数据
374
416
  */
375
417
  getProductsApplicableStrategies(input, matchVariant = true) {
376
- const { products, strategyConfigs, channel } = input;
418
+ const {
419
+ products,
420
+ strategyConfigs,
421
+ channel
422
+ } = input;
377
423
  const configs = strategyConfigs || this.strategyConfigs;
378
424
  const results = [];
379
425
  for (const product of products) {
380
426
  const applicableStrategies = [];
427
+
428
+ // 遍历所有策略,检查此商品是否适用
381
429
  for (const config of configs) {
382
430
  let contextProduct = product;
383
431
  if (!matchVariant) {
432
+ // 只匹配 product_id,忽略 product_variant_id
384
433
  const productMatchRule = this.findProductMatchRule(config);
385
434
  if (productMatchRule) {
386
435
  const configProducts = productMatchRule.value;
387
- const matchedConfig = configProducts.find(
388
- (item) => item.product_id === product.product_id
389
- );
436
+ const matchedConfig = configProducts.find(item => item.product_id === product.product_id);
390
437
  if (!matchedConfig) {
438
+ // product_id 不在策略适用范围内,跳过
391
439
  continue;
392
440
  }
441
+ // 用配置中的 variant_id 构造上下文,确保引擎 product_match 通过
393
442
  if (matchedConfig.product_variant_id !== 0 && matchedConfig.product_variant_id !== product.product_variant_id) {
394
443
  contextProduct = {
395
444
  ...product,
@@ -403,6 +452,8 @@ var PromotionEvaluator = class {
403
452
  currentProduct: contextProduct,
404
453
  channel
405
454
  });
455
+
456
+ // 评估策略
406
457
  const result = this.engine.evaluate(config, context);
407
458
  if (result.applicable && result.matchedActions.length > 0) {
408
459
  const action = result.matchedActions[0];
@@ -412,11 +463,11 @@ var PromotionEvaluator = class {
412
463
  channel
413
464
  });
414
465
  if (transformedResult.actionDetail) {
415
- const { requiredQuantity, eligibleProducts } = this.getPromotionRequirements(
416
- action.type,
417
- transformedResult.actionDetail,
418
- config
419
- );
466
+ // 计算所需数量和可参与商品列表
467
+ const {
468
+ requiredQuantity,
469
+ eligibleProducts
470
+ } = this.getPromotionRequirements(action.type, transformedResult.actionDetail, config);
420
471
  applicableStrategies.push({
421
472
  strategyId: config.metadata.id,
422
473
  strategyName: config.metadata.name,
@@ -439,6 +490,7 @@ var PromotionEvaluator = class {
439
490
  }
440
491
  return results;
441
492
  }
493
+
442
494
  /**
443
495
  * 获取促销所需数量和可参与商品列表
444
496
  *
@@ -449,41 +501,50 @@ var PromotionEvaluator = class {
449
501
  */
450
502
  getPromotionRequirements(actionType, actionDetail, config) {
451
503
  let requiredQuantity = 0;
452
- if (actionType === "BUY_X_GET_Y_FREE") {
504
+
505
+ // 根据促销类型获取所需数量
506
+ if (actionType === 'BUY_X_GET_Y_FREE') {
453
507
  const detail = actionDetail;
454
508
  requiredQuantity = detail.buyQuantity || 0;
455
- } else if (actionType === "X_ITEMS_FOR_Y_PRICE") {
509
+ } else if (actionType === 'X_ITEMS_FOR_Y_PRICE') {
456
510
  const detail = actionDetail;
457
511
  requiredQuantity = detail.x || 0;
458
512
  }
513
+
514
+ // 获取可参与商品列表
459
515
  const productMatchRule = this.findProductMatchRule(config);
460
- const eligibleProducts = (productMatchRule == null ? void 0 : productMatchRule.value) || [];
516
+ const eligibleProducts = productMatchRule?.value || [];
461
517
  return {
462
518
  requiredQuantity,
463
519
  eligibleProducts
464
520
  };
465
521
  }
522
+
466
523
  // ============================================
467
524
  // 私有辅助方法
468
525
  // ============================================
526
+
469
527
  /**
470
528
  * 按优先级排序促销组
471
529
  * 优先级从 strategyConfig.actions[0].priority 获取,数值越小优先级越高
472
530
  */
473
531
  sortPromotionGroupsByPriority(groups, promotions) {
474
- var _a, _b;
475
- const priorityMap = /* @__PURE__ */ new Map();
532
+ // 创建策略ID到优先级的映射
533
+ const priorityMap = new Map();
476
534
  for (const promo of promotions) {
477
- const actions = ((_a = promo.strategyConfig) == null ? void 0 : _a.actions) || [];
478
- const priority = ((_b = actions[0]) == null ? void 0 : _b.priority) ?? 999;
535
+ const actions = promo.strategyConfig?.actions || [];
536
+ const priority = actions[0]?.priority ?? 999;
479
537
  priorityMap.set(promo.strategyId, priority);
480
538
  }
539
+
540
+ // 按优先级排序(数值越小优先级越高)
481
541
  return [...groups].sort((a, b) => {
482
542
  const priorityA = priorityMap.get(a.strategyId) ?? 999;
483
543
  const priorityB = priorityMap.get(b.strategyId) ?? 999;
484
544
  return priorityA - priorityB;
485
545
  });
486
546
  }
547
+
487
548
  /**
488
549
  * 处理 X件Y元 促销
489
550
  *
@@ -495,19 +556,32 @@ var PromotionEvaluator = class {
495
556
  * 注意:每个商品按 id 独立处理,不会合并不同 id 的商品
496
557
  */
497
558
  processXItemsForYPrice(group, processedQuantityMap, matchedBundleIndexMap) {
498
- const { strategyId, strategyMetadata, actionDetail, products: groupProducts } = group;
559
+ const {
560
+ strategyId,
561
+ strategyMetadata,
562
+ actionDetail,
563
+ products: groupProducts
564
+ } = group;
499
565
  const detail = actionDetail;
500
- const { x, price: groupPrice, cumulative } = detail;
566
+ const {
567
+ x,
568
+ price: groupPrice,
569
+ cumulative
570
+ } = detail;
501
571
  const result = [];
502
- let originalAmount = new import_decimal.default(0);
503
- let finalAmount = new import_decimal.default(0);
572
+ let originalAmount = new _decimal.default(0);
573
+ let finalAmount = new _decimal.default(0);
574
+
575
+ // 1. 展开所有商品为单件列表(排除已处理的数量)
576
+ // 每个单件记录来自哪个原商品(通过 product.id)
504
577
  const unitProducts = [];
505
578
  for (const product of groupProducts) {
506
579
  const key = this.getProductKey(product);
507
580
  const processedQty = processedQuantityMap.get(key) || 0;
508
581
  const availableQty = product.quantity - processedQty;
509
- if (availableQty <= 0)
510
- continue;
582
+ if (availableQty <= 0) continue;
583
+
584
+ // 展开为单件,每个单件都记录来源商品
511
585
  for (let i = 0; i < availableQty; i++) {
512
586
  unitProducts.push({
513
587
  product,
@@ -518,46 +592,66 @@ var PromotionEvaluator = class {
518
592
  }
519
593
  }
520
594
  if (unitProducts.length === 0) {
521
- return { products: [], originalAmount: 0, finalAmount: 0 };
595
+ return {
596
+ products: [],
597
+ originalAmount: 0,
598
+ finalAmount: 0
599
+ };
522
600
  }
601
+
602
+ // 2. 排序:优先选择来源数量是 x 倍数的商品(可完整凑组不拆分),然后按价格从高到低
523
603
  unitProducts.sort((a, b) => {
604
+ // 优先级1:来源购物车项数量是 x 的倍数的排前面(这些商品可以完整凑组,避免拆分)
524
605
  const aIsMultiple = a.sourceAvailableQty > 0 && a.sourceAvailableQty % x === 0 ? 1 : 0;
525
606
  const bIsMultiple = b.sourceAvailableQty > 0 && b.sourceAvailableQty % x === 0 ? 1 : 0;
526
- if (aIsMultiple !== bIsMultiple)
527
- return bIsMultiple - aIsMultiple;
607
+ if (aIsMultiple !== bIsMultiple) return bIsMultiple - aIsMultiple;
608
+ // 优先级2:按价格从高到低(优先使用高价商品)
528
609
  return b.price - a.price;
529
610
  });
611
+
612
+ // 3. 计算可以凑成多少组
530
613
  const totalUnits = unitProducts.length;
531
614
  const groupCount = cumulative ? Math.floor(totalUnits / x) : totalUnits >= x ? 1 : 0;
532
615
  const promotionUnits = groupCount * x;
616
+
617
+ // 4. 分离参与促销的和不参与的商品
533
618
  const inPromotionUnits = unitProducts.slice(0, promotionUnits);
534
619
  const notInPromotionUnits = unitProducts.slice(promotionUnits);
535
- const promotionQtyByProductId = /* @__PURE__ */ new Map();
620
+
621
+ // 5. 按原商品 id 统计参与促销和不参与的数量
622
+ const promotionQtyByProductId = new Map();
536
623
  for (const unit of inPromotionUnits) {
537
624
  const id = unit.productId;
538
625
  promotionQtyByProductId.set(id, (promotionQtyByProductId.get(id) || 0) + 1);
539
626
  }
540
- const remainingQtyByProductId = /* @__PURE__ */ new Map();
627
+ const remainingQtyByProductId = new Map();
541
628
  for (const unit of notInPromotionUnits) {
542
629
  const id = unit.productId;
543
630
  remainingQtyByProductId.set(id, (remainingQtyByProductId.get(id) || 0) + 1);
544
631
  }
545
- const allocatedPricesByProductId = /* @__PURE__ */ new Map();
632
+
633
+ // 6. 为每个参与促销的单件计算均摊价格
634
+ // 按组内原价比例均摊
635
+ // 记录每个原商品的均摊价格累加(用于后续聚合)
636
+ const allocatedPricesByProductId = new Map();
546
637
  for (let g = 0; g < groupCount; g++) {
547
638
  const startIdx = g * x;
548
639
  const endIdx = startIdx + x;
549
640
  const groupUnits = inPromotionUnits.slice(startIdx, endIdx);
550
- const groupOriginalTotal = groupUnits.reduce(
551
- (sum, u) => sum.plus(u.price),
552
- new import_decimal.default(0)
553
- );
554
- const groupPriceDecimal = new import_decimal.default(groupPrice);
555
- let allocatedSum = new import_decimal.default(0);
641
+
642
+ // 计算组内原价总和
643
+ const groupOriginalTotal = groupUnits.reduce((sum, u) => sum.plus(u.price), new _decimal.default(0));
644
+
645
+ // 按原价比例均摊
646
+ const groupPriceDecimal = new _decimal.default(groupPrice);
647
+ let allocatedSum = new _decimal.default(0);
556
648
  for (let i = 0; i < groupUnits.length; i++) {
557
649
  const unit = groupUnits[i];
558
650
  const isLastUnit = i === groupUnits.length - 1;
559
- const ratio = groupOriginalTotal.eq(0) ? new import_decimal.default(0) : new import_decimal.default(unit.price).div(groupOriginalTotal);
651
+ const ratio = groupOriginalTotal.eq(0) ? new _decimal.default(0) : new _decimal.default(unit.price).div(groupOriginalTotal);
560
652
  const allocatedDecimal = isLastUnit ? groupPriceDecimal.minus(allocatedSum) : groupPriceDecimal.mul(ratio);
653
+
654
+ // 记录这个单件的均摊价格
561
655
  const productId = unit.productId;
562
656
  const prices = allocatedPricesByProductId.get(productId) || [];
563
657
  prices.push(allocatedDecimal);
@@ -565,23 +659,25 @@ var PromotionEvaluator = class {
565
659
  allocatedSum = allocatedSum.plus(allocatedDecimal);
566
660
  }
567
661
  }
662
+
663
+ // 7. 为每个原商品生成结果
568
664
  for (const product of groupProducts) {
569
665
  const key = this.getProductKey(product);
570
666
  const processedQty = processedQuantityMap.get(key) || 0;
571
667
  const availableQty = product.quantity - processedQty;
572
- if (availableQty <= 0)
573
- continue;
668
+ if (availableQty <= 0) continue;
574
669
  const promoQty = promotionQtyByProductId.get(product.id) || 0;
575
670
  const remainingQty = remainingQtyByProductId.get(product.id) || 0;
576
671
  const allocatedPrices = allocatedPricesByProductId.get(product.id) || [];
577
- const matchedBundleIndex = matchedBundleIndexMap == null ? void 0 : matchedBundleIndexMap.get(product.id);
672
+
673
+ // 获取当前商品的 matchedBundleIndex
674
+ const matchedBundleIndex = matchedBundleIndexMap?.get(product.id);
578
675
  if (promoQty > 0 && remainingQty === 0) {
579
- const totalAllocated = allocatedPrices.reduce(
580
- (sum, p) => sum.plus(p),
581
- new import_decimal.default(0)
582
- );
583
- const productOriginalAmount = new import_decimal.default(product.price).mul(promoQty);
584
- const actualTotal = import_decimal.default.min(totalAllocated, productOriginalAmount);
676
+ // 全部参与促销:保留原 id,计算总 finalPrice
677
+ const totalAllocated = allocatedPrices.reduce((sum, p) => sum.plus(p), new _decimal.default(0));
678
+ // 取促销价和原价的最小值,避免促销反而加价
679
+ const productOriginalAmount = new _decimal.default(product.price).mul(promoQty);
680
+ const actualTotal = _decimal.default.min(totalAllocated, productOriginalAmount);
585
681
  const finalPricePerUnit = this.formatPrice(actualTotal.div(promoQty));
586
682
  result.push({
587
683
  ...product,
@@ -596,25 +692,26 @@ var PromotionEvaluator = class {
596
692
  originalAmount = originalAmount.plus(productOriginalAmount);
597
693
  finalAmount = finalAmount.plus(actualTotal);
598
694
  } else if (promoQty === 0 && remainingQty > 0) {
695
+ // 全部不参与促销:保留原 id,finalPrice = 原价
599
696
  result.push({
600
697
  ...product,
601
698
  quantity: remainingQty,
602
699
  finalPrice: this.formatPrice(product.price),
603
- strategyId: void 0,
604
- strategyMetadata: void 0,
700
+ strategyId: undefined,
701
+ strategyMetadata: undefined,
605
702
  inPromotion: false,
606
703
  isSplit: false
607
704
  });
608
- const amount = new import_decimal.default(product.price).mul(remainingQty);
705
+ const amount = new _decimal.default(product.price).mul(remainingQty);
609
706
  originalAmount = originalAmount.plus(amount);
610
707
  finalAmount = finalAmount.plus(amount);
611
708
  } else if (promoQty > 0 && remainingQty > 0) {
612
- const totalAllocated = allocatedPrices.reduce(
613
- (sum, p) => sum.plus(p),
614
- new import_decimal.default(0)
615
- );
616
- const promoOriginalAmount = new import_decimal.default(product.price).mul(promoQty);
617
- const actualTotal = import_decimal.default.min(totalAllocated, promoOriginalAmount);
709
+ // 部分参与促销,部分不参与:需要拆分
710
+ // 参与促销的部分:新 id
711
+ const totalAllocated = allocatedPrices.reduce((sum, p) => sum.plus(p), new _decimal.default(0));
712
+ // 取促销价和原价的最小值,避免促销反而加价
713
+ const promoOriginalAmount = new _decimal.default(product.price).mul(promoQty);
714
+ const actualTotal = _decimal.default.min(totalAllocated, promoOriginalAmount);
618
715
  const finalPricePerUnit = this.formatPrice(actualTotal.div(promoQty));
619
716
  result.push({
620
717
  ...product,
@@ -630,44 +727,50 @@ var PromotionEvaluator = class {
630
727
  });
631
728
  originalAmount = originalAmount.plus(promoOriginalAmount);
632
729
  finalAmount = finalAmount.plus(actualTotal);
730
+
731
+ // 不参与促销的部分:保留原 id
633
732
  result.push({
634
733
  ...product,
635
734
  quantity: remainingQty,
636
735
  finalPrice: this.formatPrice(product.price),
637
- strategyId: void 0,
638
- strategyMetadata: void 0,
736
+ strategyId: undefined,
737
+ strategyMetadata: undefined,
639
738
  inPromotion: false,
640
739
  isSplit: true
641
740
  });
642
- const remainingAmount = new import_decimal.default(product.price).mul(remainingQty);
741
+ const remainingAmount = new _decimal.default(product.price).mul(remainingQty);
643
742
  originalAmount = originalAmount.plus(remainingAmount);
644
743
  finalAmount = finalAmount.plus(remainingAmount);
645
744
  }
745
+
746
+ // 更新已处理数量
646
747
  processedQuantityMap.set(key, processedQty + availableQty);
647
748
  }
749
+
750
+ // 8. 尾差调整:确保促销商品的 finalPrice × quantity 之和精确等于 groupPrice × groupCount
648
751
  if (groupCount > 0) {
649
- const expectedPromoTotal = new import_decimal.default(groupPrice).mul(groupCount);
650
- const promoItems = result.filter((p) => p.inPromotion);
651
- let actualPromoTotal = new import_decimal.default(0);
752
+ const expectedPromoTotal = new _decimal.default(groupPrice).mul(groupCount);
753
+ const promoItems = result.filter(p => p.inPromotion);
754
+ let actualPromoTotal = new _decimal.default(0);
652
755
  for (const p of promoItems) {
653
- actualPromoTotal = actualPromoTotal.plus(
654
- new import_decimal.default(p.finalPrice).mul(p.quantity)
655
- );
756
+ actualPromoTotal = actualPromoTotal.plus(new _decimal.default(p.finalPrice).mul(p.quantity));
656
757
  }
657
758
  const roundingDiff = actualPromoTotal.minus(expectedPromoTotal);
658
759
  if (!roundingDiff.eq(0)) {
659
- const adjustTarget = promoItems.find((p) => p.quantity === 1);
760
+ // 优先找 quantity=1 的促销商品调整(差额直接加减到单价上)
761
+ const adjustTarget = promoItems.find(p => p.quantity === 1);
660
762
  if (adjustTarget) {
661
- adjustTarget.finalPrice = this.formatPrice(
662
- new import_decimal.default(adjustTarget.finalPrice).minus(roundingDiff)
663
- );
763
+ adjustTarget.finalPrice = this.formatPrice(new _decimal.default(adjustTarget.finalPrice).minus(roundingDiff));
664
764
  } else if (promoItems.length > 0) {
765
+ // 没有 quantity=1 的促销商品,从最后一个促销商品中拆出1件做尾差调整
665
766
  const lastPromo = promoItems[promoItems.length - 1];
666
- const adjustedUnitPrice = this.formatPrice(
667
- new import_decimal.default(lastPromo.finalPrice).minus(roundingDiff)
668
- );
767
+ const adjustedUnitPrice = this.formatPrice(new _decimal.default(lastPromo.finalPrice).minus(roundingDiff));
768
+
769
+ // 原商品减少1件
669
770
  lastPromo.quantity -= 1;
670
771
  lastPromo.isSplit = true;
772
+
773
+ // 拆出1件用调整后的价格
671
774
  result.push({
672
775
  ...lastPromo,
673
776
  id: this.generateRandomId(),
@@ -677,11 +780,11 @@ var PromotionEvaluator = class {
677
780
  isSplit: true
678
781
  });
679
782
  }
680
- finalAmount = new import_decimal.default(0);
783
+
784
+ // 重新计算 finalAmount
785
+ finalAmount = new _decimal.default(0);
681
786
  for (const p of result) {
682
- finalAmount = finalAmount.plus(
683
- new import_decimal.default(p.finalPrice).mul(p.quantity)
684
- );
787
+ finalAmount = finalAmount.plus(new _decimal.default(p.finalPrice).mul(p.quantity));
685
788
  }
686
789
  }
687
790
  }
@@ -691,6 +794,7 @@ var PromotionEvaluator = class {
691
794
  finalAmount: finalAmount.toNumber()
692
795
  };
693
796
  }
797
+
694
798
  /**
695
799
  * 处理 买X送Y 促销
696
800
  *
@@ -706,34 +810,56 @@ var PromotionEvaluator = class {
706
810
  products: groupProducts
707
811
  } = group;
708
812
  const detail = actionDetail;
709
- const { buyQuantity, freeQuantity, cumulative, giftProducts } = detail;
813
+ const {
814
+ buyQuantity,
815
+ freeQuantity,
816
+ cumulative,
817
+ giftProducts
818
+ } = detail;
710
819
  const result = [];
711
- let originalAmount = new import_decimal.default(0);
712
- let finalAmount = new import_decimal.default(0);
820
+ let originalAmount = new _decimal.default(0);
821
+ let finalAmount = new _decimal.default(0);
822
+
823
+ // 1. 先计算可用商品总数量(第一次遍历)
824
+ // 对于 bundle 子商品匹配,促销数量 = 主商品数量 × bundle子商品数量
713
825
  let totalPromoQty = 0;
714
- const productInfoMap = /* @__PURE__ */ new Map();
826
+ const productInfoMap = new Map();
715
827
  for (const product of groupProducts) {
716
828
  const key = this.getProductKey(product);
717
829
  const processedQty = processedQuantityMap.get(key) || 0;
718
830
  const availableQty = product.quantity - processedQty;
719
- if (availableQty <= 0)
720
- continue;
721
- const matchedBundleIndex = matchedBundleIndexMap == null ? void 0 : matchedBundleIndexMap.get(product.id);
831
+ if (availableQty <= 0) continue;
832
+
833
+ // 获取 matchedBundleIndex
834
+ const matchedBundleIndex = matchedBundleIndexMap?.get(product.id);
835
+
836
+ // 计算用于促销的数量
722
837
  let promoQty = availableQty;
723
- if (matchedBundleIndex !== void 0 && product.bundle) {
838
+ if (matchedBundleIndex !== undefined && product.bundle) {
724
839
  const bundleItem = product.bundle[matchedBundleIndex];
840
+ // 促销数量 = 主商品数量 × bundle子商品数量
725
841
  promoQty = availableQty * (bundleItem.quantity || 1);
726
842
  }
727
843
  totalPromoQty += promoQty;
728
- productInfoMap.set(key, { availableQty, promoQty, matchedBundleIndex });
844
+ productInfoMap.set(key, {
845
+ availableQty,
846
+ promoQty,
847
+ matchedBundleIndex
848
+ });
729
849
  }
850
+
851
+ // 2. 判断是否满足促销条件
730
852
  const isFulfilled = totalPromoQty >= buyQuantity;
853
+
854
+ // 3. 生成商品结果(第二次遍历)
731
855
  for (const product of groupProducts) {
732
856
  const key = this.getProductKey(product);
733
857
  const info = productInfoMap.get(key);
734
- if (!info || info.availableQty <= 0)
735
- continue;
858
+ if (!info || info.availableQty <= 0) continue;
736
859
  const processedQty = processedQuantityMap.get(key) || 0;
860
+
861
+ // 商品本身价格不变,只是触发赠品
862
+ // inPromotion 只有在满足条件时才为 true
737
863
  result.push({
738
864
  ...product,
739
865
  quantity: info.availableQty,
@@ -745,15 +871,21 @@ var PromotionEvaluator = class {
745
871
  isSplit: info.availableQty < product.quantity,
746
872
  matchedBundleIndex: info.matchedBundleIndex
747
873
  });
748
- const amount = new import_decimal.default(product.price).mul(info.availableQty);
874
+ const amount = new _decimal.default(product.price).mul(info.availableQty);
749
875
  originalAmount = originalAmount.plus(amount);
750
876
  finalAmount = finalAmount.plus(amount);
877
+
878
+ // 更新已处理数量(使用主商品数量)
751
879
  processedQuantityMap.set(key, processedQty + info.availableQty);
752
880
  }
881
+
882
+ // 4. 计算赠品数量(使用促销数量)
753
883
  let giftCount = 0;
754
884
  if (isFulfilled) {
755
885
  giftCount = cumulative ? Math.floor(totalPromoQty / buyQuantity) * freeQuantity : freeQuantity;
756
886
  }
887
+
888
+ // 5. 构建赠品信息
757
889
  const giftInfo = giftCount > 0 ? {
758
890
  strategyId,
759
891
  strategyName,
@@ -767,6 +899,7 @@ var PromotionEvaluator = class {
767
899
  giftInfo
768
900
  };
769
901
  }
902
+
770
903
  /**
771
904
  * 获取未参与任何促销的商品
772
905
  */
@@ -781,7 +914,7 @@ var PromotionEvaluator = class {
781
914
  ...product,
782
915
  quantity: remainingQty,
783
916
  finalPrice: this.formatPrice(product.price),
784
- strategyId: void 0,
917
+ strategyId: undefined,
785
918
  inPromotion: false,
786
919
  isSplit: remainingQty < product.quantity
787
920
  });
@@ -789,12 +922,14 @@ var PromotionEvaluator = class {
789
922
  }
790
923
  return result;
791
924
  }
925
+
792
926
  /**
793
927
  * 价格统一保留两位小数
794
928
  */
795
929
  formatPrice(value) {
796
- return new import_decimal.default(value).toDecimalPlaces(2, import_decimal.default.ROUND_HALF_UP).toNumber();
930
+ return new _decimal.default(value).toDecimalPlaces(2, _decimal.default.ROUND_HALF_UP).toNumber();
797
931
  }
932
+
798
933
  /**
799
934
  * 获取商品唯一标识 key
800
935
  * 使用商品的 id 作为唯一标识,不同 id 的商品不会被合并
@@ -802,92 +937,107 @@ var PromotionEvaluator = class {
802
937
  getProductKey(product) {
803
938
  return String(product.id);
804
939
  }
940
+
805
941
  /**
806
942
  * 生成随机ID
807
943
  */
808
944
  generateRandomId() {
809
- return Math.floor(Math.random() * 1e9) + Date.now();
945
+ return Math.floor(Math.random() * 1000000000) + Date.now();
810
946
  }
947
+
811
948
  /**
812
949
  * 商品匹配结果信息
813
950
  */
814
951
  getProductMatchInfo(product, config) {
815
952
  const productMatchRule = this.findProductMatchRule(config);
816
953
  if (!productMatchRule) {
817
- return { isMatch: true };
954
+ // 没有商品范围条件,默认适用所有商品(匹配主商品)
955
+ return {
956
+ isMatch: true
957
+ };
818
958
  }
819
959
  const configProducts = productMatchRule.value;
960
+
961
+ // 1. 先检查主商品是否匹配
820
962
  if (this.isProductIdMatch(product.product_id, product.product_variant_id, configProducts)) {
821
- return { isMatch: true };
963
+ return {
964
+ isMatch: true
965
+ };
822
966
  }
967
+
968
+ // 2. 主商品不匹配,检查 bundle 子商品
823
969
  if (product.bundle && product.bundle.length > 0) {
824
970
  for (let i = 0; i < product.bundle.length; i++) {
825
971
  const bundleItem = product.bundle[i];
826
- if (this.isProductIdMatch(
827
- bundleItem._bundle_product_id,
828
- bundleItem.product_variant_id,
829
- configProducts
830
- )) {
831
- return { isMatch: true, matchedBundleIndex: i };
972
+ if (this.isProductIdMatch(bundleItem._bundle_product_id, bundleItem.product_variant_id, configProducts)) {
973
+ return {
974
+ isMatch: true,
975
+ matchedBundleIndex: i
976
+ };
832
977
  }
833
978
  }
834
979
  }
835
- return { isMatch: false };
980
+ return {
981
+ isMatch: false
982
+ };
836
983
  }
984
+
837
985
  /**
838
986
  * 检查商品是否在策略的适用范围内
839
987
  */
840
988
  isProductInStrategy(product, config) {
841
989
  return this.getProductMatchInfo(product, config).isMatch;
842
990
  }
991
+
843
992
  /**
844
993
  * 查找商品匹配规则
845
994
  */
846
995
  findProductMatchRule(config) {
847
- var _a;
848
- if (!((_a = config == null ? void 0 : config.conditions) == null ? void 0 : _a.rules)) {
996
+ if (!config?.conditions?.rules) {
849
997
  return null;
850
998
  }
851
- return config.conditions.rules.find(
852
- (rule) => rule.field === "productIdAndVariantId" && (rule.operator === "product_match" || rule.operator === "object_in")
853
- );
999
+ return config.conditions.rules.find(rule => rule.field === 'productIdAndVariantId' && (rule.operator === 'product_match' || rule.operator === 'object_in'));
854
1000
  }
1001
+
855
1002
  /**
856
1003
  * 检查商品ID是否匹配配置列表
857
1004
  */
858
1005
  isProductIdMatch(productId, productVariantId, configProducts) {
859
- return configProducts.some((config) => {
1006
+ return configProducts.some(config => {
1007
+ // 首先检查 product_id
860
1008
  if (config.product_id !== productId) {
861
1009
  return false;
862
1010
  }
1011
+
1012
+ // 如果配置的 product_variant_id = 0,只需要 product_id 匹配
863
1013
  if (config.product_variant_id === 0) {
864
1014
  return true;
865
1015
  }
1016
+
1017
+ // 否则需要 product_variant_id 精确匹配
866
1018
  return config.product_variant_id === productVariantId;
867
1019
  });
868
1020
  }
1021
+
869
1022
  /**
870
1023
  * 检查商品是否匹配配置(兼容旧方法)
871
1024
  */
872
1025
  isProductMatch(product, configProducts) {
873
1026
  return this.isProductIdMatch(product.product_id, product.product_variant_id, configProducts);
874
1027
  }
1028
+
875
1029
  /**
876
1030
  * 获取展示配置
877
1031
  */
878
1032
  getDisplayConfig(config) {
879
- var _a;
880
1033
  const custom = config.metadata.custom;
881
- if ((_a = custom == null ? void 0 : custom.display) == null ? void 0 : _a.product_card) {
1034
+ if (custom?.display?.product_card) {
882
1035
  return {
883
1036
  text: custom.display.product_card.text,
884
1037
  type: custom.display.product_card.type
885
1038
  };
886
1039
  }
887
- return void 0;
1040
+ return undefined;
888
1041
  }
889
- };
890
- // Annotate the CommonJS export names for ESM import in node:
891
- 0 && (module.exports = {
892
- PromotionEvaluator
893
- });
1042
+ }
1043
+ exports.PromotionEvaluator = PromotionEvaluator;