@pisell/pisellos 2.2.256 → 2.2.258

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 (52) hide show
  1. package/dist/model/strategy/adapter/promotion/index.js +9 -0
  2. package/dist/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +17 -13
  3. package/dist/modules/Order/index.d.ts +29 -1
  4. package/dist/modules/Order/index.js +508 -241
  5. package/dist/modules/Order/types.d.ts +20 -2
  6. package/dist/modules/Order/utils/bundleDiscountHydration.d.ts +5 -0
  7. package/dist/modules/Order/utils/bundleDiscountHydration.js +144 -0
  8. package/dist/modules/Order/utils.d.ts +4 -0
  9. package/dist/modules/Order/utils.js +114 -28
  10. package/dist/modules/Product/index.d.ts +1 -1
  11. package/dist/modules/Product/types.d.ts +6 -0
  12. package/dist/modules/Rules/index.d.ts +3 -3
  13. package/dist/modules/Rules/index.js +8 -3
  14. package/dist/modules/Rules/types.d.ts +2 -1
  15. package/dist/server/index.d.ts +12 -2
  16. package/dist/server/index.js +1227 -923
  17. package/dist/server/modules/order/index.d.ts +22 -3
  18. package/dist/server/modules/order/index.js +397 -314
  19. package/dist/solution/BaseSales/index.d.ts +16 -2
  20. package/dist/solution/BaseSales/index.js +484 -314
  21. package/dist/solution/BaseSales/types.d.ts +1 -0
  22. package/dist/solution/BaseSales/types.js +2 -1
  23. package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +4 -2
  24. package/dist/solution/BookingByStep/index.d.ts +1 -1
  25. package/dist/solution/BookingTicket/index.d.ts +9 -1
  26. package/dist/solution/BookingTicket/index.js +162 -6
  27. package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +11 -1
  28. package/lib/modules/Order/index.d.ts +29 -1
  29. package/lib/modules/Order/index.js +340 -57
  30. package/lib/modules/Order/types.d.ts +20 -2
  31. package/lib/modules/Order/utils/bundleDiscountHydration.d.ts +5 -0
  32. package/lib/modules/Order/utils/bundleDiscountHydration.js +139 -0
  33. package/lib/modules/Order/utils.d.ts +4 -0
  34. package/lib/modules/Order/utils.js +84 -2
  35. package/lib/modules/Product/index.d.ts +1 -1
  36. package/lib/modules/Product/types.d.ts +6 -0
  37. package/lib/modules/Rules/index.d.ts +3 -3
  38. package/lib/modules/Rules/index.js +4 -3
  39. package/lib/modules/Rules/types.d.ts +2 -1
  40. package/lib/server/index.d.ts +12 -2
  41. package/lib/server/index.js +218 -12
  42. package/lib/server/modules/order/index.d.ts +22 -3
  43. package/lib/server/modules/order/index.js +102 -51
  44. package/lib/solution/BaseSales/index.d.ts +16 -2
  45. package/lib/solution/BaseSales/index.js +143 -31
  46. package/lib/solution/BaseSales/types.d.ts +1 -0
  47. package/lib/solution/BaseSales/types.js +2 -1
  48. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +4 -5
  49. package/lib/solution/BookingByStep/index.d.ts +1 -1
  50. package/lib/solution/BookingTicket/index.d.ts +9 -1
  51. package/lib/solution/BookingTicket/index.js +112 -0
  52. package/package.json +1 -1
@@ -639,7 +639,8 @@ var Server = class {
639
639
  checkoutData,
640
640
  order: printableOrder,
641
641
  response: null,
642
- requireFullyPaid: false
642
+ requireFullyPaid: false,
643
+ isManualPrint: true
643
644
  });
644
645
  return {
645
646
  code: 200,
@@ -661,6 +662,86 @@ var Server = class {
661
662
  };
662
663
  }
663
664
  };
665
+ this.handleMachinecodeBatchMetadata = async ({ data }) => {
666
+ var _a, _b;
667
+ const title = "handleMachinecodeBatchMetadata";
668
+ const backendPath = "/shop/machinecode/batch-metadata";
669
+ const batchMetadata = Array.isArray(data == null ? void 0 : data.data) ? data.data : [];
670
+ this.logInfo(`${title}: 开始处理`, {
671
+ backendPath,
672
+ batchSize: batchMetadata.length,
673
+ order_lookup: this.resolveMachinecodeBatchMetadataLookup(data) ?? null
674
+ });
675
+ if (batchMetadata.length === 0) {
676
+ return {
677
+ code: 200,
678
+ status: true,
679
+ message: "",
680
+ data: []
681
+ };
682
+ }
683
+ try {
684
+ await this.syncMachinecodeBatchMetadataToLocalOrder(data, batchMetadata);
685
+ } catch (error) {
686
+ const message = this.getUnknownErrorMessage(error);
687
+ this.logError(`${title}: 本地订单 metadata 更新失败`, {
688
+ error: message,
689
+ error_detail: this.normalizeUnknownError(error)
690
+ });
691
+ return {
692
+ code: 500,
693
+ status: false,
694
+ message,
695
+ data: null
696
+ };
697
+ }
698
+ if (!((_b = (_a = this.app) == null ? void 0 : _a.request) == null ? void 0 : _b.post)) {
699
+ this.logError(`${title}: app.request 不可用`, { backendPath });
700
+ return {
701
+ code: 500,
702
+ status: false,
703
+ message: "app.request 不可用",
704
+ data: null
705
+ };
706
+ }
707
+ try {
708
+ const response = await this.app.request.post(backendPath, {
709
+ data: batchMetadata
710
+ }, {
711
+ isShopApi: true,
712
+ customToast: () => {
713
+ }
714
+ });
715
+ return {
716
+ code: (response == null ? void 0 : response.code) ?? 200,
717
+ status: (response == null ? void 0 : response.status) ?? true,
718
+ message: (response == null ? void 0 : response.message) ?? "",
719
+ data: (response == null ? void 0 : response.data) ?? response
720
+ };
721
+ } catch (error) {
722
+ const backendError = this.extractBackendErrorResponse(error);
723
+ if (backendError) {
724
+ return {
725
+ code: backendError.code ?? 500,
726
+ status: backendError.status ?? false,
727
+ message: backendError.message ?? "后端拒绝 batch-metadata",
728
+ data: backendError.data ?? null
729
+ };
730
+ }
731
+ const message = this.getUnknownErrorMessage(error);
732
+ this.logError(`${title}: 请求失败`, {
733
+ backendPath,
734
+ error: message,
735
+ error_detail: this.normalizeUnknownError(error)
736
+ });
737
+ return {
738
+ code: 500,
739
+ status: false,
740
+ message,
741
+ data: null
742
+ };
743
+ }
744
+ };
664
745
  this.handleOrderSalesDetail = async ({ url, data, path }) => {
665
746
  var _a;
666
747
  const requestPath = this.parseRequestPath(url, path);
@@ -737,7 +818,7 @@ var Server = class {
737
818
  };
738
819
  } catch (error) {
739
820
  const errorMessage = error instanceof Error ? error.message : String(error);
740
- this.logError("handleOrderSalesDetail: 请求失败", {
821
+ this.logInfo("handleOrderSalesDetail: 请求失败", {
741
822
  lookup,
742
823
  backendPath,
743
824
  error: errorMessage
@@ -1743,6 +1824,16 @@ var Server = class {
1743
1824
  method: "post",
1744
1825
  path: "/shop/local/print-order",
1745
1826
  handler: this.handleLocalPrintOrder.bind(this)
1827
+ },
1828
+ {
1829
+ method: "post",
1830
+ path: "/shop/machinecode/batch-metadata",
1831
+ handler: this.handleMachinecodeBatchMetadata.bind(this)
1832
+ },
1833
+ {
1834
+ method: "post",
1835
+ path: "/machinecode/batch-metadata",
1836
+ handler: this.handleMachinecodeBatchMetadata.bind(this)
1746
1837
  }
1747
1838
  ]);
1748
1839
  this.registerPrefixRoutes([
@@ -1988,6 +2079,20 @@ var Server = class {
1988
2079
  return null;
1989
2080
  return text.split("T")[0].split(" ")[0] || null;
1990
2081
  }
2082
+ /**
2083
+ * 判断时间范围是否完整落在今天。
2084
+ *
2085
+ * @example
2086
+ * this.isRangeFullyToday('2026-04-15', '2026-04-15') // => true
2087
+ */
2088
+ isRangeFullyToday(startRaw, endRaw) {
2089
+ const startDate = this.extractQueryDate(startRaw);
2090
+ const endDate = this.extractQueryDate(endRaw);
2091
+ if (!startDate || !endDate)
2092
+ return false;
2093
+ const today = (0, import_dayjs.default)().format("YYYY-MM-DD");
2094
+ return startDate === today && endDate === today;
2095
+ }
1991
2096
  /**
1992
2097
  * 判断时间范围是否「开始和结束都不是今天」。
1993
2098
  *
@@ -2004,11 +2109,11 @@ var Server = class {
2004
2109
  }
2005
2110
  /**
2006
2111
  * 订单查询是否走本地:
2007
- * - start_time 与 end_time 都不是今天时,走远端;
2008
- * - 其余情况走本地。
2112
+ * - 仅当 start_time 与 end_time 都是今天时,走本地;
2113
+ * - 任意一端不是今天时,走云端,确保跨天查询能拉取非本地窗口数据。
2009
2114
  */
2010
2115
  shouldUseLocalOrderQuery(data) {
2011
- return !this.isRangeBothNotToday(data == null ? void 0 : data.start_time, data == null ? void 0 : data.end_time);
2116
+ return this.isRangeFullyToday(data == null ? void 0 : data.start_time, data == null ? void 0 : data.end_time);
2012
2117
  }
2013
2118
  /**
2014
2119
  * 预约查询是否走本地:
@@ -2642,10 +2747,12 @@ var Server = class {
2642
2747
  action: "print_all",
2643
2748
  device_id: params.deviceId ?? deviceId,
2644
2749
  payload: {
2645
- type: params.receiptOnly ? "0" : "99",
2750
+ type: params.isManualPrint ? "0" : "99",
2646
2751
  data: params.receiptOnly ? {
2647
2752
  order_id: printIdentity,
2648
- small_ticket_data: (_b = (_a = params.syncedOrder) == null ? void 0 : _a.payment_info) == null ? void 0 : _b.small_ticket_data
2753
+ small_ticket_data: (_b = (_a = params.syncedOrder) == null ? void 0 : _a.payment_info) == null ? void 0 : _b.small_ticket_data,
2754
+ skip_wristband: true,
2755
+ skip_voucher: true
2649
2756
  } : {
2650
2757
  order_id: printIdentity,
2651
2758
  machine_code_print_info: (_d = (_c = params.syncedOrder) == null ? void 0 : _c.payment_info) == null ? void 0 : _d.machine_code_print_info,
@@ -2656,7 +2763,7 @@ var Server = class {
2656
2763
  idempotency_key: this.buildPrintOtherReceiptIdempotencyKey(params.syncedOrder, {
2657
2764
  receiptOnly: params.receiptOnly
2658
2765
  }),
2659
- name: "打印转发",
2766
+ name: "PRINT_V1",
2660
2767
  source: "server",
2661
2768
  source_type: "order",
2662
2769
  source_id: printIdentity
@@ -2696,7 +2803,8 @@ var Server = class {
2696
2803
  checkoutData: params.checkoutData,
2697
2804
  syncedOrder: printableOrder,
2698
2805
  response: params.response,
2699
- receiptOnly: true
2806
+ receiptOnly: true,
2807
+ isManualPrint: params.isManualPrint
2700
2808
  });
2701
2809
  return { printed: true, order: printableOrder };
2702
2810
  }
@@ -2714,6 +2822,107 @@ var Server = class {
2714
2822
  }
2715
2823
  return void 0;
2716
2824
  }
2825
+ resolveMachinecodeBatchMetadataLookup(data) {
2826
+ const candidates = [
2827
+ data == null ? void 0 : data.order_lookup,
2828
+ data == null ? void 0 : data.orderLookup,
2829
+ data == null ? void 0 : data.external_sale_number,
2830
+ data == null ? void 0 : data.externalSaleNumber,
2831
+ data == null ? void 0 : data.order_id,
2832
+ data == null ? void 0 : data.orderId,
2833
+ data == null ? void 0 : data.order_number,
2834
+ data == null ? void 0 : data.orderNumber,
2835
+ data == null ? void 0 : data.shop_order_number,
2836
+ data == null ? void 0 : data.shopOrderNumber,
2837
+ data == null ? void 0 : data.shop_full_order_number,
2838
+ data == null ? void 0 : data.shopFullOrderNumber
2839
+ ];
2840
+ return candidates.find((candidate) => candidate !== void 0 && candidate !== null && candidate !== "");
2841
+ }
2842
+ async syncMachinecodeBatchMetadataToLocalOrder(data, batchMetadata) {
2843
+ var _a;
2844
+ const title = "handleMachinecodeBatchMetadata";
2845
+ const lookup = this.resolveMachinecodeBatchMetadataLookup(data);
2846
+ if (lookup === void 0) {
2847
+ this.logWarning(`${title}: order lookup 缺失,跳过本地订单 metadata 更新`, {
2848
+ batchSize: batchMetadata.length
2849
+ });
2850
+ return;
2851
+ }
2852
+ if (!((_a = this.order) == null ? void 0 : _a.getLocalOrderByLookup)) {
2853
+ this.logWarning(`${title}: Order 模块未注册,跳过本地订单 metadata 更新`, {
2854
+ lookup
2855
+ });
2856
+ return;
2857
+ }
2858
+ const localOrder = await this.order.getLocalOrderByLookup(lookup);
2859
+ if (!localOrder) {
2860
+ this.logWarning(`${title}: 本地订单不存在,跳过 metadata 更新`, {
2861
+ lookup
2862
+ });
2863
+ return;
2864
+ }
2865
+ const metadataByVoucherId = /* @__PURE__ */ new Map();
2866
+ for (const item of batchMetadata) {
2867
+ const voucherId = (item == null ? void 0 : item.voucher_id) ?? (item == null ? void 0 : item.voucherId);
2868
+ if (voucherId === void 0 || voucherId === null || voucherId === "")
2869
+ continue;
2870
+ metadataByVoucherId.set(String(voucherId), (item == null ? void 0 : item.metadata) || {});
2871
+ }
2872
+ if (metadataByVoucherId.size === 0) {
2873
+ this.logWarning(`${title}: voucher metadata 缺失,跳过本地订单 metadata 更新`, {
2874
+ lookup
2875
+ });
2876
+ return;
2877
+ }
2878
+ const vouchers = Array.isArray(localOrder.vouchers) ? localOrder.vouchers : [];
2879
+ let hasUpdatedVoucher = false;
2880
+ const nextVouchers = vouchers.map((voucher) => {
2881
+ const voucherId = voucher == null ? void 0 : voucher.voucher_id;
2882
+ if (voucherId === void 0 || voucherId === null)
2883
+ return voucher;
2884
+ const metadata = metadataByVoucherId.get(String(voucherId));
2885
+ if (!metadata)
2886
+ return voucher;
2887
+ hasUpdatedVoucher = true;
2888
+ return {
2889
+ ...voucher,
2890
+ metadata
2891
+ };
2892
+ });
2893
+ if (!hasUpdatedVoucher) {
2894
+ this.logWarning(`${title}: 本地订单未命中任何 voucher,跳过 metadata 更新`, {
2895
+ lookup,
2896
+ voucher_ids: Array.from(metadataByVoucherId.keys())
2897
+ });
2898
+ return;
2899
+ }
2900
+ if (typeof this.order.overwriteExistingOrder === "function") {
2901
+ await this.order.overwriteExistingOrder({
2902
+ ...localOrder,
2903
+ vouchers: nextVouchers
2904
+ });
2905
+ this.logInfo(`${title}: 本地订单 vouchers metadata 已更新`, {
2906
+ lookup,
2907
+ updatedCount: metadataByVoucherId.size
2908
+ });
2909
+ return;
2910
+ }
2911
+ if ((data == null ? void 0 : data.external_sale_number) && typeof this.order.markOrderSyncedByExternalSaleNumber === "function") {
2912
+ await this.order.markOrderSyncedByExternalSaleNumber({
2913
+ externalSaleNumber: data.external_sale_number,
2914
+ patch: { vouchers: nextVouchers }
2915
+ });
2916
+ this.logInfo(`${title}: 本地订单 vouchers metadata 已更新`, {
2917
+ lookup,
2918
+ updatedCount: metadataByVoucherId.size
2919
+ });
2920
+ return;
2921
+ }
2922
+ this.logWarning(`${title}: Order 模块不支持本地订单覆盖,跳过 metadata 更新`, {
2923
+ lookup
2924
+ });
2925
+ }
2717
2926
  async handleOrderCheckoutSubmit(params) {
2718
2927
  const { backendPath, data, title } = params;
2719
2928
  this.logInfo(`${title}: 开始处理`, {
@@ -2911,11 +3120,8 @@ var Server = class {
2911
3120
  );
2912
3121
  }
2913
3122
  shouldBuildSmallTicketData(order) {
2914
- var _a;
2915
3123
  if (!order || typeof order !== "object")
2916
3124
  return false;
2917
- if ((0, import_small_ticket.hasSmallTicketData)((_a = order.payment_info) == null ? void 0 : _a.small_ticket_data))
2918
- return false;
2919
3125
  return Number(order.small_ticket_data_flag ?? order.smallTicketDataFlag ?? 0) === 1;
2920
3126
  }
2921
3127
  shouldPrintSyncedOrder(params) {
@@ -15,7 +15,6 @@ export declare class OrderModule extends BaseModule implements Module {
15
15
  private syncTimer?;
16
16
  private isProcessingSyncBatch;
17
17
  private isIdlePhase;
18
- private resourceIdIndex;
19
18
  private logger;
20
19
  private storage;
21
20
  private orderSQLiteSaveQueue; /** 按 storageKey 记录最近一次 SQLite 写入来源与时间,用于去重 */
@@ -89,7 +88,6 @@ export declare class OrderModule extends BaseModule implements Module {
89
88
  * @returns 刷新后的订单列表
90
89
  */
91
90
  silentRefresh(): Promise<OrderData[]>;
92
- getOrdersByResourceId(resourceId: string | number): OrderData[];
93
91
  private normalizeOrderForMemoryList;
94
92
  /**
95
93
  * 仅覆盖本地已存在的订单;不存在则直接跳过,不落库、不 emit。
@@ -126,7 +124,6 @@ export declare class OrderModule extends BaseModule implements Module {
126
124
  private isBlankIdentityValue;
127
125
  private getOrderIdentityEntries;
128
126
  private getOrderIdentityKeys;
129
- private extractResourceIds;
130
127
  private getOrderDateKey;
131
128
  /**
132
129
  * 初始化 OrderDataSource 实例
@@ -175,6 +172,28 @@ export declare class OrderModule extends BaseModule implements Module {
175
172
  * 将增量订单合并到 store
176
173
  */
177
174
  private mergeOrdersToStore;
175
+ /**
176
+ * 在身份索引中查找与目标订单共享任一身份键的已存在订单下标。
177
+ * 返回所有命中键里最小的下标,等价于原 findOrderIndexByIdentity 的「取首个匹配」语义。
178
+ *
179
+ * @example
180
+ * const idx = this.lookupOrderIndexByIdentityKeys(identityKeyToIndex, freshIdentityKeys)
181
+ */
182
+ private lookupOrderIndexByIdentityKeys;
183
+ private isRemoteEchoMergeSource;
184
+ /**
185
+ * 将订单的全部身份键登记到索引并指向其在列表中的下标。
186
+ * 合并后订单新增的身份字段(如挂单同步后拿到 order_id)也会被登记。
187
+ *
188
+ * @example
189
+ * this.registerOrderIdentityKeys(identityKeyToIndex, mergedOrder, matchIndex)
190
+ */
191
+ private registerOrderIdentityKeys;
192
+ /**
193
+ * 合并热路径去重兜底:默认直接返回原列表(增量索引已保证无重复)。
194
+ * 仅当 ORDER_MERGE_SELF_CHECK 开启时,额外做一次全表压缩并在发现残留重复时告警。
195
+ */
196
+ private runOrderStoreSelfCheck;
178
197
  private normalizeOrderSyncMessage;
179
198
  private normalizeOrderSyncPayload;
180
199
  private uniqueOrderIds;
@@ -48,6 +48,7 @@ var ORDER_BUSINESS_WRITE_SOURCES = /* @__PURE__ */ new Set([
48
48
  "overwriteExistingOrder",
49
49
  "markOrderSyncedByExternalSaleNumber"
50
50
  ]);
51
+ var ORDER_MERGE_SELF_CHECK = false;
51
52
  var OrderModule = class extends import_BaseModule.BaseModule {
52
53
  constructor(name, version) {
53
54
  super(name, version);
@@ -56,8 +57,6 @@ var OrderModule = class extends import_BaseModule.BaseModule {
56
57
  this.pendingSyncMessages = [];
57
58
  this.isProcessingSyncBatch = false;
58
59
  this.isIdlePhase = true;
59
- // Map<resource_id, OrderId[]> 资源到订单的倒排索引
60
- this.resourceIdIndex = /* @__PURE__ */ new Map();
61
60
  this.orderSQLiteSaveQueue = Promise.resolve();
62
61
  /** 按 storageKey 记录最近一次 SQLite 写入来源与时间,用于去重 */
63
62
  this.recentSqliteWriteByStorageKey = /* @__PURE__ */ new Map();
@@ -481,10 +480,6 @@ var OrderModule = class extends import_BaseModule.BaseModule {
481
480
  return this.store.list;
482
481
  }
483
482
  }
484
- getOrdersByResourceId(resourceId) {
485
- const ids = this.resourceIdIndex.get(String(resourceId)) || [];
486
- return ids.map((id) => this.store.map.get(id)).filter((order) => !!order);
487
- }
488
483
  normalizeOrderForMemoryList(order) {
489
484
  const externalSaleNumber = order == null ? void 0 : order.external_sale_number;
490
485
  if ((order == null ? void 0 : order.order_id) !== void 0 && (order == null ? void 0 : order.order_id) !== null && (order == null ? void 0 : order.order_id) !== "") {
@@ -617,22 +612,33 @@ var OrderModule = class extends import_BaseModule.BaseModule {
617
612
  const patchedOrders = [...pendingMap.values()];
618
613
  const mergeActions = {};
619
614
  const updatedList = [...this.store.list];
615
+ const identityKeyToIndex = /* @__PURE__ */ new Map();
616
+ for (let index = 0; index < updatedList.length; index += 1) {
617
+ for (const key of this.getOrderIdentityMatchKeys(updatedList[index])) {
618
+ if (!identityKeyToIndex.has(key))
619
+ identityKeyToIndex.set(key, index);
620
+ }
621
+ }
620
622
  for (const [storageKey, pendingOrder] of memoryPendingMap.entries()) {
621
- const matchIndex = this.findOrderIndexByIdentity(updatedList, pendingOrder);
622
- if (matchIndex < 0)
623
+ const matchIndex = this.lookupOrderIndexByIdentityKeys(
624
+ identityKeyToIndex,
625
+ this.getOrderIdentityMatchKeys(pendingOrder)
626
+ );
627
+ if (matchIndex >= 0) {
628
+ const mergedOrder = this.mergeOrderRecords(updatedList[matchIndex], pendingOrder, {
629
+ preferIncomingProducts: true
630
+ });
631
+ updatedList[matchIndex] = mergedOrder;
632
+ this.registerOrderIdentityKeys(identityKeyToIndex, mergedOrder, matchIndex);
633
+ mergeActions[storageKey] = "update";
623
634
  continue;
624
- updatedList[matchIndex] = this.mergeOrderRecords(updatedList[matchIndex], pendingOrder, {
625
- preferIncomingProducts: true
626
- });
627
- pendingMap.delete(storageKey);
628
- memoryPendingMap.delete(storageKey);
629
- mergeActions[storageKey] = "update";
630
- }
631
- for (const [storageKey, order] of memoryPendingMap.entries()) {
635
+ }
636
+ const insertIndex = updatedList.length;
637
+ updatedList.push(pendingOrder);
638
+ this.registerOrderIdentityKeys(identityKeyToIndex, pendingOrder, insertIndex);
632
639
  mergeActions[storageKey] = "insert";
633
- updatedList.push(order);
634
640
  }
635
- this.store.list = this.compactOrderListByIdentity(updatedList, "upsertPendingSyncOrders.store").list;
641
+ this.store.list = this.runOrderStoreSelfCheck(updatedList, "upsertPendingSyncOrders.store");
636
642
  this.syncOrdersMap();
637
643
  await this.patchOrdersInSQLite(patchedOrders, "upsertPendingSyncOrders", mergeActions);
638
644
  this.logInfo("upsertPendingSyncOrders-结束", {
@@ -674,21 +680,11 @@ var OrderModule = class extends import_BaseModule.BaseModule {
674
680
  }
675
681
  syncOrdersMap() {
676
682
  this.store.map.clear();
677
- this.resourceIdIndex.clear();
678
683
  for (const order of this.store.list) {
679
684
  const primaryIdentity = this.getOrderMapKey(order);
680
685
  if (!primaryIdentity)
681
686
  continue;
682
687
  this.store.map.set(primaryIdentity, order);
683
- const resourceIds = this.extractResourceIds(order);
684
- for (const resourceId of resourceIds) {
685
- const key = String(resourceId);
686
- const existing = this.resourceIdIndex.get(key) || [];
687
- if (existing.some((eid) => this.getIdKey(eid) === primaryIdentity))
688
- continue;
689
- existing.push(primaryIdentity);
690
- this.resourceIdIndex.set(key, existing);
691
- }
692
688
  }
693
689
  }
694
690
  getOrderMapKey(order) {
@@ -735,24 +731,6 @@ var OrderModule = class extends import_BaseModule.BaseModule {
735
731
  }
736
732
  return keys;
737
733
  }
738
- extractResourceIds(order) {
739
- const ids = /* @__PURE__ */ new Set();
740
- const visitResources = (resources) => {
741
- if (!Array.isArray(resources))
742
- return;
743
- resources.forEach((resource) => {
744
- const rid = resource == null ? void 0 : resource.relation_id;
745
- if (rid !== void 0 && rid !== null && rid !== "")
746
- ids.add(String(rid));
747
- if (Array.isArray(resource == null ? void 0 : resource.children))
748
- visitResources(resource.children);
749
- });
750
- };
751
- if (Array.isArray(order == null ? void 0 : order.bookings)) {
752
- order.bookings.forEach((booking) => visitResources((booking == null ? void 0 : booking.resources) || []));
753
- }
754
- return [...ids];
755
- }
756
734
  getOrderDateKey(order) {
757
735
  var _a, _b, _c, _d;
758
736
  const candidates = [
@@ -971,9 +949,10 @@ var OrderModule = class extends import_BaseModule.BaseModule {
971
949
  * 将增量订单合并到 store
972
950
  */
973
951
  async mergeOrdersToStore(freshOrders, source) {
952
+ const storeOrderCountBefore = this.store.list.length;
974
953
  this.logInfo("mergeOrdersToStore-开始", {
975
954
  freshOrderCount: freshOrders.length,
976
- storeOrderCountBefore: this.store.list.length,
955
+ storeOrderCountBefore,
977
956
  source
978
957
  });
979
958
  this.logDuplicateOrders(`${source}.beforeMerge`, this.store.list);
@@ -992,19 +971,45 @@ var OrderModule = class extends import_BaseModule.BaseModule {
992
971
  const patchedOrders = [];
993
972
  const mergeActions = {};
994
973
  const updatedList = [...this.store.list];
974
+ const identityKeyToIndex = /* @__PURE__ */ new Map();
975
+ for (let index = 0; index < updatedList.length; index += 1) {
976
+ const identityKeys = this.getOrderIdentityMatchKeys(updatedList[index]);
977
+ for (const key of identityKeys) {
978
+ if (!identityKeyToIndex.has(key))
979
+ identityKeyToIndex.set(key, index);
980
+ }
981
+ }
995
982
  for (const fresh of uniqueFreshOrders) {
996
- const matchIndex = this.findOrderIndexByIdentity(updatedList, fresh);
983
+ const freshIdentityKeys = this.getOrderIdentityMatchKeys(fresh);
984
+ const matchIndex = this.lookupOrderIndexByIdentityKeys(identityKeyToIndex, freshIdentityKeys);
997
985
  const storageKey = this.getOrderStorageKey(fresh);
998
986
  const mergeKey = storageKey || (fresh.order_id !== void 0 && fresh.order_id !== null ? this.getIdKey(fresh.order_id) : "");
999
987
  if (matchIndex >= 0) {
1000
- const mergedOrder = this.mergeOrderRecords(updatedList[matchIndex], fresh);
988
+ const existingOrder = updatedList[matchIndex];
989
+ if (this.isPendingSyncOrder(existingOrder) && this.isRemoteEchoMergeSource(source)) {
990
+ this.logInfo("mergeOrdersToStore-跳过 pending 订单远端覆盖", {
991
+ source,
992
+ order_id: existingOrder.order_id ?? fresh.order_id ?? null,
993
+ external_sale_number: existingOrder.external_sale_number ?? fresh.external_sale_number ?? null,
994
+ local_payment_status: existingOrder.payment_status ?? null,
995
+ remote_payment_status: fresh.payment_status ?? null,
996
+ local_need_sync: existingOrder.need_sync ?? null,
997
+ remote_need_sync: fresh.need_sync ?? null,
998
+ reason: "protect_pending_sync_order_from_remote_echo"
999
+ });
1000
+ continue;
1001
+ }
1002
+ const mergedOrder = this.mergeOrderRecords(existingOrder, fresh);
1001
1003
  updatedList[matchIndex] = mergedOrder;
1004
+ this.registerOrderIdentityKeys(identityKeyToIndex, mergedOrder, matchIndex);
1002
1005
  patchedOrders.push(mergedOrder);
1003
1006
  if (mergeKey)
1004
1007
  mergeActions[mergeKey] = "update";
1005
1008
  continue;
1006
1009
  }
1010
+ const insertIndex = updatedList.length;
1007
1011
  updatedList.push(fresh);
1012
+ this.registerOrderIdentityKeys(identityKeyToIndex, fresh, insertIndex);
1008
1013
  patchedOrders.push(fresh);
1009
1014
  if (mergeKey)
1010
1015
  mergeActions[mergeKey] = "insert";
@@ -1016,7 +1021,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
1016
1021
  updateCount,
1017
1022
  storeOrderCountAfter: this.store.list.length
1018
1023
  });
1019
- this.store.list = this.compactOrderListByIdentity(updatedList, `${source}.store`).list;
1024
+ this.store.list = this.runOrderStoreSelfCheck(updatedList, `${source}.store`);
1020
1025
  this.syncOrdersMap();
1021
1026
  await this.patchOrdersInSQLite(patchedOrders, source, mergeActions);
1022
1027
  this.logInfo("mergeOrdersToStore-结束", {
@@ -1027,6 +1032,53 @@ var OrderModule = class extends import_BaseModule.BaseModule {
1027
1032
  });
1028
1033
  await this.emitOrdersChanged(patchedOrders, source);
1029
1034
  }
1035
+ /**
1036
+ * 在身份索引中查找与目标订单共享任一身份键的已存在订单下标。
1037
+ * 返回所有命中键里最小的下标,等价于原 findOrderIndexByIdentity 的「取首个匹配」语义。
1038
+ *
1039
+ * @example
1040
+ * const idx = this.lookupOrderIndexByIdentityKeys(identityKeyToIndex, freshIdentityKeys)
1041
+ */
1042
+ lookupOrderIndexByIdentityKeys(identityKeyToIndex, identityKeys) {
1043
+ let matchIndex = -1;
1044
+ for (const key of identityKeys) {
1045
+ const index = identityKeyToIndex.get(key);
1046
+ if (index === void 0)
1047
+ continue;
1048
+ if (matchIndex < 0 || index < matchIndex)
1049
+ matchIndex = index;
1050
+ }
1051
+ return matchIndex;
1052
+ }
1053
+ isRemoteEchoMergeSource(source) {
1054
+ return source === "pubsub.bodyUpsert" || source === "pubsub.httpRefresh";
1055
+ }
1056
+ /**
1057
+ * 将订单的全部身份键登记到索引并指向其在列表中的下标。
1058
+ * 合并后订单新增的身份字段(如挂单同步后拿到 order_id)也会被登记。
1059
+ *
1060
+ * @example
1061
+ * this.registerOrderIdentityKeys(identityKeyToIndex, mergedOrder, matchIndex)
1062
+ */
1063
+ registerOrderIdentityKeys(identityKeyToIndex, order, index) {
1064
+ const identityKeys = this.getOrderIdentityMatchKeys(order);
1065
+ for (const key of identityKeys) {
1066
+ identityKeyToIndex.set(key, index);
1067
+ }
1068
+ }
1069
+ /**
1070
+ * 合并热路径去重兜底:默认直接返回原列表(增量索引已保证无重复)。
1071
+ * 仅当 ORDER_MERGE_SELF_CHECK 开启时,额外做一次全表压缩并在发现残留重复时告警。
1072
+ */
1073
+ runOrderStoreSelfCheck(orders, source) {
1074
+ if (!ORDER_MERGE_SELF_CHECK)
1075
+ return orders;
1076
+ const { list, removedCount } = this.compactOrderListByIdentity(orders, source);
1077
+ if (removedCount > 0) {
1078
+ this.logWarning("合并自检发现残留重复订单,已压缩", { source, removedCount });
1079
+ }
1080
+ return list;
1081
+ }
1030
1082
  normalizeOrderSyncMessage(res) {
1031
1083
  const data = (res == null ? void 0 : res.data) || res;
1032
1084
  if (!data)
@@ -1640,7 +1692,6 @@ var OrderModule = class extends import_BaseModule.BaseModule {
1640
1692
  async clear() {
1641
1693
  this.store.list = [];
1642
1694
  this.store.map = /* @__PURE__ */ new Map();
1643
- this.resourceIdIndex.clear();
1644
1695
  if (this.dbManager) {
1645
1696
  await this.runInOrderSQLiteSaveQueue(async () => {
1646
1697
  await this.dbManager.clear(INDEXDB_STORE_NAME);
@@ -30,7 +30,7 @@ export declare class BaseSalesImpl extends BaseModule implements Module {
30
30
  protected defaultVersion: string;
31
31
  isSolution: boolean;
32
32
  protected initializeOptions: ModuleOptions;
33
- private logger;
33
+ protected logger: any;
34
34
  protected appPlugin: AppPlugin;
35
35
  protected store: BaseSalesState;
36
36
  protected otherParams: Record<string, any>;
@@ -46,6 +46,9 @@ export declare class BaseSalesImpl extends BaseModule implements Module {
46
46
  protected hasManualDiscountSelection: boolean;
47
47
  private readonly reusedSharedSubModuleNames;
48
48
  private paymentMethodsCache;
49
+ private queuedAutoPaymentSync;
50
+ private autoPaymentSyncFlushing;
51
+ private autoPaymentSyncTimer;
49
52
  constructor(name?: string, version?: string);
50
53
  /**
51
54
  * 子类可覆盖此方法以追加/收敛要注册的子模块。
@@ -195,6 +198,7 @@ export declare class BaseSalesImpl extends BaseModule implements Module {
195
198
  payments?: OrderPaymentSource[];
196
199
  paymentStatus?: BaseSalesPaymentStatus;
197
200
  smallTicketDataFlag?: number;
201
+ confirmPendingVoucherPayments?: boolean;
198
202
  enhancePayload?: SubmitPayloadEnhancer;
199
203
  }): Promise<T>;
200
204
  /**
@@ -207,12 +211,14 @@ export declare class BaseSalesImpl extends BaseModule implements Module {
207
211
  payments?: OrderPaymentSource[];
208
212
  paymentStatus?: BaseSalesPaymentStatus;
209
213
  smallTicketDataFlag?: number;
214
+ confirmPendingVoucherPayments?: boolean;
210
215
  enhancePayload?: SubmitPayloadEnhancer;
211
216
  }): Promise<T>;
212
217
  submitTempOrderAsync<T = any>(params?: {
213
218
  payments?: OrderPaymentSource[];
214
219
  paymentStatus?: BaseSalesPaymentStatus;
215
220
  smallTicketDataFlag?: number;
221
+ confirmPendingVoucherPayments?: boolean;
216
222
  enhancePayload?: SubmitPayloadEnhancer;
217
223
  }): Promise<T>;
218
224
  printLocalOrderReceipt<T = any>(lookup?: BaseSalesPrintLocalOrderReceiptParams): Promise<T>;
@@ -222,6 +228,10 @@ export declare class BaseSalesImpl extends BaseModule implements Module {
222
228
  getOrderPayments(): OrderPaymentData[];
223
229
  /** 覆盖当前订单支付项,内部由 OrderModule 清洗为 Sales 协议字段。 */
224
230
  setOrderPayments(payments: OrderPaymentSource[]): OrderPaymentData[];
231
+ private getPaymentStatusForSync;
232
+ private queueLatestAutoPaymentSync;
233
+ private scheduleQueuedAutoPaymentSyncFlush;
234
+ private flushQueuedAutoPaymentSync;
225
235
  /** 追加单个支付项到当前订单。 */
226
236
  addOrderPayment(payment: OrderPaymentSource): OrderPaymentData[];
227
237
  /** 更新当前订单中的指定支付项。 */
@@ -229,7 +239,11 @@ export declare class BaseSalesImpl extends BaseModule implements Module {
229
239
  /** 删除当前订单中的指定支付项。 */
230
240
  deleteOrderPayment(paymentIdentity: string | number): OrderPaymentData[];
231
241
  /** 覆盖 wallet pass 支付项,同时保留普通支付项。 */
232
- updateVoucherOrderPayments(payments: OrderPaymentSource[]): OrderPaymentData[];
242
+ updateVoucherOrderPayments(payments: OrderPaymentSource[], options?: {
243
+ status?: 'paid' | 'payment_pending';
244
+ }): OrderPaymentData[];
245
+ confirmPendingVoucherPayments(): OrderPaymentData[];
246
+ discardPendingVoucherPayments(): OrderPaymentData[];
233
247
  private getWalletProductList;
234
248
  initWalletData(params?: {
235
249
  order_wait_pay_amount?: number;