import supa from '@/components/supadb/aksupainstance.uts' /** * 配送模块数据服务 (Express Service) * 依据: pages/mall/delivery/doc/需求文档/ * 功能: 对接 Supabase 生产表 (platform_express_*) */ export interface MockOrder { id?: string 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 current_status_text?: 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 const LogisticsConstants = { // 8 大标准配送状态映射 STATUS_MAP: { 'ORDER_PLACED': '已下单', 'SHIPPED': '已发货', 'PENDING': '待发货', 'IN_TRANSIT': '运输中', 'OUT_FOR_DELIVERY': '派送中', 'READY_FOR_PICKUP': '待取件', 'DELIVERED': '已签收', 'EXCEPTION': '异常', 'RETURNED': '退回/退件' } as UTSJSONObject, // 状态对应图标 STATUS_ICONS: { 'ORDER_PLACED': '📦', 'SHIPPED': '🚚', 'IN_TRANSIT': '🛤️', 'OUT_FOR_DELIVERY': '🛵', 'READY_FOR_PICKUP': '🏪', 'DELIVERED': '✅', 'EXCEPTION': '❗', 'RETURNED': '🔄' } as UTSJSONObject, // 时间线简短标签 STATUS_LABELS_SHORT: { 'ORDER_PLACED': '下单', 'SHIPPED': '出库', 'IN_TRANSIT': '运输', 'OUT_FOR_DELIVERY': '派送', 'READY_FOR_PICKUP': '待取', 'DELIVERED': '完成', 'EXCEPTION': '异常', 'RETURNED': '退回' } as UTSJSONObject, // 常用快递公司列表 CARRIERS: [ { label: '顺丰速运', value: '顺丰速运' }, { label: '圆通速递', value: '圆通速递' }, { label: '中通快递', value: '中通快递' }, { label: '申通快递', value: '申通快递' }, { label: '韵达快递', value: '韵达快递' }, { label: '京东快递', value: '京东快递' } ] as UTSJSONObject[] } class MockService { // 全局 Mock 配置项 public isTestMode: boolean = true public autoPush: boolean = true public mockUrl: string = 'http://192.168.1.100:3000/mock/v1' constructor() {} async getMockOrders(): Promise { const { data, error } = await supa.from('platform_express_waybills') .select('*,ml_orders(*)') .order('created_at', { ascending: false }) .execute() if (error != null) { console.error('Fetch orders error:', error) return [] } const list = data as Array return list.map((item: UTSJSONObject): MockOrder => { // 兼容 Supabase 关联查询返回对象或数组的情况 let ml: UTSJSONObject | null = null const mlData = item['ml_orders'] if (mlData != null) { if (mlData instanceof Array && (mlData as Array).length > 0) { ml = (mlData as Array)[0] } else if (!(mlData instanceof Array)) { ml = mlData as UTSJSONObject } } return { id: item['id'] as string, order_no: item['order_no'] as string || (ml != null ? ml['order_no'] as string : ''), status: item['current_status_code'] as string, created_at: this.formatDBTime(item['created_at'] as string), receiver_name: (ml != null ? ml['receiver_name'] as string : '测试用户'), receiver_masked_phone: this.maskPhone(ml != null ? ml['receiver_phone'] as string : '138****0000'), address: (ml != null ? ml['address'] as string : '系统模拟地址'), amount: (ml != null ? ml['amount'] as string : '0.00'), carrier: item['carrier'] as string, tracking_no: item['tracking_no'] as string, last_synced_at: this.formatDBTime(item['last_synced_at'] as string), current_status_text: item['current_status_text'] as string } as MockOrder }) } async getMockTracking(id: string): Promise { if (!id || id == '') return [] // 1. 穿透式查询:先寻找运单 ID let waybillId: string | null = null let trackingNo: string | null = null // 尝试按订单号找 const { data: b1 } = await supa.from('platform_express_waybills').select('id,tracking_no').eq('order_no', id).execute() if (b1 != null && (b1 as Array).length > 0) { const row = (b1 as Array)[0] waybillId = row['id'] as string trackingNo = row['tracking_no'] as string } else { // 尝试按运单号找 const { data: b2 } = await supa.from('platform_express_waybills').select('id,tracking_no').eq('tracking_no', id).execute() if (b2 != null && (b2 as Array).length > 0) { const row = (b2 as Array)[0] waybillId = row['id'] as string trackingNo = row['tracking_no'] as string } } if (waybillId != null) { // 2. 查该运单聚合的所有轨迹 // 同样为了稳定,分两次查或使用简单的 or (如果 or 有效) let allEvents = [] as MockTrackingEvent[] const { data: events, error } = await supa.from('platform_express_tracking_events') .select('*') .eq('waybill_id', waybillId!) .order('event_time', { ascending: false }) .execute() if (events != null) { allEvents = this.mapToEvents(events as Array) } // 如果按 UUID 没查到,尝试按单号补查 if (allEvents.length == 0 && trackingNo != null) { const { data: events2 } = await supa.from('platform_express_tracking_events') .select('*') .eq('tracking_no', trackingNo!) .order('event_time', { ascending: false }) .execute() if (events2 != null) { allEvents = this.mapToEvents(events2 as Array) } } return allEvents } // 3. 最后的保底:直接尝试在轨迹表中匹配 (单号直接搜索) const { data: directEvents } = await supa.from('platform_express_tracking_events') .select('*') .eq('tracking_no', id) .order('event_time', { ascending: false }) .execute() if (directEvents != null && (directEvents as Array).length > 0) { return this.mapToEvents(directEvents as Array) } return [] } /** * 模拟各种物流场景 (生成生产测试数据) */ async runScenario(trackingNo: string, type: string) { // 查找运单 (使用通用的单号查询逻辑) const { data: waybills } = await supa.from('platform_express_waybills') .select('id,carrier,tracking_no') .eq('tracking_no', trackingNo) .execute() if (waybills == null || (waybills as Array).length === 0) { console.error('Scenario Error: Waybill not found', trackingNo) return } const waybill = (waybills as Array)[0] const waybillId = waybill['id'] as string const carrier = waybill['carrier'] as string if (waybillId == null || waybillId == "") { console.error('Scenario Error: Waybill ID is null') return } // 模拟不同步长的数据注入 const now = new Date().toISOString() const events = [] as any[] if (type === 'full') { events.push({ waybill_id: waybillId, carrier, tracking_no: trackingNo, event_time: now, event_code: 'SIGNED', event_text: '用户已签收', status_code: 'DELIVERED', dedupe_key: 'SCENARIO_SIG_'+Date.now() }) events.push({ waybill_id: waybillId, carrier, tracking_no: trackingNo, event_time: now, event_code: 'SENT', event_text: '派送中', status_code: 'OUT_FOR_DELIVERY', dedupe_key: 'SCENARIO_SEN_'+Date.now() }) } else if (type === 'standard') { events.push({ waybill_id: waybillId, carrier, tracking_no: trackingNo, event_time: now, event_code: 'ARRIVED_HUB', event_text: '快件已到达中转站', status_code: 'IN_TRANSIT', dedupe_key: 'SCENARIO_HUB_'+Date.now() }) } else if (type === 'exception') { events.push({ waybill_id: waybillId, carrier, tracking_no: trackingNo, event_time: now, event_code: 'FAILED', event_text: '包裹由于地址不详正在退回', status_code: 'EXCEPTION', dedupe_key: 'SCENARIO_ERR_'+Date.now() }) } if (events.length > 0) { // 1. 插入轨迹节点 await supa.from('platform_express_tracking_events').insert(events).execute() // 2. 同步更新主表状态 (以最新一个节点为准) const latest = events[0] as UTSJSONObject await supa.from('platform_express_waybills').update({ current_status_code: latest['status_code'] as string, current_status_text: latest['event_text'] as string, last_synced_at: now }).eq('id', waybillId).execute() } } private mapToEvents(list: Array): MockTrackingEvent[] { return list.map((e: UTSJSONObject): MockTrackingEvent => { // 解析 evidence_urls (JSONB) let urls = [] as string[] const rawUrls = e['evidence_urls'] if (rawUrls instanceof Array) { urls = rawUrls as string[] } return { event_id: e['id'] as string, event_time: this.formatDBTime(e['event_time'] as string), event_code: e['event_code'] as string || '', event_text: e['event_text'] as string || '', status_code: e['status_code'] as string || '', node_name: e['node_name'] as string || '', location: e['location'] as string || '', evidence_urls: urls, raw_payload: JSON.stringify(e['raw_payload'] || {}) } as MockTrackingEvent }) } // 绑定发货逻辑:写库 async bindShipment(orderNo: string, carrier: string, trackingNo: string): Promise { // 1. 先查找订单主体 const { data: orderData } = await supa.from('ml_orders').select('id,order_no').eq('order_no', orderNo).single().execute() const orderId = (orderData != null) ? orderData['id'] as string : null // 2. 创建或更新运单记录 // 先查询是否存在 const { data: existingWaybills } = await supa.from('platform_express_waybills').select('id').eq('order_no', orderNo).execute() let waybillIdForEvent: string | null = null const waybillPayload = { order_id: orderId, order_no: orderNo, carrier: carrier, tracking_no: trackingNo, current_status_code: 'SHIPPED', current_status_text: '商家已发货', last_synced_at: new Date().toISOString(), source: 'mock' } as UTSJSONObject if (existingWaybills != null && (existingWaybills as Array).length > 0) { // 更新 const waybillRow = (existingWaybills as Array)[0] waybillIdForEvent = waybillRow['id'] as string await supa.from('platform_express_waybills').update(waybillPayload).eq('id', waybillIdForEvent).execute() } else { // 插入 const { data: newWaybill, error: insError } = await supa.from('platform_express_waybills').insert(waybillPayload).select().single().execute() if (insError != null) { console.error('Insert waybill error:', insError) return false } if (newWaybill != null) { waybillIdForEvent = (newWaybill as UTSJSONObject)['id'] as string } } if (waybillIdForEvent == null) { console.error('Bind shipment error: Failed to get waybill ID') return false } // 2.5 更新订单表状态 const { error: oError } = await supa.from('ml_orders').update({ order_status: 3, // SHIPPED shipping_status: 2, // 已向物流公司提交 shipped_at: new Date().toISOString() }).eq('order_no', orderNo).execute() if (oError != null) { console.error('Update ml_orders error:', oError) // 继续执行,因为运单记录已经拉起 } // 3. 产生初始轨迹 await supa.from('platform_express_tracking_events').insert({ waybill_id: waybillIdForEvent, carrier: carrier, tracking_no: trackingNo, event_time: new Date().toISOString(), event_code: 'CREATED', event_text: '商家已发货,等待快递公司揽收', status_code: 'SHIPPED', dedupe_key: 'INIT_' + orderNo + '_' + Date.now(), evidence_urls: [] }).execute() return true } // 用户确认取件逻辑 async confirmReceipt(orderNo: string): Promise { // 1. 获取运单信息 const { data: waybills } = await supa.from('platform_express_waybills') .select('id,carrier,tracking_no') .eq('order_no', orderNo) .execute() if (waybills == null || (waybills as Array).length === 0) { return false } const waybill = (waybills as Array)[0] const waybillId = waybill['id'] as string const carrier = waybill['carrier'] as string const trackingNo = waybill['tracking_no'] as string const now = new Date().toISOString() // 2. 更新运单主表状态 await supa.from('platform_express_waybills').update({ current_status_code: 'DELIVERED', current_status_text: '用户已确认取件', last_synced_at: now }).eq('id', waybillId).execute() // 3. 更新订单主表状态 (DEVLIVERED = 4) await supa.from('ml_orders').update({ order_status: 4, delivered_at: now }).eq('order_no', orderNo).execute() // 4. 插入取件轨迹 await supa.from('platform_express_tracking_events').insert({ waybill_id: waybillId, carrier: carrier, tracking_no: trackingNo, event_time: now, event_code: 'USER_PICKUP', event_text: '用户已确认取件,感谢使用', status_code: 'DELIVERED', dedupe_key: 'CONFIRM_' + orderNo + '_' + Date.now(), evidence_urls: [] }).execute() return true } /** * 核心功能:模拟第三方回调接口 (写库版) */ async pushWebhookData(payload: UTSJSONObject): Promise { 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_no = payload['txLogisticId'] as string || '' const carrier = payload['carrier'] as string || '圆通速递' console.log('--- Webhook Push Start ---') console.log('Target:', tracking_no, order_no) // 1. 记录原始日志 (Raw) await supa.from('platform_express_event_raw').insert({ carrier: carrier, tracking_no: tracking_no, body: payload, received_at: new Date().toISOString(), signature_valid: true }).execute() // 2. 查找运单 ID (手动分步查询) let final_id: string = "" // 尝试按单号 const res1 = await supa.from('platform_express_waybills').select('id').eq('tracking_no', tracking_no).execute() const list1 = res1.data as Array | null if (list1 != null && list1.length > 0) { final_id = list1[0]['id'] as string } else { // 尝试按订单号 const res2 = await supa.from('platform_express_waybills').select('id').eq('order_no', order_no).execute() const list2 = res2.data as Array | null if (list2 != null && list2.length > 0) { final_id = list2[0]['id'] as string } } if (final_id == "") { console.error('CRITICAL: Waybill NOT found in DB') return false } console.log('Found Waybill ID:', final_id) // 状态映射 (根据 状态映射表.md 规范) let status_code = 'IN_TRANSIT' if (yto_status === 'GOT' || yto_status === 'SEND' || yto_status === 'TRANSIT') { status_code = 'IN_TRANSIT' } else if (yto_status === 'SENT') { status_code = 'OUT_FOR_DELIVERY' } else if (yto_status === 'PICKUP') { status_code = 'READY_FOR_PICKUP' } else if (yto_status === 'SIGNED' || yto_status === 'DELIVERED') { status_code = 'DELIVERED' } else if (yto_status === 'FAILED' || yto_status === 'EXCEPTION') { status_code = 'EXCEPTION' } else if (yto_status === 'RETURNED') { status_code = 'RETURNED' } else { // 容错:如果是已知的 8 大状态之一,直接使用 const validStates = ['ORDER_PLACED', 'SHIPPED', 'IN_TRANSIT', 'OUT_FOR_DELIVERY', 'READY_FOR_PICKUP', 'DELIVERED', 'EXCEPTION', 'RETURNED'] if (validStates.indexOf(yto_status) > -1) { status_code = yto_status } } // 3. 执行更新 (确保 ID 不为空) const updateRes = await supa.from('platform_express_waybills').update({ current_status_code: status_code, current_status_text: event_text, last_synced_at: new Date().toISOString() }).eq('id', final_id).execute() if (updateRes.error != null) { console.error('Update Waybill Error:', updateRes.error) } // 4. 插入轨迹 (确保 ID 不为空) const acceptTimeRaw = payload['acceptTime'] as string let eventTimeToStore = new Date().toISOString() if (acceptTimeRaw != null && acceptTimeRaw != '') { const normalized = (acceptTimeRaw.indexOf('T') > -1) ? acceptTimeRaw : acceptTimeRaw.replace(' ', 'T') const parsed = new Date(normalized) const parsedMs = parsed.getTime() if (parsedMs == parsedMs) { eventTimeToStore = parsed.toISOString() } } const eventRes = await supa.from('platform_express_tracking_events').insert({ waybill_id: final_id, carrier: carrier, tracking_no: tracking_no, event_time: eventTimeToStore, event_code: yto_status, event_text: event_text, status_code: status_code, raw_payload: payload, dedupe_key: 'WEBHOOK_' + Date.now() }).execute() if (eventRes.error != null) { console.error('Insert Event Error:', eventRes.error) } return true } async getMockWebhookLogs(): Promise { const { data } = await supa.from('platform_express_event_raw') .select('*') .order('received_at', { ascending: false }) .limit(20) .execute() if (data == null) return [] const list = data as Array return list.map((item: UTSJSONObject): MockWebhookLog => { const body = item['body'] as UTSJSONObject return { time: this.formatDBTime(item['received_at'] as string).split(' ')[1], carrier: item['carrier'] as string || 'Unknown', tracking_no: item['tracking_no'] as string || '-', event_code: body['infoContent'] as string || 'Raw', success: true, result_text: '接收成功', payload: body } as MockWebhookLog }) } async syncFromCloud(id: string): Promise { return new Promise((resolve) => { setTimeout(() => resolve(true), 500) }) } private maskPhone(phone: string): string { if (!phone) return '' if (phone.length < 7) return phone return phone.substring(0, 3) + "****" + phone.substring(phone.length - 4) } private formatDBTime(iso: string): string { if (!iso) return '' const d = new Date(iso) const Y = d.getFullYear() const M = (d.getMonth() + 1).toString().padStart(2, '0') const D = d.getDate().toString().padStart(2, '0') const h = d.getHours().toString().padStart(2, '0') const m = d.getMinutes().toString().padStart(2, '0') return `${Y}-${M}-${D} ${h}:${m}` } /** * 统一的状态文本转换工具 */ public getStatusText(status: string): string { const text = LogisticsConstants.STATUS_MAP[status] as string | null return (text != null) ? text : status } /** * 依照需求文档:展示第三方回传的原始轨迹文案 */ public getDisplayMessage(text: string, status: string): string { if (!text) return '暂无动态' // 不再进行脱敏/泛化处理,直接返回原始文案 return text } /** * 统一的状态图标转换工具 */ public getStatusIcon(status: string): string { const icon = LogisticsConstants.STATUS_ICONS[status] as string | null return (icon != null) ? icon : '📦' } } export const mockService = new MockService()