@pisell/pisellos 2.2.121 → 2.2.123
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/modules/Rules/index.js +2 -2
- package/dist/server/index.d.ts +57 -3
- package/dist/server/index.js +735 -190
- package/dist/server/modules/order/index.d.ts +25 -3
- package/dist/server/modules/order/index.js +282 -136
- package/dist/solution/BookingTicket/utils/scan/cloudSearch.js +1 -1
- package/lib/modules/Rules/index.js +4 -4
- package/lib/server/index.d.ts +57 -3
- package/lib/server/index.js +471 -33
- package/lib/server/modules/order/index.d.ts +25 -3
- package/lib/server/modules/order/index.js +105 -24
- package/lib/solution/BookingTicket/utils/scan/cloudSearch.js +1 -1
- package/package.json +1 -1
package/lib/server/index.js
CHANGED
|
@@ -65,6 +65,10 @@ var Server = class {
|
|
|
65
65
|
// ---- 订单 / 预约列表查询订阅者 ----
|
|
66
66
|
this.orderQuerySubscribers = /* @__PURE__ */ new Map();
|
|
67
67
|
this.bookingQuerySubscribers = /* @__PURE__ */ new Map();
|
|
68
|
+
this.bookingRemoteQuerySubscribers = /* @__PURE__ */ new Map();
|
|
69
|
+
this.bookingRemoteCache = /* @__PURE__ */ new Map();
|
|
70
|
+
this.BOOKING_REMOTE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
71
|
+
this.BOOKING_REMOTE_CACHE_MAX_ENTRIES = 80;
|
|
68
72
|
this.floorPlanQuerySubscribers = /* @__PURE__ */ new Map();
|
|
69
73
|
// 模块注册表 - 定义所有可用的模块配置
|
|
70
74
|
this.moduleRegistry = {
|
|
@@ -180,18 +184,32 @@ var Server = class {
|
|
|
180
184
|
console.log("[Server] handleOrderList:", url, method, data, config);
|
|
181
185
|
const queryPayload = data && typeof data === "object" ? { ...data } : {};
|
|
182
186
|
const { callback, subscriberId } = config || {};
|
|
183
|
-
this.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
const useLocal = this.shouldUseLocalOrderQuery(queryPayload);
|
|
188
|
+
this.logInfo("handleOrderList: 开始处理订单列表请求", {
|
|
189
|
+
data: queryPayload,
|
|
190
|
+
useLocal
|
|
191
|
+
});
|
|
192
|
+
if (useLocal) {
|
|
193
|
+
if (subscriberId && typeof callback === "function") {
|
|
194
|
+
this.orderQuerySubscribers.set(subscriberId, {
|
|
195
|
+
callback,
|
|
196
|
+
context: queryPayload
|
|
197
|
+
});
|
|
198
|
+
this.logInfo("handleOrderList: 已注册订阅者", {
|
|
199
|
+
subscriberId,
|
|
200
|
+
totalSubscribers: this.orderQuerySubscribers.size
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return this.computeOrderQueryResult(queryPayload);
|
|
204
|
+
}
|
|
205
|
+
if (subscriberId) {
|
|
206
|
+
this.orderQuerySubscribers.delete(subscriberId);
|
|
207
|
+
this.logInfo("handleOrderList: 已清理订阅者(走远端)", {
|
|
190
208
|
subscriberId,
|
|
191
|
-
|
|
209
|
+
remaining: this.orderQuerySubscribers.size
|
|
192
210
|
});
|
|
193
211
|
}
|
|
194
|
-
return this.
|
|
212
|
+
return this.fetchOrderListFromAPI(queryPayload);
|
|
195
213
|
};
|
|
196
214
|
/**
|
|
197
215
|
* 取消订单列表查询订阅(HTTP 路由入口)
|
|
@@ -215,12 +233,15 @@ var Server = class {
|
|
|
215
233
|
console.log("[Server] handleBookingList:", url, method, data, config);
|
|
216
234
|
const queryPayload = data && typeof data === "object" ? { ...data } : {};
|
|
217
235
|
const { callback, subscriberId } = config || {};
|
|
218
|
-
const
|
|
236
|
+
const useLocal = this.shouldUseLocalBookingQuery(queryPayload);
|
|
219
237
|
this.logInfo("handleBookingList: 开始处理预约列表请求", {
|
|
220
238
|
data: queryPayload,
|
|
221
|
-
|
|
239
|
+
useLocal
|
|
222
240
|
});
|
|
223
|
-
if (
|
|
241
|
+
if (useLocal) {
|
|
242
|
+
if (subscriberId) {
|
|
243
|
+
this.bookingRemoteQuerySubscribers.delete(subscriberId);
|
|
244
|
+
}
|
|
224
245
|
if (subscriberId && typeof callback === "function") {
|
|
225
246
|
this.bookingQuerySubscribers.set(subscriberId, {
|
|
226
247
|
callback,
|
|
@@ -235,6 +256,26 @@ var Server = class {
|
|
|
235
256
|
} else {
|
|
236
257
|
if (subscriberId) {
|
|
237
258
|
this.bookingQuerySubscribers.delete(subscriberId);
|
|
259
|
+
}
|
|
260
|
+
if (subscriberId && typeof callback === "function") {
|
|
261
|
+
const withFields = this.resolveBookingSalesWith(queryPayload);
|
|
262
|
+
const requestPayload = this.normalizeBookingRemoteQueryPayload(
|
|
263
|
+
queryPayload,
|
|
264
|
+
withFields
|
|
265
|
+
);
|
|
266
|
+
const cacheKey = this.buildBookingRemoteCacheKey(requestPayload);
|
|
267
|
+
this.bookingRemoteQuerySubscribers.set(subscriberId, {
|
|
268
|
+
callback,
|
|
269
|
+
context: queryPayload,
|
|
270
|
+
cacheKey
|
|
271
|
+
});
|
|
272
|
+
this.logInfo("handleBookingList: 已注册订阅者(非今天)", {
|
|
273
|
+
subscriberId,
|
|
274
|
+
cacheKey,
|
|
275
|
+
totalSubscribers: this.bookingRemoteQuerySubscribers.size
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (subscriberId) {
|
|
238
279
|
this.logInfo("handleBookingList: 已清理订阅者(非今天)", {
|
|
239
280
|
subscriberId,
|
|
240
281
|
remaining: this.bookingQuerySubscribers.size
|
|
@@ -301,9 +342,11 @@ var Server = class {
|
|
|
301
342
|
const { subscriberId } = data || {};
|
|
302
343
|
if (subscriberId) {
|
|
303
344
|
this.bookingQuerySubscribers.delete(subscriberId);
|
|
345
|
+
this.bookingRemoteQuerySubscribers.delete(subscriberId);
|
|
304
346
|
this.logInfo("handleUnsubscribeBookingQuery: 已移除订阅者", {
|
|
305
347
|
subscriberId,
|
|
306
|
-
|
|
348
|
+
remainingLocal: this.bookingQuerySubscribers.size,
|
|
349
|
+
remainingRemote: this.bookingRemoteQuerySubscribers.size
|
|
307
350
|
});
|
|
308
351
|
}
|
|
309
352
|
return { code: 200, message: "ok", status: true };
|
|
@@ -634,6 +677,7 @@ var Server = class {
|
|
|
634
677
|
this.core.effects.on(import_types3.OrderHooks.onOrdersChanged, () => {
|
|
635
678
|
this.recomputeAndNotifyOrderQuery();
|
|
636
679
|
this.recomputeAndNotifyBookingQuery();
|
|
680
|
+
this.syncBookingRemoteCacheFromOrderChanges();
|
|
637
681
|
});
|
|
638
682
|
const duration = Date.now() - startTime;
|
|
639
683
|
this.logInfo("Server 初始化完成", {
|
|
@@ -927,23 +971,307 @@ var Server = class {
|
|
|
927
971
|
}
|
|
928
972
|
}
|
|
929
973
|
/**
|
|
930
|
-
*
|
|
974
|
+
* 从查询值中提取 YYYY-MM-DD 日期串。
|
|
975
|
+
*
|
|
976
|
+
* @example
|
|
977
|
+
* this.extractQueryDate('2026-04-15 09:58:00') // => '2026-04-15'
|
|
978
|
+
*/
|
|
979
|
+
extractQueryDate(raw) {
|
|
980
|
+
if (raw == null)
|
|
981
|
+
return null;
|
|
982
|
+
const date = (0, import_dayjs.default)(raw);
|
|
983
|
+
if (date.isValid()) {
|
|
984
|
+
return date.format("YYYY-MM-DD");
|
|
985
|
+
}
|
|
986
|
+
const text = String(raw).trim();
|
|
987
|
+
if (!text)
|
|
988
|
+
return null;
|
|
989
|
+
return text.split("T")[0].split(" ")[0] || null;
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* 判断时间范围是否「开始和结束都不是今天」。
|
|
993
|
+
*
|
|
994
|
+
* @example
|
|
995
|
+
* this.isRangeBothNotToday('2026-04-13', '2026-04-14') // => true
|
|
996
|
+
*/
|
|
997
|
+
isRangeBothNotToday(startRaw, endRaw) {
|
|
998
|
+
const startDate = this.extractQueryDate(startRaw);
|
|
999
|
+
const endDate = this.extractQueryDate(endRaw);
|
|
1000
|
+
if (!startDate || !endDate)
|
|
1001
|
+
return false;
|
|
1002
|
+
const today = (0, import_dayjs.default)().format("YYYY-MM-DD");
|
|
1003
|
+
return startDate !== today && endDate !== today;
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* 订单查询是否走本地:
|
|
1007
|
+
* - 当 start_time 与 end_time 都不是今天时,走远端;
|
|
1008
|
+
* - 其余情况走本地。
|
|
1009
|
+
*/
|
|
1010
|
+
shouldUseLocalOrderQuery(data) {
|
|
1011
|
+
return !this.isRangeBothNotToday(data == null ? void 0 : data.start_time, data == null ? void 0 : data.end_time);
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* 预约查询是否走本地:
|
|
1015
|
+
* - 当 sales_time_between 的开始和结束都不是今天时,走远端;
|
|
1016
|
+
* - 其余情况走本地。
|
|
931
1017
|
*/
|
|
932
|
-
|
|
1018
|
+
shouldUseLocalBookingQuery(data) {
|
|
933
1019
|
const range = data == null ? void 0 : data.sales_time_between;
|
|
934
|
-
if (!Array.isArray(range) || range.length <
|
|
1020
|
+
if (!Array.isArray(range) || range.length < 2)
|
|
935
1021
|
return true;
|
|
936
|
-
|
|
937
|
-
const todayStr = (0, import_dayjs.default)().format("YYYY-MM-DD");
|
|
938
|
-
return startDateStr === todayStr;
|
|
1022
|
+
return !this.isRangeBothNotToday(range[0], range[1]);
|
|
939
1023
|
}
|
|
940
1024
|
/**
|
|
941
|
-
*
|
|
1025
|
+
* 日历 SSE 精简 with:只拉日历渲染必需的关联字段
|
|
942
1026
|
*/
|
|
943
|
-
|
|
1027
|
+
resolveBookingSalesWith(_queryPayload) {
|
|
1028
|
+
return [
|
|
1029
|
+
"bookings:booking_id,start_date,start_time,end_date,end_time,holder,metadata,parent_id,item_type",
|
|
1030
|
+
"customer:id,display_name,phone",
|
|
1031
|
+
"products"
|
|
1032
|
+
];
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* 日历 SSE 精简 select:渲染 + filterBookingsFromOrders 所需的主表字段
|
|
1036
|
+
* business_code / payment_status 不直接用于渲染,但 matchOrder 过滤依赖
|
|
1037
|
+
*/
|
|
1038
|
+
resolveBookingSalesSelect() {
|
|
1039
|
+
return "status,payment_status,business_code,phone,customer_name,schedule_date,created_at,metadata";
|
|
1040
|
+
}
|
|
1041
|
+
normalizeBookingRemoteQueryPayload(data, withFields) {
|
|
1042
|
+
return {
|
|
1043
|
+
...data,
|
|
1044
|
+
form_record_ids: void 0,
|
|
1045
|
+
enable_remote_memory_cache: void 0,
|
|
1046
|
+
with: withFields,
|
|
1047
|
+
select: this.resolveBookingSalesSelect(),
|
|
1048
|
+
chunk_size: 50
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* 远端预约查询内存缓存开关:默认 false,仅显式 true 才启用。
|
|
1053
|
+
*/
|
|
1054
|
+
isBookingRemoteMemoryCacheEnabled(queryPayload) {
|
|
1055
|
+
return (queryPayload == null ? void 0 : queryPayload.enable_remote_memory_cache) === true;
|
|
1056
|
+
}
|
|
1057
|
+
stableSerialize(value) {
|
|
1058
|
+
if (value === null || value === void 0)
|
|
1059
|
+
return "null";
|
|
1060
|
+
if (Array.isArray(value)) {
|
|
1061
|
+
return `[${value.map((item) => this.stableSerialize(item)).join(",")}]`;
|
|
1062
|
+
}
|
|
1063
|
+
if (typeof value === "object") {
|
|
1064
|
+
const obj = value;
|
|
1065
|
+
const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
|
|
1066
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${this.stableSerialize(obj[k])}`).join(",")}}`;
|
|
1067
|
+
}
|
|
1068
|
+
return JSON.stringify(value);
|
|
1069
|
+
}
|
|
1070
|
+
buildBookingRemoteCacheKey(payload) {
|
|
1071
|
+
return this.stableSerialize(payload);
|
|
1072
|
+
}
|
|
1073
|
+
parseBookingSalesRangeMeta(payload) {
|
|
1074
|
+
const range = payload == null ? void 0 : payload.sales_time_between;
|
|
1075
|
+
if (!Array.isArray(range) || range.length < 2)
|
|
1076
|
+
return null;
|
|
1077
|
+
const startAtMs = (0, import_dayjs.default)(range[0]).valueOf();
|
|
1078
|
+
const endAtMs = (0, import_dayjs.default)(range[1]).valueOf();
|
|
1079
|
+
if (!Number.isFinite(startAtMs) || !Number.isFinite(endAtMs))
|
|
1080
|
+
return null;
|
|
1081
|
+
if (endAtMs <= startAtMs)
|
|
1082
|
+
return null;
|
|
1083
|
+
return { startAtMs, endAtMs };
|
|
1084
|
+
}
|
|
1085
|
+
readBookingRemoteCache(cacheKey) {
|
|
1086
|
+
const entry = this.bookingRemoteCache.get(cacheKey);
|
|
1087
|
+
if (!entry)
|
|
1088
|
+
return null;
|
|
1089
|
+
if (entry.stale) {
|
|
1090
|
+
this.bookingRemoteCache.delete(cacheKey);
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
const now = Date.now();
|
|
1094
|
+
if (now - entry.updatedAt > this.BOOKING_REMOTE_CACHE_TTL_MS) {
|
|
1095
|
+
this.bookingRemoteCache.delete(cacheKey);
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
return entry;
|
|
1099
|
+
}
|
|
1100
|
+
writeBookingRemoteCache(entry) {
|
|
1101
|
+
this.bookingRemoteCache.set(entry.key, entry);
|
|
1102
|
+
if (this.bookingRemoteCache.size <= this.BOOKING_REMOTE_CACHE_MAX_ENTRIES) {
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
let oldestKey = null;
|
|
1106
|
+
let oldestTime = Number.POSITIVE_INFINITY;
|
|
1107
|
+
for (const [key, cache] of this.bookingRemoteCache.entries()) {
|
|
1108
|
+
if (cache.updatedAt < oldestTime) {
|
|
1109
|
+
oldestTime = cache.updatedAt;
|
|
1110
|
+
oldestKey = key;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (oldestKey)
|
|
1114
|
+
this.bookingRemoteCache.delete(oldestKey);
|
|
1115
|
+
}
|
|
1116
|
+
toOrderIdKey(order) {
|
|
1117
|
+
if (!order || typeof order !== "object")
|
|
1118
|
+
return null;
|
|
1119
|
+
const raw = order.order_id;
|
|
1120
|
+
if (raw === void 0 || raw === null || raw === "")
|
|
1121
|
+
return null;
|
|
1122
|
+
return String(raw);
|
|
1123
|
+
}
|
|
1124
|
+
markBookingRemoteCacheAllStale(reason) {
|
|
1125
|
+
if (this.bookingRemoteCache.size === 0)
|
|
1126
|
+
return;
|
|
1127
|
+
for (const entry of this.bookingRemoteCache.values()) {
|
|
1128
|
+
entry.stale = true;
|
|
1129
|
+
}
|
|
1130
|
+
this.logWarning("bookingRemoteCache: 全量标记失效", {
|
|
1131
|
+
reason,
|
|
1132
|
+
cacheSize: this.bookingRemoteCache.size
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
buildBookingResponse(bookingResult, extra) {
|
|
1136
|
+
return {
|
|
1137
|
+
code: 200,
|
|
1138
|
+
data: {
|
|
1139
|
+
...extra ?? {},
|
|
1140
|
+
list: bookingResult.list || [],
|
|
1141
|
+
count: bookingResult.count || 0,
|
|
1142
|
+
size: bookingResult.size || 0,
|
|
1143
|
+
skip: bookingResult.skip || 0
|
|
1144
|
+
},
|
|
1145
|
+
message: "",
|
|
1146
|
+
status: true
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
notifyBookingRemoteSubscribersByCacheKey(cacheKey, entry, reason) {
|
|
1150
|
+
if (this.bookingRemoteQuerySubscribers.size === 0)
|
|
1151
|
+
return;
|
|
1152
|
+
for (const [subscriberId, subscriber] of this.bookingRemoteQuerySubscribers.entries()) {
|
|
1153
|
+
if (subscriber.cacheKey !== cacheKey)
|
|
1154
|
+
continue;
|
|
1155
|
+
try {
|
|
1156
|
+
const callbackStartAt = Date.now();
|
|
1157
|
+
subscriber.callback(
|
|
1158
|
+
this.buildBookingResponse(entry.bookingResult, {
|
|
1159
|
+
cache_hit: true,
|
|
1160
|
+
from_cache_sync: true,
|
|
1161
|
+
sync_reason: reason
|
|
1162
|
+
})
|
|
1163
|
+
);
|
|
1164
|
+
const callbackEndAt = Date.now();
|
|
1165
|
+
this.logInfo("notifyBookingRemoteSubscribersByCacheKey: 已推送", {
|
|
1166
|
+
subscriberId,
|
|
1167
|
+
cacheKey,
|
|
1168
|
+
reason,
|
|
1169
|
+
callbackDurationMs: callbackEndAt - callbackStartAt
|
|
1170
|
+
});
|
|
1171
|
+
} catch (error) {
|
|
1172
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1173
|
+
this.logError("notifyBookingRemoteSubscribersByCacheKey: 推送失败", {
|
|
1174
|
+
subscriberId,
|
|
1175
|
+
cacheKey,
|
|
1176
|
+
reason,
|
|
1177
|
+
error: errMsg
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
syncBookingRemoteCacheFromOrderChanges() {
|
|
944
1183
|
var _a, _b;
|
|
1184
|
+
if (this.bookingRemoteCache.size === 0)
|
|
1185
|
+
return;
|
|
1186
|
+
const syncStartAt = Date.now();
|
|
1187
|
+
const latestOrders = ((_b = (_a = this.order) == null ? void 0 : _a.getOrders) == null ? void 0 : _b.call(_a)) ?? [];
|
|
1188
|
+
if (!Array.isArray(latestOrders) || latestOrders.length === 0) {
|
|
1189
|
+
this.markBookingRemoteCacheAllStale("orderStoreEmptyAfterChange");
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
let syncedEntryCount = 0;
|
|
1193
|
+
for (const [cacheKey, entry] of this.bookingRemoteCache.entries()) {
|
|
1194
|
+
if (entry.stale) {
|
|
1195
|
+
this.bookingRemoteCache.delete(cacheKey);
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
try {
|
|
1199
|
+
const queryPayload = entry.queryPayload;
|
|
1200
|
+
const orderMap = /* @__PURE__ */ new Map();
|
|
1201
|
+
for (const order of entry.rawOrders) {
|
|
1202
|
+
const key = this.toOrderIdKey(order);
|
|
1203
|
+
if (key)
|
|
1204
|
+
orderMap.set(key, order);
|
|
1205
|
+
}
|
|
1206
|
+
let changed = false;
|
|
1207
|
+
for (const latest of latestOrders) {
|
|
1208
|
+
const key = this.toOrderIdKey(latest);
|
|
1209
|
+
if (!key)
|
|
1210
|
+
continue;
|
|
1211
|
+
if (orderMap.has(key)) {
|
|
1212
|
+
orderMap.set(key, latest);
|
|
1213
|
+
changed = true;
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
orderMap.set(key, latest);
|
|
1217
|
+
changed = true;
|
|
1218
|
+
}
|
|
1219
|
+
if (!changed)
|
|
1220
|
+
continue;
|
|
1221
|
+
const mergedOrders = [...orderMap.values()];
|
|
1222
|
+
const filterStartAt = Date.now();
|
|
1223
|
+
const bookingResult = (0, import_filterBookings.sortBookings)(
|
|
1224
|
+
(0, import_filterBookings.filterBookingsFromOrders)(mergedOrders, queryPayload),
|
|
1225
|
+
queryPayload
|
|
1226
|
+
);
|
|
1227
|
+
const filterEndAt = Date.now();
|
|
1228
|
+
const nextEntry = {
|
|
1229
|
+
...entry,
|
|
1230
|
+
rawOrders: mergedOrders,
|
|
1231
|
+
bookingResult,
|
|
1232
|
+
updatedAt: Date.now(),
|
|
1233
|
+
stale: false
|
|
1234
|
+
};
|
|
1235
|
+
this.writeBookingRemoteCache(nextEntry);
|
|
1236
|
+
this.notifyBookingRemoteSubscribersByCacheKey(
|
|
1237
|
+
cacheKey,
|
|
1238
|
+
nextEntry,
|
|
1239
|
+
"orders_changed"
|
|
1240
|
+
);
|
|
1241
|
+
syncedEntryCount += 1;
|
|
1242
|
+
this.logInfo("bookingRemoteCache: 单条缓存同步完成", {
|
|
1243
|
+
cacheKey,
|
|
1244
|
+
mergedOrderCount: mergedOrders.length,
|
|
1245
|
+
filteredBookingCount: bookingResult.count,
|
|
1246
|
+
filterDurationMs: filterEndAt - filterStartAt
|
|
1247
|
+
});
|
|
1248
|
+
} catch (error) {
|
|
1249
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1250
|
+
this.logWarning("bookingRemoteCache: 同步失败并标记失效", {
|
|
1251
|
+
cacheKey,
|
|
1252
|
+
error: errMsg
|
|
1253
|
+
});
|
|
1254
|
+
const current = this.bookingRemoteCache.get(cacheKey);
|
|
1255
|
+
if (current)
|
|
1256
|
+
current.stale = true;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
const syncEndAt = Date.now();
|
|
1260
|
+
if (syncedEntryCount > 0) {
|
|
1261
|
+
this.logInfo("bookingRemoteCache: 已同步 Ably 变更", {
|
|
1262
|
+
syncedEntryCount,
|
|
1263
|
+
cacheSize: this.bookingRemoteCache.size,
|
|
1264
|
+
totalSyncDurationMs: syncEndAt - syncStartAt
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* 非本地窗口的订单查询:走真实 API。
|
|
1270
|
+
*/
|
|
1271
|
+
async fetchOrderListFromAPI(data) {
|
|
1272
|
+
var _a;
|
|
945
1273
|
if (!((_a = this.app) == null ? void 0 : _a.request)) {
|
|
946
|
-
this.logError("
|
|
1274
|
+
this.logError("fetchOrderListFromAPI: app.request 不可用");
|
|
947
1275
|
return {
|
|
948
1276
|
code: 500,
|
|
949
1277
|
message: "app.request 不可用",
|
|
@@ -952,24 +1280,118 @@ var Server = class {
|
|
|
952
1280
|
};
|
|
953
1281
|
}
|
|
954
1282
|
try {
|
|
955
|
-
const response = await this.app.request.
|
|
1283
|
+
const response = await this.app.request.post("/shop/order/v2/list", data, {
|
|
956
1284
|
isShopApi: true
|
|
957
1285
|
});
|
|
958
|
-
const
|
|
959
|
-
const list = (0
|
|
960
|
-
|
|
961
|
-
rawCount: rawList.length,
|
|
962
|
-
flattenedCount: list.count
|
|
963
|
-
});
|
|
1286
|
+
const payload = (response == null ? void 0 : response.data) ?? response;
|
|
1287
|
+
const list = Array.isArray(payload == null ? void 0 : payload.list) ? payload.list : [];
|
|
1288
|
+
const count = typeof (payload == null ? void 0 : payload.count) === "number" ? payload.count : list.length;
|
|
964
1289
|
return {
|
|
965
1290
|
code: 200,
|
|
966
|
-
data: { ...
|
|
1291
|
+
data: { ...payload, list, count },
|
|
967
1292
|
message: "",
|
|
968
1293
|
status: true
|
|
969
1294
|
};
|
|
970
1295
|
} catch (error) {
|
|
971
1296
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
972
|
-
this.logError("
|
|
1297
|
+
this.logError("fetchOrderListFromAPI: 请求失败", { error: errorMessage });
|
|
1298
|
+
return {
|
|
1299
|
+
code: 500,
|
|
1300
|
+
message: errorMessage,
|
|
1301
|
+
data: { list: [], count: 0 },
|
|
1302
|
+
status: false
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* 非今天的预约查询:通过 SSE 流式拉取订单,再做本地 booking 筛选展开
|
|
1308
|
+
*/
|
|
1309
|
+
async fetchBookingListFromAPI(data) {
|
|
1310
|
+
var _a;
|
|
1311
|
+
if (!this.order) {
|
|
1312
|
+
this.logError("fetchBookingListFromAPI: Order 模块不可用");
|
|
1313
|
+
return {
|
|
1314
|
+
code: 500,
|
|
1315
|
+
message: "Order 模块不可用",
|
|
1316
|
+
data: { list: [], count: 0 },
|
|
1317
|
+
status: false
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
try {
|
|
1321
|
+
const memoryCacheEnabled = this.isBookingRemoteMemoryCacheEnabled(data);
|
|
1322
|
+
const withFields = this.resolveBookingSalesWith(data);
|
|
1323
|
+
const requestPayload = this.normalizeBookingRemoteQueryPayload(
|
|
1324
|
+
data,
|
|
1325
|
+
withFields
|
|
1326
|
+
);
|
|
1327
|
+
const cacheKey = this.buildBookingRemoteCacheKey(requestPayload);
|
|
1328
|
+
if (memoryCacheEnabled) {
|
|
1329
|
+
const cached = this.readBookingRemoteCache(cacheKey);
|
|
1330
|
+
if (cached) {
|
|
1331
|
+
this.logInfo("fetchBookingListFromAPI: 命中内存缓存", {
|
|
1332
|
+
cacheKey,
|
|
1333
|
+
listCount: cached.bookingResult.count,
|
|
1334
|
+
withFields: cached.withFields
|
|
1335
|
+
});
|
|
1336
|
+
return this.buildBookingResponse(cached.bookingResult, {
|
|
1337
|
+
cache_hit: true
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
const rawList = await this.order.fetchOrdersBySSE(requestPayload);
|
|
1342
|
+
const bookingResult = (0, import_filterBookings.sortBookings)(
|
|
1343
|
+
(0, import_filterBookings.filterBookingsFromOrders)(rawList, data),
|
|
1344
|
+
data
|
|
1345
|
+
);
|
|
1346
|
+
if (typeof globalThis !== "undefined") {
|
|
1347
|
+
globalThis.__SSE_BOOKING_DEBUG__ = {
|
|
1348
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1349
|
+
sseRequestPayload: requestPayload,
|
|
1350
|
+
calendarFilters: data,
|
|
1351
|
+
rawOrderCount: rawList.length,
|
|
1352
|
+
rawOrderSample: rawList.slice(0, 2).map((o) => {
|
|
1353
|
+
var _a2, _b, _c;
|
|
1354
|
+
return {
|
|
1355
|
+
order_id: o == null ? void 0 : o.order_id,
|
|
1356
|
+
business_code: o == null ? void 0 : o.business_code,
|
|
1357
|
+
status: o == null ? void 0 : o.status,
|
|
1358
|
+
payment_status: o == null ? void 0 : o.payment_status,
|
|
1359
|
+
bookingsCount: (_a2 = o == null ? void 0 : o.bookings) == null ? void 0 : _a2.length,
|
|
1360
|
+
firstBooking: ((_b = o == null ? void 0 : o.bookings) == null ? void 0 : _b[0]) ? {
|
|
1361
|
+
start_date: o.bookings[0].start_date,
|
|
1362
|
+
start_time: o.bookings[0].start_time,
|
|
1363
|
+
parent_id: o.bookings[0].parent_id,
|
|
1364
|
+
item_type: o.bookings[0].item_type,
|
|
1365
|
+
resourcesCount: (_c = o.bookings[0].resources) == null ? void 0 : _c.length
|
|
1366
|
+
} : null
|
|
1367
|
+
};
|
|
1368
|
+
}),
|
|
1369
|
+
filteredBookingCount: bookingResult.count,
|
|
1370
|
+
filteredListLength: (_a = bookingResult.list) == null ? void 0 : _a.length
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
if (memoryCacheEnabled) {
|
|
1374
|
+
this.writeBookingRemoteCache({
|
|
1375
|
+
key: cacheKey,
|
|
1376
|
+
queryPayload: data,
|
|
1377
|
+
withFields,
|
|
1378
|
+
rawOrders: Array.isArray(rawList) ? rawList : [],
|
|
1379
|
+
bookingResult,
|
|
1380
|
+
updatedAt: Date.now(),
|
|
1381
|
+
stale: false,
|
|
1382
|
+
rangeMeta: this.parseBookingSalesRangeMeta(requestPayload)
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
this.logInfo("fetchBookingListFromAPI: SSE 返回并拆分完成", {
|
|
1386
|
+
rawCount: rawList.length,
|
|
1387
|
+
flattenedCount: bookingResult.count,
|
|
1388
|
+
cacheKey,
|
|
1389
|
+
withFields
|
|
1390
|
+
});
|
|
1391
|
+
return this.buildBookingResponse(bookingResult);
|
|
1392
|
+
} catch (error) {
|
|
1393
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1394
|
+
this.logError("fetchBookingListFromAPI: SSE 请求失败", { error: errorMessage });
|
|
973
1395
|
return {
|
|
974
1396
|
code: 500,
|
|
975
1397
|
message: errorMessage,
|
|
@@ -1266,14 +1688,25 @@ var Server = class {
|
|
|
1266
1688
|
async recomputeAndNotifyOrderQuery() {
|
|
1267
1689
|
if (this.orderQuerySubscribers.size === 0)
|
|
1268
1690
|
return;
|
|
1691
|
+
const notifyStartAt = Date.now();
|
|
1269
1692
|
this.logInfo("recomputeAndNotifyOrderQuery: 开始推送", {
|
|
1270
|
-
subscriberCount: this.orderQuerySubscribers.size
|
|
1693
|
+
subscriberCount: this.orderQuerySubscribers.size,
|
|
1694
|
+
notifyStartAt: new Date(notifyStartAt).toISOString()
|
|
1271
1695
|
});
|
|
1272
1696
|
for (const [subscriberId, subscriber] of this.orderQuerySubscribers.entries()) {
|
|
1273
1697
|
try {
|
|
1698
|
+
const computeStartAt = Date.now();
|
|
1274
1699
|
const result = await this.computeOrderQueryResult(subscriber.context);
|
|
1700
|
+
const computeEndAt = Date.now();
|
|
1701
|
+
const callbackStartAt = Date.now();
|
|
1275
1702
|
subscriber.callback(result);
|
|
1276
|
-
|
|
1703
|
+
const callbackEndAt = Date.now();
|
|
1704
|
+
this.logInfo("recomputeAndNotifyOrderQuery: 已推送", {
|
|
1705
|
+
subscriberId,
|
|
1706
|
+
computeDurationMs: computeEndAt - computeStartAt,
|
|
1707
|
+
callbackDurationMs: callbackEndAt - callbackStartAt,
|
|
1708
|
+
totalDurationMs: callbackEndAt - computeStartAt
|
|
1709
|
+
});
|
|
1277
1710
|
} catch (error) {
|
|
1278
1711
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1279
1712
|
this.logError("recomputeAndNotifyOrderQuery: 推送失败", {
|
|
@@ -1282,6 +1715,11 @@ var Server = class {
|
|
|
1282
1715
|
});
|
|
1283
1716
|
}
|
|
1284
1717
|
}
|
|
1718
|
+
const notifyEndAt = Date.now();
|
|
1719
|
+
this.logInfo("recomputeAndNotifyOrderQuery: 推送完成", {
|
|
1720
|
+
subscriberCount: this.orderQuerySubscribers.size,
|
|
1721
|
+
totalNotifyDurationMs: notifyEndAt - notifyStartAt
|
|
1722
|
+
});
|
|
1285
1723
|
}
|
|
1286
1724
|
/**
|
|
1287
1725
|
* 预约数据变更后,遍历订阅者重新计算并通过 callback 推送
|
|
@@ -13,7 +13,8 @@ export declare class OrderModule extends BaseModule implements Module {
|
|
|
13
13
|
private orderDataSource;
|
|
14
14
|
private pendingSyncMessages;
|
|
15
15
|
private syncTimer?;
|
|
16
|
-
private
|
|
16
|
+
private isProcessingSyncBatch;
|
|
17
|
+
private isIdlePhase;
|
|
17
18
|
private resourceIdIndex;
|
|
18
19
|
private logger;
|
|
19
20
|
private storage;
|
|
@@ -31,6 +32,7 @@ export declare class OrderModule extends BaseModule implements Module {
|
|
|
31
32
|
* @param metadata 日志元数据
|
|
32
33
|
*/
|
|
33
34
|
private logError;
|
|
35
|
+
private getOrderSyncThrottleMs;
|
|
34
36
|
preload(): Promise<void>;
|
|
35
37
|
getOrders(): OrderData[];
|
|
36
38
|
getOrderById(id: OrderId): OrderData | undefined;
|
|
@@ -38,6 +40,10 @@ export declare class OrderModule extends BaseModule implements Module {
|
|
|
38
40
|
getOrderByOrderId(orderId: OrderId): OrderData | undefined;
|
|
39
41
|
loadOrdersByServer(): Promise<OrderData[]>;
|
|
40
42
|
getOrdersByResourceId(resourceId: string | number): OrderData[];
|
|
43
|
+
/**
|
|
44
|
+
* 通过 SSE 按自定义 query 拉取订单(支持 select/with 精简字段)
|
|
45
|
+
*/
|
|
46
|
+
fetchOrdersBySSE(query?: Record<string, any>): Promise<OrderData[]>;
|
|
41
47
|
getRoutes(): RouteDefinition[];
|
|
42
48
|
destroy(): void;
|
|
43
49
|
private syncOrdersMap;
|
|
@@ -48,15 +54,31 @@ export declare class OrderModule extends BaseModule implements Module {
|
|
|
48
54
|
*/
|
|
49
55
|
private initOrderDataSource;
|
|
50
56
|
/**
|
|
51
|
-
* 初始化 pubsub
|
|
57
|
+
* 初始化 pubsub 订阅,监听订单变更(纯订阅,不拉数据;全量 SSE 由 loadOrdersByServer 负责)
|
|
52
58
|
*/
|
|
53
59
|
private setupOrderSync;
|
|
54
60
|
/**
|
|
55
|
-
*
|
|
61
|
+
* 调度订单同步节流窗口(leading-edge throttle):
|
|
62
|
+
* - 空闲状态下收到的第一条消息立即触发批处理,无需等待;
|
|
63
|
+
* - 首条处理后进入冷却窗口,窗口期间的后续消息聚合,窗口结束后批量处理;
|
|
64
|
+
* - 若当前批处理执行中,则由批处理结束后决定是否开启下一轮窗口。
|
|
65
|
+
*/
|
|
66
|
+
private scheduleOrderSyncThrottle;
|
|
67
|
+
/**
|
|
68
|
+
* 执行一次节流批处理并在需要时续约下一轮窗口。
|
|
69
|
+
* 批处理完成且无后续消息时恢复空闲状态,下一条消息将立即触发。
|
|
70
|
+
*/
|
|
71
|
+
private flushOrderSyncMessagesByThrottle;
|
|
72
|
+
/**
|
|
73
|
+
* 处理节流窗口内聚合后的同步消息批次
|
|
56
74
|
*
|
|
57
75
|
* 后端统一发送 change 消息,不再区分新增/编辑/删除。
|
|
58
76
|
* - 单条(id/order_id):若携带 body/data 则直接 upsert 到本地
|
|
59
77
|
* - 批量(ids):通过 HTTP 按 ids 增量拉取,再 merge 到本地
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* // pending 队列中存在 8 条消息时,仅处理一次批次并统一 merge
|
|
81
|
+
* await this.processOrderSyncMessages()
|
|
60
82
|
*/
|
|
61
83
|
private processOrderSyncMessages;
|
|
62
84
|
/**
|