@pisell/pisellos 0.10.28 → 0.10.30
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/core/index.d.ts +7 -0
- package/dist/core/index.js +108 -65
- package/dist/model/strategy/adapter/dataVariant/evaluator.js +13 -2
- package/dist/model/strategy/adapter/promotion/index.js +0 -9
- package/dist/modules/Discount/index.js +4 -1
- package/dist/modules/Discount/types.d.ts +14 -4
- package/dist/modules/Order/index.d.ts +3 -1
- package/dist/modules/Order/index.js +89 -16
- package/dist/modules/Order/types.d.ts +13 -0
- package/dist/modules/Order/utils.d.ts +17 -2
- package/dist/modules/Order/utils.js +133 -27
- package/dist/modules/Quotation/index.d.ts +6 -0
- package/dist/modules/Quotation/index.js +77 -25
- package/dist/modules/ResourcePlanner/planner.js +14 -4
- package/dist/modules/ResourcePlanner/types.d.ts +2 -0
- package/dist/plugins/request.d.ts +1 -0
- package/dist/server/index.d.ts +13 -2
- package/dist/server/index.js +1531 -1230
- package/dist/server/modules/floor-plan/index.d.ts +4 -0
- package/dist/server/modules/floor-plan/index.js +240 -98
- package/dist/server/modules/menu/index.js +48 -23
- package/dist/server/modules/order/index.d.ts +17 -7
- package/dist/server/modules/order/index.js +419 -207
- package/dist/server/modules/order/types.d.ts +50 -0
- package/dist/server/modules/order/types.js +2 -0
- package/dist/server/modules/products/index.d.ts +10 -4
- package/dist/server/modules/products/index.js +177 -84
- package/dist/server/modules/resource/index.js +21 -13
- package/dist/server/utils/product.d.ts +1 -1
- package/dist/server/utils/product.js +186 -22
- package/dist/server/utils/small-ticket.d.ts +4 -1
- package/dist/server/utils/small-ticket.js +543 -101
- package/dist/solution/BaseSales/index.d.ts +3 -3
- package/dist/solution/BaseSales/index.js +171 -198
- package/dist/solution/BaseSales/utils/parseSalesResponse.d.ts +2 -0
- package/dist/solution/BaseSales/utils/parseSalesResponse.js +1 -1
- package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +3 -2
- package/dist/solution/BookingByStep/index.d.ts +2 -2
- package/dist/solution/Sales/index.d.ts +7 -0
- package/dist/solution/Sales/index.js +87 -31
- package/dist/solution/UnifiedBookingSales/index.d.ts +2 -2
- package/dist/solution/UnifiedBookingSales/index.js +38 -21
- package/lib/core/index.d.ts +7 -0
- package/lib/core/index.js +20 -2
- package/lib/model/strategy/adapter/dataVariant/evaluator.js +12 -2
- package/lib/model/strategy/adapter/promotion/index.js +46 -1
- package/lib/modules/Discount/index.js +4 -1
- package/lib/modules/Discount/types.d.ts +14 -4
- package/lib/modules/Order/index.d.ts +3 -1
- package/lib/modules/Order/index.js +69 -1
- package/lib/modules/Order/types.d.ts +13 -0
- package/lib/modules/Order/utils.d.ts +17 -2
- package/lib/modules/Order/utils.js +131 -18
- package/lib/modules/Quotation/index.d.ts +6 -0
- package/lib/modules/Quotation/index.js +52 -8
- package/lib/modules/ResourcePlanner/planner.js +9 -1
- package/lib/modules/ResourcePlanner/types.d.ts +2 -0
- package/lib/plugins/request.d.ts +1 -0
- package/lib/server/index.d.ts +13 -2
- package/lib/server/index.js +210 -19
- package/lib/server/modules/floor-plan/index.d.ts +4 -0
- package/lib/server/modules/floor-plan/index.js +60 -7
- package/lib/server/modules/menu/index.js +26 -4
- package/lib/server/modules/order/index.d.ts +17 -7
- package/lib/server/modules/order/index.js +194 -57
- package/lib/server/modules/order/types.d.ts +50 -0
- package/lib/server/modules/order/types.js +1 -0
- package/lib/server/modules/products/index.d.ts +10 -4
- package/lib/server/modules/products/index.js +93 -23
- package/lib/server/modules/resource/index.js +12 -2
- package/lib/server/utils/product.d.ts +1 -1
- package/lib/server/utils/product.js +163 -3
- package/lib/server/utils/small-ticket.d.ts +4 -1
- package/lib/server/utils/small-ticket.js +507 -75
- package/lib/solution/BaseSales/index.d.ts +3 -3
- package/lib/solution/BaseSales/index.js +9 -14
- package/lib/solution/BaseSales/utils/parseSalesResponse.d.ts +2 -0
- package/lib/solution/BaseSales/utils/parseSalesResponse.js +1 -1
- package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +3 -2
- package/lib/solution/BookingByStep/index.d.ts +2 -2
- package/lib/solution/Sales/index.d.ts +7 -0
- package/lib/solution/Sales/index.js +48 -3
- package/lib/solution/UnifiedBookingSales/index.d.ts +2 -2
- package/lib/solution/UnifiedBookingSales/index.js +19 -3
- package/package.json +1 -1
|
@@ -213,7 +213,11 @@ function evaluateCandidateTimeGate(params) {
|
|
|
213
213
|
};
|
|
214
214
|
}
|
|
215
215
|
if (policy.type === 'before_start') {
|
|
216
|
-
|
|
216
|
+
// Flexible-duration candidates start at minute precision. Keep the current
|
|
217
|
+
// minute selectable instead of treating the freshly generated candidate as
|
|
218
|
+
// already started because the query clock still contains seconds.
|
|
219
|
+
const hasNotReachedStart = product.isFlexibleDuration ? floorToMinute(now) <= candidateRange.start : now < candidateRange.start;
|
|
220
|
+
if (hasNotReachedStart) return null;
|
|
217
221
|
return {
|
|
218
222
|
status: 'unavailable',
|
|
219
223
|
reasonCodes: ['booking_cutoff_before_start'],
|
|
@@ -946,6 +950,7 @@ function getProductTimeRanges(projection, request = {}, runtimeOptions = {}) {
|
|
|
946
950
|
}
|
|
947
951
|
const requiredGroups = (product.requirementGroups || []).filter(group => group.required !== false);
|
|
948
952
|
const operatingRanges = buildOperatingRanges(projection, dateRange);
|
|
953
|
+
const operatingEndAt = dateRange.startDate === dateRange.endDate && operatingRanges.length > 0 ? formatDateTime(Math.max(...operatingRanges.map(range => range.end)), projection.meta.timezone, fixedOffsetMinutes) : undefined;
|
|
949
954
|
const resourceRanges = requiredGroups.length ? requiredGroups.map(group => buildGroupWorkingRanges({
|
|
950
955
|
projection,
|
|
951
956
|
product,
|
|
@@ -1012,6 +1017,9 @@ function getProductTimeRanges(projection, request = {}, runtimeOptions = {}) {
|
|
|
1012
1017
|
title: product.title,
|
|
1013
1018
|
kind: product.kind,
|
|
1014
1019
|
slotStepMinutes,
|
|
1020
|
+
...(operatingEndAt ? {
|
|
1021
|
+
operatingEndAt
|
|
1022
|
+
} : {}),
|
|
1015
1023
|
ranges: dedupeProductRanges(ranges)
|
|
1016
1024
|
};
|
|
1017
1025
|
});
|
|
@@ -213,6 +213,8 @@ export interface ProductTimeRangeGroup {
|
|
|
213
213
|
title?: string;
|
|
214
214
|
kind: AvailabilityProductKind;
|
|
215
215
|
slotStepMinutes?: number;
|
|
216
|
+
/** 单日查询对应营业窗口的最终结束时刻,不受资源排班是否为空影响。 */
|
|
217
|
+
operatingEndAt?: string;
|
|
216
218
|
ranges: ProductTimeRange[];
|
|
217
219
|
}
|
|
218
220
|
export interface AvailabilityAssignment extends AvailabilityAbsoluteRange {
|
package/lib/plugins/request.d.ts
CHANGED
package/lib/server/index.d.ts
CHANGED
|
@@ -51,6 +51,8 @@ declare class Server {
|
|
|
51
51
|
private productQueryScheduleTimers;
|
|
52
52
|
/** 餐牌筛选为空时才触发的菜单/日程缓存自愈,避免每次商品查询都请求远端。 */
|
|
53
53
|
private menuScheduleRefreshInFlight;
|
|
54
|
+
/** 启动阶段模块预加载任务;预渲染商品查询复用该任务,避免缓存尚未 ready 时误触发远端自愈。 */
|
|
55
|
+
private startupModulePreloadInFlight;
|
|
54
56
|
private lastMenuScheduleRefreshAt;
|
|
55
57
|
private readonly MENU_SCHEDULE_REFRESH_COOLDOWN_MS;
|
|
56
58
|
private orderQuerySubscribers;
|
|
@@ -126,6 +128,9 @@ declare class Server {
|
|
|
126
128
|
private runCheckoutSync;
|
|
127
129
|
private omitCheckoutSyncMode;
|
|
128
130
|
private buildRemoteCheckoutData;
|
|
131
|
+
private buildCheckoutDeviceHeaders;
|
|
132
|
+
private applyKioskCheckoutOperator;
|
|
133
|
+
private resolveKioskCheckoutOperatorId;
|
|
129
134
|
private mergeCheckoutSyncPayments;
|
|
130
135
|
private persistSyncedCheckoutOrder;
|
|
131
136
|
/**
|
|
@@ -157,13 +162,17 @@ declare class Server {
|
|
|
157
162
|
* 重新拉取全量 SSE 接口,更新本地数据后触发 onProductsSyncCompleted
|
|
158
163
|
* 不影响当前界面展示,适用于切回前台、定时刷新等场景
|
|
159
164
|
*/
|
|
160
|
-
refreshProductsInBackground(
|
|
165
|
+
refreshProductsInBackground(options?: {
|
|
166
|
+
trigger?: string;
|
|
167
|
+
}): Promise<void>;
|
|
161
168
|
/**
|
|
162
169
|
* 后台静默刷新订单数据
|
|
163
170
|
* 重新拉取全量 SSE 接口,更新本地数据后触发 onOrdersSyncCompleted
|
|
164
171
|
* 不影响当前界面展示,适用于切回前台、定时刷新等场景
|
|
165
172
|
*/
|
|
166
|
-
refreshOrdersInBackground(
|
|
173
|
+
refreshOrdersInBackground(options?: {
|
|
174
|
+
trigger?: string;
|
|
175
|
+
}): Promise<void>;
|
|
167
176
|
/**
|
|
168
177
|
* 编辑正式订单时强制查询 Server 最新快照并覆盖本地缓存。
|
|
169
178
|
* 商品成员增删由 OrderModule 在 SQLite/Store 更新完成后广播,
|
|
@@ -524,6 +533,8 @@ declare class Server {
|
|
|
524
533
|
private getLocalPrintOrderLookup;
|
|
525
534
|
private mergeLocalPrintOrderInput;
|
|
526
535
|
private handleLocalPrintOrder;
|
|
536
|
+
private normalizeRequestedReceiptLocales;
|
|
537
|
+
private handleLocalOrderReceiptSnapshot;
|
|
527
538
|
private handleMachinecodeBatchMeta;
|
|
528
539
|
private resolveMachinecodeBatchMetadataLookup;
|
|
529
540
|
private syncMachinecodeRedeemStatusToLocalOrder;
|
package/lib/server/index.js
CHANGED
|
@@ -103,6 +103,8 @@ class Server {
|
|
|
103
103
|
productQueryScheduleTimers = new Map();
|
|
104
104
|
/** 餐牌筛选为空时才触发的菜单/日程缓存自愈,避免每次商品查询都请求远端。 */
|
|
105
105
|
menuScheduleRefreshInFlight = null;
|
|
106
|
+
/** 启动阶段模块预加载任务;预渲染商品查询复用该任务,避免缓存尚未 ready 时误触发远端自愈。 */
|
|
107
|
+
startupModulePreloadInFlight = null;
|
|
106
108
|
lastMenuScheduleRefreshAt = 0;
|
|
107
109
|
MENU_SCHEDULE_REFRESH_COOLDOWN_MS = 30 * 1000;
|
|
108
110
|
|
|
@@ -505,7 +507,15 @@ class Server {
|
|
|
505
507
|
|
|
506
508
|
// 自动预加载数据
|
|
507
509
|
if (autoPreload) {
|
|
508
|
-
|
|
510
|
+
const preloadTask = this.preloadModulesData(registeredModules, options);
|
|
511
|
+
this.startupModulePreloadInFlight = preloadTask;
|
|
512
|
+
try {
|
|
513
|
+
await preloadTask;
|
|
514
|
+
} finally {
|
|
515
|
+
if (this.startupModulePreloadInFlight === preloadTask) {
|
|
516
|
+
this.startupModulePreloadInFlight = null;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
509
519
|
} else {
|
|
510
520
|
this.logInfo('跳过自动预加载', {
|
|
511
521
|
autoPreload
|
|
@@ -634,12 +644,16 @@ class Server {
|
|
|
634
644
|
const numberedData = await this.ensureCheckoutOrderNumbers(normalizedData, title);
|
|
635
645
|
const checkoutData = this.normalizeCheckoutCollections(this.applyCheckoutFulfillmentBuzzer(numberedData, title));
|
|
636
646
|
const remoteCheckoutData = this.buildRemoteCheckoutData(checkoutData, title);
|
|
647
|
+
const deviceHeaders = await this.buildCheckoutDeviceHeaders(title);
|
|
637
648
|
if (!this.app?.request?.post) {
|
|
638
649
|
throw new Error('app.request 不可用');
|
|
639
650
|
}
|
|
640
651
|
const response = await this.app.request.post(backendPath, remoteCheckoutData, {
|
|
641
652
|
isShopApi: true,
|
|
642
|
-
customToast: () => {}
|
|
653
|
+
customToast: () => {},
|
|
654
|
+
...(Object.keys(deviceHeaders).length > 0 ? {
|
|
655
|
+
headers: deviceHeaders
|
|
656
|
+
} : {})
|
|
643
657
|
});
|
|
644
658
|
if (this.isCheckoutResponseRejected(response)) {
|
|
645
659
|
return {
|
|
@@ -705,6 +719,7 @@ class Server {
|
|
|
705
719
|
const record = next;
|
|
706
720
|
delete record[OS_FULFILLMENT_REF_MODE_FIELD];
|
|
707
721
|
delete record[OS_INVOICE_LAYOUT_CONFIG_FIELD];
|
|
722
|
+
this.applyKioskCheckoutOperator(record);
|
|
708
723
|
if (!Array.isArray(record.payments)) return next;
|
|
709
724
|
const originalPayments = record.payments;
|
|
710
725
|
const filteredPayments = originalPayments.filter(payment => !(0, _paymentUtils.isPendingOrderPayment)(payment));
|
|
@@ -719,6 +734,44 @@ class Server {
|
|
|
719
734
|
});
|
|
720
735
|
return next;
|
|
721
736
|
}
|
|
737
|
+
async buildCheckoutDeviceHeaders(title) {
|
|
738
|
+
const getAsyncIotDeviceInfo = this.getAppData('async_iot_device_info');
|
|
739
|
+
if (!getAsyncIotDeviceInfo) return {};
|
|
740
|
+
let deviceInfo;
|
|
741
|
+
try {
|
|
742
|
+
deviceInfo = typeof getAsyncIotDeviceInfo === 'function' ? await getAsyncIotDeviceInfo() : getAsyncIotDeviceInfo;
|
|
743
|
+
} catch (error) {
|
|
744
|
+
this.logWarning(`${title}: 读取 async_iot_device_info 失败,跳过 checkout 设备请求头`, {
|
|
745
|
+
error: this.getUnknownErrorMessage(error)
|
|
746
|
+
});
|
|
747
|
+
return {};
|
|
748
|
+
}
|
|
749
|
+
if (!deviceInfo || typeof deviceInfo !== 'object') return {};
|
|
750
|
+
const headers = {};
|
|
751
|
+
const assignHeader = (key, value) => {
|
|
752
|
+
if (this.isBlankCheckoutValue(value)) return;
|
|
753
|
+
headers[key] = String(value);
|
|
754
|
+
};
|
|
755
|
+
assignHeader('Device-ID', deviceInfo.id);
|
|
756
|
+
assignHeader('Device-Type-Code', deviceInfo.type_code);
|
|
757
|
+
assignHeader('Device-Model-Code', deviceInfo.model_code);
|
|
758
|
+
return headers;
|
|
759
|
+
}
|
|
760
|
+
applyKioskCheckoutOperator(record) {
|
|
761
|
+
if (record.platform !== 'kiosk') return;
|
|
762
|
+
record.operator_type = 'customer';
|
|
763
|
+
record.operator_id = this.resolveKioskCheckoutOperatorId(record.customer_id);
|
|
764
|
+
}
|
|
765
|
+
resolveKioskCheckoutOperatorId(customerId) {
|
|
766
|
+
if (customerId === undefined || customerId === null) return 1;
|
|
767
|
+
if (typeof customerId === 'string') {
|
|
768
|
+
const trimmed = customerId.trim();
|
|
769
|
+
if (trimmed === '' || trimmed === '0' || trimmed === '1') return 1;
|
|
770
|
+
return customerId;
|
|
771
|
+
}
|
|
772
|
+
if (customerId === 0 || customerId === 1) return 1;
|
|
773
|
+
return customerId;
|
|
774
|
+
}
|
|
722
775
|
mergeCheckoutSyncPayments(checkoutData, fresh, title) {
|
|
723
776
|
const localPayments = Array.isArray(checkoutData?.payments) ? checkoutData.payments : [];
|
|
724
777
|
const freshRecord = fresh;
|
|
@@ -820,11 +873,24 @@ class Server {
|
|
|
820
873
|
return;
|
|
821
874
|
}
|
|
822
875
|
try {
|
|
823
|
-
await this.schedule.loadAllSchedule();
|
|
824
876
|
this.products.clearPriceCache();
|
|
825
877
|
await this.products.setupQuotationPriceBridge({
|
|
826
878
|
scheduleModule: this.schedule,
|
|
827
|
-
channel: this.core.context?.channel
|
|
879
|
+
channel: this.core.context?.channel,
|
|
880
|
+
onQuotationUpdated: () => {
|
|
881
|
+
void this.recomputeAndNotifyProductQuery();
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
// Schedule 已在模块 preload 阶段加载完成。价格桥接先使用本地数据,
|
|
886
|
+
// 远端刷新转为后台校准,避免二次启动重复阻塞请求。
|
|
887
|
+
void this.schedule.loadAllSchedule().then(() => {
|
|
888
|
+
this.products?.clearPriceCache();
|
|
889
|
+
void this.recomputeAndNotifyProductQuery();
|
|
890
|
+
}).catch(error => {
|
|
891
|
+
this.logWarning('Products 报价单价格桥接后台刷新 Schedule 失败,继续使用本地数据', {
|
|
892
|
+
error: error instanceof Error ? error.message : String(error)
|
|
893
|
+
});
|
|
828
894
|
});
|
|
829
895
|
this.logInfo('Products 报价单价格桥接初始化完成');
|
|
830
896
|
} catch (error) {
|
|
@@ -883,17 +949,23 @@ class Server {
|
|
|
883
949
|
* 重新拉取全量 SSE 接口,更新本地数据后触发 onProductsSyncCompleted
|
|
884
950
|
* 不影响当前界面展示,适用于切回前台、定时刷新等场景
|
|
885
951
|
*/
|
|
886
|
-
async refreshProductsInBackground() {
|
|
952
|
+
async refreshProductsInBackground(options = {}) {
|
|
887
953
|
if (!this.products) {
|
|
888
954
|
this.logWarning('refreshProductsInBackground: Products 模块未注册');
|
|
889
955
|
return;
|
|
890
956
|
}
|
|
891
|
-
|
|
957
|
+
const trigger = options.trigger || 'background';
|
|
958
|
+
this.logInfo('refreshProductsInBackground 开始', {
|
|
959
|
+
trigger
|
|
960
|
+
});
|
|
892
961
|
const startTime = Date.now();
|
|
893
962
|
try {
|
|
894
|
-
await this.products.silentRefresh(
|
|
963
|
+
await this.products.silentRefresh({
|
|
964
|
+
trigger
|
|
965
|
+
});
|
|
895
966
|
const duration = Date.now() - startTime;
|
|
896
967
|
this.logInfo('refreshProductsInBackground 完成', {
|
|
968
|
+
trigger,
|
|
897
969
|
duration: `${duration}ms`
|
|
898
970
|
});
|
|
899
971
|
} catch (error) {
|
|
@@ -901,6 +973,7 @@ class Server {
|
|
|
901
973
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
902
974
|
console.error('[Server] refreshProductsInBackground 失败:', error);
|
|
903
975
|
this.logError('refreshProductsInBackground 失败', {
|
|
976
|
+
trigger,
|
|
904
977
|
duration: `${duration}ms`,
|
|
905
978
|
error: errorMessage
|
|
906
979
|
});
|
|
@@ -912,17 +985,23 @@ class Server {
|
|
|
912
985
|
* 重新拉取全量 SSE 接口,更新本地数据后触发 onOrdersSyncCompleted
|
|
913
986
|
* 不影响当前界面展示,适用于切回前台、定时刷新等场景
|
|
914
987
|
*/
|
|
915
|
-
async refreshOrdersInBackground() {
|
|
988
|
+
async refreshOrdersInBackground(options = {}) {
|
|
916
989
|
if (!this.order) {
|
|
917
990
|
this.logWarning('refreshOrdersInBackground: Order 模块未注册');
|
|
918
991
|
return;
|
|
919
992
|
}
|
|
920
|
-
|
|
993
|
+
const trigger = options.trigger || 'background';
|
|
994
|
+
this.logInfo('refreshOrdersInBackground 开始', {
|
|
995
|
+
trigger
|
|
996
|
+
});
|
|
921
997
|
const startTime = Date.now();
|
|
922
998
|
try {
|
|
923
|
-
await this.order.silentRefresh(
|
|
999
|
+
await this.order.silentRefresh({
|
|
1000
|
+
trigger
|
|
1001
|
+
});
|
|
924
1002
|
const duration = Date.now() - startTime;
|
|
925
1003
|
this.logInfo('refreshOrdersInBackground 完成', {
|
|
1004
|
+
trigger,
|
|
926
1005
|
duration: `${duration}ms`
|
|
927
1006
|
});
|
|
928
1007
|
} catch (error) {
|
|
@@ -930,6 +1009,7 @@ class Server {
|
|
|
930
1009
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
931
1010
|
console.error('[Server] refreshOrdersInBackground 失败:', error);
|
|
932
1011
|
this.logError('refreshOrdersInBackground 失败', {
|
|
1012
|
+
trigger,
|
|
933
1013
|
duration: `${duration}ms`,
|
|
934
1014
|
error: errorMessage
|
|
935
1015
|
});
|
|
@@ -1240,6 +1320,10 @@ class Server {
|
|
|
1240
1320
|
method: 'post',
|
|
1241
1321
|
path: '/shop/local/print-order',
|
|
1242
1322
|
handler: this.handleLocalPrintOrder.bind(this)
|
|
1323
|
+
}, {
|
|
1324
|
+
method: 'post',
|
|
1325
|
+
path: '/shop/local/order-receipt-snapshot',
|
|
1326
|
+
handler: this.handleLocalOrderReceiptSnapshot.bind(this)
|
|
1243
1327
|
}, {
|
|
1244
1328
|
method: 'post',
|
|
1245
1329
|
path: '/shop/local/getLocalPayment',
|
|
@@ -1466,7 +1550,7 @@ class Server {
|
|
|
1466
1550
|
// timerIds.push(timerId);
|
|
1467
1551
|
// }
|
|
1468
1552
|
setInterval(() => {
|
|
1469
|
-
const currentTime = (0, _dayjs.default)().format('
|
|
1553
|
+
const currentTime = (0, _dayjs.default)().format('HH:mm');
|
|
1470
1554
|
if (timePoints.includes(currentTime)) {
|
|
1471
1555
|
this.onScheduleTimePointTimerFire(subscriberId);
|
|
1472
1556
|
}
|
|
@@ -3688,7 +3772,8 @@ class Server {
|
|
|
3688
3772
|
order_id: printIdentity,
|
|
3689
3773
|
small_ticket_data: params.syncedOrder?.payment_info?.small_ticket_data,
|
|
3690
3774
|
skip_wristband: true,
|
|
3691
|
-
skip_voucher: true
|
|
3775
|
+
skip_voucher: true,
|
|
3776
|
+
skip_discount: true
|
|
3692
3777
|
} : {
|
|
3693
3778
|
order_id: printIdentity,
|
|
3694
3779
|
machine_code_print_info: params.syncedOrder?.payment_info?.machine_code_print_info,
|
|
@@ -3904,6 +3989,99 @@ class Server {
|
|
|
3904
3989
|
};
|
|
3905
3990
|
}
|
|
3906
3991
|
};
|
|
3992
|
+
normalizeRequestedReceiptLocales(value) {
|
|
3993
|
+
if (!Array.isArray(value)) return [];
|
|
3994
|
+
return Array.from(new Set(value.filter(locale => typeof locale === 'string').map(locale => locale.trim()).filter(Boolean)));
|
|
3995
|
+
}
|
|
3996
|
+
handleLocalOrderReceiptSnapshot = async ({
|
|
3997
|
+
data
|
|
3998
|
+
}) => {
|
|
3999
|
+
const input = data && typeof data === 'object' && !Array.isArray(data) ? data : {};
|
|
4000
|
+
const lookup = this.getLocalPrintOrderLookup(input);
|
|
4001
|
+
if (lookup === undefined) {
|
|
4002
|
+
return {
|
|
4003
|
+
code: 400,
|
|
4004
|
+
status: false,
|
|
4005
|
+
message: '订单 lookup 缺失',
|
|
4006
|
+
data: {
|
|
4007
|
+
small_ticket_data: null
|
|
4008
|
+
}
|
|
4009
|
+
};
|
|
4010
|
+
}
|
|
4011
|
+
if (!this.order?.getLocalOrderByLookup) {
|
|
4012
|
+
return {
|
|
4013
|
+
code: 500,
|
|
4014
|
+
status: false,
|
|
4015
|
+
message: 'Order 模块未注册',
|
|
4016
|
+
data: {
|
|
4017
|
+
small_ticket_data: null
|
|
4018
|
+
}
|
|
4019
|
+
};
|
|
4020
|
+
}
|
|
4021
|
+
try {
|
|
4022
|
+
let sourceOrder = await this.order.getLocalOrderByLookup(lookup);
|
|
4023
|
+
if (!sourceOrder) {
|
|
4024
|
+
const remoteResult = await this.fetchAndPersistSalesOrderByLookup(lookup, {
|
|
4025
|
+
with: ['all']
|
|
4026
|
+
}, 'handleLocalOrderReceiptSnapshot');
|
|
4027
|
+
if (!remoteResult.order) {
|
|
4028
|
+
return {
|
|
4029
|
+
code: remoteResult.code,
|
|
4030
|
+
status: false,
|
|
4031
|
+
message: remoteResult.message,
|
|
4032
|
+
data: {
|
|
4033
|
+
small_ticket_data: null
|
|
4034
|
+
}
|
|
4035
|
+
};
|
|
4036
|
+
}
|
|
4037
|
+
sourceOrder = remoteResult.order;
|
|
4038
|
+
}
|
|
4039
|
+
const paymentInfo = {
|
|
4040
|
+
...(sourceOrder.payment_info || {})
|
|
4041
|
+
};
|
|
4042
|
+
delete paymentInfo.small_ticket_data;
|
|
4043
|
+
const rebuiltOrder = await this.withLocalSmallTicketData({
|
|
4044
|
+
...sourceOrder,
|
|
4045
|
+
small_ticket_data_flag: 1,
|
|
4046
|
+
payment_info: paymentInfo
|
|
4047
|
+
}, {
|
|
4048
|
+
locales: this.normalizeRequestedReceiptLocales(input.locales)
|
|
4049
|
+
});
|
|
4050
|
+
const smallTicketData = rebuiltOrder.payment_info?.small_ticket_data;
|
|
4051
|
+
if (!(0, _smallTicket.hasSmallTicketData)(smallTicketData)) {
|
|
4052
|
+
return {
|
|
4053
|
+
code: 500,
|
|
4054
|
+
status: false,
|
|
4055
|
+
message: '本地小票 Snapshot 生成失败',
|
|
4056
|
+
data: {
|
|
4057
|
+
small_ticket_data: null
|
|
4058
|
+
}
|
|
4059
|
+
};
|
|
4060
|
+
}
|
|
4061
|
+
return {
|
|
4062
|
+
code: 200,
|
|
4063
|
+
status: true,
|
|
4064
|
+
message: '',
|
|
4065
|
+
data: {
|
|
4066
|
+
small_ticket_data: smallTicketData
|
|
4067
|
+
}
|
|
4068
|
+
};
|
|
4069
|
+
} catch (error) {
|
|
4070
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4071
|
+
this.logError('handleLocalOrderReceiptSnapshot: 生成失败', {
|
|
4072
|
+
lookup,
|
|
4073
|
+
error: message
|
|
4074
|
+
});
|
|
4075
|
+
return {
|
|
4076
|
+
code: 500,
|
|
4077
|
+
status: false,
|
|
4078
|
+
message,
|
|
4079
|
+
data: {
|
|
4080
|
+
small_ticket_data: null
|
|
4081
|
+
}
|
|
4082
|
+
};
|
|
4083
|
+
}
|
|
4084
|
+
};
|
|
3907
4085
|
handleMachinecodeBatchMeta = async ({
|
|
3908
4086
|
data
|
|
3909
4087
|
}) => {
|
|
@@ -4740,7 +4918,7 @@ class Server {
|
|
|
4740
4918
|
const smallTicketData = (0, _smallTicket.buildSmallTicketData)({
|
|
4741
4919
|
order: enrichedOrder,
|
|
4742
4920
|
shopInfo: this.getSmallTicketShopInfo(),
|
|
4743
|
-
locales: (0, _OpenData.resolveInvoiceReceiptLocales)(options?.invoiceTemplateSettings),
|
|
4921
|
+
locales: [...(0, _OpenData.resolveInvoiceReceiptLocales)(options?.invoiceTemplateSettings), ...(options?.locales || [])],
|
|
4744
4922
|
productMap
|
|
4745
4923
|
});
|
|
4746
4924
|
return {
|
|
@@ -5638,12 +5816,6 @@ class Server {
|
|
|
5638
5816
|
strategy_context
|
|
5639
5817
|
} = context;
|
|
5640
5818
|
const queryIds = Array.isArray(ids) ? ids.map(id => Number(id)).filter(id => Number.isFinite(id)) : [];
|
|
5641
|
-
const formatterContext = this.buildProductFormatterContext({
|
|
5642
|
-
schedule_date,
|
|
5643
|
-
schedule_datetime,
|
|
5644
|
-
customer_id,
|
|
5645
|
-
strategy_context
|
|
5646
|
-
});
|
|
5647
5819
|
this.logInfo('computeProductQueryResult 开始', {
|
|
5648
5820
|
menuListIdsCount: menu_list_ids?.length ?? 0,
|
|
5649
5821
|
ids: queryIds,
|
|
@@ -5655,6 +5827,23 @@ class Server {
|
|
|
5655
5827
|
strategyContextKeys: strategy_context ? Object.keys(strategy_context) : [],
|
|
5656
5828
|
changedIds: options?.changedIds
|
|
5657
5829
|
});
|
|
5830
|
+
|
|
5831
|
+
// 预渲染可能在 PisellOS 顺序 preload 尚未完成时发起商品查询。此时 Menu/Schedule
|
|
5832
|
+
// 暂时为空不代表缓存失效;等待当前 preload 后再按原逻辑查询,避免并发远端刷新先 clear
|
|
5833
|
+
// SQLite/IndexDB,造成 preload 读到空数据并重复请求。正常运行期该字段为 null,不增加等待。
|
|
5834
|
+
const startupPreloadTask = this.startupModulePreloadInFlight;
|
|
5835
|
+
if (startupPreloadTask) {
|
|
5836
|
+
await startupPreloadTask;
|
|
5837
|
+
}
|
|
5838
|
+
|
|
5839
|
+
// Menu/Schedule preload 会替换 store 中的数组;必须在等待完成后再读取,
|
|
5840
|
+
// 避免 formatterContext 持有预加载前的空数组快照。
|
|
5841
|
+
const formatterContext = this.buildProductFormatterContext({
|
|
5842
|
+
schedule_date,
|
|
5843
|
+
schedule_datetime,
|
|
5844
|
+
customer_id,
|
|
5845
|
+
strategy_context
|
|
5846
|
+
});
|
|
5658
5847
|
if (!this.products) {
|
|
5659
5848
|
this.logError('computeProductQueryResult: Products 模块未注册');
|
|
5660
5849
|
return {
|
|
@@ -5867,6 +6056,8 @@ class Server {
|
|
|
5867
6056
|
schedule_datetime: context.schedule_datetime,
|
|
5868
6057
|
customer_id: context.customer_id,
|
|
5869
6058
|
strategy_context: context.strategy_context,
|
|
6059
|
+
// 固定本次商品查询使用的语言,确保格式化与缓存使用同一份 locale 快照。
|
|
6060
|
+
locale: this.core.context?.locale,
|
|
5870
6061
|
menuList: this.menu?.getMenuList?.() || [],
|
|
5871
6062
|
scheduleList: scheduleModule?.getScheduleList?.() || [],
|
|
5872
6063
|
dataVariantEvaluator: this.core.context?.dataVariantEvaluator
|
|
@@ -10,6 +10,7 @@ export declare class FloorPlanModule extends BaseModule implements Module {
|
|
|
10
10
|
private store;
|
|
11
11
|
private request;
|
|
12
12
|
private logger;
|
|
13
|
+
private dbManager;
|
|
13
14
|
private floorPlanDataSource;
|
|
14
15
|
private pendingSyncMessages;
|
|
15
16
|
private syncTimer?;
|
|
@@ -28,6 +29,9 @@ export declare class FloorPlanModule extends BaseModule implements Module {
|
|
|
28
29
|
private mergeItems;
|
|
29
30
|
private removeByIds;
|
|
30
31
|
private loadListFromServer;
|
|
32
|
+
private loadListFromSQLite;
|
|
33
|
+
private saveListToSQLite;
|
|
34
|
+
private refreshListFromServer;
|
|
31
35
|
fetchByIdFromServer(id: number | string): Promise<FloorPlanItem | null>;
|
|
32
36
|
fetchByCodeFromServer(code: string): Promise<FloorPlanItem | null>;
|
|
33
37
|
preload(): Promise<void>;
|
|
@@ -7,6 +7,7 @@ exports.FloorPlanModule = void 0;
|
|
|
7
7
|
var _BaseModule = require("../../../modules/BaseModule");
|
|
8
8
|
var _types = require("./types");
|
|
9
9
|
const FLOOR_PLAN_SYNC_DEBOUNCE_MS = 3000;
|
|
10
|
+
const FLOOR_PLAN_STORE_NAME = 'floor_plans';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* 平面图模块:预加载列表、pubsub 同步、供 OS Server 路由读本地 store
|
|
@@ -17,6 +18,7 @@ class FloorPlanModule extends _BaseModule.BaseModule {
|
|
|
17
18
|
store;
|
|
18
19
|
request;
|
|
19
20
|
logger;
|
|
21
|
+
dbManager;
|
|
20
22
|
floorPlanDataSource;
|
|
21
23
|
pendingSyncMessages = [];
|
|
22
24
|
syncTimer;
|
|
@@ -40,6 +42,7 @@ class FloorPlanModule extends _BaseModule.BaseModule {
|
|
|
40
42
|
if (appPlugin) {
|
|
41
43
|
const app = appPlugin.getApp();
|
|
42
44
|
this.logger = app.logger;
|
|
45
|
+
this.dbManager = app.sqlite || app.dbManager;
|
|
43
46
|
}
|
|
44
47
|
this.initFloorPlanDataSource();
|
|
45
48
|
this.setupFloorPlanSync();
|
|
@@ -147,6 +150,45 @@ class FloorPlanModule extends _BaseModule.BaseModule {
|
|
|
147
150
|
return [];
|
|
148
151
|
}
|
|
149
152
|
}
|
|
153
|
+
async loadListFromSQLite() {
|
|
154
|
+
if (!this.dbManager) return [];
|
|
155
|
+
try {
|
|
156
|
+
const list = await this.dbManager.getAll(FLOOR_PLAN_STORE_NAME);
|
|
157
|
+
return Array.isArray(list) ? list : [];
|
|
158
|
+
} catch (error) {
|
|
159
|
+
this.logWarning('从 SQLite 读取平面图失败', {
|
|
160
|
+
error: error instanceof Error ? error.message : String(error)
|
|
161
|
+
});
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async saveListToSQLite(list) {
|
|
166
|
+
if (!this.dbManager) return;
|
|
167
|
+
try {
|
|
168
|
+
await this.dbManager.clear(FLOOR_PLAN_STORE_NAME);
|
|
169
|
+
if (list.length > 0) {
|
|
170
|
+
await this.dbManager.bulkAdd(FLOOR_PLAN_STORE_NAME, list);
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
this.logWarning('保存平面图到 SQLite 失败', {
|
|
174
|
+
count: list.length,
|
|
175
|
+
error: error instanceof Error ? error.message : String(error)
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async refreshListFromServer() {
|
|
180
|
+
const list = await this.loadListFromServer();
|
|
181
|
+
if (list.length === 0) {
|
|
182
|
+
this.logWarning('远端平面图刷新完成但列表为空');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
this.replaceStore(list);
|
|
186
|
+
await this.saveListToSQLite(list);
|
|
187
|
+
this.core.effects.emit(_types.FloorPlanHooks.onFloorPlansChanged, this.getSortedList());
|
|
188
|
+
this.logInfo('远端平面图刷新完成', {
|
|
189
|
+
count: list.length
|
|
190
|
+
});
|
|
191
|
+
}
|
|
150
192
|
async fetchByIdFromServer(id) {
|
|
151
193
|
try {
|
|
152
194
|
const res = await this.request.get(`/schedule/floor-plan/${id}`, {}, {
|
|
@@ -174,16 +216,25 @@ class FloorPlanModule extends _BaseModule.BaseModule {
|
|
|
174
216
|
}
|
|
175
217
|
async preload() {
|
|
176
218
|
this.logInfo('开始预加载平面图列表');
|
|
177
|
-
const
|
|
178
|
-
if (
|
|
179
|
-
this.replaceStore(
|
|
219
|
+
const cachedList = await this.loadListFromSQLite();
|
|
220
|
+
if (cachedList.length > 0) {
|
|
221
|
+
this.replaceStore(cachedList);
|
|
180
222
|
this.core.effects.emit(_types.FloorPlanHooks.onFloorPlansChanged, this.getSortedList());
|
|
181
|
-
this.logInfo('
|
|
182
|
-
count:
|
|
223
|
+
this.logInfo('预加载完成(SQLite)', {
|
|
224
|
+
count: cachedList.length
|
|
183
225
|
});
|
|
184
|
-
|
|
185
|
-
|
|
226
|
+
|
|
227
|
+
// 二次启动立即使用本地平面图,远端刷新完成后自动更新 store 与订阅者。
|
|
228
|
+
void this.refreshListFromServer().catch(error => {
|
|
229
|
+
this.logWarning('后台刷新平面图失败,继续使用本地数据', {
|
|
230
|
+
error: error instanceof Error ? error.message : String(error)
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
186
234
|
}
|
|
235
|
+
|
|
236
|
+
// 首次启动没有缓存时仍等待一次远端请求,并为后续启动建立缓存。
|
|
237
|
+
await this.refreshListFromServer();
|
|
187
238
|
}
|
|
188
239
|
initFloorPlanDataSource() {
|
|
189
240
|
const Ctor = this.core.serverOptions?.All_DATA_SOURCES?.FloorPlanDataSource;
|
|
@@ -282,6 +333,7 @@ class FloorPlanModule extends _BaseModule.BaseModule {
|
|
|
282
333
|
}
|
|
283
334
|
}
|
|
284
335
|
if (changed) {
|
|
336
|
+
await this.saveListToSQLite(this.store.list);
|
|
285
337
|
this.core.effects.emit(_types.FloorPlanHooks.onFloorPlansChanged, this.getSortedList());
|
|
286
338
|
this.core.effects.emit(_types.FloorPlanHooks.onFloorPlanSyncCompleted, null);
|
|
287
339
|
}
|
|
@@ -290,6 +342,7 @@ class FloorPlanModule extends _BaseModule.BaseModule {
|
|
|
290
342
|
this.store.list = [];
|
|
291
343
|
this.store.map.clear();
|
|
292
344
|
this.store.codeMap.clear();
|
|
345
|
+
await this.saveListToSQLite([]);
|
|
293
346
|
}
|
|
294
347
|
destroy() {
|
|
295
348
|
if (this.syncTimer) {
|
|
@@ -190,17 +190,20 @@ class MenuModule extends _BaseModule.BaseModule {
|
|
|
190
190
|
this.logWarning('loadMenuFromIndexDB: dbManager 不可用');
|
|
191
191
|
return [];
|
|
192
192
|
}
|
|
193
|
+
const startedAt = performance.now();
|
|
193
194
|
try {
|
|
194
195
|
// 直接获取所有餐牌记录
|
|
195
196
|
const menuList = await this.dbManager.getAll(INDEXDB_STORE_NAME);
|
|
196
197
|
this.logInfo('从 IndexDB 加载餐牌数据', {
|
|
197
|
-
menuCount: menuList?.length ?? 0
|
|
198
|
+
menuCount: menuList?.length ?? 0,
|
|
199
|
+
durationMs: +(performance.now() - startedAt).toFixed(2)
|
|
198
200
|
});
|
|
199
201
|
return menuList || [];
|
|
200
202
|
} catch (error) {
|
|
201
203
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
202
204
|
console.error('[Menu] 从 IndexDB 读取数据失败:', error);
|
|
203
205
|
this.logError('从 IndexDB 读取数据失败', {
|
|
206
|
+
durationMs: +(performance.now() - startedAt).toFixed(2),
|
|
204
207
|
error: errorMessage
|
|
205
208
|
});
|
|
206
209
|
return [];
|
|
@@ -216,24 +219,43 @@ class MenuModule extends _BaseModule.BaseModule {
|
|
|
216
219
|
this.logWarning('saveMenuToIndexDB: dbManager 不可用');
|
|
217
220
|
return;
|
|
218
221
|
}
|
|
222
|
+
const startedAt = performance.now();
|
|
223
|
+
let clearDurationMs = 0;
|
|
224
|
+
let writeDurationMs = 0;
|
|
219
225
|
this.logInfo('开始保存餐牌数据到 IndexDB', {
|
|
220
226
|
menuCount: menuList.length
|
|
221
227
|
});
|
|
222
228
|
try {
|
|
223
229
|
// 先清空现有的餐牌数据
|
|
224
|
-
|
|
230
|
+
const clearStartedAt = performance.now();
|
|
231
|
+
try {
|
|
232
|
+
await this.dbManager.clear(INDEXDB_STORE_NAME);
|
|
233
|
+
} finally {
|
|
234
|
+
clearDurationMs = +(performance.now() - clearStartedAt).toFixed(2);
|
|
235
|
+
}
|
|
225
236
|
|
|
226
237
|
// 逐个保存餐牌(每个餐牌是独立的记录)
|
|
238
|
+
const writeStartedAt = performance.now();
|
|
227
239
|
const savePromises = menuList.map(menu => this.dbManager.add(INDEXDB_STORE_NAME, menu));
|
|
228
|
-
|
|
240
|
+
try {
|
|
241
|
+
await Promise.all(savePromises);
|
|
242
|
+
} finally {
|
|
243
|
+
writeDurationMs = +(performance.now() - writeStartedAt).toFixed(2);
|
|
244
|
+
}
|
|
229
245
|
this.logInfo('餐牌数据已保存到 IndexDB', {
|
|
230
|
-
menuCount: menuList.length
|
|
246
|
+
menuCount: menuList.length,
|
|
247
|
+
clearDurationMs,
|
|
248
|
+
writeDurationMs,
|
|
249
|
+
totalDurationMs: +(performance.now() - startedAt).toFixed(2)
|
|
231
250
|
});
|
|
232
251
|
} catch (error) {
|
|
233
252
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
234
253
|
console.error('[Menu] 保存数据到 IndexDB 失败:', error);
|
|
235
254
|
this.logError('保存数据到 IndexDB 失败', {
|
|
236
255
|
menuCount: menuList.length,
|
|
256
|
+
clearDurationMs,
|
|
257
|
+
writeDurationMs,
|
|
258
|
+
totalDurationMs: +(performance.now() - startedAt).toFixed(2),
|
|
237
259
|
error: errorMessage
|
|
238
260
|
});
|
|
239
261
|
}
|