@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.
@@ -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.logInfo("handleOrderList: 开始处理订单列表请求", { data: queryPayload });
184
- if (subscriberId && typeof callback === "function") {
185
- this.orderQuerySubscribers.set(subscriberId, {
186
- callback,
187
- context: queryPayload
188
- });
189
- this.logInfo("handleOrderList: 已注册订阅者", {
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
- totalSubscribers: this.orderQuerySubscribers.size
209
+ remaining: this.orderQuerySubscribers.size
192
210
  });
193
211
  }
194
- return this.computeOrderQueryResult(queryPayload);
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 isToday = this.isBookingQueryForToday(queryPayload);
246
+ const useLocal = this.shouldUseLocalBookingQuery(queryPayload);
219
247
  this.logInfo("handleBookingList: 开始处理预约列表请求", {
220
248
  data: queryPayload,
221
- isToday
249
+ useLocal
222
250
  });
223
- if (isToday) {
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
- remaining: this.bookingQuerySubscribers.size
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
- * 判断预约查询的 sales_time_between 起始日期是否为今天
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
- isBookingQueryForToday(data) {
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 < 1)
1030
+ if (!Array.isArray(range) || range.length < 2)
935
1031
  return true;
936
- const startDateStr = String(range[0]).split("T")[0].split(" ")[0];
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
- * 非今天的预约查询:通过真实 API 获取数据,再做 flattenOrdersToBookings 拆分
1035
+ * 日历 SSE 精简 with:由调用方显式声明 compact_fields 决定
1036
+ * - compact_fields === true → 精简字段(周/月视图)
1037
+ * - 其余(false / 未传)→ ['all'](日视图/表格/平面图合并)
942
1038
  */
943
- async fetchBookingListFromAPI(data) {
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("fetchBookingListFromAPI: app.request 不可用");
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.get("/shop/order/sales", { ...data, form_record_ids: void 0, with: ["all"] }, {
1303
+ const response = await this.app.request.post("/shop/order/v2/list", data, {
956
1304
  isShopApi: true
957
1305
  });
958
- const rawList = ((_b = response == null ? void 0 : response.data) == null ? void 0 : _b.list) ?? (response == null ? void 0 : response.list) ?? [];
959
- const list = (0, import_filterBookings.filterBookingsFromOrders)(rawList, data);
960
- this.logInfo("fetchBookingListFromAPI: API 返回并拆分完成", {
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: { ...response.data, list: (list == null ? void 0 : list.list) || [] },
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("fetchBookingListFromAPI: 请求失败", { error: errorMessage });
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
- this.logInfo("recomputeAndNotifyOrderQuery: 已推送", { subscriberId });
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 readonly ORDER_SYNC_DEBOUNCE_MS;
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
  /**