@pisell/pisellos 2.2.315 → 2.2.316

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 (97) hide show
  1. package/dist/model/strategy/adapter/promotion/evaluator.js +89 -104
  2. package/dist/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.d.ts +3 -1
  3. package/dist/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +28 -4
  4. package/dist/modules/Holder/utils.d.ts +2 -0
  5. package/dist/modules/Holder/utils.js +32 -2
  6. package/dist/modules/OpenData/contactInfo.d.ts +20 -0
  7. package/dist/modules/OpenData/contactInfo.js +70 -0
  8. package/dist/modules/OpenData/index.d.ts +1 -0
  9. package/dist/modules/OpenData/index.js +1 -0
  10. package/dist/modules/OpenData/types.d.ts +9 -0
  11. package/dist/modules/Order/holderRelations.d.ts +14 -0
  12. package/dist/modules/Order/holderRelations.js +154 -0
  13. package/dist/modules/Order/index.d.ts +2 -2
  14. package/dist/modules/Order/index.js +89 -44
  15. package/dist/modules/Order/types.d.ts +19 -1
  16. package/dist/modules/Order/types.js +2 -0
  17. package/dist/modules/Order/utils/discountProductLineIdentity.d.ts +1 -1
  18. package/dist/modules/Order/utils/discountProductLineIdentity.js +142 -7
  19. package/dist/modules/Order/utils/orderCollectionIdentity.js +24 -10
  20. package/dist/modules/Order/utils.d.ts +2 -1
  21. package/dist/modules/Order/utils.js +36 -10
  22. package/dist/modules/Rules/index.js +62 -61
  23. package/dist/server/index.js +36 -28
  24. package/dist/server/modules/order/index.d.ts +4 -0
  25. package/dist/server/modules/order/index.js +255 -190
  26. package/dist/server/modules/order/types.d.ts +3 -0
  27. package/dist/server/modules/products/index.d.ts +28 -7
  28. package/dist/server/modules/products/index.js +1297 -770
  29. package/dist/server/modules/products/replication.d.ts +8 -0
  30. package/dist/server/modules/products/replication.js +69 -0
  31. package/dist/server/modules/products/types.d.ts +12 -1
  32. package/dist/server/modules/products/types.js +4 -0
  33. package/dist/shared/receipt/small-ticket.js +24 -12
  34. package/dist/shared/receipt/templateVisibility.d.ts +3 -0
  35. package/dist/shared/receipt/templateVisibility.js +6 -2
  36. package/dist/shared/sales/loadSalesOrderSnapshot.js +1 -1
  37. package/dist/solution/BaseSales/index.d.ts +4 -0
  38. package/dist/solution/BaseSales/index.js +1116 -935
  39. package/dist/solution/BaseSales/types.d.ts +2 -1
  40. package/dist/solution/BaseSales/utils/parseSalesResponse.d.ts +3 -1
  41. package/dist/solution/BaseSales/utils/parseSalesResponse.js +7 -3
  42. package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +14 -4
  43. package/dist/solution/BookingByStep/index.d.ts +1 -1
  44. package/dist/solution/BookingTicket/index.d.ts +15 -1
  45. package/dist/solution/BookingTicket/index.js +784 -501
  46. package/dist/solution/ScanOrder/index.js +65 -31
  47. package/dist/solution/ScanOrder/types.d.ts +3 -0
  48. package/dist/solution/UnifiedBookingSales/index.js +1 -1
  49. package/lib/model/strategy/adapter/promotion/evaluator.js +9 -14
  50. package/lib/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.d.ts +3 -1
  51. package/lib/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +17 -1
  52. package/lib/modules/Holder/utils.d.ts +2 -0
  53. package/lib/modules/Holder/utils.js +27 -1
  54. package/lib/modules/OpenData/contactInfo.d.ts +20 -0
  55. package/lib/modules/OpenData/contactInfo.js +56 -0
  56. package/lib/modules/OpenData/index.d.ts +1 -0
  57. package/lib/modules/OpenData/index.js +12 -0
  58. package/lib/modules/OpenData/types.d.ts +9 -0
  59. package/lib/modules/Order/holderRelations.d.ts +14 -0
  60. package/lib/modules/Order/holderRelations.js +98 -0
  61. package/lib/modules/Order/index.d.ts +2 -2
  62. package/lib/modules/Order/index.js +44 -12
  63. package/lib/modules/Order/types.d.ts +19 -1
  64. package/lib/modules/Order/types.js +1 -0
  65. package/lib/modules/Order/utils/discountProductLineIdentity.d.ts +1 -1
  66. package/lib/modules/Order/utils/discountProductLineIdentity.js +51 -2
  67. package/lib/modules/Order/utils/orderCollectionIdentity.js +5 -0
  68. package/lib/modules/Order/utils.d.ts +2 -1
  69. package/lib/modules/Order/utils.js +28 -6
  70. package/lib/modules/Rules/index.js +2 -1
  71. package/lib/server/index.js +8 -2
  72. package/lib/server/modules/order/index.d.ts +4 -0
  73. package/lib/server/modules/order/index.js +46 -4
  74. package/lib/server/modules/order/types.d.ts +3 -0
  75. package/lib/server/modules/products/index.d.ts +28 -7
  76. package/lib/server/modules/products/index.js +226 -63
  77. package/lib/server/modules/products/replication.d.ts +8 -0
  78. package/lib/server/modules/products/replication.js +30 -0
  79. package/lib/server/modules/products/types.d.ts +12 -1
  80. package/lib/server/modules/products/types.js +3 -0
  81. package/lib/shared/receipt/small-ticket.js +22 -13
  82. package/lib/shared/receipt/templateVisibility.d.ts +3 -0
  83. package/lib/shared/receipt/templateVisibility.js +6 -2
  84. package/lib/shared/sales/loadSalesOrderSnapshot.js +1 -1
  85. package/lib/solution/BaseSales/index.d.ts +4 -0
  86. package/lib/solution/BaseSales/index.js +68 -2
  87. package/lib/solution/BaseSales/types.d.ts +2 -1
  88. package/lib/solution/BaseSales/utils/parseSalesResponse.d.ts +3 -1
  89. package/lib/solution/BaseSales/utils/parseSalesResponse.js +4 -0
  90. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +16 -8
  91. package/lib/solution/BookingByStep/index.d.ts +1 -1
  92. package/lib/solution/BookingTicket/index.d.ts +15 -1
  93. package/lib/solution/BookingTicket/index.js +132 -3
  94. package/lib/solution/ScanOrder/index.js +59 -20
  95. package/lib/solution/ScanOrder/types.d.ts +3 -0
  96. package/lib/solution/UnifiedBookingSales/index.js +1 -1
  97. package/package.json +1 -1
@@ -5,9 +5,11 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.ProductsModule = void 0;
7
7
  var _BaseModule = require("../../../modules/BaseModule");
8
+ var _utils = require("../../../modules/Holder/utils");
8
9
  var _Quotation = require("../../../modules/Quotation");
9
10
  var _types = require("./types");
10
11
  var _product = require("../../utils/product");
12
+ var _replication = require("./replication");
11
13
  /**
12
14
  * IndexDB 存储名称
13
15
  */
@@ -68,6 +70,119 @@ class ProductsModule extends _BaseModule.BaseModule {
68
70
 
69
71
  /** 最近一次智能定价评估提取的 schedule 变价时间点(HH:mm),供 Server 订阅定时刷新使用 */
70
72
  lastScheduleTimePoints = [];
73
+ committedProductState;
74
+ productReplicationSourceGeneration = 0;
75
+ productPersistenceFailed = false;
76
+ runtimeSyncMode = 'normal';
77
+ runtimeSyncGeneration = 0;
78
+ requestReplicaRefresh;
79
+ replicaApplyQueue = Promise.resolve();
80
+ productSyncQueue = Promise.resolve();
81
+ replicaPriceDirtyIds = new Map();
82
+
83
+ /** Installed after initial preload; initial cache/network loading stays unchanged. */
84
+ async setRuntimeSyncMode(mode, requestRefresh) {
85
+ this.runtimeSyncGeneration += 1;
86
+ this.runtimeSyncMode = mode;
87
+ this.requestReplicaRefresh = requestRefresh;
88
+ if (mode === 'memoryReplica') {
89
+ if (this.syncTimer) clearTimeout(this.syncTimer);
90
+ this.pendingSyncMessages = [];
91
+ await this.productSyncQueue;
92
+ await this.productIndexDBSaveQueue;
93
+ }
94
+ }
95
+ isMemoryReplica() {
96
+ return this.runtimeSyncMode === 'memoryReplica';
97
+ }
98
+
99
+ /** Read persisted data once on the main screen to establish a committed baseline. */
100
+ async enableProductReplicationSource() {
101
+ const generation = this.productReplicationSourceGeneration;
102
+ await this.runInProductIndexDBSaveQueue(async () => {
103
+ if (generation !== this.productReplicationSourceGeneration) return;
104
+ if (this.committedProductState) return;
105
+ if (!this.dbManager || this.productPersistenceFailed) {
106
+ throw new Error('Product replication requires a successful persistence baseline');
107
+ }
108
+ const products = await this.dbManager.getAll(INDEXDB_STORE_NAME);
109
+ if (generation !== this.productReplicationSourceGeneration) return;
110
+ if (!Array.isArray(products)) throw new Error('Invalid persisted product snapshot');
111
+ this.committedProductState = new _replication.CommittedProductState();
112
+ this.committedProductState.commit({
113
+ kind: 'snapshot',
114
+ upserts: products,
115
+ deletedIds: []
116
+ });
117
+ });
118
+ }
119
+
120
+ /** Release the extra catalog without changing the normal product store or persistence. */
121
+ disableProductReplicationSource() {
122
+ this.productReplicationSourceGeneration += 1;
123
+ this.committedProductState = undefined;
124
+ }
125
+ getCommittedProductSnapshot() {
126
+ return this.committedProductState?.snapshot() ?? null;
127
+ }
128
+ async recordProductCommit(change) {
129
+ this.productPersistenceFailed = false;
130
+ if (!this.committedProductState) return;
131
+ const committed = this.committedProductState.commit(change);
132
+ try {
133
+ await this.core.effects.emit(_types.ProductsHooks.onProductsCommitted, committed);
134
+ } catch (error) {
135
+ // Transport failure is not a persistence failure; peers recover using the snapshot.
136
+ this.logWarning('商品提交通知失败', {
137
+ revision: committed.revision,
138
+ error: String(error)
139
+ });
140
+ }
141
+ }
142
+ async applyReplicatedProducts(change) {
143
+ if (!this.isMemoryReplica()) throw new Error('Product module is not a memory replica');
144
+ const generation = this.runtimeSyncGeneration;
145
+ const apply = async () => {
146
+ if (!this.isMemoryReplica() || generation !== this.runtimeSyncGeneration) return;
147
+ const changedIds = [...new Set([...change.deletedIds, ...change.upserts.map(p => p.id)])];
148
+ if (change.kind === 'snapshot') {
149
+ this.store.list = change.upserts;
150
+ this.syncProductsMap();
151
+ this.clearPriceCache();
152
+ } else {
153
+ const removed = new Set(change.deletedIds);
154
+ const updates = new Map(change.upserts.map(p => [p.id, p]));
155
+ this.store.list = this.store.list.filter(p => !removed.has(p.id)).map(p => {
156
+ const next = updates.get(p.id);
157
+ if (!next) return p;
158
+ updates.delete(p.id);
159
+ return next;
160
+ });
161
+ this.store.list.push(...updates.values());
162
+ for (const id of removed) this.store.map.delete(id);
163
+ for (const product of change.upserts) this.store.map.set(product.id, product);
164
+ for (const [key, cached] of this.productsPriceCache) {
165
+ this.productsPriceCache.set(key, cached.filter(p => !removed.has(p.id)));
166
+ const dirty = this.replicaPriceDirtyIds.get(key) ?? new Set();
167
+ change.upserts.forEach(p => dirty.add(p.id));
168
+ removed.forEach(id => dirty.delete(id));
169
+ this.replicaPriceDirtyIds.set(key, dirty);
170
+ }
171
+ }
172
+ await this.core.effects.emit(_types.ProductsHooks.onProductsChanged, this.store.list);
173
+ if (generation !== this.runtimeSyncGeneration) return;
174
+ await this.core.effects.emit(_types.ProductsHooks.onProductsSyncCompleted, {
175
+ changedIds
176
+ });
177
+ };
178
+ const task = this.replicaApplyQueue.then(apply, apply);
179
+ this.replicaApplyQueue = task.catch(() => undefined);
180
+ return task;
181
+ }
182
+ async refreshFromReplica(trigger) {
183
+ await this.requestReplicaRefresh?.(trigger);
184
+ return this.store.list;
185
+ }
71
186
  constructor(name, version) {
72
187
  super(name, version);
73
188
  }
@@ -408,7 +523,8 @@ class ProductsModule extends _BaseModule.BaseModule {
408
523
  const scopedProductIds = this.normalizeProductIds(options?.productIds);
409
524
  const cacheKey = this.getProductsPriceCacheKey(schedule_date, extraContext, scopedProductIds);
410
525
  const scopedProductIdSet = scopedProductIds ? new Set(scopedProductIds) : undefined;
411
- const changedIds = scopedProductIdSet ? options?.changedIds?.filter(id => scopedProductIdSet.has(id)) : options?.changedIds;
526
+ const pendingIds = [...new Set([...(options?.changedIds ?? []), ...(this.replicaPriceDirtyIds.get(cacheKey) ?? [])])];
527
+ const changedIds = scopedProductIdSet ? pendingIds.filter(id => scopedProductIdSet.has(id)) : pendingIds;
412
528
  if (this.productsPriceCache.has(cacheKey)) {
413
529
  const cachedProducts = this.productsPriceCache.get(cacheKey);
414
530
 
@@ -423,7 +539,7 @@ class ProductsModule extends _BaseModule.BaseModule {
423
539
  const updatedProducts = await this.prepareProductsWithPrice(schedule_date, extraContext, {
424
540
  productIds: changedIds
425
541
  });
426
- if (updatedProducts.length > 0) {
542
+ if (updatedProducts.length > 0 || this.isMemoryReplica()) {
427
543
  // merge:用更新结果替换缓存中对应的商品,新增的追加
428
544
  const updatedMap = new Map(updatedProducts.map(p => [p.id, p]));
429
545
  const mergedCache = [];
@@ -431,7 +547,7 @@ class ProductsModule extends _BaseModule.BaseModule {
431
547
  if (updatedMap.has(p.id)) {
432
548
  mergedCache.push(updatedMap.get(p.id));
433
549
  updatedMap.delete(p.id);
434
- } else {
550
+ } else if (!this.isMemoryReplica() || !changedIds.includes(p.id)) {
435
551
  mergedCache.push(p);
436
552
  }
437
553
  }
@@ -440,6 +556,7 @@ class ProductsModule extends _BaseModule.BaseModule {
440
556
  mergedCache.push(p);
441
557
  }
442
558
  this.productsPriceCache.set(cacheKey, mergedCache);
559
+ this.replicaPriceDirtyIds.delete(cacheKey);
443
560
  this.logInfo('增量更新完成', {
444
561
  cacheKey,
445
562
  changedCount: updatedProducts.length,
@@ -485,6 +602,7 @@ class ProductsModule extends _BaseModule.BaseModule {
485
602
  productIds: scopedProductIds
486
603
  } : undefined);
487
604
  this.productsPriceCache.set(cacheKey, result);
605
+ this.replicaPriceDirtyIds.delete(cacheKey);
488
606
  this.logInfo('商品价格已缓存', {
489
607
  cacheKey,
490
608
  productCount: result.length
@@ -823,6 +941,7 @@ class ProductsModule extends _BaseModule.BaseModule {
823
941
  const keys = Array.from(this.productsPriceCache.keys());
824
942
  const oldestKey = keys[0];
825
943
  this.productsPriceCache.delete(oldestKey);
944
+ this.replicaPriceDirtyIds.delete(oldestKey);
826
945
  }
827
946
  }
828
947
 
@@ -832,6 +951,7 @@ class ProductsModule extends _BaseModule.BaseModule {
832
951
  */
833
952
  clearPriceCache() {
834
953
  this.productsPriceCache.clear();
954
+ this.replicaPriceDirtyIds.clear();
835
955
  this.quotationScheduleCache.clear();
836
956
  this.quotationScheduleCacheLoaded = false;
837
957
  }
@@ -840,6 +960,7 @@ class ProductsModule extends _BaseModule.BaseModule {
840
960
  * 通过 ProductDataSource SSE 加载完整商品列表
841
961
  */
842
962
  async loadProductsByServer(options = {}) {
963
+ if (this.isMemoryReplica()) return this.refreshFromReplica(options.trigger || 'loadProductsByServer');
843
964
  if (!this.productDataSource) {
844
965
  this.logWarning('loadProductsByServer: ProductDataSource 不可用');
845
966
  return [];
@@ -855,6 +976,7 @@ class ProductsModule extends _BaseModule.BaseModule {
855
976
  sse: {}
856
977
  });
857
978
  const list = productList || [];
979
+ if (this.isMemoryReplica()) return this.store.list;
858
980
  (0, _product.perfMark)('loadProductsByServer.SSE', performance.now() - tSSE, {
859
981
  count: list.length
860
982
  });
@@ -949,7 +1071,9 @@ class ProductsModule extends _BaseModule.BaseModule {
949
1071
  * @param params 查询参数
950
1072
  */
951
1073
  async loadProductsByServerHttp(params) {
1074
+ if (this.isMemoryReplica()) return this.refreshFromReplica('loadProductsByServerHttp');
952
1075
  const productList = await this.fetchProductsByHttp(params);
1076
+ if (this.isMemoryReplica()) return this.store.list;
953
1077
  if (productList.length > 0) {
954
1078
  await this.replaceProductsSnapshotInIndexDB(productList, 'loadProductsByServerHttp');
955
1079
  this.lastServerFullFetchAtMs = Date.now();
@@ -983,7 +1107,7 @@ class ProductsModule extends _BaseModule.BaseModule {
983
1107
  if (!product) continue;
984
1108
  snapshots[String(id)] = {
985
1109
  cover: product.cover,
986
- holder_config: product.holder_config
1110
+ holder_config: (0, _utils.resolveProductHolderConfig)(product) ?? undefined
987
1111
  };
988
1112
  }
989
1113
  return snapshots;
@@ -1022,7 +1146,11 @@ class ProductsModule extends _BaseModule.BaseModule {
1022
1146
  * 根据 ID 列表删除商品(用于 pubsub 同步删除场景)
1023
1147
  * 同时更新 store.list、store.map、IndexDB 和价格缓存
1024
1148
  */
1025
- async removeProductsByIds(ids) {
1149
+ async removeProductsByIds(ids, options) {
1150
+ if (this.isMemoryReplica() && !options?.skipIndexDB) {
1151
+ await this.refreshFromReplica('removeProducts');
1152
+ return;
1153
+ }
1026
1154
  const idSet = new Set(ids);
1027
1155
  this.logInfo('removeProductsByIds', {
1028
1156
  ids,
@@ -1032,23 +1160,11 @@ class ProductsModule extends _BaseModule.BaseModule {
1032
1160
  for (const id of ids) {
1033
1161
  this.store.map.delete(id);
1034
1162
  }
1035
- if (this.dbManager) {
1036
- try {
1037
- for (const id of ids) {
1038
- await this.dbManager.delete(INDEXDB_STORE_NAME, id);
1039
- }
1040
- } catch (error) {
1041
- const errorMessage = error instanceof Error ? error.message : String(error);
1042
- this.logError('removeProductsByIds: IndexDB 删除失败', {
1043
- ids,
1044
- error: errorMessage
1045
- });
1046
- }
1047
- }
1163
+ if (!options?.skipIndexDB) await this.persistProductDelta([], ids, 'removeProductsByIds');
1048
1164
  for (const [dateKey, cachedProducts] of this.productsPriceCache.entries()) {
1049
1165
  this.productsPriceCache.set(dateKey, cachedProducts.filter(p => !idSet.has(p.id)));
1050
1166
  }
1051
- this.core.effects.emit(_types.ProductsHooks.onProductsChanged, this.store.list);
1167
+ if (!options?.skipEmit) this.core.effects.emit(_types.ProductsHooks.onProductsChanged, this.store.list);
1052
1168
  this.logInfo('removeProductsByIds 完成', {
1053
1169
  remaining: this.store.list.length
1054
1170
  });
@@ -1059,6 +1175,7 @@ class ProductsModule extends _BaseModule.BaseModule {
1059
1175
  * 用于 pubsub 同步 create / update / batch_update 场景
1060
1176
  */
1061
1177
  async refreshProducts() {
1178
+ if (this.isMemoryReplica()) return this.refreshFromReplica('refreshProducts');
1062
1179
  const tTotal = performance.now();
1063
1180
  this.logInfo('refreshProducts 开始');
1064
1181
  const products = await this.loadProductsByServer({
@@ -1161,9 +1278,10 @@ class ProductsModule extends _BaseModule.BaseModule {
1161
1278
  * await this.replaceProductsSnapshotInIndexDB(list, 'loadProductsByServer')
1162
1279
  */
1163
1280
  async replaceProductsSnapshotInIndexDB(products, source) {
1281
+ if (this.isMemoryReplica()) return false;
1164
1282
  if (!this.dbManager) {
1165
1283
  this.logWarning('replaceProductsSnapshotInIndexDB: dbManager 不可用');
1166
- return;
1284
+ return false;
1167
1285
  }
1168
1286
  const enqueuedAt = performance.now();
1169
1287
  let queueWaitMs = 0;
@@ -1192,6 +1310,11 @@ class ProductsModule extends _BaseModule.BaseModule {
1192
1310
  writeDurationMs = +(performance.now() - writeStartedAt).toFixed(2);
1193
1311
  }
1194
1312
  }
1313
+ await this.recordProductCommit({
1314
+ kind: 'snapshot',
1315
+ upserts: products,
1316
+ deletedIds: []
1317
+ });
1195
1318
  const executeDurationMs = +(performance.now() - operationStartedAt).toFixed(2);
1196
1319
  (0, _product.perfMark)('replaceProductsSnapshotInIndexDB', executeDurationMs, {
1197
1320
  count: products.length
@@ -1206,7 +1329,9 @@ class ProductsModule extends _BaseModule.BaseModule {
1206
1329
  totalDurationMs: +(performance.now() - enqueuedAt).toFixed(2)
1207
1330
  });
1208
1331
  });
1332
+ return true;
1209
1333
  } catch (error) {
1334
+ this.productPersistenceFailed = true;
1210
1335
  const errorMessage = error instanceof Error ? error.message : String(error);
1211
1336
  this.logError('全量保存商品到 IndexDB 失败', {
1212
1337
  source,
@@ -1217,52 +1342,51 @@ class ProductsModule extends _BaseModule.BaseModule {
1217
1342
  totalDurationMs: +(performance.now() - enqueuedAt).toFixed(2),
1218
1343
  error: errorMessage
1219
1344
  });
1345
+ return false;
1220
1346
  }
1221
1347
  }
1222
-
1223
- /**
1224
- * 增量 upsert 商品到 IndexDB,不清空表;用于 pubsub 批次合并后的变更商品。
1225
- *
1226
- * @example
1227
- * await this.patchProductsInIndexDB(changedProducts, 'pubsub.processProductSyncMessages', mergeActions)
1228
- */
1229
- async patchProductsInIndexDB(products, source, mergeActions = {}) {
1230
- if (!this.dbManager || products.length === 0) {
1231
- return;
1232
- }
1348
+ async patchProductsInIndexDB(products, source, _mergeActions = {}) {
1349
+ return this.persistProductDelta(products, [], source);
1350
+ }
1351
+ async persistProductDelta(products, deletedIds, source) {
1352
+ if (this.isMemoryReplica() || !this.dbManager) return false;
1233
1353
  const validProducts = products.filter(p => p?.id !== undefined && p?.id !== null);
1234
- if (validProducts.length === 0) {
1235
- return;
1236
- }
1354
+ if (!validProducts.length && !deletedIds.length) return true;
1355
+ const startedAt = performance.now();
1237
1356
  try {
1238
1357
  await this.runInProductIndexDBSaveQueue(async () => {
1239
- this.logInfo('patchProductsInIndexDB 开始', {
1240
- source,
1241
- count: validProducts.length
1242
- });
1243
- const t0 = performance.now();
1244
- if (typeof this.dbManager.bulkUpdate === 'function') {
1245
- await this.dbManager.bulkUpdate(INDEXDB_STORE_NAME, validProducts);
1246
- } else if (typeof this.dbManager.update === 'function') {
1247
- for (const product of validProducts) {
1248
- await this.dbManager.update(INDEXDB_STORE_NAME, product);
1358
+ for (const id of deletedIds) await this.dbManager.delete(INDEXDB_STORE_NAME, id);
1359
+ if (validProducts.length) {
1360
+ if (typeof this.dbManager.bulkUpdate === 'function') {
1361
+ await this.dbManager.bulkUpdate(INDEXDB_STORE_NAME, validProducts);
1362
+ } else if (typeof this.dbManager.update === 'function') {
1363
+ for (const product of validProducts) await this.dbManager.update(INDEXDB_STORE_NAME, product);
1364
+ } else {
1365
+ throw new Error('Product persistence adapter has no upsert method');
1249
1366
  }
1250
1367
  }
1251
- (0, _product.perfMark)('patchProductsInIndexDB', performance.now() - t0, {
1252
- count: validProducts.length
1368
+ await this.recordProductCommit({
1369
+ kind: 'delta',
1370
+ upserts: validProducts,
1371
+ deletedIds
1253
1372
  });
1254
1373
  this.logInfo('patchProductsInIndexDB 完成', {
1255
1374
  source,
1256
- count: validProducts.length
1375
+ productCount: validProducts.length,
1376
+ deletedCount: deletedIds.length,
1377
+ durationMs: Math.round(performance.now() - startedAt)
1257
1378
  });
1258
1379
  });
1380
+ return true;
1259
1381
  } catch (error) {
1260
- const errorMessage = error instanceof Error ? error.message : String(error);
1261
- this.logError('增量保存商品到 IndexDB 失败', {
1382
+ this.productPersistenceFailed = true;
1383
+ this.logError('增量保存商品失败', {
1262
1384
  source,
1263
- error: errorMessage,
1264
- productCount: validProducts.length
1385
+ productCount: validProducts.length,
1386
+ deletedCount: deletedIds.length,
1387
+ error: String(error)
1265
1388
  });
1389
+ return false;
1266
1390
  }
1267
1391
  }
1268
1392
 
@@ -1287,6 +1411,10 @@ class ProductsModule extends _BaseModule.BaseModule {
1287
1411
  * 在模块注册后自动调用
1288
1412
  */
1289
1413
  async preload() {
1414
+ if (this.isMemoryReplica()) {
1415
+ await this.refreshFromReplica('preload');
1416
+ return;
1417
+ }
1290
1418
  const tTotal = performance.now();
1291
1419
  this.logInfo('开始预加载数据');
1292
1420
  try {
@@ -1391,6 +1519,7 @@ class ProductsModule extends _BaseModule.BaseModule {
1391
1519
  return;
1392
1520
  }
1393
1521
  const createHandler = channelKey => message => {
1522
+ if (this.isMemoryReplica()) return;
1394
1523
  const data = message?.data || message;
1395
1524
  if (!data) return;
1396
1525
  this.logInfo('收到同步消息', {
@@ -1449,14 +1578,25 @@ class ProductsModule extends _BaseModule.BaseModule {
1449
1578
  * 处理完成后 emit onProductsSyncCompleted(携带 changedIds),
1450
1579
  * Server 层监听该事件后对变更商品增量执行 prepareProductsWithPrice 并更新缓存
1451
1580
  */
1452
- async processProductSyncMessages() {
1581
+ processProductSyncMessages() {
1582
+ const run = () => this.processProductSyncBatch();
1583
+ const task = this.productSyncQueue.then(run, run);
1584
+ this.productSyncQueue = task.catch(error => {
1585
+ this.logError('商品同步批次失败', {
1586
+ error: String(error)
1587
+ });
1588
+ });
1589
+ return this.productSyncQueue;
1590
+ }
1591
+ async processProductSyncBatch() {
1592
+ if (this.isMemoryReplica()) return;
1453
1593
  const messages = [...this.pendingSyncMessages];
1454
1594
  this.pendingSyncMessages = [];
1455
1595
  if (messages.length === 0) return;
1456
1596
  this.logInfo('processProductSyncMessages: 开始处理', {
1457
1597
  count: messages.length
1458
1598
  });
1459
- const deleteIds = [];
1599
+ const deleteIds = new Set();
1460
1600
  const bodyUpdates = new Map();
1461
1601
  const sseRefreshIds = [];
1462
1602
  const priceRefreshIds = [];
@@ -1469,10 +1609,18 @@ class ProductsModule extends _BaseModule.BaseModule {
1469
1609
  }
1470
1610
  // 1. 删除场景:operation === 'delete' 或 action === 'delete'
1471
1611
  if (msg.operation === 'delete' || msg.action === 'delete') {
1472
- if (msg.ids?.length) deleteIds.push(...msg.ids);else if (msg.id) deleteIds.push(msg.id);
1612
+ const ids = msg.ids?.length ? msg.ids : msg.id ? [msg.id] : [];
1613
+ for (const id of ids) {
1614
+ deleteIds.add(id);
1615
+ bodyUpdates.delete(id);
1616
+ }
1473
1617
  continue;
1474
1618
  }
1475
1619
 
1620
+ // The last explicit product operation wins within the existing sync batch.
1621
+ for (const id of msg.ids || (msg.id ? [msg.id] : [])) deleteIds.delete(id);
1622
+ if (msg.body?.id) deleteIds.delete(msg.body.id);
1623
+
1476
1624
  // 2. change_types 包含 price → 仅收集需要刷新价格的 IDs(不走 SSE 拉取)
1477
1625
  if (msg.change_types?.includes('price')) {
1478
1626
  const ids = msg.ids || (msg.id ? [msg.id] : []);
@@ -1513,18 +1661,20 @@ class ProductsModule extends _BaseModule.BaseModule {
1513
1661
  }
1514
1662
  }
1515
1663
  }
1516
- const uniqueDeleteIds = [...new Set(deleteIds)];
1517
- const uniqueSSEIds = [...new Set(sseRefreshIds)];
1518
- const uniquePriceIds = [...new Set(priceRefreshIds)];
1664
+ const uniqueDeleteIds = [...deleteIds];
1665
+ const uniqueSSEIds = [...new Set(sseRefreshIds)].filter(id => !deleteIds.has(id));
1666
+ const uniquePriceIds = [...new Set(priceRefreshIds)].filter(id => !deleteIds.has(id));
1519
1667
 
1520
1668
  // 1. 处理删除
1521
1669
  if (uniqueDeleteIds.length > 0) {
1522
- await this.removeProductsByIds(uniqueDeleteIds);
1670
+ await this.removeProductsByIds(uniqueDeleteIds, {
1671
+ skipIndexDB: true,
1672
+ skipEmit: true
1673
+ });
1523
1674
  }
1524
1675
 
1525
1676
  /** 本批次待 patch 的商品(按 id 去重,SSE 覆盖 body) */
1526
1677
  const patchProductMap = new Map();
1527
- const patchMergeActions = {};
1528
1678
  let storeMutated = uniqueDeleteIds.length > 0;
1529
1679
 
1530
1680
  // 2. 处理 body 直接覆盖(仅内存,IndexDB 批次末统一 patch)
@@ -1535,7 +1685,6 @@ class ProductsModule extends _BaseModule.BaseModule {
1535
1685
  });
1536
1686
  for (const product of bodyResult.changedProducts) {
1537
1687
  patchProductMap.set(product.id, product);
1538
- patchMergeActions[product.id] = bodyResult.mergeActions[product.id];
1539
1688
  }
1540
1689
  storeMutated = true;
1541
1690
  }
@@ -1551,7 +1700,6 @@ class ProductsModule extends _BaseModule.BaseModule {
1551
1700
  });
1552
1701
  for (const product of mergeResult.changedProducts) {
1553
1702
  patchProductMap.set(product.id, product);
1554
- patchMergeActions[product.id] = mergeResult.mergeActions[product.id];
1555
1703
  }
1556
1704
  storeMutated = true;
1557
1705
  }
@@ -1561,8 +1709,14 @@ class ProductsModule extends _BaseModule.BaseModule {
1561
1709
  });
1562
1710
  }
1563
1711
  const patchProducts = [...patchProductMap.values()];
1564
- if (patchProducts.length > 0) {
1565
- await this.patchProductsInIndexDB(patchProducts, 'pubsub.processProductSyncMessages', patchMergeActions);
1712
+ // Publish one committed delta for the entire batch, including deletions and price-only IDs.
1713
+ const committedUpserts = new Map(patchProducts.map(p => [p.id, p]));
1714
+ for (const id of uniquePriceIds) {
1715
+ const product = this.store.map.get(id);
1716
+ if (product && !uniqueDeleteIds.includes(id)) committedUpserts.set(id, product);
1717
+ }
1718
+ if (committedUpserts.size || uniqueDeleteIds.length) {
1719
+ await this.persistProductDelta([...committedUpserts.values()], uniqueDeleteIds, 'pubsub.batch');
1566
1720
  }
1567
1721
  if (storeMutated) {
1568
1722
  this.core.effects.emit(_types.ProductsHooks.onProductsChanged, this.store.list);
@@ -1761,6 +1915,7 @@ class ProductsModule extends _BaseModule.BaseModule {
1761
1915
  * @returns 刷新后的商品列表
1762
1916
  */
1763
1917
  async silentRefresh(options = {}) {
1918
+ if (this.isMemoryReplica()) return this.refreshFromReplica(options.trigger || 'background');
1764
1919
  const t0 = performance.now();
1765
1920
  const now = Date.now();
1766
1921
  const trigger = options.trigger || 'background';
@@ -1820,6 +1975,9 @@ class ProductsModule extends _BaseModule.BaseModule {
1820
1975
  * 销毁同步资源(取消 pubsub 订阅、清除定时器)
1821
1976
  */
1822
1977
  destroyProductSync() {
1978
+ void this.core.effects.emit(_types.ProductsHooks.onProductSyncDestroyed, null);
1979
+ this.disableProductReplicationSource();
1980
+ this.requestReplicaRefresh = undefined;
1823
1981
  if (this.syncTimer) {
1824
1982
  clearTimeout(this.syncTimer);
1825
1983
  this.syncTimer = undefined;
@@ -1830,6 +1988,11 @@ class ProductsModule extends _BaseModule.BaseModule {
1830
1988
  this.pendingSyncMessages = [];
1831
1989
  this.logInfo('destroyProductSync: 同步资源已销毁');
1832
1990
  }
1991
+ destroy() {
1992
+ this.runtimeSyncGeneration += 1;
1993
+ this.destroyProductSync();
1994
+ super.destroy();
1995
+ }
1833
1996
 
1834
1997
  /**
1835
1998
  * 获取模块的路由定义
@@ -0,0 +1,8 @@
1
+ import type { CommittedProducts, ReplicatedProducts } from './types';
2
+ /** A separate committed view: failed writes must never leak into recovery snapshots. */
3
+ export declare class CommittedProductState {
4
+ private products;
5
+ private revision;
6
+ commit(change: ReplicatedProducts): CommittedProducts;
7
+ snapshot(): CommittedProducts;
8
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.CommittedProductState = void 0;
7
+ /** A separate committed view: failed writes must never leak into recovery snapshots. */
8
+ class CommittedProductState {
9
+ products = new Map();
10
+ revision = 0;
11
+ commit(change) {
12
+ const copy = structuredClone(change);
13
+ if (copy.kind === 'snapshot') this.products.clear();
14
+ for (const id of copy.deletedIds) this.products.delete(id);
15
+ for (const product of copy.upserts) this.products.set(product.id, product);
16
+ return {
17
+ ...copy,
18
+ revision: ++this.revision
19
+ };
20
+ }
21
+ snapshot() {
22
+ return {
23
+ kind: 'snapshot',
24
+ revision: this.revision,
25
+ upserts: structuredClone([...this.products.values()]),
26
+ deletedIds: []
27
+ };
28
+ }
29
+ }
30
+ exports.CommittedProductState = CommittedProductState;
@@ -32,6 +32,15 @@ export interface ProductSnapshot {
32
32
  cover?: ProductData['cover'];
33
33
  holder_config?: ProductData['holder_config'];
34
34
  }
35
+ /** Raw product data only; each screen applies its own query/pricing context. */
36
+ export interface ReplicatedProducts {
37
+ kind: 'snapshot' | 'delta';
38
+ upserts: ProductData[];
39
+ deletedIds: number[];
40
+ }
41
+ export interface CommittedProducts extends ReplicatedProducts {
42
+ revision: number;
43
+ }
35
44
  /**
36
45
  * 加载报价单商品价格参数
37
46
  */
@@ -96,7 +105,9 @@ export declare enum ProductsHooks {
96
105
  onProductSelected = "products:onProductSelected",
97
106
  onProductsPriceApplied = "products:onProductsPriceApplied",
98
107
  /** pubsub 同步批次处理完成后触发,Server 层监听此事件重新计算并推送 */
99
- onProductsSyncCompleted = "products:onProductsSyncCompleted"
108
+ onProductsSyncCompleted = "products:onProductsSyncCompleted",
109
+ onProductsCommitted = "products:onProductsCommitted",
110
+ onProductSyncDestroyed = "products:onProductSyncDestroyed"
100
111
  }
101
112
  /** pubsub 商品同步消息结构 */
102
113
  export interface ProductSyncMessage {
@@ -13,6 +13,7 @@ exports.ProductsHooks = void 0;
13
13
  /**
14
14
  * 商品轻量查询范围
15
15
  */
16
+ /** Raw product data only; each screen applies its own query/pricing context. */
16
17
  /**
17
18
  * 加载报价单商品价格参数
18
19
  */
@@ -34,6 +35,8 @@ let ProductsHooks = exports.ProductsHooks = /*#__PURE__*/function (ProductsHooks
34
35
  ProductsHooks["onProductSelected"] = "products:onProductSelected";
35
36
  ProductsHooks["onProductsPriceApplied"] = "products:onProductsPriceApplied";
36
37
  ProductsHooks["onProductsSyncCompleted"] = "products:onProductsSyncCompleted";
38
+ ProductsHooks["onProductsCommitted"] = "products:onProductsCommitted";
39
+ ProductsHooks["onProductSyncDestroyed"] = "products:onProductSyncDestroyed";
37
40
  return ProductsHooks;
38
41
  }({});
39
42
  /** pubsub 商品同步消息结构 */