@pisell/pisellos 2.2.122 → 2.2.124
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/server/index.d.ts +64 -3
- package/dist/server/index.js +754 -190
- package/dist/server/modules/order/index.d.ts +25 -3
- package/dist/server/modules/order/index.js +282 -136
- package/lib/server/index.d.ts +64 -3
- package/lib/server/index.js +491 -33
- package/lib/server/modules/order/index.d.ts +25 -3
- package/lib/server/modules/order/index.js +105 -24
- 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 路由入口)
|
|
@@ -207,6 +225,16 @@ var Server = class {
|
|
|
207
225
|
}
|
|
208
226
|
return { code: 200, message: "ok", status: true };
|
|
209
227
|
};
|
|
228
|
+
/**
|
|
229
|
+
* 日历(compact_fields)场景下要直接在 SSE 层排除的订单类型:
|
|
230
|
+
* 这些类型不会出现在日历上,拉取它们只会浪费带宽并增加前端过滤成本。
|
|
231
|
+
*/
|
|
232
|
+
this.BOOKING_SALES_COMPACT_EXCLUDE_TYPES = [
|
|
233
|
+
"virtual",
|
|
234
|
+
"shipping",
|
|
235
|
+
"delivery",
|
|
236
|
+
"pickup"
|
|
237
|
+
];
|
|
210
238
|
/**
|
|
211
239
|
* 处理预约列表查询
|
|
212
240
|
* 今天:注册订阅者 + 本地数据筛选;非今天:清理订阅者 + 走真实 API
|
|
@@ -215,12 +243,15 @@ var Server = class {
|
|
|
215
243
|
console.log("[Server] handleBookingList:", url, method, data, config);
|
|
216
244
|
const queryPayload = data && typeof data === "object" ? { ...data } : {};
|
|
217
245
|
const { callback, subscriberId } = config || {};
|
|
218
|
-
const
|
|
246
|
+
const useLocal = this.shouldUseLocalBookingQuery(queryPayload);
|
|
219
247
|
this.logInfo("handleBookingList: 开始处理预约列表请求", {
|
|
220
248
|
data: queryPayload,
|
|
221
|
-
|
|
249
|
+
useLocal
|
|
222
250
|
});
|
|
223
|
-
if (
|
|
251
|
+
if (useLocal) {
|
|
252
|
+
if (subscriberId) {
|
|
253
|
+
this.bookingRemoteQuerySubscribers.delete(subscriberId);
|
|
254
|
+
}
|
|
224
255
|
if (subscriberId && typeof callback === "function") {
|
|
225
256
|
this.bookingQuerySubscribers.set(subscriberId, {
|
|
226
257
|
callback,
|
|
@@ -235,6 +266,26 @@ var Server = class {
|
|
|
235
266
|
} else {
|
|
236
267
|
if (subscriberId) {
|
|
237
268
|
this.bookingQuerySubscribers.delete(subscriberId);
|
|
269
|
+
}
|
|
270
|
+
if (subscriberId && typeof callback === "function") {
|
|
271
|
+
const withFields = this.resolveBookingSalesWith(queryPayload);
|
|
272
|
+
const requestPayload = this.normalizeBookingRemoteQueryPayload(
|
|
273
|
+
queryPayload,
|
|
274
|
+
withFields
|
|
275
|
+
);
|
|
276
|
+
const cacheKey = this.buildBookingRemoteCacheKey(requestPayload);
|
|
277
|
+
this.bookingRemoteQuerySubscribers.set(subscriberId, {
|
|
278
|
+
callback,
|
|
279
|
+
context: queryPayload,
|
|
280
|
+
cacheKey
|
|
281
|
+
});
|
|
282
|
+
this.logInfo("handleBookingList: 已注册订阅者(非今天)", {
|
|
283
|
+
subscriberId,
|
|
284
|
+
cacheKey,
|
|
285
|
+
totalSubscribers: this.bookingRemoteQuerySubscribers.size
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (subscriberId) {
|
|
238
289
|
this.logInfo("handleBookingList: 已清理订阅者(非今天)", {
|
|
239
290
|
subscriberId,
|
|
240
291
|
remaining: this.bookingQuerySubscribers.size
|
|
@@ -301,9 +352,11 @@ var Server = class {
|
|
|
301
352
|
const { subscriberId } = data || {};
|
|
302
353
|
if (subscriberId) {
|
|
303
354
|
this.bookingQuerySubscribers.delete(subscriberId);
|
|
355
|
+
this.bookingRemoteQuerySubscribers.delete(subscriberId);
|
|
304
356
|
this.logInfo("handleUnsubscribeBookingQuery: 已移除订阅者", {
|
|
305
357
|
subscriberId,
|
|
306
|
-
|
|
358
|
+
remainingLocal: this.bookingQuerySubscribers.size,
|
|
359
|
+
remainingRemote: this.bookingRemoteQuerySubscribers.size
|
|
307
360
|
});
|
|
308
361
|
}
|
|
309
362
|
return { code: 200, message: "ok", status: true };
|
|
@@ -634,6 +687,7 @@ var Server = class {
|
|
|
634
687
|
this.core.effects.on(import_types3.OrderHooks.onOrdersChanged, () => {
|
|
635
688
|
this.recomputeAndNotifyOrderQuery();
|
|
636
689
|
this.recomputeAndNotifyBookingQuery();
|
|
690
|
+
this.syncBookingRemoteCacheFromOrderChanges();
|
|
637
691
|
});
|
|
638
692
|
const duration = Date.now() - startTime;
|
|
639
693
|
this.logInfo("Server 初始化完成", {
|
|
@@ -927,23 +981,317 @@ var Server = class {
|
|
|
927
981
|
}
|
|
928
982
|
}
|
|
929
983
|
/**
|
|
930
|
-
*
|
|
984
|
+
* 从查询值中提取 YYYY-MM-DD 日期串。
|
|
985
|
+
*
|
|
986
|
+
* @example
|
|
987
|
+
* this.extractQueryDate('2026-04-15 09:58:00') // => '2026-04-15'
|
|
988
|
+
*/
|
|
989
|
+
extractQueryDate(raw) {
|
|
990
|
+
if (raw == null)
|
|
991
|
+
return null;
|
|
992
|
+
const date = (0, import_dayjs.default)(raw);
|
|
993
|
+
if (date.isValid()) {
|
|
994
|
+
return date.format("YYYY-MM-DD");
|
|
995
|
+
}
|
|
996
|
+
const text = String(raw).trim();
|
|
997
|
+
if (!text)
|
|
998
|
+
return null;
|
|
999
|
+
return text.split("T")[0].split(" ")[0] || null;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* 判断时间范围是否「开始和结束都不是今天」。
|
|
1003
|
+
*
|
|
1004
|
+
* @example
|
|
1005
|
+
* this.isRangeBothNotToday('2026-04-13', '2026-04-14') // => true
|
|
1006
|
+
*/
|
|
1007
|
+
isRangeBothNotToday(startRaw, endRaw) {
|
|
1008
|
+
const startDate = this.extractQueryDate(startRaw);
|
|
1009
|
+
const endDate = this.extractQueryDate(endRaw);
|
|
1010
|
+
if (!startDate || !endDate)
|
|
1011
|
+
return false;
|
|
1012
|
+
const today = (0, import_dayjs.default)().format("YYYY-MM-DD");
|
|
1013
|
+
return startDate !== today && endDate !== today;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* 订单查询是否走本地:
|
|
1017
|
+
* - 当 start_time 与 end_time 都不是今天时,走远端;
|
|
1018
|
+
* - 其余情况走本地。
|
|
931
1019
|
*/
|
|
932
|
-
|
|
1020
|
+
shouldUseLocalOrderQuery(data) {
|
|
1021
|
+
return !this.isRangeBothNotToday(data == null ? void 0 : data.start_time, data == null ? void 0 : data.end_time);
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* 预约查询是否走本地:
|
|
1025
|
+
* - 当 sales_time_between 的开始和结束都不是今天时,走远端;
|
|
1026
|
+
* - 其余情况走本地。
|
|
1027
|
+
*/
|
|
1028
|
+
shouldUseLocalBookingQuery(data) {
|
|
933
1029
|
const range = data == null ? void 0 : data.sales_time_between;
|
|
934
|
-
if (!Array.isArray(range) || range.length <
|
|
1030
|
+
if (!Array.isArray(range) || range.length < 2)
|
|
935
1031
|
return true;
|
|
936
|
-
|
|
937
|
-
const todayStr = (0, import_dayjs.default)().format("YYYY-MM-DD");
|
|
938
|
-
return startDateStr === todayStr;
|
|
1032
|
+
return !this.isRangeBothNotToday(range[0], range[1]);
|
|
939
1033
|
}
|
|
940
1034
|
/**
|
|
941
|
-
*
|
|
1035
|
+
* 日历 SSE 精简 with:由调用方显式声明 compact_fields 决定
|
|
1036
|
+
* - compact_fields === true → 精简字段(周/月视图)
|
|
1037
|
+
* - 其余(false / 未传)→ ['all'](日视图/表格/平面图合并)
|
|
942
1038
|
*/
|
|
943
|
-
|
|
1039
|
+
resolveBookingSalesWith(queryPayload) {
|
|
1040
|
+
if ((queryPayload == null ? void 0 : queryPayload.compact_fields) === true) {
|
|
1041
|
+
return [
|
|
1042
|
+
"bookings:booking_id,start_date,start_time,end_date,end_time,holder,metadata,parent_id,item_type",
|
|
1043
|
+
"customer:id,display_name,phone",
|
|
1044
|
+
"products"
|
|
1045
|
+
// 'resources'
|
|
1046
|
+
];
|
|
1047
|
+
}
|
|
1048
|
+
return ["all"];
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* 日历 SSE 精简 select:渲染 + filterBookingsFromOrders 所需的主表字段
|
|
1052
|
+
* business_code / payment_status 不直接用于渲染,但 matchOrder 过滤依赖
|
|
1053
|
+
*/
|
|
1054
|
+
resolveBookingSalesSelect() {
|
|
1055
|
+
return "status,payment_status,business_code,phone,customer_name,schedule_date,created_at,metadata";
|
|
1056
|
+
}
|
|
1057
|
+
normalizeBookingRemoteQueryPayload(data, withFields) {
|
|
1058
|
+
const isFullWith = withFields.length === 1 && withFields[0] === "all";
|
|
1059
|
+
const isCompact = (data == null ? void 0 : data.compact_fields) === true;
|
|
1060
|
+
return {
|
|
1061
|
+
...data,
|
|
1062
|
+
form_record_ids: void 0,
|
|
1063
|
+
enable_remote_memory_cache: void 0,
|
|
1064
|
+
compact_fields: void 0,
|
|
1065
|
+
with: withFields,
|
|
1066
|
+
...isFullWith ? {} : { select: this.resolveBookingSalesSelect() },
|
|
1067
|
+
...isCompact ? { exclude_types: this.BOOKING_SALES_COMPACT_EXCLUDE_TYPES } : {},
|
|
1068
|
+
chunk_size: 50
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* 远端预约查询内存缓存开关:默认 false,仅显式 true 才启用。
|
|
1073
|
+
*/
|
|
1074
|
+
isBookingRemoteMemoryCacheEnabled(queryPayload) {
|
|
1075
|
+
return (queryPayload == null ? void 0 : queryPayload.enable_remote_memory_cache) === true;
|
|
1076
|
+
}
|
|
1077
|
+
stableSerialize(value) {
|
|
1078
|
+
if (value === null || value === void 0)
|
|
1079
|
+
return "null";
|
|
1080
|
+
if (Array.isArray(value)) {
|
|
1081
|
+
return `[${value.map((item) => this.stableSerialize(item)).join(",")}]`;
|
|
1082
|
+
}
|
|
1083
|
+
if (typeof value === "object") {
|
|
1084
|
+
const obj = value;
|
|
1085
|
+
const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
|
|
1086
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${this.stableSerialize(obj[k])}`).join(",")}}`;
|
|
1087
|
+
}
|
|
1088
|
+
return JSON.stringify(value);
|
|
1089
|
+
}
|
|
1090
|
+
buildBookingRemoteCacheKey(payload) {
|
|
1091
|
+
return this.stableSerialize(payload);
|
|
1092
|
+
}
|
|
1093
|
+
parseBookingSalesRangeMeta(payload) {
|
|
1094
|
+
const range = payload == null ? void 0 : payload.sales_time_between;
|
|
1095
|
+
if (!Array.isArray(range) || range.length < 2)
|
|
1096
|
+
return null;
|
|
1097
|
+
const startAtMs = (0, import_dayjs.default)(range[0]).valueOf();
|
|
1098
|
+
const endAtMs = (0, import_dayjs.default)(range[1]).valueOf();
|
|
1099
|
+
if (!Number.isFinite(startAtMs) || !Number.isFinite(endAtMs))
|
|
1100
|
+
return null;
|
|
1101
|
+
if (endAtMs <= startAtMs)
|
|
1102
|
+
return null;
|
|
1103
|
+
return { startAtMs, endAtMs };
|
|
1104
|
+
}
|
|
1105
|
+
readBookingRemoteCache(cacheKey) {
|
|
1106
|
+
const entry = this.bookingRemoteCache.get(cacheKey);
|
|
1107
|
+
if (!entry)
|
|
1108
|
+
return null;
|
|
1109
|
+
if (entry.stale) {
|
|
1110
|
+
this.bookingRemoteCache.delete(cacheKey);
|
|
1111
|
+
return null;
|
|
1112
|
+
}
|
|
1113
|
+
const now = Date.now();
|
|
1114
|
+
if (now - entry.updatedAt > this.BOOKING_REMOTE_CACHE_TTL_MS) {
|
|
1115
|
+
this.bookingRemoteCache.delete(cacheKey);
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
return entry;
|
|
1119
|
+
}
|
|
1120
|
+
writeBookingRemoteCache(entry) {
|
|
1121
|
+
this.bookingRemoteCache.set(entry.key, entry);
|
|
1122
|
+
if (this.bookingRemoteCache.size <= this.BOOKING_REMOTE_CACHE_MAX_ENTRIES) {
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
let oldestKey = null;
|
|
1126
|
+
let oldestTime = Number.POSITIVE_INFINITY;
|
|
1127
|
+
for (const [key, cache] of this.bookingRemoteCache.entries()) {
|
|
1128
|
+
if (cache.updatedAt < oldestTime) {
|
|
1129
|
+
oldestTime = cache.updatedAt;
|
|
1130
|
+
oldestKey = key;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (oldestKey)
|
|
1134
|
+
this.bookingRemoteCache.delete(oldestKey);
|
|
1135
|
+
}
|
|
1136
|
+
toOrderIdKey(order) {
|
|
1137
|
+
if (!order || typeof order !== "object")
|
|
1138
|
+
return null;
|
|
1139
|
+
const raw = order.order_id;
|
|
1140
|
+
if (raw === void 0 || raw === null || raw === "")
|
|
1141
|
+
return null;
|
|
1142
|
+
return String(raw);
|
|
1143
|
+
}
|
|
1144
|
+
markBookingRemoteCacheAllStale(reason) {
|
|
1145
|
+
if (this.bookingRemoteCache.size === 0)
|
|
1146
|
+
return;
|
|
1147
|
+
for (const entry of this.bookingRemoteCache.values()) {
|
|
1148
|
+
entry.stale = true;
|
|
1149
|
+
}
|
|
1150
|
+
this.logWarning("bookingRemoteCache: 全量标记失效", {
|
|
1151
|
+
reason,
|
|
1152
|
+
cacheSize: this.bookingRemoteCache.size
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
buildBookingResponse(bookingResult, extra) {
|
|
1156
|
+
return {
|
|
1157
|
+
code: 200,
|
|
1158
|
+
data: {
|
|
1159
|
+
...extra ?? {},
|
|
1160
|
+
list: bookingResult.list || [],
|
|
1161
|
+
count: bookingResult.count || 0,
|
|
1162
|
+
size: bookingResult.size || 0,
|
|
1163
|
+
skip: bookingResult.skip || 0
|
|
1164
|
+
},
|
|
1165
|
+
message: "",
|
|
1166
|
+
status: true
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
notifyBookingRemoteSubscribersByCacheKey(cacheKey, entry, reason) {
|
|
1170
|
+
if (this.bookingRemoteQuerySubscribers.size === 0)
|
|
1171
|
+
return;
|
|
1172
|
+
for (const [subscriberId, subscriber] of this.bookingRemoteQuerySubscribers.entries()) {
|
|
1173
|
+
if (subscriber.cacheKey !== cacheKey)
|
|
1174
|
+
continue;
|
|
1175
|
+
try {
|
|
1176
|
+
const callbackStartAt = Date.now();
|
|
1177
|
+
subscriber.callback(
|
|
1178
|
+
this.buildBookingResponse(entry.bookingResult, {
|
|
1179
|
+
cache_hit: true,
|
|
1180
|
+
from_cache_sync: true,
|
|
1181
|
+
sync_reason: reason
|
|
1182
|
+
})
|
|
1183
|
+
);
|
|
1184
|
+
const callbackEndAt = Date.now();
|
|
1185
|
+
this.logInfo("notifyBookingRemoteSubscribersByCacheKey: 已推送", {
|
|
1186
|
+
subscriberId,
|
|
1187
|
+
cacheKey,
|
|
1188
|
+
reason,
|
|
1189
|
+
callbackDurationMs: callbackEndAt - callbackStartAt
|
|
1190
|
+
});
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1193
|
+
this.logError("notifyBookingRemoteSubscribersByCacheKey: 推送失败", {
|
|
1194
|
+
subscriberId,
|
|
1195
|
+
cacheKey,
|
|
1196
|
+
reason,
|
|
1197
|
+
error: errMsg
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
syncBookingRemoteCacheFromOrderChanges() {
|
|
944
1203
|
var _a, _b;
|
|
1204
|
+
if (this.bookingRemoteCache.size === 0)
|
|
1205
|
+
return;
|
|
1206
|
+
const syncStartAt = Date.now();
|
|
1207
|
+
const latestOrders = ((_b = (_a = this.order) == null ? void 0 : _a.getOrders) == null ? void 0 : _b.call(_a)) ?? [];
|
|
1208
|
+
if (!Array.isArray(latestOrders) || latestOrders.length === 0) {
|
|
1209
|
+
this.markBookingRemoteCacheAllStale("orderStoreEmptyAfterChange");
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
let syncedEntryCount = 0;
|
|
1213
|
+
for (const [cacheKey, entry] of this.bookingRemoteCache.entries()) {
|
|
1214
|
+
if (entry.stale) {
|
|
1215
|
+
this.bookingRemoteCache.delete(cacheKey);
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
try {
|
|
1219
|
+
const queryPayload = entry.queryPayload;
|
|
1220
|
+
const orderMap = /* @__PURE__ */ new Map();
|
|
1221
|
+
for (const order of entry.rawOrders) {
|
|
1222
|
+
const key = this.toOrderIdKey(order);
|
|
1223
|
+
if (key)
|
|
1224
|
+
orderMap.set(key, order);
|
|
1225
|
+
}
|
|
1226
|
+
let changed = false;
|
|
1227
|
+
for (const latest of latestOrders) {
|
|
1228
|
+
const key = this.toOrderIdKey(latest);
|
|
1229
|
+
if (!key)
|
|
1230
|
+
continue;
|
|
1231
|
+
if (orderMap.has(key)) {
|
|
1232
|
+
orderMap.set(key, latest);
|
|
1233
|
+
changed = true;
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
orderMap.set(key, latest);
|
|
1237
|
+
changed = true;
|
|
1238
|
+
}
|
|
1239
|
+
if (!changed)
|
|
1240
|
+
continue;
|
|
1241
|
+
const mergedOrders = [...orderMap.values()];
|
|
1242
|
+
const filterStartAt = Date.now();
|
|
1243
|
+
const bookingResult = (0, import_filterBookings.sortBookings)(
|
|
1244
|
+
(0, import_filterBookings.filterBookingsFromOrders)(mergedOrders, queryPayload),
|
|
1245
|
+
queryPayload
|
|
1246
|
+
);
|
|
1247
|
+
const filterEndAt = Date.now();
|
|
1248
|
+
const nextEntry = {
|
|
1249
|
+
...entry,
|
|
1250
|
+
rawOrders: mergedOrders,
|
|
1251
|
+
bookingResult,
|
|
1252
|
+
updatedAt: Date.now(),
|
|
1253
|
+
stale: false
|
|
1254
|
+
};
|
|
1255
|
+
this.writeBookingRemoteCache(nextEntry);
|
|
1256
|
+
this.notifyBookingRemoteSubscribersByCacheKey(
|
|
1257
|
+
cacheKey,
|
|
1258
|
+
nextEntry,
|
|
1259
|
+
"orders_changed"
|
|
1260
|
+
);
|
|
1261
|
+
syncedEntryCount += 1;
|
|
1262
|
+
this.logInfo("bookingRemoteCache: 单条缓存同步完成", {
|
|
1263
|
+
cacheKey,
|
|
1264
|
+
mergedOrderCount: mergedOrders.length,
|
|
1265
|
+
filteredBookingCount: bookingResult.count,
|
|
1266
|
+
filterDurationMs: filterEndAt - filterStartAt
|
|
1267
|
+
});
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1270
|
+
this.logWarning("bookingRemoteCache: 同步失败并标记失效", {
|
|
1271
|
+
cacheKey,
|
|
1272
|
+
error: errMsg
|
|
1273
|
+
});
|
|
1274
|
+
const current = this.bookingRemoteCache.get(cacheKey);
|
|
1275
|
+
if (current)
|
|
1276
|
+
current.stale = true;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
const syncEndAt = Date.now();
|
|
1280
|
+
if (syncedEntryCount > 0) {
|
|
1281
|
+
this.logInfo("bookingRemoteCache: 已同步 Ably 变更", {
|
|
1282
|
+
syncedEntryCount,
|
|
1283
|
+
cacheSize: this.bookingRemoteCache.size,
|
|
1284
|
+
totalSyncDurationMs: syncEndAt - syncStartAt
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* 非本地窗口的订单查询:走真实 API。
|
|
1290
|
+
*/
|
|
1291
|
+
async fetchOrderListFromAPI(data) {
|
|
1292
|
+
var _a;
|
|
945
1293
|
if (!((_a = this.app) == null ? void 0 : _a.request)) {
|
|
946
|
-
this.logError("
|
|
1294
|
+
this.logError("fetchOrderListFromAPI: app.request 不可用");
|
|
947
1295
|
return {
|
|
948
1296
|
code: 500,
|
|
949
1297
|
message: "app.request 不可用",
|
|
@@ -952,24 +1300,118 @@ var Server = class {
|
|
|
952
1300
|
};
|
|
953
1301
|
}
|
|
954
1302
|
try {
|
|
955
|
-
const response = await this.app.request.
|
|
1303
|
+
const response = await this.app.request.post("/shop/order/v2/list", data, {
|
|
956
1304
|
isShopApi: true
|
|
957
1305
|
});
|
|
958
|
-
const
|
|
959
|
-
const list = (0
|
|
960
|
-
|
|
961
|
-
rawCount: rawList.length,
|
|
962
|
-
flattenedCount: list.count
|
|
963
|
-
});
|
|
1306
|
+
const payload = (response == null ? void 0 : response.data) ?? response;
|
|
1307
|
+
const list = Array.isArray(payload == null ? void 0 : payload.list) ? payload.list : [];
|
|
1308
|
+
const count = typeof (payload == null ? void 0 : payload.count) === "number" ? payload.count : list.length;
|
|
964
1309
|
return {
|
|
965
1310
|
code: 200,
|
|
966
|
-
data: { ...
|
|
1311
|
+
data: { ...payload, list, count },
|
|
967
1312
|
message: "",
|
|
968
1313
|
status: true
|
|
969
1314
|
};
|
|
970
1315
|
} catch (error) {
|
|
971
1316
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
972
|
-
this.logError("
|
|
1317
|
+
this.logError("fetchOrderListFromAPI: 请求失败", { error: errorMessage });
|
|
1318
|
+
return {
|
|
1319
|
+
code: 500,
|
|
1320
|
+
message: errorMessage,
|
|
1321
|
+
data: { list: [], count: 0 },
|
|
1322
|
+
status: false
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* 非今天的预约查询:通过 SSE 流式拉取订单,再做本地 booking 筛选展开
|
|
1328
|
+
*/
|
|
1329
|
+
async fetchBookingListFromAPI(data) {
|
|
1330
|
+
var _a;
|
|
1331
|
+
if (!this.order) {
|
|
1332
|
+
this.logError("fetchBookingListFromAPI: Order 模块不可用");
|
|
1333
|
+
return {
|
|
1334
|
+
code: 500,
|
|
1335
|
+
message: "Order 模块不可用",
|
|
1336
|
+
data: { list: [], count: 0 },
|
|
1337
|
+
status: false
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
try {
|
|
1341
|
+
const memoryCacheEnabled = this.isBookingRemoteMemoryCacheEnabled(data);
|
|
1342
|
+
const withFields = this.resolveBookingSalesWith(data);
|
|
1343
|
+
const requestPayload = this.normalizeBookingRemoteQueryPayload(
|
|
1344
|
+
data,
|
|
1345
|
+
withFields
|
|
1346
|
+
);
|
|
1347
|
+
const cacheKey = this.buildBookingRemoteCacheKey(requestPayload);
|
|
1348
|
+
if (memoryCacheEnabled) {
|
|
1349
|
+
const cached = this.readBookingRemoteCache(cacheKey);
|
|
1350
|
+
if (cached) {
|
|
1351
|
+
this.logInfo("fetchBookingListFromAPI: 命中内存缓存", {
|
|
1352
|
+
cacheKey,
|
|
1353
|
+
listCount: cached.bookingResult.count,
|
|
1354
|
+
withFields: cached.withFields
|
|
1355
|
+
});
|
|
1356
|
+
return this.buildBookingResponse(cached.bookingResult, {
|
|
1357
|
+
cache_hit: true
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
const rawList = await this.order.fetchOrdersBySSE(requestPayload);
|
|
1362
|
+
const bookingResult = (0, import_filterBookings.sortBookings)(
|
|
1363
|
+
(0, import_filterBookings.filterBookingsFromOrders)(rawList, data),
|
|
1364
|
+
data
|
|
1365
|
+
);
|
|
1366
|
+
if (typeof globalThis !== "undefined") {
|
|
1367
|
+
globalThis.__SSE_BOOKING_DEBUG__ = {
|
|
1368
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1369
|
+
sseRequestPayload: requestPayload,
|
|
1370
|
+
calendarFilters: data,
|
|
1371
|
+
rawOrderCount: rawList.length,
|
|
1372
|
+
rawOrderSample: rawList.slice(0, 2).map((o) => {
|
|
1373
|
+
var _a2, _b, _c;
|
|
1374
|
+
return {
|
|
1375
|
+
order_id: o == null ? void 0 : o.order_id,
|
|
1376
|
+
business_code: o == null ? void 0 : o.business_code,
|
|
1377
|
+
status: o == null ? void 0 : o.status,
|
|
1378
|
+
payment_status: o == null ? void 0 : o.payment_status,
|
|
1379
|
+
bookingsCount: (_a2 = o == null ? void 0 : o.bookings) == null ? void 0 : _a2.length,
|
|
1380
|
+
firstBooking: ((_b = o == null ? void 0 : o.bookings) == null ? void 0 : _b[0]) ? {
|
|
1381
|
+
start_date: o.bookings[0].start_date,
|
|
1382
|
+
start_time: o.bookings[0].start_time,
|
|
1383
|
+
parent_id: o.bookings[0].parent_id,
|
|
1384
|
+
item_type: o.bookings[0].item_type,
|
|
1385
|
+
resourcesCount: (_c = o.bookings[0].resources) == null ? void 0 : _c.length
|
|
1386
|
+
} : null
|
|
1387
|
+
};
|
|
1388
|
+
}),
|
|
1389
|
+
filteredBookingCount: bookingResult.count,
|
|
1390
|
+
filteredListLength: (_a = bookingResult.list) == null ? void 0 : _a.length
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
if (memoryCacheEnabled) {
|
|
1394
|
+
this.writeBookingRemoteCache({
|
|
1395
|
+
key: cacheKey,
|
|
1396
|
+
queryPayload: data,
|
|
1397
|
+
withFields,
|
|
1398
|
+
rawOrders: Array.isArray(rawList) ? rawList : [],
|
|
1399
|
+
bookingResult,
|
|
1400
|
+
updatedAt: Date.now(),
|
|
1401
|
+
stale: false,
|
|
1402
|
+
rangeMeta: this.parseBookingSalesRangeMeta(requestPayload)
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
this.logInfo("fetchBookingListFromAPI: SSE 返回并拆分完成", {
|
|
1406
|
+
rawCount: rawList.length,
|
|
1407
|
+
flattenedCount: bookingResult.count,
|
|
1408
|
+
cacheKey,
|
|
1409
|
+
withFields
|
|
1410
|
+
});
|
|
1411
|
+
return this.buildBookingResponse(bookingResult);
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1414
|
+
this.logError("fetchBookingListFromAPI: SSE 请求失败", { error: errorMessage });
|
|
973
1415
|
return {
|
|
974
1416
|
code: 500,
|
|
975
1417
|
message: errorMessage,
|
|
@@ -1266,14 +1708,25 @@ var Server = class {
|
|
|
1266
1708
|
async recomputeAndNotifyOrderQuery() {
|
|
1267
1709
|
if (this.orderQuerySubscribers.size === 0)
|
|
1268
1710
|
return;
|
|
1711
|
+
const notifyStartAt = Date.now();
|
|
1269
1712
|
this.logInfo("recomputeAndNotifyOrderQuery: 开始推送", {
|
|
1270
|
-
subscriberCount: this.orderQuerySubscribers.size
|
|
1713
|
+
subscriberCount: this.orderQuerySubscribers.size,
|
|
1714
|
+
notifyStartAt: new Date(notifyStartAt).toISOString()
|
|
1271
1715
|
});
|
|
1272
1716
|
for (const [subscriberId, subscriber] of this.orderQuerySubscribers.entries()) {
|
|
1273
1717
|
try {
|
|
1718
|
+
const computeStartAt = Date.now();
|
|
1274
1719
|
const result = await this.computeOrderQueryResult(subscriber.context);
|
|
1720
|
+
const computeEndAt = Date.now();
|
|
1721
|
+
const callbackStartAt = Date.now();
|
|
1275
1722
|
subscriber.callback(result);
|
|
1276
|
-
|
|
1723
|
+
const callbackEndAt = Date.now();
|
|
1724
|
+
this.logInfo("recomputeAndNotifyOrderQuery: 已推送", {
|
|
1725
|
+
subscriberId,
|
|
1726
|
+
computeDurationMs: computeEndAt - computeStartAt,
|
|
1727
|
+
callbackDurationMs: callbackEndAt - callbackStartAt,
|
|
1728
|
+
totalDurationMs: callbackEndAt - computeStartAt
|
|
1729
|
+
});
|
|
1277
1730
|
} catch (error) {
|
|
1278
1731
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1279
1732
|
this.logError("recomputeAndNotifyOrderQuery: 推送失败", {
|
|
@@ -1282,6 +1735,11 @@ var Server = class {
|
|
|
1282
1735
|
});
|
|
1283
1736
|
}
|
|
1284
1737
|
}
|
|
1738
|
+
const notifyEndAt = Date.now();
|
|
1739
|
+
this.logInfo("recomputeAndNotifyOrderQuery: 推送完成", {
|
|
1740
|
+
subscriberCount: this.orderQuerySubscribers.size,
|
|
1741
|
+
totalNotifyDurationMs: notifyEndAt - notifyStartAt
|
|
1742
|
+
});
|
|
1285
1743
|
}
|
|
1286
1744
|
/**
|
|
1287
1745
|
* 预约数据变更后,遍历订阅者重新计算并通过 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
|
/**
|