@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.
- package/dist/model/strategy/adapter/promotion/evaluator.js +89 -104
- package/dist/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.d.ts +3 -1
- package/dist/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +28 -4
- package/dist/modules/Holder/utils.d.ts +2 -0
- package/dist/modules/Holder/utils.js +32 -2
- package/dist/modules/OpenData/contactInfo.d.ts +20 -0
- package/dist/modules/OpenData/contactInfo.js +70 -0
- package/dist/modules/OpenData/index.d.ts +1 -0
- package/dist/modules/OpenData/index.js +1 -0
- package/dist/modules/OpenData/types.d.ts +9 -0
- package/dist/modules/Order/holderRelations.d.ts +14 -0
- package/dist/modules/Order/holderRelations.js +154 -0
- package/dist/modules/Order/index.d.ts +2 -2
- package/dist/modules/Order/index.js +89 -44
- package/dist/modules/Order/types.d.ts +19 -1
- package/dist/modules/Order/types.js +2 -0
- package/dist/modules/Order/utils/discountProductLineIdentity.d.ts +1 -1
- package/dist/modules/Order/utils/discountProductLineIdentity.js +142 -7
- package/dist/modules/Order/utils/orderCollectionIdentity.js +24 -10
- package/dist/modules/Order/utils.d.ts +2 -1
- package/dist/modules/Order/utils.js +36 -10
- package/dist/modules/Rules/index.js +62 -61
- package/dist/server/index.js +36 -28
- package/dist/server/modules/order/index.d.ts +4 -0
- package/dist/server/modules/order/index.js +255 -190
- package/dist/server/modules/order/types.d.ts +3 -0
- package/dist/server/modules/products/index.d.ts +28 -7
- package/dist/server/modules/products/index.js +1297 -770
- package/dist/server/modules/products/replication.d.ts +8 -0
- package/dist/server/modules/products/replication.js +69 -0
- package/dist/server/modules/products/types.d.ts +12 -1
- package/dist/server/modules/products/types.js +4 -0
- package/dist/shared/receipt/small-ticket.js +24 -12
- package/dist/shared/receipt/templateVisibility.d.ts +3 -0
- package/dist/shared/receipt/templateVisibility.js +6 -2
- package/dist/shared/sales/loadSalesOrderSnapshot.js +1 -1
- package/dist/solution/BaseSales/index.d.ts +4 -0
- package/dist/solution/BaseSales/index.js +1116 -935
- package/dist/solution/BaseSales/types.d.ts +2 -1
- package/dist/solution/BaseSales/utils/parseSalesResponse.d.ts +3 -1
- package/dist/solution/BaseSales/utils/parseSalesResponse.js +7 -3
- package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +14 -4
- package/dist/solution/BookingByStep/index.d.ts +1 -1
- package/dist/solution/BookingTicket/index.d.ts +15 -1
- package/dist/solution/BookingTicket/index.js +784 -501
- package/dist/solution/ScanOrder/index.js +65 -31
- package/dist/solution/ScanOrder/types.d.ts +3 -0
- package/dist/solution/UnifiedBookingSales/index.js +1 -1
- package/lib/model/strategy/adapter/promotion/evaluator.js +9 -14
- package/lib/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.d.ts +3 -1
- package/lib/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +17 -1
- package/lib/modules/Holder/utils.d.ts +2 -0
- package/lib/modules/Holder/utils.js +27 -1
- package/lib/modules/OpenData/contactInfo.d.ts +20 -0
- package/lib/modules/OpenData/contactInfo.js +56 -0
- package/lib/modules/OpenData/index.d.ts +1 -0
- package/lib/modules/OpenData/index.js +12 -0
- package/lib/modules/OpenData/types.d.ts +9 -0
- package/lib/modules/Order/holderRelations.d.ts +14 -0
- package/lib/modules/Order/holderRelations.js +98 -0
- package/lib/modules/Order/index.d.ts +2 -2
- package/lib/modules/Order/index.js +44 -12
- package/lib/modules/Order/types.d.ts +19 -1
- package/lib/modules/Order/types.js +1 -0
- package/lib/modules/Order/utils/discountProductLineIdentity.d.ts +1 -1
- package/lib/modules/Order/utils/discountProductLineIdentity.js +51 -2
- package/lib/modules/Order/utils/orderCollectionIdentity.js +5 -0
- package/lib/modules/Order/utils.d.ts +2 -1
- package/lib/modules/Order/utils.js +28 -6
- package/lib/modules/Rules/index.js +2 -1
- package/lib/server/index.js +8 -2
- package/lib/server/modules/order/index.d.ts +4 -0
- package/lib/server/modules/order/index.js +46 -4
- package/lib/server/modules/order/types.d.ts +3 -0
- package/lib/server/modules/products/index.d.ts +28 -7
- package/lib/server/modules/products/index.js +226 -63
- package/lib/server/modules/products/replication.d.ts +8 -0
- package/lib/server/modules/products/replication.js +30 -0
- package/lib/server/modules/products/types.d.ts +12 -1
- package/lib/server/modules/products/types.js +3 -0
- package/lib/shared/receipt/small-ticket.js +22 -13
- package/lib/shared/receipt/templateVisibility.d.ts +3 -0
- package/lib/shared/receipt/templateVisibility.js +6 -2
- package/lib/shared/sales/loadSalesOrderSnapshot.js +1 -1
- package/lib/solution/BaseSales/index.d.ts +4 -0
- package/lib/solution/BaseSales/index.js +68 -2
- package/lib/solution/BaseSales/types.d.ts +2 -1
- package/lib/solution/BaseSales/utils/parseSalesResponse.d.ts +3 -1
- package/lib/solution/BaseSales/utils/parseSalesResponse.js +4 -0
- package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +16 -8
- package/lib/solution/BookingByStep/index.d.ts +1 -1
- package/lib/solution/BookingTicket/index.d.ts +15 -1
- package/lib/solution/BookingTicket/index.js +132 -3
- package/lib/solution/ScanOrder/index.js +59 -20
- package/lib/solution/ScanOrder/types.d.ts +3 -0
- package/lib/solution/UnifiedBookingSales/index.js +1 -1
- 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
|
|
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
|
|
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.
|
|
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
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
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
|
|
1235
|
-
|
|
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.
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
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
|
-
|
|
1252
|
-
|
|
1368
|
+
await this.recordProductCommit({
|
|
1369
|
+
kind: 'delta',
|
|
1370
|
+
upserts: validProducts,
|
|
1371
|
+
deletedIds
|
|
1253
1372
|
});
|
|
1254
1373
|
this.logInfo('patchProductsInIndexDB 完成', {
|
|
1255
1374
|
source,
|
|
1256
|
-
|
|
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
|
-
|
|
1261
|
-
this.logError('
|
|
1382
|
+
this.productPersistenceFailed = true;
|
|
1383
|
+
this.logError('增量保存商品失败', {
|
|
1262
1384
|
source,
|
|
1263
|
-
|
|
1264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = [...
|
|
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
|
-
|
|
1565
|
-
|
|
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 商品同步消息结构 */
|