@pisell/pisellos 2.3.95 → 2.3.97

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 (29) hide show
  1. package/dist/model/strategy/adapter/dataVariant/evaluator.js +13 -2
  2. package/dist/model/strategy/adapter/promotion/index.js +0 -9
  3. package/dist/modules/Order/index.d.ts +2 -0
  4. package/dist/modules/Order/index.js +51 -28
  5. package/dist/modules/SalesSummary/index.js +6 -4
  6. package/dist/server/index.js +14 -8
  7. package/dist/server/modules/products/index.d.ts +2 -2
  8. package/dist/server/modules/products/index.js +10 -9
  9. package/dist/server/utils/product.d.ts +1 -1
  10. package/dist/server/utils/product.js +186 -22
  11. package/dist/server/utils/small-ticket.d.ts +9 -1
  12. package/dist/server/utils/small-ticket.js +136 -41
  13. package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +3 -2
  14. package/dist/solution/BookingByStep/index.d.ts +2 -2
  15. package/lib/model/strategy/adapter/dataVariant/evaluator.js +12 -2
  16. package/lib/model/strategy/adapter/promotion/index.js +46 -1
  17. package/lib/modules/Order/index.d.ts +2 -0
  18. package/lib/modules/Order/index.js +33 -14
  19. package/lib/modules/SalesSummary/index.js +3 -1
  20. package/lib/server/index.js +10 -6
  21. package/lib/server/modules/products/index.d.ts +2 -2
  22. package/lib/server/modules/products/index.js +5 -4
  23. package/lib/server/utils/product.d.ts +1 -1
  24. package/lib/server/utils/product.js +163 -3
  25. package/lib/server/utils/small-ticket.d.ts +9 -1
  26. package/lib/server/utils/small-ticket.js +126 -33
  27. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +3 -2
  28. package/lib/solution/BookingByStep/index.d.ts +2 -2
  29. package/package.json +1 -1
@@ -234,15 +234,25 @@ class DataVariantEvaluator {
234
234
  if (!appliedInfo?.appliedFields?.length) {
235
235
  return originalItem;
236
236
  }
237
+
238
+ // 套餐加价/减价使用配置价;仅商品原价模式跟随子商品的 Data Variant 价格。
239
+ const isOriginalPriceBundleItem = originalItem.price_type === 'markup' && originalItem.price_type_ext === 'product_price';
240
+ const appliedFields = appliedInfo.appliedFields.filter(field => field !== 'price' || isOriginalPriceBundleItem);
241
+ if (!appliedFields.length) {
242
+ return originalItem;
243
+ }
237
244
  const result = {
238
245
  ...originalItem
239
246
  };
240
- for (const field of appliedInfo.appliedFields) {
247
+ for (const field of appliedFields) {
241
248
  if (resolvedItem[field] !== undefined) {
242
249
  result[field] = resolvedItem[field];
243
250
  }
244
251
  }
245
- result._dataVariant = appliedInfo;
252
+ result._dataVariant = {
253
+ ...appliedInfo,
254
+ appliedFields
255
+ };
246
256
  return result;
247
257
  }
248
258
  evaluateProductRules(rule, product, businessData, menuScopeMap) {
@@ -1 +1,46 @@
1
- "use strict";
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "BUY_X_GET_Y_FREE_STRATEGY", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _examples.BUY_X_GET_Y_FREE_STRATEGY;
10
+ }
11
+ });
12
+ Object.defineProperty(exports, "ITEM_REWARD_STRATEGY", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _examples.ITEM_REWARD_STRATEGY;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "PromotionAdapter", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _adapter.PromotionAdapter;
22
+ }
23
+ });
24
+ Object.defineProperty(exports, "PromotionEvaluator", {
25
+ enumerable: true,
26
+ get: function () {
27
+ return _evaluator.PromotionEvaluator;
28
+ }
29
+ });
30
+ Object.defineProperty(exports, "X_ITEMS_FOR_Y_PRICE_STRATEGY", {
31
+ enumerable: true,
32
+ get: function () {
33
+ return _examples.X_ITEMS_FOR_Y_PRICE_STRATEGY;
34
+ }
35
+ });
36
+ Object.defineProperty(exports, "default", {
37
+ enumerable: true,
38
+ get: function () {
39
+ return _adapter.default;
40
+ }
41
+ });
42
+ var _evaluator = require("./evaluator");
43
+ var _adapter = _interopRequireWildcard(require("./adapter"));
44
+ var _examples = require("./examples");
45
+ function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
46
+ function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
@@ -179,6 +179,8 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
179
179
  private calculateOrderPaidPaymentTotal;
180
180
  private getDepositAmount;
181
181
  private normalizeDepositAmount;
182
+ private normalizeDepositFlag;
183
+ private getHistoricalDepositSnapshot;
182
184
  private getLastOrderDepositSnapshot;
183
185
  private getLastOrderSubmitSnapshot;
184
186
  private applyDepositAmountToSummary;
@@ -1008,19 +1008,26 @@ class OrderModule extends _BaseModule.BaseModule {
1008
1008
  normalizeDepositAmount(amount) {
1009
1009
  return _decimal.default.max(new _decimal.default(Number(amount) || 0), 0).toDecimalPlaces(2).toFixed(2);
1010
1010
  }
1011
- getLastOrderDepositSnapshot() {
1012
- const lastOrderInfo = this.store.lastOrderInfo;
1013
- if (!lastOrderInfo || typeof lastOrderInfo !== 'object') return null;
1014
- const rawDepositAmount = lastOrderInfo.deposit_amount ?? lastOrderInfo.summary?.deposit_amount;
1011
+ normalizeDepositFlag(value) {
1012
+ if (value === 0 || value === '0') return 0;
1013
+ if (value === 1 || value === '1') return 1;
1014
+ return null;
1015
+ }
1016
+ getHistoricalDepositSnapshot(source) {
1017
+ if (!source || typeof source !== 'object') return null;
1018
+ const record = source;
1019
+ const rawDepositAmount = record.deposit_amount ?? record.summary?.deposit_amount;
1015
1020
  const depositAmount = this.normalizeDepositAmount(rawDepositAmount ?? 0);
1016
- const hasDepositAmount = new _decimal.default(depositAmount).gt(0);
1017
- const isDeposit = Number(lastOrderInfo.is_deposit) === 1 || hasDepositAmount;
1018
- if (!isDeposit || !hasDepositAmount) return null;
1021
+ if (!new _decimal.default(depositAmount).gt(0)) return null;
1019
1022
  return {
1020
- isDeposit: 1,
1023
+ // 兼容历史数据:旧快照缺少 is_deposit 时,沿用正定金金额的推导规则。
1024
+ isDeposit: this.normalizeDepositFlag(record.is_deposit) ?? 1,
1021
1025
  depositAmount
1022
1026
  };
1023
1027
  }
1028
+ getLastOrderDepositSnapshot() {
1029
+ return this.getHistoricalDepositSnapshot(this.store.lastOrderInfo);
1030
+ }
1024
1031
  getLastOrderSubmitSnapshot() {
1025
1032
  const lastOrderInfo = this.store.lastOrderInfo;
1026
1033
  if (!lastOrderInfo || typeof lastOrderInfo !== 'object') return {};
@@ -1068,7 +1075,7 @@ class OrderModule extends _BaseModule.BaseModule {
1068
1075
  }
1069
1076
  };
1070
1077
  tempOrder.deposit_amount = snapshot.depositAmount;
1071
- tempOrder.is_deposit = tempOrder.is_deposit === 0 && new _decimal.default(tempOrder.deposit_amount || 0).gt(0) ? 0 : snapshot.isDeposit;
1078
+ tempOrder.is_deposit = snapshot.isDeposit;
1072
1079
  return nextSummary;
1073
1080
  }
1074
1081
  isDepositOrder(tempOrder, depositAmount) {
@@ -1605,10 +1612,17 @@ class OrderModule extends _BaseModule.BaseModule {
1605
1612
  ...(0, _lodashEs.cloneDeep)(snapshot.summary || {})
1606
1613
  };
1607
1614
  const shouldUseManualDepositOverride = Boolean(manualDepositOverride && manualDepositOverride.productStructuralFingerprint === this.buildOrderProductsStructuralFingerprint(tempOrder.products));
1615
+ const historicalDepositSnapshot = this.getHistoricalDepositSnapshot(snapshot);
1608
1616
  if (shouldUseManualDepositOverride) {
1609
1617
  summary = this.applyDepositAmountToSummary(tempOrder, summary, manualDepositOverride.amount);
1610
- } else if (manualDepositOverride) {
1611
- this.clearManualDepositOverride(tempOrder);
1618
+ } else {
1619
+ if (manualDepositOverride) {
1620
+ this.clearManualDepositOverride(tempOrder);
1621
+ }
1622
+ if (historicalDepositSnapshot) {
1623
+ summary = this.applyDepositAmountToSummary(tempOrder, summary, historicalDepositSnapshot.depositAmount);
1624
+ tempOrder.is_deposit = historicalDepositSnapshot.isDeposit;
1625
+ }
1612
1626
  }
1613
1627
  const isPayableAmountUnchanged = this.isSnapshotPayableAmountUnchanged(tempOrder, snapshot, summary);
1614
1628
  if (!isPayableAmountUnchanged) return null;
@@ -1625,8 +1639,8 @@ class OrderModule extends _BaseModule.BaseModule {
1625
1639
  tempOrder.surcharges = (0, _lodashEs.cloneDeep)(snapshot.surcharges);
1626
1640
  }
1627
1641
  tempOrder.surcharge_fee = summary.surcharge_fee || tempOrder.surcharge_fee;
1628
- tempOrder.deposit_amount = summary.deposit_amount || '0.00';
1629
- tempOrder.is_deposit = summary.deposit_amount !== '0.00' ? 1 : 0;
1642
+ tempOrder.deposit_amount = this.normalizeDepositAmount(summary.deposit_amount || 0);
1643
+ tempOrder.is_deposit = shouldUseManualDepositOverride ? new _decimal.default(tempOrder.deposit_amount).gt(0) ? 1 : 0 : historicalDepositSnapshot?.isDeposit ?? (new _decimal.default(tempOrder.deposit_amount).gt(0) ? 1 : 0);
1630
1644
  const previousSummary = this.store.summary;
1631
1645
  this.store.summary = summary;
1632
1646
  this.updateTempOrderPaymentStatus(tempOrder, {
@@ -2083,7 +2097,7 @@ class OrderModule extends _BaseModule.BaseModule {
2083
2097
  const isExistingDepositDisabled = tempOrder.is_deposit === 0 && new _decimal.default(tempOrder.deposit_amount || 0).gt(0);
2084
2098
  tempOrder.deposit_amount = depositAmount;
2085
2099
  tempOrder.is_deposit = hasDepositAmount && !isExistingDepositDisabled ? 1 : 0;
2086
- return this.applyLastOrderDepositSnapshot(tempOrder, nextSummary);
2100
+ return shouldUseManualDepositOverride ? nextSummary : this.applyLastOrderDepositSnapshot(tempOrder, nextSummary);
2087
2101
  }
2088
2102
  async recalculateSummary(options) {
2089
2103
  const tempOrder = options?.createIfMissing ? this.ensureTempOrder() : this.store.tempOrder;
@@ -4469,6 +4483,7 @@ class OrderModule extends _BaseModule.BaseModule {
4469
4483
  externalSaleNumber: nextTempOrder.external_sale_number
4470
4484
  };
4471
4485
  const rawSummary = raw.summary && typeof raw.summary === 'object' ? raw.summary : {};
4486
+ const historicalDepositSnapshot = this.getHistoricalDepositSnapshot(sourceLastOrderInfo);
4472
4487
  const summarySurcharges = Array.isArray(rawSummary.surcharges) ? rawSummary.surcharges.map(s => ({
4473
4488
  ...(s || {})
4474
4489
  })) : (0, _lodashEs.cloneDeep)(nextTempOrder.surcharges || []);
@@ -4487,6 +4502,10 @@ class OrderModule extends _BaseModule.BaseModule {
4487
4502
  surcharges: (0, _lodashEs.cloneDeep)(nextTempOrder.surcharges || []),
4488
4503
  shop_discount: nextTempOrder.shop_discount || '0.00',
4489
4504
  productFingerprint: this.buildOrderProductsFingerprint(nextTempOrder.products || []),
4505
+ ...(historicalDepositSnapshot ? {
4506
+ is_deposit: historicalDepositSnapshot.isDeposit,
4507
+ deposit_amount: historicalDepositSnapshot.depositAmount
4508
+ } : {}),
4490
4509
  // merged record 的 summary 仍是远端旧值,保留快照仅用于完整重算的价格清理基线。
4491
4510
  ...(isMergedSalesDetailHydrate ? {
4492
4511
  requiresFullSummaryRecalculation: true
@@ -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 = {};
@@ -3464,26 +3464,28 @@ class Server {
3464
3464
  if (!Array.isArray(bookings)) return '';
3465
3465
  const values = [];
3466
3466
  const seen = new Set();
3467
- const visitResource = resource => {
3468
- const identifier = this.getCheckoutResourceIdentifier(resource);
3467
+ const visitResource = (resource, fallbackName) => {
3468
+ const identifier = this.getCheckoutResourceIdentifier(resource, fallbackName);
3469
3469
  if (identifier && !seen.has(identifier)) {
3470
3470
  seen.add(identifier);
3471
3471
  values.push(identifier);
3472
3472
  }
3473
3473
  if (Array.isArray(resource?.children)) {
3474
- resource.children.forEach(visitResource);
3474
+ resource.children.forEach(child => visitResource(child));
3475
3475
  }
3476
3476
  };
3477
3477
  bookings.forEach(booking => {
3478
3478
  if (Array.isArray(booking?.resources)) {
3479
- booking.resources.forEach(visitResource);
3479
+ booking.resources.forEach((resource, index) => {
3480
+ visitResource(resource, index === 0 ? booking?.relation_form_name : undefined);
3481
+ });
3480
3482
  }
3481
3483
  });
3482
3484
  return values.join(',');
3483
3485
  }
3484
- getCheckoutResourceIdentifier(resource) {
3486
+ getCheckoutResourceIdentifier(resource, fallbackName) {
3485
3487
  if (!resource || typeof resource !== 'object') return '';
3486
- 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];
3487
3489
  for (const candidate of candidates) {
3488
3490
  const value = this.stringifyCheckoutResourceIdentifier(candidate);
3489
3491
  if (value) return value;
@@ -5740,6 +5742,8 @@ class Server {
5740
5742
  schedule_datetime: context.schedule_datetime,
5741
5743
  customer_id: context.customer_id,
5742
5744
  strategy_context: context.strategy_context,
5745
+ // 固定本次商品查询使用的语言,确保格式化与缓存使用同一份 locale 快照。
5746
+ locale: this.core.context?.locale,
5743
5747
  menuList: this.menu?.getMenuList?.() || [],
5744
5748
  scheduleList: scheduleModule?.getScheduleList?.() || [],
5745
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
  }
@@ -35,6 +35,8 @@ export interface SmallTicketProduct {
35
35
  original_price: string;
36
36
  total_original_price: string;
37
37
  extension_list: any[];
38
+ product_uid?: string | number;
39
+ booking_uid?: string | number;
38
40
  options?: Array<{
39
41
  name: string;
40
42
  num?: number;
@@ -44,6 +46,11 @@ export interface SmallTicketProduct {
44
46
  variant?: string;
45
47
  combinations?: any[];
46
48
  }
49
+ export interface SmallTicketBookingSnapshot extends Record<string, any> {
50
+ product_uid?: string | number;
51
+ booking_uid?: string | number;
52
+ }
53
+ export type SmallTicketAppointment = SmallTicketItem[] | SmallTicketBookingSnapshot[];
47
54
  export interface SmallTicketLocaleData {
48
55
  base_data: Record<string, any>;
49
56
  products: SmallTicketProduct[];
@@ -52,7 +59,7 @@ export interface SmallTicketLocaleData {
52
59
  countFee: Record<string, any>;
53
60
  };
54
61
  customer: SmallTicketItem[];
55
- appointment: SmallTicketItem[];
62
+ appointment: SmallTicketAppointment;
56
63
  delivery?: SmallTicketItem[];
57
64
  resources: Array<{
58
65
  id: string | number;
@@ -61,6 +68,7 @@ export interface SmallTicketLocaleData {
61
68
  form_title: string;
62
69
  }>;
63
70
  payment_data: SmallTicketItem[][];
71
+ wallets: SmallTicketItem[][];
64
72
  tickets: any[];
65
73
  source_shop: any[];
66
74
  refund_data: any[];