@pisell/pisellos 2.2.124 → 2.2.126

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.
@@ -86,12 +86,41 @@ export declare class SalesImpl extends BaseModule implements Module, SalesModule
86
86
  */
87
87
  private pickBookingsForCurrentPoint;
88
88
  /**
89
- * 标准化单条 booking
90
- * - 过滤终态(rejected/cancelled/completed)
91
- * - deviceTime 早于 currentTime 时,过滤 end_time 早于 currentTime 的历史数据
92
- * - 注入 status / isTimeout / reserved_status
89
+ * 计算 booking 在 current 时刻的执行进度百分比(0-100)。
90
+ * - startAt/endAt 非法或 totalMinutes <= 0 返回 0
91
+ * - current 早于 startAt 返回 0,晚于 endAt 返回 100
92
+ */
93
+ private calcProgressPercent;
94
+ /**
95
+ * 标准化单条 booking 的分发入口:
96
+ * - 统一过滤 rejected / cancelled(视为没来过)
97
+ * - current === deviceCurrent 走实时分支(信任 appointment_status)
98
+ * - 否则走快照分支(纯时间窗判定,忽略 appointment_status 的 arrived/started/completed 差异)
93
99
  */
94
100
  private normalizeMatchedBooking;
101
+ /**
102
+ * 实时模式(current === deviceCurrent):信任 appointment_status
103
+ * - 过滤 new / completed(实时视角下视为非活跃数据)
104
+ * - status 由 getBookingStatus 映射
105
+ * - occupied 且 current > endAt 标记 isTimeout + timeoutTime
106
+ * - reserved 且 current < startAt 标记 not_arrived,否则标记 late
107
+ */
108
+ private buildRealtimeBooking;
109
+ /**
110
+ * 快照模式(current !== deviceCurrent,过去或未来):忽略 appointment_status
111
+ * 的实时态差异,仅按 booking 时间窗判定,为 current 提供时间点快照视图。
112
+ *
113
+ * - new:视作尚未成为真实预约,过滤
114
+ * - locked:保留 status=locked,不计算 timeout/late/progress
115
+ * - 其他非终态:
116
+ * - startAt/endAt 非法 → 过滤
117
+ * - current > endAt → 过滤(已结束,不入池)
118
+ * - current ∈ [startAt, endAt] → occupied,附带 progressPercent
119
+ * - current < startAt → reserved + not_arrived,附带 remainingReserveTime
120
+ *
121
+ * 快照模式下 isTimeout 恒为 false,lateTime / timeoutTime 始终置空。
122
+ */
123
+ private buildSnapshotBooking;
95
124
  getResourceBookingList(currentTime: string, bookingList?: BookingData[], deviceTime?: string): Promise<SalesResourceBookingItem[]>;
96
125
  }
97
126
  export { SalesImpl as Sales };
@@ -429,36 +429,57 @@ export var SalesImpl = /*#__PURE__*/function (_BaseModule) {
429
429
  }
430
430
 
431
431
  /**
432
- * 标准化单条 booking
433
- * - 过滤终态(rejected/cancelled/completed)
434
- * - deviceTime 早于 currentTime 时,过滤 end_time 早于 currentTime 的历史数据
435
- * - 注入 status / isTimeout / reserved_status
432
+ * 计算 booking 在 current 时刻的执行进度百分比(0-100)。
433
+ * - startAt/endAt 非法或 totalMinutes <= 0 返回 0
434
+ * - current 早于 startAt 返回 0,晚于 endAt 返回 100
435
+ */
436
+ }, {
437
+ key: "calcProgressPercent",
438
+ value: function calcProgressPercent(current, startAt, endAt) {
439
+ if (!startAt.isValid() || !endAt.isValid()) return 0;
440
+ var totalMinutes = endAt.diff(startAt, 'minute');
441
+ if (totalMinutes <= 0) return 0;
442
+ var elapsedMinutes = current.diff(startAt, 'minute');
443
+ if (elapsedMinutes <= 0) return 0;
444
+ if (elapsedMinutes >= totalMinutes) return 100;
445
+ return Math.floor(elapsedMinutes / totalMinutes * 100);
446
+ }
447
+
448
+ /**
449
+ * 标准化单条 booking 的分发入口:
450
+ * - 统一过滤 rejected / cancelled(视为没来过)
451
+ * - current === deviceCurrent 走实时分支(信任 appointment_status)
452
+ * - 否则走快照分支(纯时间窗判定,忽略 appointment_status 的 arrived/started/completed 差异)
436
453
  */
437
454
  }, {
438
455
  key: "normalizeMatchedBooking",
439
456
  value: function normalizeMatchedBooking(current, deviceCurrent, booking) {
440
457
  var _ref3, _booking$appointment_;
441
458
  var appointmentStatus = String((_ref3 = (_booking$appointment_ = booking.appointment_status) !== null && _booking$appointment_ !== void 0 ? _booking$appointment_ : booking.status) !== null && _ref3 !== void 0 ? _ref3 : '');
442
- if (appointmentStatus === 'new' || appointmentStatus === 'rejected' || appointmentStatus === 'cancelled' || appointmentStatus === 'completed') {
443
- return null;
444
- }
459
+ if (appointmentStatus === 'rejected' || appointmentStatus === 'cancelled') return null;
460
+ var startAt = this.toBookingDateTime(booking.start_date, booking.start_time);
445
461
  var endAt = this.toBookingDateTime(booking.end_date, booking.end_time);
446
- var shouldFilterHistoryByCurrent = deviceCurrent.isBefore(current);
447
- if (shouldFilterHistoryByCurrent && endAt.isValid() && endAt.isBefore(current)) return null;
462
+ if (current.isSame(deviceCurrent)) {
463
+ return this.buildRealtimeBooking(current, booking, appointmentStatus, startAt, endAt);
464
+ }
465
+ return this.buildSnapshotBooking(current, booking, appointmentStatus, startAt, endAt);
466
+ }
467
+
468
+ /**
469
+ * 实时模式(current === deviceCurrent):信任 appointment_status
470
+ * - 过滤 new / completed(实时视角下视为非活跃数据)
471
+ * - status 由 getBookingStatus 映射
472
+ * - occupied 且 current > endAt 标记 isTimeout + timeoutTime
473
+ * - reserved 且 current < startAt 标记 not_arrived,否则标记 late
474
+ */
475
+ }, {
476
+ key: "buildRealtimeBooking",
477
+ value: function buildRealtimeBooking(current, booking, appointmentStatus, startAt, endAt) {
478
+ if (appointmentStatus === 'new' || appointmentStatus === 'completed') return null;
448
479
  var bookingStatus = this.getBookingStatus(appointmentStatus);
449
- var startAt = this.toBookingDateTime(booking.start_date, booking.start_time);
450
480
  var isTimeout = bookingStatus === 'occupied' && endAt.isValid() && current.isAfter(endAt);
451
481
  var timeoutTime = isTimeout ? current.diff(endAt, 'minute') : undefined;
452
- var progressPercent = function () {
453
- if (bookingStatus !== 'occupied') return 0;
454
- if (!startAt.isValid() || !endAt.isValid()) return 0;
455
- var totalMinutes = endAt.diff(startAt, 'minute');
456
- if (totalMinutes <= 0) return 0;
457
- var elapsedMinutes = current.diff(startAt, 'minute');
458
- if (elapsedMinutes <= 0) return 0;
459
- if (elapsedMinutes >= totalMinutes) return 100;
460
- return Math.floor(elapsedMinutes / totalMinutes * 100);
461
- }();
482
+ var progressPercent = bookingStatus === 'occupied' ? this.calcProgressPercent(current, startAt, endAt) : 0;
462
483
  var reservedStatus;
463
484
  var lateTime;
464
485
  var remainingReserveTime;
@@ -471,24 +492,6 @@ export var SalesImpl = /*#__PURE__*/function (_BaseModule) {
471
492
  lateTime = Math.max(current.diff(startAt, 'minute'), 0);
472
493
  }
473
494
  }
474
- // 未来查询投影:查询时间晚于设备时间时,预留+迟到且 end_time 仍在查询时间之后的预约强制视为占用
475
- if (current.isAfter(deviceCurrent) && bookingStatus === 'reserved' && reservedStatus === 'late' && endAt.isValid() && endAt.isSameOrAfter(current)) {
476
- bookingStatus = 'occupied';
477
- reservedStatus = undefined;
478
- lateTime = undefined;
479
- remainingReserveTime = undefined;
480
- isTimeout = false;
481
- timeoutTime = undefined;
482
- progressPercent = function () {
483
- if (!startAt.isValid() || !endAt.isValid()) return 0;
484
- var totalMinutes = endAt.diff(startAt, 'minute');
485
- if (totalMinutes <= 0) return 0;
486
- var elapsedMinutes = current.diff(startAt, 'minute');
487
- if (elapsedMinutes <= 0) return 0;
488
- if (elapsedMinutes >= totalMinutes) return 100;
489
- return Math.floor(elapsedMinutes / totalMinutes * 100);
490
- }();
491
- }
492
495
  return _objectSpread(_objectSpread({}, booking), {}, {
493
496
  status: bookingStatus,
494
497
  isTimeout: isTimeout,
@@ -499,6 +502,59 @@ export var SalesImpl = /*#__PURE__*/function (_BaseModule) {
499
502
  remainingReserveTime: remainingReserveTime
500
503
  });
501
504
  }
505
+
506
+ /**
507
+ * 快照模式(current !== deviceCurrent,过去或未来):忽略 appointment_status
508
+ * 的实时态差异,仅按 booking 时间窗判定,为 current 提供时间点快照视图。
509
+ *
510
+ * - new:视作尚未成为真实预约,过滤
511
+ * - locked:保留 status=locked,不计算 timeout/late/progress
512
+ * - 其他非终态:
513
+ * - startAt/endAt 非法 → 过滤
514
+ * - current > endAt → 过滤(已结束,不入池)
515
+ * - current ∈ [startAt, endAt] → occupied,附带 progressPercent
516
+ * - current < startAt → reserved + not_arrived,附带 remainingReserveTime
517
+ *
518
+ * 快照模式下 isTimeout 恒为 false,lateTime / timeoutTime 始终置空。
519
+ */
520
+ }, {
521
+ key: "buildSnapshotBooking",
522
+ value: function buildSnapshotBooking(current, booking, appointmentStatus, startAt, endAt) {
523
+ if (appointmentStatus === 'new') return null;
524
+ if (appointmentStatus === 'locked') {
525
+ return _objectSpread(_objectSpread({}, booking), {}, {
526
+ status: 'locked',
527
+ isTimeout: false,
528
+ timeoutTime: undefined,
529
+ progressPercent: 0,
530
+ lateTime: undefined,
531
+ reserved_status: undefined,
532
+ remainingReserveTime: undefined
533
+ });
534
+ }
535
+ if (!startAt.isValid() || !endAt.isValid()) return null;
536
+ if (current.isAfter(endAt)) return null;
537
+ if (this.isSameOrAfter(current, startAt) && this.isSameOrBefore(current, endAt)) {
538
+ return _objectSpread(_objectSpread({}, booking), {}, {
539
+ status: 'occupied',
540
+ isTimeout: false,
541
+ timeoutTime: undefined,
542
+ progressPercent: this.calcProgressPercent(current, startAt, endAt),
543
+ lateTime: undefined,
544
+ reserved_status: undefined,
545
+ remainingReserveTime: undefined
546
+ });
547
+ }
548
+ return _objectSpread(_objectSpread({}, booking), {}, {
549
+ status: 'reserved',
550
+ isTimeout: false,
551
+ timeoutTime: undefined,
552
+ progressPercent: 0,
553
+ lateTime: undefined,
554
+ reserved_status: 'not_arrived',
555
+ remainingReserveTime: Math.max(startAt.diff(current, 'minute'), 0)
556
+ });
557
+ }
502
558
  }, {
503
559
  key: "getResourceBookingList",
504
560
  value: function () {
package/lib/core/index.js CHANGED
@@ -50,6 +50,7 @@ var PisellOSCore = class {
50
50
  this.serverOptions = options.server;
51
51
  }
52
52
  this.initialize(options);
53
+ console.log("initialize12341");
53
54
  }
54
55
  initialize(options) {
55
56
  if (options == null ? void 0 : options.plugins) {
@@ -0,0 +1,48 @@
1
+ import { Module, PisellCore, ModuleOptions } from '../../types';
2
+ import { BaseModule } from '../BaseModule';
3
+ import type { ScheduleItem } from '../Schedule/types';
4
+ import type { QuotationItem } from './types';
5
+ export type { QuotationItem, QuotationProductData, QuotationSchedule, QuotationState } from './types';
6
+ export declare class QuotationModule extends BaseModule implements Module {
7
+ protected defaultName: string;
8
+ protected defaultVersion: string;
9
+ private request;
10
+ private store;
11
+ private scheduleResolver?;
12
+ constructor(name?: string, version?: string);
13
+ initialize(core: PisellCore, options: ModuleOptions): Promise<void>;
14
+ loadQuotations(params?: {
15
+ channel?: string;
16
+ }): Promise<void>;
17
+ getQuotationList(): QuotationItem[];
18
+ /**
19
+ * Look up the quotation price for a specific product (+ optional variant) at a given datetime.
20
+ * Returns the price as a string (e.g. "300.00"), or null if no quotation applies.
21
+ *
22
+ * Priority: iterates quotations already sorted by `sort` ascending (lowest = highest priority).
23
+ * First matching quotation whose schedule covers `datetime` and whose product_data contains
24
+ * the requested productId wins.
25
+ */
26
+ getPriceForProduct(params: {
27
+ productId: number;
28
+ variantId?: number;
29
+ datetime: string;
30
+ }): string | null;
31
+ getQuotationShelfId(params: {
32
+ productId: number;
33
+ variantId?: number;
34
+ datetime: string;
35
+ }): number;
36
+ /**
37
+ * Batch pre-compute quotation prices for a set of products across multiple time points.
38
+ * Key format: `${productId}:${timePoint}`
39
+ * This avoids repeated schedule matching when the same product appears in multiple resource rows.
40
+ */
41
+ buildProductPriceMap(params: {
42
+ productIds: number[];
43
+ timePoints: string[];
44
+ }): Map<string, string | null>;
45
+ setScheduleResolver(resolver: (id: number) => ScheduleItem | undefined): void;
46
+ private isQuotationActiveAt;
47
+ private findProductData;
48
+ }
@@ -0,0 +1,152 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/modules/Quotation/index.ts
20
+ var Quotation_exports = {};
21
+ __export(Quotation_exports, {
22
+ QuotationModule: () => QuotationModule
23
+ });
24
+ module.exports = __toCommonJS(Quotation_exports);
25
+ var import_BaseModule = require("../BaseModule");
26
+ var import_getDateIsInSchedule = require("../Schedule/getDateIsInSchedule");
27
+ var QuotationModule = class extends import_BaseModule.BaseModule {
28
+ constructor(name, version) {
29
+ super(name, version);
30
+ this.defaultName = "quotation";
31
+ this.defaultVersion = "1.0.0";
32
+ this.store = { list: [] };
33
+ }
34
+ async initialize(core, options) {
35
+ this.core = core;
36
+ this.request = core.getPlugin("request");
37
+ if (!this.request)
38
+ throw new Error("QuotationModule 需要 request 插件支持");
39
+ this.store = { list: [] };
40
+ }
41
+ async loadQuotations(params) {
42
+ var _a;
43
+ const query = {};
44
+ if (params == null ? void 0 : params.channel)
45
+ query.channel = params.channel;
46
+ if ((params == null ? void 0 : params.channel) === "online_store") {
47
+ query.channel = "online-store";
48
+ }
49
+ const res = await this.request.get(
50
+ "/quotation/available",
51
+ query,
52
+ { useCache: false }
53
+ );
54
+ const list = ((_a = res == null ? void 0 : res.data) == null ? void 0 : _a.list) || (res == null ? void 0 : res.list) || [];
55
+ list.sort((a, b) => a.sort - b.sort);
56
+ this.store.list = list;
57
+ }
58
+ getQuotationList() {
59
+ return this.store.list;
60
+ }
61
+ /**
62
+ * Look up the quotation price for a specific product (+ optional variant) at a given datetime.
63
+ * Returns the price as a string (e.g. "300.00"), or null if no quotation applies.
64
+ *
65
+ * Priority: iterates quotations already sorted by `sort` ascending (lowest = highest priority).
66
+ * First matching quotation whose schedule covers `datetime` and whose product_data contains
67
+ * the requested productId wins.
68
+ */
69
+ getPriceForProduct(params) {
70
+ const { productId, variantId, datetime } = params;
71
+ for (const quotation of this.store.list) {
72
+ if (!this.isQuotationActiveAt(quotation, datetime))
73
+ continue;
74
+ const match = this.findProductData(quotation.product_data, productId, variantId);
75
+ if (!match)
76
+ continue;
77
+ if (match.value === 0)
78
+ continue;
79
+ return String(match.value);
80
+ }
81
+ return null;
82
+ }
83
+ getQuotationShelfId(params) {
84
+ const { productId, variantId, datetime } = params;
85
+ for (const quotation of this.store.list) {
86
+ if (!this.isQuotationActiveAt(quotation, datetime))
87
+ continue;
88
+ const match = this.findProductData(quotation.product_data, productId, variantId);
89
+ if (!match || match.value === 0)
90
+ continue;
91
+ return quotation.id;
92
+ }
93
+ return 0;
94
+ }
95
+ /**
96
+ * Batch pre-compute quotation prices for a set of products across multiple time points.
97
+ * Key format: `${productId}:${timePoint}`
98
+ * This avoids repeated schedule matching when the same product appears in multiple resource rows.
99
+ */
100
+ buildProductPriceMap(params) {
101
+ const map = /* @__PURE__ */ new Map();
102
+ if (!this.store.list.length)
103
+ return map;
104
+ for (const productId of params.productIds) {
105
+ for (const timePoint of params.timePoints) {
106
+ const key = `${productId}:${timePoint}`;
107
+ const price = this.getPriceForProduct({ productId, datetime: timePoint });
108
+ if (price !== null) {
109
+ map.set(key, price);
110
+ }
111
+ }
112
+ }
113
+ return map;
114
+ }
115
+ setScheduleResolver(resolver) {
116
+ this.scheduleResolver = resolver;
117
+ }
118
+ isQuotationActiveAt(quotation, datetime) {
119
+ var _a;
120
+ if (!((_a = quotation.schedule) == null ? void 0 : _a.length))
121
+ return false;
122
+ const scheduleItems = quotation.schedule.map((s) => {
123
+ var _a2;
124
+ const full = (_a2 = this.scheduleResolver) == null ? void 0 : _a2.call(this, s.id);
125
+ if (full)
126
+ return full;
127
+ return {
128
+ ...s,
129
+ repeat_type: s.repeat_type || "none",
130
+ repeat_rule: s.repeat_rule || null,
131
+ time_slot: s.time_slot || []
132
+ };
133
+ });
134
+ return (0, import_getDateIsInSchedule.getDateIsInSchedule)(datetime, scheduleItems);
135
+ }
136
+ findProductData(productData, productId, variantId) {
137
+ if (variantId && variantId !== 0) {
138
+ const variantMatch = productData.find(
139
+ (p) => p.product_id === productId && p.variant_id === variantId
140
+ );
141
+ if (variantMatch)
142
+ return variantMatch;
143
+ }
144
+ return productData.find(
145
+ (p) => p.product_id === productId && p.variant_id === 0
146
+ );
147
+ }
148
+ };
149
+ // Annotate the CommonJS export names for ESM import in node:
150
+ 0 && (module.exports = {
151
+ QuotationModule
152
+ });
@@ -0,0 +1,42 @@
1
+ export interface QuotationProductData {
2
+ id: number;
3
+ shelf_id: number;
4
+ product_id: number;
5
+ variant_id: number;
6
+ type: string;
7
+ value: number;
8
+ }
9
+ export interface QuotationSchedule {
10
+ id: number;
11
+ name: string;
12
+ type: 'standard' | 'time-slots' | 'designation';
13
+ start_time: string;
14
+ end_time: string;
15
+ is_all: number;
16
+ repeat_type?: string;
17
+ repeat_rule?: any;
18
+ designation?: any;
19
+ time_slot?: Array<{
20
+ start_time: string;
21
+ end_time: string;
22
+ }>;
23
+ pivot?: Record<string, any>;
24
+ }
25
+ export interface QuotationItem {
26
+ id: number;
27
+ shop_id: number;
28
+ shelf_type_id?: number;
29
+ name: string;
30
+ description?: string;
31
+ status: string;
32
+ sort: number;
33
+ channel: string[];
34
+ created_at?: string;
35
+ updated_at?: string;
36
+ deleted_at?: string | null;
37
+ schedule: QuotationSchedule[];
38
+ product_data: QuotationProductData[];
39
+ }
40
+ export interface QuotationState {
41
+ list: QuotationItem[];
42
+ }
@@ -0,0 +1,17 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __copyProps = (to, from, except, desc) => {
6
+ if (from && typeof from === "object" || typeof from === "function") {
7
+ for (let key of __getOwnPropNames(from))
8
+ if (!__hasOwnProp.call(to, key) && key !== except)
9
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
10
+ }
11
+ return to;
12
+ };
13
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
14
+
15
+ // src/modules/Quotation/types.ts
16
+ var types_exports = {};
17
+ module.exports = __toCommonJS(types_exports);
@@ -254,6 +254,17 @@ declare class Server {
254
254
  * 转发到资源模块去
255
255
  */
256
256
  private handleResourceList;
257
+ /**
258
+ * 代理刷新单个本地订单:
259
+ * - 入参:order_id(data 或 url query)
260
+ * - 流程:本地存在性校验 → 调后端 /order/sales/{id}?with[]=... → 覆盖本地 → 落 SQLite → emit onOrdersChanged
261
+ * - 监听级联:onOrdersChanged 触发订单/预约/bookingRemoteCache 三路订阅者推送
262
+ */
263
+ private handleUpdateLocalOrder;
264
+ /**
265
+ * 从 url 中解析指定 query 参数,兼容相对路径与绝对路径
266
+ */
267
+ private extractQueryParam;
257
268
  /**
258
269
  * 从 url 或路由 path 解析 pathname(不含 query,去掉末尾 /)
259
270
  */
@@ -313,6 +313,86 @@ var Server = class {
313
313
  status: true
314
314
  };
315
315
  };
316
+ /**
317
+ * 代理刷新单个本地订单:
318
+ * - 入参:order_id(data 或 url query)
319
+ * - 流程:本地存在性校验 → 调后端 /order/sales/{id}?with[]=... → 覆盖本地 → 落 SQLite → emit onOrdersChanged
320
+ * - 监听级联:onOrdersChanged 触发订单/预约/bookingRemoteCache 三路订阅者推送
321
+ */
322
+ this.handleUpdateLocalOrder = async ({ url, data }) => {
323
+ var _a;
324
+ const rawOrderId = (data && typeof data === "object" ? data.order_id : void 0) ?? this.extractQueryParam(url, "order_id");
325
+ const orderId = rawOrderId === void 0 || rawOrderId === null || rawOrderId === "" ? null : String(rawOrderId).trim();
326
+ this.logInfo("handleUpdateLocalOrder: 开始处理", { url, orderId });
327
+ if (!orderId) {
328
+ this.logWarning("handleUpdateLocalOrder: order_id 缺失", { url, data });
329
+ return { code: 400, status: false, message: "order_id 缺失", data: null };
330
+ }
331
+ if (!this.order) {
332
+ this.logError("handleUpdateLocalOrder: Order 模块未注册");
333
+ return { code: 500, status: false, message: "Order 模块未注册", data: null };
334
+ }
335
+ if (!this.order.getOrderByOrderId(orderId)) {
336
+ this.logInfo("handleUpdateLocalOrder: 本地不存在该订单,忽略", { orderId });
337
+ return {
338
+ code: 200,
339
+ status: true,
340
+ message: "本地无此订单,已忽略",
341
+ data: { overwritten: false }
342
+ };
343
+ }
344
+ if (!((_a = this.app) == null ? void 0 : _a.request)) {
345
+ this.logError("handleUpdateLocalOrder: app.request 不可用");
346
+ return { code: 500, status: false, message: "app.request 不可用", data: null };
347
+ }
348
+ const backendPath = `/shop/order/sales/${encodeURIComponent(
349
+ orderId
350
+ )}?with%5B%5D=products&with%5B%5D=scheduleEvents&with%5B%5D=customer`;
351
+ try {
352
+ const response = await this.app.request.get(backendPath, void 0, {
353
+ isShopApi: true
354
+ });
355
+ const fresh = (response == null ? void 0 : response.data) ?? response;
356
+ if (!fresh || typeof fresh !== "object" || fresh.order_id == null) {
357
+ this.logError("handleUpdateLocalOrder: 后端返回订单为空", {
358
+ orderId,
359
+ backendPath
360
+ });
361
+ return {
362
+ code: 500,
363
+ status: false,
364
+ message: "后端返回订单为空",
365
+ data: null
366
+ };
367
+ }
368
+ const { overwritten } = await this.order.overwriteExistingOrder(
369
+ fresh
370
+ );
371
+ this.logInfo("handleUpdateLocalOrder: 覆盖完成", {
372
+ orderId,
373
+ overwritten
374
+ });
375
+ return {
376
+ code: 200,
377
+ status: true,
378
+ message: "",
379
+ data: { overwritten, order_id: orderId }
380
+ };
381
+ } catch (error) {
382
+ const errorMessage = error instanceof Error ? error.message : String(error);
383
+ this.logError("handleUpdateLocalOrder: 请求失败", {
384
+ orderId,
385
+ backendPath,
386
+ error: errorMessage
387
+ });
388
+ return {
389
+ code: 500,
390
+ status: false,
391
+ message: errorMessage,
392
+ data: null
393
+ };
394
+ }
395
+ };
316
396
  /**
317
397
  * GET /shop/schedule/floor-plan* 前缀路由:读本地 store;支持 subscriberId + callback 订阅更新
318
398
  */
@@ -958,6 +1038,11 @@ var Server = class {
958
1038
  method: "get",
959
1039
  path: "/shop/form/resource/page",
960
1040
  handler: this.handleResourceList.bind(this)
1041
+ },
1042
+ {
1043
+ method: "get",
1044
+ path: "/update/localOrder",
1045
+ handler: this.handleUpdateLocalOrder.bind(this)
961
1046
  }
962
1047
  ]);
963
1048
  this.registerPrefixRoutes([
@@ -1420,6 +1505,37 @@ var Server = class {
1420
1505
  };
1421
1506
  }
1422
1507
  }
1508
+ /**
1509
+ * 从 url 中解析指定 query 参数,兼容相对路径与绝对路径
1510
+ */
1511
+ extractQueryParam(url, key) {
1512
+ if (!url)
1513
+ return null;
1514
+ try {
1515
+ const target = url.startsWith("http") ? new URL(url) : new URL(url, "http://placeholder.local");
1516
+ const value = target.searchParams.get(key);
1517
+ return value ?? null;
1518
+ } catch {
1519
+ const queryIndex = url.indexOf("?");
1520
+ if (queryIndex < 0)
1521
+ return null;
1522
+ const query = url.slice(queryIndex + 1);
1523
+ for (const pair of query.split("&")) {
1524
+ if (!pair)
1525
+ continue;
1526
+ const [rawKey, rawValue = ""] = pair.split("=");
1527
+ try {
1528
+ if (decodeURIComponent(rawKey) === key) {
1529
+ return decodeURIComponent(rawValue);
1530
+ }
1531
+ } catch {
1532
+ if (rawKey === key)
1533
+ return rawValue;
1534
+ }
1535
+ }
1536
+ return null;
1537
+ }
1538
+ }
1423
1539
  /**
1424
1540
  * 从 url 或路由 path 解析 pathname(不含 query,去掉末尾 /)
1425
1541
  */
@@ -40,6 +40,13 @@ export declare class OrderModule extends BaseModule implements Module {
40
40
  getOrderByOrderId(orderId: OrderId): OrderData | undefined;
41
41
  loadOrdersByServer(): Promise<OrderData[]>;
42
42
  getOrdersByResourceId(resourceId: string | number): OrderData[];
43
+ /**
44
+ * 仅覆盖本地已存在的订单;不存在则直接跳过,不落库、不 emit。
45
+ * 适用于"按 order_id 主动刷新本地订单详情"的代理场景。
46
+ */
47
+ overwriteExistingOrder(fresh: OrderData): Promise<{
48
+ overwritten: boolean;
49
+ }>;
43
50
  /**
44
51
  * 通过 SSE 按自定义 query 拉取订单(支持 select/with 精简字段)
45
52
  */