@pisell/pisellos 2.3.94 → 2.3.96

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 (88) hide show
  1. package/dist/model/strategy/adapter/dataVariant/evaluator.js +13 -2
  2. package/dist/model/strategy/adapter/itemRule/adapter.d.ts +4 -1
  3. package/dist/model/strategy/adapter/itemRule/adapter.js +135 -35
  4. package/dist/model/strategy/adapter/itemRule/evaluator.d.ts +8 -1
  5. package/dist/model/strategy/adapter/itemRule/evaluator.js +25 -1
  6. package/dist/model/strategy/adapter/itemRule/type.d.ts +44 -0
  7. package/dist/model/strategy/adapter/itemRule/type.js +19 -1
  8. package/dist/model/strategy/adapter/promotion/index.js +9 -0
  9. package/dist/modules/Order/index.d.ts +20 -3
  10. package/dist/modules/Order/index.js +931 -508
  11. package/dist/modules/Order/salesNotes.d.ts +31 -0
  12. package/dist/modules/Order/salesNotes.js +492 -0
  13. package/dist/modules/Order/types.d.ts +91 -5
  14. package/dist/modules/Order/types.js +5 -0
  15. package/dist/modules/Order/utils.d.ts +12 -0
  16. package/dist/modules/Order/utils.js +75 -26
  17. package/dist/modules/ProductList/index.d.ts +2 -1
  18. package/dist/modules/ProductList/index.js +100 -20
  19. package/dist/modules/ProductList/types.d.ts +10 -0
  20. package/dist/modules/SalesSummary/index.js +6 -4
  21. package/dist/server/index.d.ts +6 -0
  22. package/dist/server/index.js +57 -11
  23. package/dist/server/modules/products/index.d.ts +2 -2
  24. package/dist/server/modules/products/index.js +10 -9
  25. package/dist/server/utils/product.d.ts +1 -1
  26. package/dist/server/utils/product.js +186 -22
  27. package/dist/server/utils/small-ticket.js +23 -2
  28. package/dist/solution/BaseSales/index.d.ts +9 -1
  29. package/dist/solution/BaseSales/index.js +894 -706
  30. package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +3 -2
  31. package/dist/solution/BookingByStep/index.d.ts +2 -2
  32. package/dist/solution/BookingTicket/utils/orderCustomer.js +3 -2
  33. package/dist/solution/RegisterAndLogin/config.d.ts +9 -3
  34. package/dist/solution/RegisterAndLogin/config.js +95 -40
  35. package/dist/solution/RegisterAndLogin/index.js +4 -1
  36. package/dist/solution/ScanOrder/index.d.ts +1 -0
  37. package/dist/solution/ScanOrder/index.js +31 -27
  38. package/dist/solution/ScanOrder/utils.d.ts +1 -0
  39. package/dist/solution/ScanOrder/utils.js +2 -0
  40. package/dist/solution/UnifiedBookingSales/index.d.ts +15 -2
  41. package/dist/solution/UnifiedBookingSales/index.js +751 -483
  42. package/dist/solution/UnifiedBookingSales/types.d.ts +14 -1
  43. package/dist/solution/VenueBooking/index.d.ts +1 -0
  44. package/dist/solution/VenueBooking/index.js +8 -4
  45. package/lib/model/strategy/adapter/dataVariant/evaluator.js +12 -2
  46. package/lib/model/strategy/adapter/itemRule/adapter.d.ts +4 -1
  47. package/lib/model/strategy/adapter/itemRule/adapter.js +73 -4
  48. package/lib/model/strategy/adapter/itemRule/evaluator.d.ts +8 -1
  49. package/lib/model/strategy/adapter/itemRule/evaluator.js +23 -1
  50. package/lib/model/strategy/adapter/itemRule/type.d.ts +44 -0
  51. package/lib/model/strategy/adapter/itemRule/type.js +19 -1
  52. package/lib/modules/Order/index.d.ts +20 -3
  53. package/lib/modules/Order/index.js +358 -42
  54. package/lib/modules/Order/salesNotes.d.ts +31 -0
  55. package/lib/modules/Order/salesNotes.js +382 -0
  56. package/lib/modules/Order/types.d.ts +91 -5
  57. package/lib/modules/Order/types.js +4 -0
  58. package/lib/modules/Order/utils.d.ts +12 -0
  59. package/lib/modules/Order/utils.js +68 -16
  60. package/lib/modules/ProductList/index.d.ts +2 -1
  61. package/lib/modules/ProductList/index.js +47 -2
  62. package/lib/modules/ProductList/types.d.ts +10 -0
  63. package/lib/modules/SalesSummary/index.js +3 -1
  64. package/lib/server/index.d.ts +6 -0
  65. package/lib/server/index.js +47 -9
  66. package/lib/server/modules/products/index.d.ts +2 -2
  67. package/lib/server/modules/products/index.js +5 -4
  68. package/lib/server/utils/product.d.ts +1 -1
  69. package/lib/server/utils/product.js +163 -3
  70. package/lib/server/utils/small-ticket.js +22 -1
  71. package/lib/solution/BaseSales/index.d.ts +9 -1
  72. package/lib/solution/BaseSales/index.js +105 -1
  73. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +3 -2
  74. package/lib/solution/BookingByStep/index.d.ts +2 -2
  75. package/lib/solution/BookingTicket/utils/orderCustomer.js +2 -1
  76. package/lib/solution/RegisterAndLogin/config.d.ts +9 -3
  77. package/lib/solution/RegisterAndLogin/config.js +49 -8
  78. package/lib/solution/RegisterAndLogin/index.js +4 -1
  79. package/lib/solution/ScanOrder/index.d.ts +1 -0
  80. package/lib/solution/ScanOrder/index.js +5 -1
  81. package/lib/solution/ScanOrder/utils.d.ts +1 -0
  82. package/lib/solution/ScanOrder/utils.js +1 -0
  83. package/lib/solution/UnifiedBookingSales/index.d.ts +15 -2
  84. package/lib/solution/UnifiedBookingSales/index.js +125 -1
  85. package/lib/solution/UnifiedBookingSales/types.d.ts +14 -1
  86. package/lib/solution/VenueBooking/index.d.ts +1 -0
  87. package/lib/solution/VenueBooking/index.js +5 -1
  88. package/package.json +1 -1
@@ -101,6 +101,7 @@ Object.defineProperty(exports, "resolveSafeProductNum", {
101
101
  }
102
102
  });
103
103
  exports.resolveSubmitCollectPax = resolveSubmitCollectPax;
104
+ exports.resolveSubmitOrderType = resolveSubmitOrderType;
104
105
  Object.defineProperty(exports, "shouldBuildManualProductDiscount", {
105
106
  enumerable: true,
106
107
  get: function () {
@@ -125,6 +126,7 @@ var _decimal = _interopRequireDefault(require("decimal.js"));
125
126
  var _utils = require("../Holder/utils");
126
127
  var _utils2 = require("../../solution/ScanOrder/utils");
127
128
  var _orderCollectionIdentity = require("./utils/orderCollectionIdentity");
129
+ var _salesNotes = require("./salesNotes");
128
130
  var _manualProductDiscount = require("./utils/manualProductDiscount");
129
131
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
130
132
  /**
@@ -1127,6 +1129,7 @@ function createDefaultTempOrder(params) {
1127
1129
  request_unique_idempotency_token: '',
1128
1130
  vouchers: [],
1129
1131
  products: [],
1132
+ notes: [],
1130
1133
  bookings: [],
1131
1134
  payments: [],
1132
1135
  surcharges: [],
@@ -1136,10 +1139,58 @@ function createDefaultTempOrder(params) {
1136
1139
  holder: null,
1137
1140
  metadata: {},
1138
1141
  _extend: {
1139
- productsByUid: {}
1142
+ productsByUid: {},
1143
+ salesNotesState: {
1144
+ loaded: true,
1145
+ dirty: false
1146
+ }
1140
1147
  }
1141
1148
  };
1142
1149
  }
1150
+ function hasBoundBookingResource(bookings) {
1151
+ if (!Array.isArray(bookings)) return false;
1152
+ return bookings.some(booking => {
1153
+ if (!booking || typeof booking !== 'object') return false;
1154
+ if (Number(booking.parent_id) === 0) return false;
1155
+ const resources = booking.resources;
1156
+ if (!Array.isArray(resources)) return false;
1157
+ return resources.some(resource => {
1158
+ if (!resource || typeof resource !== 'object') return false;
1159
+ const relationId = String(resource.relation_id ?? '').trim();
1160
+ return relationId !== '' && relationId !== '0';
1161
+ });
1162
+ });
1163
+ }
1164
+ function isPersistedSubmitOrderId(orderId) {
1165
+ if (orderId === undefined || orderId === null || orderId === '') return false;
1166
+ const normalizedOrderId = Number(orderId);
1167
+ return Number.isFinite(normalizedOrderId) && normalizedOrderId > 0;
1168
+ }
1169
+
1170
+ /**
1171
+ * 解析 checkout 的订单类型。
1172
+ * dine_in 普通落库单在新增桌台资源后,即使原类型仍是 virtual,也必须按 table-order 提交。
1173
+ */
1174
+ function resolveSubmitOrderType(params) {
1175
+ const {
1176
+ requestedType,
1177
+ tempOrderType,
1178
+ orderId,
1179
+ businessCode,
1180
+ tempOrderBusinessCode,
1181
+ bookings
1182
+ } = params;
1183
+ const currentType = requestedType ?? tempOrderType;
1184
+ const resolvedBusinessCode = businessCode ?? tempOrderBusinessCode;
1185
+ if (isPersistedSubmitOrderId(orderId) && resolvedBusinessCode === 'dine_in' && currentType === 'virtual' && hasBoundBookingResource(bookings)) {
1186
+ return 'table-order';
1187
+ }
1188
+ if (currentType) return currentType;
1189
+ if (Array.isArray(bookings) && bookings.length > 0) {
1190
+ return businessCode === 'dine_in' ? 'table-order' : 'appointment_booking';
1191
+ }
1192
+ return 'virtual';
1193
+ }
1143
1194
  function buildSubmitPayload(params) {
1144
1195
  const {
1145
1196
  tempOrder,
@@ -1173,22 +1224,18 @@ function buildSubmitPayload(params) {
1173
1224
  total_refund_amount: _totalRefundAmount,
1174
1225
  create_date: _createDate,
1175
1226
  holder_id: _holderId,
1227
+ notes: _notes,
1176
1228
  ...tempOrderRest
1177
1229
  } = tempOrder;
1178
- let submitType = type ?? tempOrder.type;
1179
- if (!submitType) {
1180
- // 如果外部没有传递 types,根据订单里当前是否有 bookings 来决定是 appointment_booking 还是 virtual
1181
- if (tempOrder?.bookings?.length) {
1182
- // 260807 如果 biz 是堂食,type 使用table-order
1183
- if (businessCode === 'dine_in') {
1184
- submitType = 'table-order';
1185
- } else {
1186
- submitType = 'appointment_booking';
1187
- }
1188
- } else {
1189
- submitType = 'virtual';
1190
- }
1191
- }
1230
+ const resolvedBusinessCode = businessCode ?? tempOrder.business_code;
1231
+ const submitType = resolveSubmitOrderType({
1232
+ requestedType: type,
1233
+ tempOrderType: tempOrder.type,
1234
+ orderId: tempOrder.order_id,
1235
+ businessCode,
1236
+ tempOrderBusinessCode: tempOrder.business_code,
1237
+ bookings: tempOrder.bookings
1238
+ });
1192
1239
  const payload = {
1193
1240
  ...tempOrderRest,
1194
1241
  customer_id: tempOrder.customer_id ?? 1,
@@ -1202,7 +1249,7 @@ function buildSubmitPayload(params) {
1202
1249
  platform: normalizeSubmitPlatform(platform),
1203
1250
  request_unique_idempotency_token: params?.request_unique_idempotency_token || tempOrder.request_unique_idempotency_token || `${tempOrder.external_sale_number}_${now.getTime()}`,
1204
1251
  type: submitType,
1205
- business_code: businessCode ?? tempOrder.business_code,
1252
+ business_code: resolvedBusinessCode,
1206
1253
  sales_channel: tempOrder.sales_channel || 'my_pisel',
1207
1254
  order_sales_channel: channel ?? tempOrder.order_sales_channel ?? 'online_store',
1208
1255
  status: tempOrder.status || 'normal',
@@ -1235,6 +1282,11 @@ function buildSubmitPayload(params) {
1235
1282
  ...rest
1236
1283
  };
1237
1284
  })(),
1285
+ ...(tempOrder._extend?.salesNotesState?.dirty ? {
1286
+ notes: (0, _salesNotes.encodeSalesNotes)(tempOrder.notes || [], {
1287
+ allowUnresolvedOrderTarget: true
1288
+ })
1289
+ } : {}),
1238
1290
  products: (tempOrder.products || []).map(product => normalizeSubmitProduct(product))
1239
1291
  };
1240
1292
  if (!enhance) return payload;
@@ -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[];
@@ -133,7 +133,9 @@ class SalesSummaryModule extends _BaseModule.BaseModule {
133
133
  await this.getSurchargeList(summaryChannel);
134
134
  }
135
135
  const taxConfig = this.getTaxConfig();
136
- const summarySchedule = this.core.getModule(`${this.name.split('_')[0]}_schedule`);
136
+ const fatherModuleName = this.otherParams?.fatherModule || this.name.replace(/_salesSummary$/, '');
137
+ const scheduleModuleName = `${fatherModuleName}_schedule`;
138
+ const summarySchedule = this.core.getModule(scheduleModuleName);
137
139
  const needScheduleIds = (this.store.surchargeList?.map(item => item.available_schedule_ids)?.flat() || []).filter(id => id !== undefined && id !== null && id !== '');
138
140
  const scheduleList = needScheduleIds.length > 0 ? summarySchedule?.getScheduleListByIds(needScheduleIds) : [];
139
141
  const scheduleById = {};
@@ -520,6 +520,12 @@ declare class Server {
520
520
  private stringifyCheckoutResourceIdentifier;
521
521
  private normalizeCheckoutSubmitData;
522
522
  private ensureCheckoutOrderNumbers;
523
+ /**
524
+ * Notes 允许在本地草稿阶段暂存空订单目标;checkout 的统一出口负责用最终
525
+ * shop_order_number 完成绑定并校验,保证后端请求、pending、设备任务和打印
526
+ * 都使用同一份完整快照。
527
+ */
528
+ private finalizeCheckoutSalesNotes;
523
529
  private dispatchCheckoutSyncTask;
524
530
  private dispatchPrintOtherReceiptTask;
525
531
  private buildPrintOtherReceiptIdempotencyKey;
@@ -27,6 +27,7 @@ var _filterBookings = require("./modules/order/utils/filterBookings");
27
27
  var _smallTicket = require("./utils/small-ticket");
28
28
  var _BaseSales = require("../solution/BaseSales");
29
29
  var _paymentUtils = require("../modules/Order/payment-utils");
30
+ var _salesNotes = require("../modules/Order/salesNotes");
30
31
  var _modules = require("./modules");
31
32
  Object.keys(_modules).forEach(function (key) {
32
33
  if (key === "default" || key === "__esModule") return;
@@ -3463,26 +3464,28 @@ class Server {
3463
3464
  if (!Array.isArray(bookings)) return '';
3464
3465
  const values = [];
3465
3466
  const seen = new Set();
3466
- const visitResource = resource => {
3467
- const identifier = this.getCheckoutResourceIdentifier(resource);
3467
+ const visitResource = (resource, fallbackName) => {
3468
+ const identifier = this.getCheckoutResourceIdentifier(resource, fallbackName);
3468
3469
  if (identifier && !seen.has(identifier)) {
3469
3470
  seen.add(identifier);
3470
3471
  values.push(identifier);
3471
3472
  }
3472
3473
  if (Array.isArray(resource?.children)) {
3473
- resource.children.forEach(visitResource);
3474
+ resource.children.forEach(child => visitResource(child));
3474
3475
  }
3475
3476
  };
3476
3477
  bookings.forEach(booking => {
3477
3478
  if (Array.isArray(booking?.resources)) {
3478
- booking.resources.forEach(visitResource);
3479
+ booking.resources.forEach((resource, index) => {
3480
+ visitResource(resource, index === 0 ? booking?.relation_form_name : undefined);
3481
+ });
3479
3482
  }
3480
3483
  });
3481
3484
  return values.join(',');
3482
3485
  }
3483
- getCheckoutResourceIdentifier(resource) {
3486
+ getCheckoutResourceIdentifier(resource, fallbackName) {
3484
3487
  if (!resource || typeof resource !== 'object') return '';
3485
- const candidates = [resource.main_field, resource.name, resource.title, resource.metadata?.resource_name, resource.metadata?.name, resource.relation_id, resource.id];
3488
+ const candidates = [resource.main_field, resource.name, resource.title, resource.metadata?.resource_name, resource.metadata?.name, fallbackName, resource.relation_id, resource.id];
3486
3489
  for (const candidate of candidates) {
3487
3490
  const value = this.stringifyCheckoutResourceIdentifier(candidate);
3488
3491
  if (value) return value;
@@ -3585,14 +3588,16 @@ class Server {
3585
3588
  };
3586
3589
  const needsShopOrderNumber = next.shop_order_number === undefined || next.shop_order_number === null || next.shop_order_number === '';
3587
3590
  const needsShopFullOrderNumber = next.shop_full_order_number === undefined || next.shop_full_order_number === null || next.shop_full_order_number === '';
3588
- if (!needsShopOrderNumber && !needsShopFullOrderNumber) return next;
3591
+ if (!needsShopOrderNumber && !needsShopFullOrderNumber) {
3592
+ return this.finalizeCheckoutSalesNotes(next, title);
3593
+ }
3589
3594
  const idGenerator = this.getIdGeneratorPlugin();
3590
3595
  if (!idGenerator) {
3591
3596
  this.logWarning(`${title}: idGenerator 插件不可用,无法生成订单号`, {
3592
3597
  needsShopOrderNumber,
3593
3598
  needsShopFullOrderNumber
3594
3599
  });
3595
- return next;
3600
+ return this.finalizeCheckoutSalesNotes(next, title);
3596
3601
  }
3597
3602
  try {
3598
3603
  if (needsShopOrderNumber && idGenerator.generateShopOrderNumber) {
@@ -3614,7 +3619,38 @@ class Server {
3614
3619
  error: error instanceof Error ? error.message : String(error)
3615
3620
  });
3616
3621
  }
3617
- return next;
3622
+ return this.finalizeCheckoutSalesNotes(next, title);
3623
+ }
3624
+
3625
+ /**
3626
+ * Notes 允许在本地草稿阶段暂存空订单目标;checkout 的统一出口负责用最终
3627
+ * shop_order_number 完成绑定并校验,保证后端请求、pending、设备任务和打印
3628
+ * 都使用同一份完整快照。
3629
+ */
3630
+ finalizeCheckoutSalesNotes(data, title) {
3631
+ if (!data || typeof data !== 'object') return data;
3632
+ if (!Object.prototype.hasOwnProperty.call(data, 'notes')) return data;
3633
+ if (data.notes === undefined || data.notes === null) return data;
3634
+ if (!Array.isArray(data.notes)) {
3635
+ throw new Error('[Order Notes] checkout notes 必须为数组');
3636
+ }
3637
+ if (data.notes.length === 0) return data;
3638
+ const productTargetUuids = (Array.isArray(data.products) ? data.products : []).map(product => product?.metadata?.unique_identification_number ?? product?.unique_identification_number).filter(targetUuid => targetUuid !== undefined && targetUuid !== null && String(targetUuid).trim().length > 0).map(targetUuid => String(targetUuid));
3639
+ const shopOrderNumber = String(data.shop_order_number ?? '').trim();
3640
+ const notes = (0, _salesNotes.finalizeSalesNotesForCheckout)({
3641
+ notes: data.notes,
3642
+ shopOrderNumber,
3643
+ productTargetUuids
3644
+ });
3645
+ this.logInfo(`${title}: checkout Notes 已绑定订单号`, {
3646
+ shop_order_number: shopOrderNumber,
3647
+ note_count: notes.length
3648
+ });
3649
+ return {
3650
+ ...data,
3651
+ shop_order_number: shopOrderNumber,
3652
+ notes
3653
+ };
3618
3654
  }
3619
3655
  async dispatchCheckoutSyncTask(params) {
3620
3656
  this.registerDeviceTaskActions();
@@ -5706,6 +5742,8 @@ class Server {
5706
5742
  schedule_datetime: context.schedule_datetime,
5707
5743
  customer_id: context.customer_id,
5708
5744
  strategy_context: context.strategy_context,
5745
+ // 固定本次商品查询使用的语言,确保格式化与缓存使用同一份 locale 快照。
5746
+ locale: this.core.context?.locale,
5709
5747
  menuList: this.menu?.getMenuList?.() || [],
5710
5748
  scheduleList: scheduleModule?.getScheduleList?.() || [],
5711
5749
  dataVariantEvaluator: this.core.context?.dataVariantEvaluator
@@ -89,8 +89,8 @@ export declare class ProductsModule extends BaseModule implements Module {
89
89
  private buildBundleGroupPriceData;
90
90
  /**
91
91
  * 获取应用了价格的商品列表(带缓存)
92
- * 使用日期作为缓存 key,同一天的商品价格会被缓存
93
- * 缓存的是已经应用了价格的完整商品列表,避免重复转换
92
+ * 使用时间、客户、策略、商品范围和语言作为缓存 key
93
+ * 缓存的是已经应用了价格与多语言的完整商品列表,避免重复转换
94
94
  * @param schedule_date 日期
95
95
  * @param extraContext 额外的上下文数据(可选,由 Server 层传入)
96
96
  * @param options 可选参数
@@ -30,7 +30,7 @@ class ProductsModule extends _BaseModule.BaseModule {
30
30
  logger; // LoggerManager 实例
31
31
  otherParams = {};
32
32
 
33
- // 商品价格缓存:Map<日期, 应用了价格的商品列表>
33
+ // 商品格式化缓存:Map<查询上下文 key, 已应用价格与多语言的商品列表>
34
34
  productsPriceCache = new Map();
35
35
 
36
36
  // 缓存配置
@@ -394,8 +394,8 @@ class ProductsModule extends _BaseModule.BaseModule {
394
394
 
395
395
  /**
396
396
  * 获取应用了价格的商品列表(带缓存)
397
- * 使用日期作为缓存 key,同一天的商品价格会被缓存
398
- * 缓存的是已经应用了价格的完整商品列表,避免重复转换
397
+ * 使用时间、客户、策略、商品范围和语言作为缓存 key
398
+ * 缓存的是已经应用了价格与多语言的完整商品列表,避免重复转换
399
399
  * @param schedule_date 日期
400
400
  * @param extraContext 额外的上下文数据(可选,由 Server 层传入)
401
401
  * @param options 可选参数
@@ -600,9 +600,10 @@ class ProductsModule extends _BaseModule.BaseModule {
600
600
  }
601
601
  getProductsPriceCacheKey(schedule_date, extraContext, productIds) {
602
602
  const datetime = extraContext?.schedule_datetime || schedule_date;
603
+ const locale = extraContext?.locale ?? this.core?.context?.locale ?? '';
603
604
  const productScope = productIds ? `:products:${productIds.join(',')}` : '';
604
605
  const strategyScope = this.stringifyStable(extraContext?.strategy_context || {});
605
- return `${datetime}:customer:${extraContext?.customer_id ?? ''}:strategy:${strategyScope}${productScope}`;
606
+ return `${datetime}:locale:${locale}:customer:${extraContext?.customer_id ?? ''}` + `:strategy:${strategyScope}${productScope}`;
606
607
  }
607
608
 
608
609
  /**
@@ -11,7 +11,7 @@ export declare function perfMark(label: string, durationMs: number, meta?: Recor
11
11
  export declare function applyPriceDataToProducts(products: ProductData[], priceData: LoadProductsPriceData[]): ProductData[];
12
12
  export declare const getIsSessionProduct: (product: ProductData) => boolean;
13
13
  /**
14
- * 根据 locale 将商品的 i18n 字段覆盖到对应原始字段
14
+ * 根据 locale 格式化商品、分类、规格及套餐文案,缺少对应语言时回退 original。
15
15
  */
16
16
  export declare function applyI18nToProducts(products: ProductData[], locale?: string): ProductData[];
17
17
  /**
@@ -194,9 +194,139 @@ const I18N_FIELD_MAP = {
194
194
  description_i18n: 'description',
195
195
  cover_i18n: 'cover'
196
196
  };
197
+ /**
198
+ * 按当前语言读取 i18n 文案;当前语言为空时回退到商家原始文案。
199
+ */
200
+ function resolveI18nValue(i18nDict, locale) {
201
+ if (!i18nDict) return undefined;
202
+ return i18nDict[locale] || i18nDict.original || undefined;
203
+ }
204
+
205
+ /**
206
+ * 递归格式化商品分类名称,同时保留未变化节点的引用。
207
+ */
208
+ function applyI18nToCategory(category, locale) {
209
+ if (!category || typeof category !== 'object') return category;
210
+ const localizedName = resolveI18nValue(category.name_i18n, locale);
211
+ const localizedParent = applyI18nToCategory(category.parent_category, locale);
212
+ const nameChanged = localizedName !== undefined && localizedName !== category.name;
213
+ const parentChanged = localizedParent !== category.parent_category;
214
+ if (!nameChanged && !parentChanged) return category;
215
+ return {
216
+ ...category,
217
+ ...(nameChanged ? {
218
+ name: localizedName
219
+ } : {}),
220
+ ...(parentChanged ? {
221
+ parent_category: localizedParent
222
+ } : {})
223
+ };
224
+ }
225
+ function applyI18nToCategories(categories, locale) {
226
+ if (!Array.isArray(categories)) return categories;
227
+ let changed = false;
228
+ const localizedCategories = categories.map(category => {
229
+ const localizedCategory = applyI18nToCategory(category, locale);
230
+ if (localizedCategory !== category) changed = true;
231
+ return localizedCategory;
232
+ });
233
+ return changed ? localizedCategories : categories;
234
+ }
235
+ function applyI18nToNamedItems(items, locale) {
236
+ if (!Array.isArray(items)) return items;
237
+ let changed = false;
238
+ const localizedItems = items.map(item => {
239
+ if (!item || typeof item !== 'object') return item;
240
+ const localizedName = resolveI18nValue(item.name_i18n, locale);
241
+ if (localizedName === undefined || localizedName === item.name) return item;
242
+ changed = true;
243
+ return {
244
+ ...item,
245
+ name: localizedName
246
+ };
247
+ });
248
+ return changed ? localizedItems : items;
249
+ }
250
+
251
+ /**
252
+ * 格式化选项组、规格组及其子项名称,同时保留未变化节点的引用。
253
+ */
254
+ function applyI18nToNamedGroups(groups, itemKey, locale) {
255
+ if (!Array.isArray(groups)) return groups;
256
+ let changed = false;
257
+ const localizedGroups = groups.map(group => {
258
+ if (!group || typeof group !== 'object') return group;
259
+ const localizedName = resolveI18nValue(group.name_i18n, locale);
260
+ const localizedItems = applyI18nToNamedItems(group[itemKey], locale);
261
+ const nameChanged = localizedName !== undefined && localizedName !== group.name;
262
+ const itemsChanged = localizedItems !== group[itemKey];
263
+ if (!nameChanged && !itemsChanged) return group;
264
+ changed = true;
265
+ return {
266
+ ...group,
267
+ ...(nameChanged ? {
268
+ name: localizedName
269
+ } : {}),
270
+ ...(itemsChanged ? {
271
+ [itemKey]: localizedItems
272
+ } : {})
273
+ };
274
+ });
275
+ return changed ? localizedGroups : groups;
276
+ }
277
+ function applyI18nToBundleItems(items, locale) {
278
+ if (!Array.isArray(items)) return items;
279
+ let changed = false;
280
+ const localizedItems = items.map(item => {
281
+ if (!item || typeof item !== 'object') return item;
282
+ const localizedTitle = resolveI18nValue(item.trans_title, locale);
283
+ const localizedOptionGroups = applyI18nToNamedGroups(item.option_group, 'option_item', locale);
284
+ const titleChanged = localizedTitle !== undefined && localizedTitle !== item.title;
285
+ const optionGroupsChanged = localizedOptionGroups !== item.option_group;
286
+ if (!titleChanged && !optionGroupsChanged) return item;
287
+ changed = true;
288
+ return {
289
+ ...item,
290
+ ...(titleChanged ? {
291
+ title: localizedTitle
292
+ } : {}),
293
+ ...(optionGroupsChanged ? {
294
+ option_group: localizedOptionGroups
295
+ } : {})
296
+ };
297
+ });
298
+ return changed ? localizedItems : items;
299
+ }
197
300
 
198
301
  /**
199
- * 根据 locale 将商品的 i18n 字段覆盖到对应原始字段
302
+ * 格式化套餐组名称、套餐子商品标题及子商品选项,同时保留未变化节点的引用。
303
+ */
304
+ function applyI18nToBundleGroups(groups, locale) {
305
+ if (!Array.isArray(groups)) return groups;
306
+ let changed = false;
307
+ const localizedGroups = groups.map(group => {
308
+ if (!group || typeof group !== 'object') return group;
309
+ const localizedName = resolveI18nValue(group.name_i18n, locale);
310
+ const localizedItems = applyI18nToBundleItems(group.bundle_item, locale);
311
+ const nameChanged = localizedName !== undefined && localizedName !== group.name;
312
+ const itemsChanged = localizedItems !== group.bundle_item;
313
+ if (!nameChanged && !itemsChanged) return group;
314
+ changed = true;
315
+ return {
316
+ ...group,
317
+ ...(nameChanged ? {
318
+ name: localizedName
319
+ } : {}),
320
+ ...(itemsChanged ? {
321
+ bundle_item: localizedItems
322
+ } : {})
323
+ };
324
+ });
325
+ return changed ? localizedGroups : groups;
326
+ }
327
+
328
+ /**
329
+ * 根据 locale 格式化商品、分类、规格及套餐文案,缺少对应语言时回退 original。
200
330
  */
201
331
  function applyI18nToProducts(products, locale) {
202
332
  if (!locale) return products;
@@ -205,14 +335,44 @@ function applyI18nToProducts(products, locale) {
205
335
  for (const [i18nKey, originalKey] of Object.entries(I18N_FIELD_MAP)) {
206
336
  const i18nDict = product[i18nKey];
207
337
  if (!i18nDict) continue;
208
- const localizedValue = i18nDict[locale];
209
- if (localizedValue) {
338
+ const localizedValue = resolveI18nValue(i18nDict, locale);
339
+ if (localizedValue !== undefined && localizedValue !== product[originalKey]) {
210
340
  if (!patched) patched = {
211
341
  ...product
212
342
  };
213
343
  patched[originalKey] = localizedValue;
214
344
  }
215
345
  }
346
+ for (const categoryKey of ['category', 'categories']) {
347
+ const categories = product[categoryKey];
348
+ const localizedCategories = applyI18nToCategories(categories, locale);
349
+ if (localizedCategories !== categories) {
350
+ if (!patched) patched = {
351
+ ...product
352
+ };
353
+ patched[categoryKey] = localizedCategories;
354
+ }
355
+ }
356
+ for (const [groupKey, itemKey] of [['option_group', 'option_item'], ['variant_group', 'variant_item']]) {
357
+ const groups = product[groupKey];
358
+ const localizedGroups = applyI18nToNamedGroups(groups, itemKey, locale);
359
+ if (localizedGroups !== groups) {
360
+ if (!patched) patched = {
361
+ ...product
362
+ };
363
+ patched[groupKey] = localizedGroups;
364
+ }
365
+ }
366
+ for (const bundleGroupKey of ['bundle_group', 'bundle_groups']) {
367
+ const groups = product[bundleGroupKey];
368
+ const localizedGroups = applyI18nToBundleGroups(groups, locale);
369
+ if (localizedGroups !== groups) {
370
+ if (!patched) patched = {
371
+ ...product
372
+ };
373
+ patched[bundleGroupKey] = localizedGroups;
374
+ }
375
+ }
216
376
  return patched ?? product;
217
377
  });
218
378
  }