/** * 配送模块模拟数据服务 (Mock Service) * 依据: pages/mall/delivery/doc/需求文档/ * 涵盖: 商家端订单、平台端轨迹、Webhook 日志与场景模拟 */ export interface MockOrder { order_no: string status: string created_at: string receiver_name: string receiver_masked_phone: string address: string amount: string carrier: string tracking_no: string last_synced_at?: string } export interface MockTrackingEvent { event_id: string event_time: string event_code: string event_text: string status_code: string node_name?: string location?: string evidence_urls: string[] raw_payload?: string } export interface MockWebhookLog { time: string carrier: string tracking_no: string event_code: string success: boolean result_text: string payload: UTSJSONObject } export interface MockCarrierOption { code: string name: string } class MockService { private orders: MockOrder[] = [ { order_no: 'ORD20260205001', status: 'PENDING', created_at: '2026-02-05 10:00', receiver_name: '张三', receiver_masked_phone: '138****8000', address: '北京市朝阳区某某街道100号', amount: '299.00', tracking_no: '', carrier: '' }, { order_no: 'ORD20260205002', status: 'DELIVERED', created_at: '2026-02-05 09:30', receiver_name: '李四', receiver_masked_phone: '139****1234', address: '上海市浦东新区某某路200号', amount: '158.50', tracking_no: 'YD987654321', carrier: '韵达', last_synced_at: '2026-02-05 14:35' }, { order_no: 'ORD20260205003', status: 'SHIPPED', created_at: '2026-02-04 15:00', receiver_name: '王五', receiver_masked_phone: '137****5566', address: '广州市天河区某某大厦15楼', amount: '88.00', tracking_no: 'ZT123456789', carrier: '中通', last_synced_at: '2026-02-05 10:00' }, { order_no: 'ORD20260205004', status: 'OUT_FOR_DELIVERY', created_at: '2026-02-05 08:00', receiver_name: '赵六', receiver_masked_phone: '135****0011', address: '杭州市西湖区某某创意园', amount: '450.00', tracking_no: 'SF666888999', carrier: '顺丰', last_synced_at: '2026-02-06 09:00' }, { order_no: 'ORD20260205005', status: 'EXCEPTION', created_at: '2026-02-03 12:00', receiver_name: '孙七', receiver_masked_phone: '136****9988', address: '成都市武侯区某某软件园', amount: '120.00', tracking_no: 'YT555444333', carrier: '圆通', last_synced_at: '2026-02-04 18:00' } ] // 全局 Mock 配置项,支持页面间同步 public isTestMode: boolean = true public autoPush: boolean = true public mockUrl: string = 'http://192.168.1.100:3000/mock/v1' // 持久化存储轨迹:按订单号存储 private trackingHistory: Map = new Map() private webhookLogs: MockWebhookLog[] = [] constructor() { this.initDefaultHistory() this.initDefaultLogs() } private initDefaultLogs() { this.webhookLogs = [ { time: '14:35:22', carrier: '韵达', tracking_no: 'YD987654321', event_code: 'DELIVERED', success: true, result_text: '成功入库', payload: { trackingNo: 'YD987654321', status: 'DELIVERED', msg: '您的快件已由本人签收' } as UTSJSONObject } ] } private initDefaultHistory() { const tip = "【物流问题无需找商家或平台,请致电(95338)(专属热线:400-811-1111)更快解决】" // 已签收订单轨迹 this.trackingHistory.set('ORD20260205002', [ { event_id: 'e210', event_time: '2026-02-05 18:30', event_code: 'DELIVERED', event_text: '您的快件已由本人签收。感谢使用韵达快递,期待再次为您服务!', status_code: 'DELIVERED', evidence_urls: ['https://img-shop.gmugmu.com/mock/pod_sample.png'] }, { event_id: 'e209', event_time: '2026-02-05 15:25', event_code: 'OUT_FOR_DELIVERY', event_text: '派送员张师傅(13800138000)正在派件(事先呼我,勿找商家)', status_code: 'OUT_FOR_DELIVERY', evidence_urls: [] }, { event_id: 'e208', event_time: '2026-02-05 15:24', event_code: 'ARRIVED_HUB', event_text: '【朝阳分部】已收入', status_code: 'ARRIVED_HUB', evidence_urls: [] }, { event_id: 'e207', event_time: '2026-02-05 15:23', event_code: 'ARRIVED', event_text: '您的快件已经到达【朝阳区】' + tip, status_code: 'IN_TRANSIT', evidence_urls: [] }, { event_id: 'e200', event_time: '2026-02-05 09:46', event_code: 'SHIPPED', event_text: '包裹正在等待揽收', status_code: 'SHIPPED', evidence_urls: [] } ]) // 运输中订单轨迹 this.trackingHistory.set('ORD20260205003', [ { event_id: 'e305', event_time: '2026-02-05 10:00', event_code: 'TRANSIT', event_text: '快件离开【广州分拣中心】,已发往【天河分部】', status_code: 'IN_TRANSIT', evidence_urls: [] }, { event_id: 'e301', event_time: '2026-02-04 15:30', event_code: 'PICKED', event_text: '包裹已揽收', status_code: 'ARRIVED_HUB', evidence_urls: [] } ]) // 派送中订单轨迹 this.trackingHistory.set('ORD20260205004', [ { event_id: 'e405', event_time: '2026-02-06 09:00', event_code: 'OUT_FOR_DELIVERY', event_text: '派送员王师傅(13700137000)正在派件', status_code: 'OUT_FOR_DELIVERY', evidence_urls: [] }, { event_id: 'e401', event_time: '2026-02-05 18:00', event_code: 'ARRIVED', event_text: '快件到达【杭州西湖分拨中心】', status_code: 'IN_TRANSIT', evidence_urls: [] } ]) // 异常订单轨迹 this.trackingHistory.set('ORD20260205005', [ { event_id: 'e505', event_time: '2026-02-04 18:00', event_code: 'EXCEPTION', event_text: '【包裹异常】由于天气原因,快件将在分拨中心稍作停留', status_code: 'EXCEPTION', evidence_urls: [] }, { event_id: 'e501', event_time: '2026-02-03 14:00', event_code: 'PICKED', event_text: '包裹已揽收', status_code: 'ARRIVED_HUB', evidence_urls: [] } ]) } getAvailableCarriers(): MockCarrierOption[] { return [ { code: 'YUNDA', name: '韵达' }, { code: 'YTO', name: '圆通' }, { code: 'ZTO', name: '中通' }, { code: 'STO', name: '申通' } ] } getMockOrders(): MockOrder[] { return this.orders } bindShipment(orderNo: string, carrier: string, trackingNo: string): MockOrder | null { const order = this.orders.find(o => o.order_no === orderNo) if (!order) return null order.carrier = carrier order.tracking_no = trackingNo order.status = 'SHIPPED' order.last_synced_at = this.formatDate(new Date()) // 初始化轨迹 this.trackingHistory.set(orderNo, [ { event_id: 'init_' + Date.now(), event_time: order.last_synced_at, event_code: 'CREATED', event_text: '商家已发货,等待快递公司揽收', status_code: '已发货', evidence_urls: [] } ]) return order } getMockTracking(id: string): MockTrackingEvent[] { const order = this.orders.find(o => o.order_no === id || o.tracking_no === id) if (order != null && this.trackingHistory.has(order.order_no)) { return this.trackingHistory.get(order.order_no)! } return [] } /** * 生成符合消费者端高保真展示的物流轨迹 */ generateFullProcess(id: string) { const order = this.orders.find(o => o.order_no === id || o.tracking_no === id) if (!order) return const now = new Date() const getPastTime = (days: number, hours: number): string => { const d = new Date(now.getTime() - (days * 24 + hours) * 3600 * 1000) const YY = d.getFullYear() const M = (d.getMonth() + 1).toString().padStart(2, '0') const DD = d.getDate().toString().padStart(2, '0') const h = d.getHours().toString().padStart(2, '0') const m = d.getMinutes().toString().padStart(2, '0') return `${YY}-${M}-${DD} ${h}:${m}` } const tip = "【物流问题无需找商家或平台,请致电(95338)(专属热线:400-811-1111)更快解决】" const fullProcess: MockTrackingEvent[] = [ { event_id: 'f7', event_time: getPastTime(0, 1), event_code: 'OUT_FOR_DELIVERY', event_text: '派送员张师傅(13800138000)正在派件(事先呼我,勿找商家)', status_code: 'OUT_FOR_DELIVERY', evidence_urls: [] }, { event_id: 'f6', event_time: getPastTime(0, 4), event_code: 'TRANSIT', event_text: '【朝阳分部】已收入', status_code: 'ARRIVED_HUB', evidence_urls: [] }, { event_id: 'f5', event_time: getPastTime(0, 12), event_code: 'ARRIVED', event_text: '您的快件已经到达【北京朝阳区】' + tip, status_code: 'IN_TRANSIT', evidence_urls: [] }, { event_id: 'f4', event_time: getPastTime(1, 2), event_code: 'DEPARTED', event_text: '您的快件离开【顺义转运中心】,已发往【北京朝阳区】', status_code: 'IN_TRANSIT', evidence_urls: [] }, { event_id: 'f3', event_time: getPastTime(1, 10), event_code: 'ARRIVED_HUB', event_text: '您的快件已经到达【顺义转运中心】' + tip, status_code: 'ARRIVED_HUB', evidence_urls: [] }, { event_id: 'f2', event_time: getPastTime(1, 20), event_code: 'PICKED', event_text: '您的快件在【北京海淀区】已揽收,揽收人:李师傅(13911112222)' + tip, status_code: 'ARRIVED_HUB', evidence_urls: [] }, { event_id: 'f1', event_time: getPastTime(2, 1), event_code: 'SHIPPED', event_text: '包裹正在等待揽收', status_code: 'SHIPPED', evidence_urls: [] } ] this.trackingHistory.set(order.order_no, fullProcess) order.status = 'OUT_FOR_DELIVERY' order.last_synced_at = this.formatDate(now) } runScenario(waybillNo: string, scenario: string) { // 兼容逻辑:优先按运单号搜,搜不到按订单号搜 let order = this.orders.find(o => o.tracking_no === waybillNo) if (!order) { order = this.orders.find(o => o.order_no === waybillNo) } if (!order) return if (scenario === 'full') { this.generateFullProcess(order.order_no) } else if (scenario === 'exception') { const history = this.getMockTracking(order.order_no) history.unshift({ event_id: 'ex_' + Date.now(), event_time: this.formatDate(new Date()), event_code: 'EXCEPTION', event_text: '【包裹异常】由于天气原因,快件将在分拨中心稍作停留', status_code: 'EXCEPTION', evidence_urls: [] }) order.status = 'EXCEPTION' } else { // 默认:模拟一个新的在途节点 let history = this.getMockTracking(order.order_no) // 如果该订单还没轨迹(比如刚发货),先初始化数组 if (!this.trackingHistory.has(order.order_no)) { this.trackingHistory.set(order.order_no, []) history = this.trackingHistory.get(order.order_no)! } const now = new Date() history.unshift({ event_id: 'st_' + Date.now(), event_time: this.formatDate(now), event_code: 'TRANS_UPDATE', event_text: '快件已到达新的中转场进行分拣,准备发往目的地', status_code: 'IN_TRANSIT', evidence_urls: [] }) order.last_synced_at = this.formatDate(now) } } /** * 模拟从云端同步最新轨迹(使刷新按钮生效) */ async syncFromCloud(orderNo: string): Promise { const order = this.orders.find(o => o.order_no === orderNo) if (!order || order.status === 'DELIVERED') return false // 模拟网络延迟 return new Promise((resolve) => { setTimeout(() => { this.runScenario(order.tracking_no, 'step') resolve(true) }, 800) }) } getMockWebhookLogs(): MockWebhookLog[] { return this.webhookLogs } /** * 核心功能:模拟第三方回调接口 (适配圆通推送协议) * 对应字段: txLogisticId(订单号), mailNo(票号), infoContent(状态), remark(描述) */ pushWebhookData(payload: UTSJSONObject): boolean { // 兼容圆通协议字段 const tracking_no = (payload['mailNo'] != null) ? payload['mailNo'] as string : (payload['tracking_no'] as string) const yto_status = (payload['infoContent'] != null) ? payload['infoContent'] as string : (payload['status_code'] as string) const event_text = (payload['remark'] != null) ? payload['remark'] as string : (payload['event_text'] as string) const order_id = payload['txLogisticId'] as string || '' const carrier = payload['carrier'] as string || '圆通速递' // 状态映射:圆通状态 -> 本系统状态 let status_code = 'IN_TRANSIT' if (yto_status === 'GOT' || yto_status === 'SEND') status_code = 'IN_TRANSIT' else if (yto_status === 'SENT') status_code = 'OUT_FOR_DELIVERY' else if (yto_status === 'SIGNED') status_code = 'DELIVERED' else if (yto_status === 'FAILED') status_code = 'EXCEPTION' else status_code = yto_status // 1. 记录原始日志 const now = new Date() const log: MockWebhookLog = { time: this.formatDate(now).split(' ')[1], carrier: carrier, tracking_no: tracking_no, event_code: yto_status, // 保留圆通原始代码 success: true, result_text: '接收成功', payload: payload } this.webhookLogs.unshift(log) // 2. 更新系统内部轨迹 const order = this.orders.find(o => o.tracking_no === tracking_no || o.order_no === order_id) if (order != null) { if (!this.trackingHistory.has(order.order_no)) { this.trackingHistory.set(order.order_no, []) } const history = this.trackingHistory.get(order.order_no)! history.unshift({ event_id: 'yto_' + Date.now(), event_time: payload['acceptTime'] as string || this.formatDate(now), event_code: yto_status, event_text: event_text, status_code: status_code, evidence_urls: [] }) order.status = status_code order.last_synced_at = this.formatDate(now) return true } log.success = false log.result_text = '未找到对应的运单或订单号' return false } private formatDate(date: Date): string { const Y = date.getFullYear() const M = (date.getMonth() + 1).toString().padStart(2, '0') const D = date.getDate().toString().padStart(2, '0') const h = date.getHours().toString().padStart(2, '0') const m = date.getMinutes().toString().padStart(2, '0') return `${Y}-${M}-${D} ${h}:${m}` } } export const mockService = new MockService()