@pisell/pisellos 0.10.42 → 0.10.44

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 (101) hide show
  1. package/dist/model/strategy/adapter/itemRule/adapter.d.ts +4 -1
  2. package/dist/model/strategy/adapter/itemRule/adapter.js +135 -35
  3. package/dist/model/strategy/adapter/itemRule/evaluator.d.ts +8 -1
  4. package/dist/model/strategy/adapter/itemRule/evaluator.js +25 -1
  5. package/dist/model/strategy/adapter/itemRule/type.d.ts +44 -0
  6. package/dist/model/strategy/adapter/itemRule/type.js +19 -1
  7. package/dist/model/strategy/adapter/promotion/index.js +0 -9
  8. package/dist/modules/Order/index.d.ts +40 -8
  9. package/dist/modules/Order/index.js +1438 -878
  10. package/dist/modules/Order/salesNotes.d.ts +31 -0
  11. package/dist/modules/Order/salesNotes.js +492 -0
  12. package/dist/modules/Order/types.d.ts +105 -8
  13. package/dist/modules/Order/types.js +6 -1
  14. package/dist/modules/Order/utils.d.ts +3 -1
  15. package/dist/modules/Order/utils.js +191 -71
  16. package/dist/modules/Payment/cashRecommendationAlgorithm.d.ts +4 -5
  17. package/dist/modules/Payment/cashRecommendationAlgorithm.js +17 -367
  18. package/dist/modules/ProductList/index.d.ts +2 -1
  19. package/dist/modules/ProductList/index.js +100 -20
  20. package/dist/modules/ProductList/types.d.ts +10 -0
  21. package/dist/modules/Rules/index.js +2 -1
  22. package/dist/server/index.d.ts +7 -0
  23. package/dist/server/index.js +45 -3
  24. package/dist/server/modules/order/index.js +6 -0
  25. package/dist/server/modules/order/types.d.ts +9 -0
  26. package/dist/server/utils/small-ticket.d.ts +1 -0
  27. package/dist/server/utils/small-ticket.js +142 -14
  28. package/dist/solution/BaseSales/index.d.ts +15 -1
  29. package/dist/solution/BaseSales/index.js +1240 -877
  30. package/dist/solution/BookingByStep/index.d.ts +1 -1
  31. package/dist/solution/BookingTicket/index.d.ts +1 -0
  32. package/dist/solution/BookingTicket/index.js +37 -16
  33. package/dist/solution/BookingTicket/types.d.ts +5 -1
  34. package/dist/solution/BookingTicket/utils/orderCustomer.js +3 -2
  35. package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.js +171 -96
  36. package/dist/solution/BookingTicket/utils/scan/cloudSearch.js +1 -1
  37. package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +2 -0
  38. package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.js +12 -3
  39. package/dist/solution/RegisterAndLogin/config.d.ts +9 -3
  40. package/dist/solution/RegisterAndLogin/config.js +95 -40
  41. package/dist/solution/RegisterAndLogin/index.js +4 -1
  42. package/dist/solution/ScanOrder/index.d.ts +1 -0
  43. package/dist/solution/ScanOrder/index.js +31 -27
  44. package/dist/solution/ScanOrder/utils.d.ts +1 -0
  45. package/dist/solution/ScanOrder/utils.js +2 -0
  46. package/dist/solution/UnifiedBookingSales/index.d.ts +15 -2
  47. package/dist/solution/UnifiedBookingSales/index.js +777 -510
  48. package/dist/solution/UnifiedBookingSales/types.d.ts +14 -1
  49. package/dist/solution/VenueBooking/index.d.ts +1 -0
  50. package/dist/solution/VenueBooking/index.js +8 -4
  51. package/lib/model/strategy/adapter/itemRule/adapter.d.ts +4 -1
  52. package/lib/model/strategy/adapter/itemRule/adapter.js +73 -4
  53. package/lib/model/strategy/adapter/itemRule/evaluator.d.ts +8 -1
  54. package/lib/model/strategy/adapter/itemRule/evaluator.js +23 -1
  55. package/lib/model/strategy/adapter/itemRule/type.d.ts +44 -0
  56. package/lib/model/strategy/adapter/itemRule/type.js +19 -1
  57. package/lib/model/strategy/adapter/promotion/index.js +1 -46
  58. package/lib/modules/Order/index.d.ts +40 -8
  59. package/lib/modules/Order/index.js +434 -61
  60. package/lib/modules/Order/salesNotes.d.ts +31 -0
  61. package/lib/modules/Order/salesNotes.js +382 -0
  62. package/lib/modules/Order/types.d.ts +105 -8
  63. package/lib/modules/Order/types.js +5 -1
  64. package/lib/modules/Order/utils.d.ts +3 -1
  65. package/lib/modules/Order/utils.js +114 -1
  66. package/lib/modules/Payment/cashRecommendationAlgorithm.d.ts +4 -5
  67. package/lib/modules/Payment/cashRecommendationAlgorithm.js +9 -354
  68. package/lib/modules/ProductList/index.d.ts +2 -1
  69. package/lib/modules/ProductList/index.js +47 -2
  70. package/lib/modules/ProductList/types.d.ts +10 -0
  71. package/lib/modules/Rules/index.js +2 -1
  72. package/lib/server/index.d.ts +7 -0
  73. package/lib/server/index.js +39 -3
  74. package/lib/server/modules/order/index.js +6 -0
  75. package/lib/server/modules/order/types.d.ts +9 -0
  76. package/lib/server/utils/small-ticket.d.ts +1 -0
  77. package/lib/server/utils/small-ticket.js +136 -12
  78. package/lib/solution/BaseSales/index.d.ts +15 -1
  79. package/lib/solution/BaseSales/index.js +192 -22
  80. package/lib/solution/BookingByStep/index.d.ts +1 -1
  81. package/lib/solution/BookingTicket/index.d.ts +1 -0
  82. package/lib/solution/BookingTicket/index.js +23 -5
  83. package/lib/solution/BookingTicket/types.d.ts +5 -1
  84. package/lib/solution/BookingTicket/utils/orderCustomer.js +2 -1
  85. package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.js +65 -18
  86. package/lib/solution/BookingTicket/utils/scan/cloudSearch.js +1 -1
  87. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +2 -0
  88. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.js +5 -1
  89. package/lib/solution/RegisterAndLogin/config.d.ts +9 -3
  90. package/lib/solution/RegisterAndLogin/config.js +49 -8
  91. package/lib/solution/RegisterAndLogin/index.js +4 -1
  92. package/lib/solution/ScanOrder/index.d.ts +1 -0
  93. package/lib/solution/ScanOrder/index.js +5 -1
  94. package/lib/solution/ScanOrder/utils.d.ts +1 -0
  95. package/lib/solution/ScanOrder/utils.js +1 -0
  96. package/lib/solution/UnifiedBookingSales/index.d.ts +15 -2
  97. package/lib/solution/UnifiedBookingSales/index.js +125 -1
  98. package/lib/solution/UnifiedBookingSales/types.d.ts +14 -1
  99. package/lib/solution/VenueBooking/index.d.ts +1 -0
  100. package/lib/solution/VenueBooking/index.js +5 -1
  101. package/package.json +1 -1
@@ -11,6 +11,7 @@ Object.defineProperty(exports, "buildManualProductDiscountItem", {
11
11
  });
12
12
  exports.buildPointCardSnapshotFromWalletPassList = buildPointCardSnapshotFromWalletPassList;
13
13
  exports.buildSubmitPayload = buildSubmitPayload;
14
+ exports.buildVirtualCurrencySnapshotFromList = buildVirtualCurrencySnapshotFromList;
14
15
  exports.calculateOrderProductsDeposit = calculateOrderProductsDeposit;
15
16
  exports.clearTempOrderHolderAssignments = clearTempOrderHolderAssignments;
16
17
  exports.composeLinePrice = composeLinePrice;
@@ -74,6 +75,7 @@ exports.normalizePointCardSnapshotItem = normalizePointCardSnapshotItem;
74
75
  exports.normalizeProductSkuOptions = normalizeProductSkuOptions;
75
76
  exports.normalizeSubmitBooking = normalizeSubmitBooking;
76
77
  exports.normalizeSubmitCollectPaxValue = normalizeSubmitCollectPaxValue;
78
+ exports.normalizeVirtualCurrencySnapshotItem = normalizeVirtualCurrencySnapshotItem;
77
79
  exports.productRequiresFormSubject = productRequiresFormSubject;
78
80
  exports.resolveEffectivePerUnitDiscount = resolveEffectivePerUnitDiscount;
79
81
  exports.resolveIsFormSubject = resolveIsFormSubject;
@@ -129,9 +131,11 @@ var _decimal = _interopRequireDefault(require("decimal.js"));
129
131
  var _utils = require("../Holder/utils");
130
132
  var _utils2 = require("../../solution/ScanOrder/utils");
131
133
  var _orderCollectionIdentity = require("./utils/orderCollectionIdentity");
134
+ var _salesNotes = require("./salesNotes");
132
135
  var _manualProductDiscount = require("./utils/manualProductDiscount");
133
136
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
134
137
  const POINT_CARD_SNAPSHOT_TAG = 'point_card';
138
+ const VIRTUAL_CURRENCY_SNAPSHOT_TAG = 'virtual_currency';
135
139
  function isPlainObject(value) {
136
140
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
137
141
  }
@@ -169,6 +173,101 @@ function buildPointCardSnapshotFromWalletPassList(value) {
169
173
  return result;
170
174
  }, []);
171
175
  }
176
+ function normalizeSnapshotNameMap(value) {
177
+ if (typeof value === 'string' || typeof value === 'number') {
178
+ const normalizedText = String(value).trim();
179
+ return normalizedText ? {
180
+ original: normalizedText
181
+ } : {};
182
+ }
183
+ if (!isPlainObject(value)) return {};
184
+ return Object.entries(value).reduce((result, [locale, text]) => {
185
+ if (!locale.trim()) return result;
186
+ if (typeof text !== 'string' && typeof text !== 'number') return result;
187
+ const normalizedText = String(text).trim();
188
+ if (normalizedText) result[locale] = normalizedText;
189
+ return result;
190
+ }, {});
191
+ }
192
+ function buildVirtualCurrencySnapshotName(value) {
193
+ if (!isPlainObject(value)) return {};
194
+ const amountFormat = String(value.amount_format ?? '').trim();
195
+ let configuredName;
196
+ if (amountFormat === '{{amount}} {{unit}}') {
197
+ configuredName = value.unit_label;
198
+ } else if (amountFormat === '{{amount}} {{sym}}') {
199
+ configuredName = value.symbol;
200
+ } else {
201
+ return {};
202
+ }
203
+ const configuredNameMap = normalizeSnapshotNameMap(configuredName);
204
+ const shortNameMap = normalizeSnapshotNameMap(value.short_name);
205
+
206
+ // The configured unit/symbol is resolved before short_name. If it already
207
+ // carries a global fallback, keep it authoritative so (for example)
208
+ // short_name.en cannot mask unit_label.original. When it only has explicit
209
+ // locale entries, merge short_name so other receipt locales can still fall
210
+ // back after the configured value has no match.
211
+ const hasConfiguredGlobalFallback = ['original', 'auto', 'default', 'en'].some(locale => Boolean(configuredNameMap[locale]));
212
+ if (hasConfiguredGlobalFallback) return configuredNameMap;
213
+ return {
214
+ ...shortNameMap,
215
+ ...configuredNameMap
216
+ };
217
+ }
218
+ function normalizeVirtualCurrencySnapshotItem(value) {
219
+ if (!isPlainObject(value)) return null;
220
+ const id = normalizeSnapshotScalar(value.id);
221
+ if (id === undefined) return null;
222
+ if (String(value.tag || '').trim().toLowerCase() !== VIRTUAL_CURRENCY_SNAPSHOT_TAG) {
223
+ return null;
224
+ }
225
+ const snapshot = {
226
+ id,
227
+ tag: 'virtual_currency',
228
+ product_name: String(value.product_name ?? ''),
229
+ par_value: String(value.par_value ?? ''),
230
+ used_par_value: String(value.used_par_value ?? ''),
231
+ name: normalizeSnapshotNameMap(value.name)
232
+ };
233
+ const productId = normalizeSnapshotScalar(value.product_id);
234
+ if (productId !== undefined) {
235
+ snapshot.product_id = productId;
236
+ }
237
+ return snapshot;
238
+ }
239
+ function normalizeVirtualCurrencySnapshotList(value) {
240
+ if (!Array.isArray(value)) return [];
241
+ return value.reduce((result, item) => {
242
+ const normalized = normalizeVirtualCurrencySnapshotItem(item);
243
+ if (normalized) result.push(normalized);
244
+ return result;
245
+ }, []);
246
+ }
247
+ function buildVirtualCurrencySnapshotFromList(value) {
248
+ if (!Array.isArray(value)) return [];
249
+ return value.reduce((result, item) => {
250
+ if (!isPlainObject(item)) return result;
251
+ const id = normalizeSnapshotScalar(item.id);
252
+ if (id === undefined) return result;
253
+ const sourceTag = String(item.tag ?? '').trim().toLowerCase();
254
+ if (sourceTag !== VIRTUAL_CURRENCY_SNAPSHOT_TAG) return result;
255
+ const snapshot = {
256
+ id,
257
+ tag: 'virtual_currency',
258
+ product_name: String(item.product_name ?? ''),
259
+ par_value: String(item.par_value ?? ''),
260
+ used_par_value: String(item.used_par_value ?? ''),
261
+ name: buildVirtualCurrencySnapshotName(item.virtual_currency_display)
262
+ };
263
+ const productId = normalizeSnapshotScalar(item.product_id);
264
+ if (productId !== undefined) {
265
+ snapshot.product_id = productId;
266
+ }
267
+ result.push(snapshot);
268
+ return result;
269
+ }, []);
270
+ }
172
271
  function normalizeOrderMetaList(value) {
173
272
  if (value === undefined) return undefined;
174
273
  if (!isPlainObject(value)) return undefined;
@@ -176,6 +275,9 @@ function normalizeOrderMetaList(value) {
176
275
  if (Object.prototype.hasOwnProperty.call(value, 'point_cards_snapshot')) {
177
276
  normalized.point_cards_snapshot = buildPointCardSnapshotFromWalletPassList(value.point_cards_snapshot);
178
277
  }
278
+ if (Object.prototype.hasOwnProperty.call(value, 'virtual_currency_snapshot')) {
279
+ normalized.virtual_currency_snapshot = normalizeVirtualCurrencySnapshotList(value.virtual_currency_snapshot);
280
+ }
179
281
  if (Object.keys(normalized).length > 0) return normalized;
180
282
  return {};
181
283
  }
@@ -1225,6 +1327,7 @@ function createDefaultTempOrder(params) {
1225
1327
  request_unique_idempotency_token: '',
1226
1328
  vouchers: [],
1227
1329
  products: [],
1330
+ notes: [],
1228
1331
  bookings: [],
1229
1332
  payments: [],
1230
1333
  surcharges: [],
@@ -1234,7 +1337,11 @@ function createDefaultTempOrder(params) {
1234
1337
  holder: null,
1235
1338
  metadata: {},
1236
1339
  _extend: {
1237
- productsByUid: {}
1340
+ productsByUid: {},
1341
+ salesNotesState: {
1342
+ loaded: true,
1343
+ dirty: false
1344
+ }
1238
1345
  }
1239
1346
  };
1240
1347
  }
@@ -1318,6 +1425,7 @@ function buildSubmitPayload(params) {
1318
1425
  create_date: _createDate,
1319
1426
  holder_id: _holderId,
1320
1427
  payment_status: _paymentStatus,
1428
+ notes: _notes,
1321
1429
  ...tempOrderRest
1322
1430
  } = tempOrder;
1323
1431
  const resolvedBusinessCode = businessCode ?? tempOrder.business_code;
@@ -1381,6 +1489,11 @@ function buildSubmitPayload(params) {
1381
1489
  ...rest
1382
1490
  };
1383
1491
  })(),
1492
+ ...(tempOrder._extend?.salesNotesState?.dirty ? {
1493
+ notes: (0, _salesNotes.encodeSalesNotes)(tempOrder.notes || [], {
1494
+ allowUnresolvedOrderTarget: true
1495
+ })
1496
+ } : {}),
1384
1497
  products: (tempOrder.products || []).map(product => normalizeSubmitProduct(product, {
1385
1498
  virtualSettlementMode
1386
1499
  })),
@@ -2,10 +2,9 @@
2
2
  * 现金支付推荐算法
3
3
  *
4
4
  * 核心原理:
5
- * 1. 每个推荐金额都应该是独立的最优组合
6
- * 2. 不推荐在已有最优解基础上添加额外面额的组合
7
- * 3. 优先推荐使用不同数量币种的组合
8
- * 4. 根据组合判断去重,避免扩展组合
5
+ * 1. 第一项始终是精确应付金额
6
+ * 2. 其余推荐只能是严格大于应付金额的真实币种面额
7
+ * 3. 最多推荐三个更高面额
9
8
  */
10
9
  /**
11
10
  * 常见国家货币面额配置
@@ -13,7 +12,7 @@
13
12
  export declare const CURRENCY_DENOMINATIONS: Record<string, number[]>;
14
13
  /**
15
14
  * 最优支付金额推荐算法
16
- * 推荐通过不同数量面额组合刚好足够支付的最小金额
15
+ * 返回精确应付金额,以及最多三个更高的真实币种面额
17
16
  *
18
17
  * @param targetAmount 目标金额
19
18
  * @param denominations 币种面值数组
@@ -9,10 +9,9 @@ exports.recommendOptimalPayments = recommendOptimalPayments;
9
9
  * 现金支付推荐算法
10
10
  *
11
11
  * 核心原理:
12
- * 1. 每个推荐金额都应该是独立的最优组合
13
- * 2. 不推荐在已有最优解基础上添加额外面额的组合
14
- * 3. 优先推荐使用不同数量币种的组合
15
- * 4. 根据组合判断去重,避免扩展组合
12
+ * 1. 第一项始终是精确应付金额
13
+ * 2. 其余推荐只能是严格大于应付金额的真实币种面额
14
+ * 3. 最多推荐三个更高面额
16
15
  */
17
16
 
18
17
  /**
@@ -48,364 +47,20 @@ const CURRENCY_DENOMINATIONS = exports.CURRENCY_DENOMINATIONS = {
48
47
  'DEFAULT': [100, 50, 20, 10, 5, 1, 0.5, 0.25, 0.1, 0.05, 0.01]
49
48
  };
50
49
 
51
- /**
52
- * 支付组合类型定义
53
- */
54
-
55
50
  /**
56
51
  * 最优支付金额推荐算法
57
- * 推荐通过不同数量面额组合刚好足够支付的最小金额
52
+ * 返回精确应付金额,以及最多三个更高的真实币种面额
58
53
  *
59
54
  * @param targetAmount 目标金额
60
55
  * @param denominations 币种面值数组
61
56
  * @returns 推荐支付金额数组
62
57
  */
63
58
  function recommendOptimalPayments(targetAmount, denominations) {
64
- // 参数验证
65
- if (targetAmount <= 0 || !isFinite(targetAmount) || isNaN(targetAmount)) {
59
+ if (targetAmount <= 0 || !Number.isFinite(targetAmount)) {
66
60
  return [];
67
61
  }
68
- if (!denominations || !Array.isArray(denominations) || denominations.length === 0) {
69
- return [Math.ceil(targetAmount)];
70
- }
71
-
72
- // 过滤有效面额并限制搜索空间以提高性能
73
- const maxReasonableDenom = targetAmount * 5; // 适度限制搜索空间
74
- const validDenoms = denominations.filter(denom => denom > 0 && isFinite(denom) && !isNaN(denom) && denom <= maxReasonableDenom).slice(0, 8); // 限制面额数量以提高性能
75
-
76
- if (validDenoms.length === 0) {
77
- // 如果没有合适的面额,返回最接近的整数金额
78
- return [Math.ceil(targetAmount)];
79
- }
80
- try {
81
- // 处理精度问题,将金额转换为整数计算
82
- const precision = 100; // 假设最小单位是0.01
83
- const target = Math.round(targetAmount * precision);
84
- const denoms = validDenoms.map(d => Math.round(d * precision)).sort((a, b) => b - a);
85
-
86
- // 存储不同的支付组合方案
87
- const paymentOptions = [];
88
-
89
- // 首先尝试找到精确匹配的组合
90
- const exactCombination = findExactCombination(target, denoms, precision);
91
- if (exactCombination) {
92
- paymentOptions.push(exactCombination);
93
- }
94
-
95
- // 为了提高效率,我们将按币种数量来生成组合
96
- // 1币种组合:只用一种面额
97
- for (let i = 0; i < Math.min(denoms.length, 6); i++) {
98
- const denom = denoms[i];
99
- const count = Math.ceil(target / denom);
100
- const sum = count * denom;
101
- if (sum >= target && count <= 12) {
102
- // 限制硬币数量以提高性能
103
- paymentOptions.push({
104
- combination: Array(count).fill(denom / precision),
105
- sum: sum / precision,
106
- coinCount: count,
107
- denomTypes: 1
108
- });
109
- }
110
- }
111
-
112
- // 2币种组合:使用两种面额(大幅限制搜索范围以提高性能)
113
- for (let i = 0; i < Math.min(denoms.length - 1, 5); i++) {
114
- for (let j = i + 1; j < Math.min(denoms.length, 6); j++) {
115
- const denom1 = denoms[i];
116
- const denom2 = denoms[j];
117
-
118
- // 大幅限制搜索范围,基于目标金额动态调整
119
- const maxCount1 = Math.min(Math.ceil(target / denom1) + 1, 8);
120
- const maxCount2 = Math.min(Math.ceil(target / denom2) + 1, 10);
121
- for (let count1 = 1; count1 <= maxCount1; count1++) {
122
- for (let count2 = 1; count2 <= maxCount2; count2++) {
123
- const sum = count1 * denom1 + count2 * denom2;
124
- if (sum >= target) {
125
- const combination = [...Array(count1).fill(denom1 / precision), ...Array(count2).fill(denom2 / precision)];
126
- paymentOptions.push({
127
- combination: combination,
128
- sum: sum / precision,
129
- coinCount: count1 + count2,
130
- denomTypes: 2
131
- });
132
- }
133
- }
134
- }
135
- }
136
- }
137
-
138
- // 3币种组合:使用三种面额(大幅限制搜索以提高效率)
139
- for (let i = 0; i < Math.min(denoms.length - 2, 3); i++) {
140
- for (let j = i + 1; j < Math.min(denoms.length - 1, 4); j++) {
141
- for (let k = j + 1; k < Math.min(denoms.length, 5); k++) {
142
- const denom1 = denoms[i];
143
- const denom2 = denoms[j];
144
- const denom3 = denoms[k];
145
-
146
- // 大幅限制搜索范围
147
- const maxCount1 = Math.min(Math.ceil(target / denom1) + 1, 5);
148
- const maxCount2 = Math.min(Math.ceil(target / denom2) + 1, 6);
149
- const maxCount3 = Math.min(Math.ceil(target / denom3) + 1, 8);
150
- for (let count1 = 1; count1 <= maxCount1; count1++) {
151
- for (let count2 = 1; count2 <= maxCount2; count2++) {
152
- for (let count3 = 1; count3 <= maxCount3; count3++) {
153
- const sum = count1 * denom1 + count2 * denom2 + count3 * denom3;
154
- if (sum >= target) {
155
- const combination = [...Array(count1).fill(denom1 / precision), ...Array(count2).fill(denom2 / precision), ...Array(count3).fill(denom3 / precision)];
156
- paymentOptions.push({
157
- combination: combination,
158
- sum: sum / precision,
159
- coinCount: count1 + count2 + count3,
160
- denomTypes: 3
161
- });
162
- }
163
- }
164
- }
165
- }
166
- }
167
- }
168
- }
169
-
170
- // 4币种组合:使用四种面额(极度限制搜索以提高性能)
171
- for (let i = 0; i < Math.min(denoms.length - 3, 2); i++) {
172
- for (let j = i + 1; j < Math.min(denoms.length - 2, 3); j++) {
173
- for (let k = j + 1; k < Math.min(denoms.length - 1, 4); k++) {
174
- for (let l = k + 1; l < Math.min(denoms.length, 5); l++) {
175
- const denom1 = denoms[i];
176
- const denom2 = denoms[j];
177
- const denom3 = denoms[k];
178
- const denom4 = denoms[l];
179
-
180
- // 极度限制搜索范围
181
- const maxCount1 = Math.min(Math.ceil(target / denom1) + 1, 3);
182
- const maxCount2 = Math.min(Math.ceil(target / denom2) + 1, 4);
183
- const maxCount3 = Math.min(Math.ceil(target / denom3) + 1, 5);
184
- const maxCount4 = Math.min(Math.ceil(target / denom4) + 1, 6);
185
- for (let count1 = 1; count1 <= maxCount1; count1++) {
186
- for (let count2 = 1; count2 <= maxCount2; count2++) {
187
- for (let count3 = 1; count3 <= maxCount3; count3++) {
188
- for (let count4 = 1; count4 <= maxCount4; count4++) {
189
- const sum = count1 * denom1 + count2 * denom2 + count3 * denom3 + count4 * denom4;
190
- if (sum >= target) {
191
- const combination = [...Array(count1).fill(denom1 / precision), ...Array(count2).fill(denom2 / precision), ...Array(count3).fill(denom3 / precision), ...Array(count4).fill(denom4 / precision)];
192
- paymentOptions.push({
193
- combination: combination,
194
- sum: sum / precision,
195
- coinCount: count1 + count2 + count3 + count4,
196
- denomTypes: 4
197
- });
198
- }
199
- }
200
- }
201
- }
202
- }
203
- }
204
- }
205
- }
206
- }
207
-
208
- // 如果没有找到任何组合,提供默认建议
209
- if (paymentOptions.length === 0) {
210
- // 找到大于等于目标金额的最小面额
211
- const minValidDenom = validDenoms.find(denom => denom >= targetAmount);
212
- if (minValidDenom) {
213
- return [minValidDenom];
214
- } else {
215
- // 使用最大面额的组合
216
- const maxDenom = Math.max(...validDenoms);
217
- const count = Math.ceil(targetAmount / maxDenom);
218
- return [count * maxDenom];
219
- }
220
- }
221
-
222
- // 移除重复和扩展组合
223
- const uniqueCombinations = removeDuplicateAndExtendedCombinations(paymentOptions, targetAmount);
224
-
225
- // 按币种类型数量排序,然后按总硬币数量排序,最后按金额排序
226
- uniqueCombinations.sort((a, b) => {
227
- if (a.denomTypes !== b.denomTypes) {
228
- return a.denomTypes - b.denomTypes;
229
- }
230
- if (a.coinCount !== b.coinCount) {
231
- return a.coinCount - b.coinCount;
232
- }
233
- return a.sum - b.sum;
234
- });
235
-
236
- // 按金额去重并返回推荐的支付金额
237
- const uniqueAmounts = new Set();
238
- const finalResults = [];
239
- for (const item of uniqueCombinations) {
240
- const roundedAmount = Math.round(item.sum * 100) / 100; // 处理浮点数精度
241
- if (!uniqueAmounts.has(roundedAmount) && finalResults.length < 10) {
242
- uniqueAmounts.add(roundedAmount);
243
- finalResults.push(roundedAmount);
244
- }
245
- }
246
- return finalResults.sort((a, b) => a - b);
247
- } catch (error) {
248
- // 发生错误时返回安全的默认值
249
- console.warn('推荐支付金额计算出错:', error);
250
- // 返回最接近的整数金额作为兜底
251
- const safeAmount = Math.ceil(targetAmount);
252
- return [targetAmount, safeAmount];
253
- }
254
- }
255
-
256
- /**
257
- * 尝试找到精确匹配目标金额的组合
258
- */
259
- function findExactCombination(target, denoms, precision) {
260
- // 简单的深度优先搜索,限制搜索深度
261
- function dfs(remaining, denomIndex, currentCombination) {
262
- if (remaining === 0) {
263
- return currentCombination;
264
- }
265
- if (remaining < 0 || denomIndex >= denoms.length || currentCombination.length > 10) {
266
- return null;
267
- }
268
- const denom = denoms[denomIndex];
269
-
270
- // 尝试使用当前面额 0 到 maxCount 次
271
- const maxCount = Math.min(Math.floor(remaining / denom), 8);
272
- for (let count = maxCount; count >= 0; count--) {
273
- const newCombination = [...currentCombination, ...Array(count).fill(denom)];
274
- const result = dfs(remaining - count * denom, denomIndex + 1, newCombination);
275
- if (result) {
276
- return result;
277
- }
278
- }
279
- return null;
280
- }
281
- const exactMatch = dfs(target, 0, []);
282
- if (exactMatch && exactMatch.length > 0) {
283
- const denomTypes = new Set(exactMatch).size;
284
- return {
285
- combination: exactMatch.map(d => d / precision),
286
- sum: target / precision,
287
- coinCount: exactMatch.length,
288
- denomTypes: denomTypes
289
- };
290
- }
291
- return null;
292
- }
293
-
294
- /**
295
- * 移除重复和扩展组合
296
- * 核心原理:如果组合A包含组合B的所有硬币,并且还有额外硬币,则A是B的扩展,应该被移除
297
- */
298
- function removeDuplicateAndExtendedCombinations(combinations, targetAmount) {
299
- const result = [];
300
-
301
- // 按币种类型数量、硬币数量、金额排序,确保较简单的组合在前面
302
- combinations.sort((a, b) => {
303
- if (a.denomTypes !== b.denomTypes) {
304
- return a.denomTypes - b.denomTypes;
305
- }
306
- if (a.coinCount !== b.coinCount) {
307
- return a.coinCount - b.coinCount;
308
- }
309
- return a.sum - b.sum;
310
- });
311
- for (let i = 0; i < combinations.length; i++) {
312
- const current = combinations[i];
313
- let shouldSkip = false;
314
-
315
- // 检查当前组合是否与已添加的组合重复或者是扩展
316
- for (let j = 0; j < result.length; j++) {
317
- const existing = result[j];
318
-
319
- // 如果完全相同,跳过
320
- if (isSameCombination(current.combination, existing.combination)) {
321
- shouldSkip = true;
322
- break;
323
- }
324
-
325
- // 如果当前组合是已存在组合的扩展,跳过
326
- if (isExtensionOf(current.combination, existing.combination)) {
327
- shouldSkip = true;
328
- break;
329
- }
330
- }
331
-
332
- // 检查是否有其他组合是当前组合的扩展,如果有,移除那些扩展
333
- if (!shouldSkip) {
334
- // 移除所有当前组合的扩展
335
- for (let j = result.length - 1; j >= 0; j--) {
336
- if (isExtensionOf(result[j].combination, current.combination)) {
337
- result.splice(j, 1);
338
- }
339
- }
340
- result.push(current);
341
- }
342
- }
343
- return result;
344
- }
345
-
346
- /**
347
- * 检查组合A是否是组合B的扩展(即A包含B的所有硬币,并且还有额外的硬币)
348
- */
349
- function isExtensionOf(combinationA, combinationB) {
350
- // 如果A的硬币数量少于等于B,A不可能是B的扩展
351
- if (combinationA.length <= combinationB.length) {
352
- return false;
353
- }
354
-
355
- // 统计每个面额的数量
356
- const countA = {};
357
- const countB = {};
358
- combinationA.forEach(coin => {
359
- countA[coin] = (countA[coin] || 0) + 1;
360
- });
361
- combinationB.forEach(coin => {
362
- countB[coin] = (countB[coin] || 0) + 1;
363
- });
364
-
365
- // 检查B的所有面额在A中是否都有足够的数量
366
- for (const coin in countB) {
367
- if (!countA[coin] || countA[coin] < countB[coin]) {
368
- return false;
369
- }
370
- }
371
-
372
- // 检查A是否有B没有的额外硬币
373
- let hasExtra = false;
374
- for (const coin in countA) {
375
- if (countA[coin] > (countB[coin] || 0)) {
376
- hasExtra = true;
377
- break;
378
- }
379
- }
380
- return hasExtra;
381
- }
382
-
383
- /**
384
- * 检查两个组合是否本质相同(相同的面额组合)
385
- */
386
- function isSameCombination(combinationA, combinationB) {
387
- if (combinationA.length !== combinationB.length) {
388
- return false;
389
- }
390
- const countA = {};
391
- const countB = {};
392
- combinationA.forEach(coin => {
393
- countA[coin] = (countA[coin] || 0) + 1;
394
- });
395
- combinationB.forEach(coin => {
396
- countB[coin] = (countB[coin] || 0) + 1;
397
- });
398
-
399
- // 检查两个组合是否有相同的面额和数量
400
- for (const coin in countA) {
401
- if (countA[coin] !== (countB[coin] || 0)) {
402
- return false;
403
- }
404
- }
405
- for (const coin in countB) {
406
- if (countB[coin] !== (countA[coin] || 0)) {
407
- return false;
408
- }
409
- }
410
- return true;
62
+ const precision = 100;
63
+ const normalizedTarget = Math.round(targetAmount * precision) / precision;
64
+ const higherDenominations = [...new Set((Array.isArray(denominations) ? denominations : []).filter(denomination => denomination > 0 && Number.isFinite(denomination)).map(denomination => Math.round(denomination * precision) / precision))].filter(denomination => denomination > normalizedTarget).sort((a, b) => a - b).slice(0, 3);
65
+ return [normalizedTarget, ...higherDenominations];
411
66
  }
@@ -1,6 +1,6 @@
1
1
  import { Module, PisellCore } from '../../types';
2
2
  import { BaseModule } from '../BaseModule';
3
- import { ProductListLoadProductsParams } from './types';
3
+ import { ProductListLoadProductsParams, ProductListStoreQueryParams } from './types';
4
4
  import { ProductData } from '../Product/types';
5
5
  export * from './types';
6
6
  export declare class ProductList extends BaseModule implements Module {
@@ -52,6 +52,7 @@ export declare class ProductList extends BaseModule implements Module {
52
52
  getRawProductPriceSources(ids: number[]): Map<number, ProductData>;
53
53
  getProducts(): Promise<ProductData[]>;
54
54
  getProduct(id: number): Promise<ProductData | undefined>;
55
+ queryProducts(params?: ProductListStoreQueryParams): Promise<ProductData[]>;
55
56
  getProductByIds(ids: number[]): Promise<ProductData[] | undefined>;
56
57
  addProduct(products: ProductData[]): Promise<void>;
57
58
  selectProducts(products: ProductData[]): Promise<void>;
@@ -25,6 +25,15 @@ Object.keys(_types).forEach(function (key) {
25
25
  });
26
26
  });
27
27
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
28
+ function normalizeQuerySet(values) {
29
+ if (!Array.isArray(values) || values.length === 0) return null;
30
+ const normalized = values.map(value => String(value ?? '').trim()).filter(Boolean);
31
+ return normalized.length ? new Set(normalized) : null;
32
+ }
33
+ function matchesAny(values, querySet) {
34
+ if (!querySet) return true;
35
+ return values.some(value => querySet.has(String(value ?? '').trim()));
36
+ }
28
37
  class ProductList extends _BaseModule.BaseModule {
29
38
  defaultName = 'productList';
30
39
  defaultVersion = '1.0.0';
@@ -281,10 +290,46 @@ class ProductList extends _BaseModule.BaseModule {
281
290
  const product = this.store.list.find(product => product.id === id);
282
291
  return product ? (0, _lodashEs.cloneDeep)(product) : undefined;
283
292
  }
293
+ async queryProducts(params = {}) {
294
+ const includeIds = normalizeQuerySet(params.includeIds);
295
+ const excludeIds = normalizeQuerySet(params.excludeIds);
296
+ const categoryIds = normalizeQuerySet(params.categoryIds);
297
+ const collectionIds = normalizeQuerySet(params.collectionIds);
298
+ const extensionTypes = normalizeQuerySet(params.extensionTypes);
299
+ const statuses = normalizeQuerySet(params.statuses);
300
+ const channelApplications = normalizeQuerySet(params.channelApplications);
301
+ const codeOrBarcode = typeof params.codeOrBarcode === 'string' ? params.codeOrBarcode.trim() : '';
302
+ const products = (this.store.list || []).filter(product => {
303
+ const productId = String(product.id ?? '').trim();
304
+ if (includeIds && !includeIds.has(productId)) return false;
305
+ if (excludeIds && excludeIds.has(productId)) return false;
306
+ if (categoryIds) {
307
+ const productCategoryIds = (product.categories || []).flatMap(category => [category?.id, category?.pivot?.product_category_id]);
308
+ if (!matchesAny(productCategoryIds, categoryIds)) return false;
309
+ }
310
+ if (collectionIds) {
311
+ const productCollectionIds = (product.collection || []).map(collection => collection?.id);
312
+ if (!matchesAny(productCollectionIds, collectionIds)) return false;
313
+ }
314
+ if (extensionTypes && !extensionTypes.has(String(product.extension_type ?? '').trim())) return false;
315
+ if (statuses && !statuses.has(String(product.status ?? '').trim())) return false;
316
+ if (channelApplications) {
317
+ if (!matchesAny(product.channel_application || [], channelApplications)) return false;
318
+ }
319
+ if (codeOrBarcode) {
320
+ const code = typeof product.code === 'string' ? product.code.trim() : '';
321
+ const barcode = typeof product.barcode === 'string' ? product.barcode.trim() : '';
322
+ if (code !== codeOrBarcode && barcode !== codeOrBarcode) return false;
323
+ }
324
+ return true;
325
+ });
326
+ return (0, _lodashEs.cloneDeep)(products);
327
+ }
284
328
  async getProductByIds(ids) {
285
329
  await this.core.effects.emit(`${this.name}:onGetProductByIds`, this.store.list);
286
- const products = this.store.list.filter(product => ids.includes(product.id));
287
- return products;
330
+ return this.queryProducts({
331
+ includeIds: ids
332
+ });
288
333
  }
289
334
  async addProduct(products) {
290
335
  // list 需要根据 id 去重
@@ -15,6 +15,16 @@ export interface ProductListLoadProductsParams {
15
15
  /** 智能定价条件上下文,仅影响策略命中,不参与商品集合筛选 */
16
16
  strategy_context?: Record<string, any>;
17
17
  }
18
+ export interface ProductListStoreQueryParams {
19
+ includeIds?: Array<number | string>;
20
+ excludeIds?: Array<number | string>;
21
+ categoryIds?: Array<number | string>;
22
+ collectionIds?: Array<number | string>;
23
+ extensionTypes?: string[];
24
+ statuses?: ProductData['status'][];
25
+ channelApplications?: string[];
26
+ codeOrBarcode?: string;
27
+ }
18
28
  export interface ProductListData {
19
29
  list: ProductData[];
20
30
  selectProducts: ProductData[];