Files
medical-mall/pages/mall/delivery/test/mock-service.uts
2026-03-12 18:05:32 +08:00

674 lines
24 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import supa from '@/components/supadb/aksupainstance.uts'
import { getCurrentUserId } from '@/utils/store.uts'
/**
* 配送模块数据服务 (Express Service)
* 依据: pages/mall/delivery/doc/需求文档/
* 功能: 对接 Supabase 生产表 (platform_express_*)
*/
export interface MockOrder {
id?: string
order_no: string
user_id?: string
merchant_id?: 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<MockOrder[]> {
const userId = getCurrentUserId()
console.log('DEBUG: currentUserId =', userId)
// 方案 1直接通过运单表关联订单表的 user_id 过滤
// 注意:确保 ml_orders!inner(*) 强制关联,否则 eq 会失效
const query = supa.from('platform_express_waybills')
.select('*, ml_orders!inner(*)')
.order('created_at', { ascending: false })
if (userId != '' && userId != 'admin') {
console.log('DEBUG: filtering by ml_orders.user_id =', userId)
query.eq('ml_orders.user_id', userId)
}
const { data: d1, error: e1 } = await query.execute()
// 如果请求本身报错,先记录并尝试降级策略
if (e1 != null) {
console.error('DEBUG: primary query error =', e1)
}
// 规范化返回的数据为数组Supabase 客户端在某些情况下可能返回包装对象)
let resultArray: Array<UTSJSONObject> | null = null
if (Array.isArray(d1)) {
resultArray = d1 as Array<UTSJSONObject>
} else if (d1 != null && (d1 as any).data && Array.isArray((d1 as any).data)) {
resultArray = (d1 as any).data as Array<UTSJSONObject>
}
// 方案 2保底如果方案 1 没查到数据,可能是因为某些运单没有关联订单,或者 !inner 过滤太严格
if (resultArray == null || resultArray.length === 0) {
console.log('DEBUG: first query empty or error, trying fallback...')
const query2 = supa.from('platform_express_waybills')
.select('*, ml_orders(*)')
.order('created_at', { ascending: false })
const { data: d2, error: e2 } = await query2.execute()
if (e2 != null) {
console.error('DEBUG: fallback query error =', e2)
}
const allData = Array.isArray(d2) ? (d2 as Array<UTSJSONObject>) : ((d2 && (d2 as any).data && Array.isArray((d2 as any).data)) ? (d2 as any).data : null)
if (allData != null) {
// 在内存中过滤归属于当前用户的订单(用于数据还没同步完全的情况)
const filtered = allData.filter((item: UTSJSONObject): boolean => {
if (userId == '' || userId == 'admin') return true
let ml: UTSJSONObject | null = null
const mlData = item['ml_orders']
if (mlData instanceof Array && (mlData as Array<any>).length > 0) {
ml = (mlData as Array<UTSJSONObject>)[0]
} else if (!(mlData instanceof Array)) {
ml = mlData as UTSJSONObject
}
try {
return ml != null && (ml as any).user_id == userId
} catch (err) {
return false
}
})
console.log('DEBUG: filtered in memory length =', filtered.length)
return this.mapDataToOrders(filtered)
}
}
console.log('DEBUG: query result data length =', resultArray ? resultArray.length : 0)
return this.mapDataToOrders(resultArray)
}
// 抽离数据映射逻辑
private mapDataToOrders(data: Array<UTSJSONObject> | null): MockOrder[] {
if (data == null) return []
return data.map((item: UTSJSONObject): MockOrder => {
let ml: UTSJSONObject | null = null
const mlData = item['ml_orders']
if (mlData != null) {
if (mlData instanceof Array && (mlData as Array<any>).length > 0) {
ml = (mlData as Array<UTSJSONObject>)[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,
merchant_id: (ml != null ? ml['merchant_id'] 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<MockTrackingEvent[]> {
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<any>).length > 0) {
const row = (b1 as Array<UTSJSONObject>)[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<any>).length > 0) {
const row = (b2 as Array<UTSJSONObject>)[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<UTSJSONObject>)
}
// 如果按 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<UTSJSONObject>)
}
}
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<any>).length > 0) {
return this.mapToEvents(directEvents as Array<UTSJSONObject>)
}
return []
}
/**
* 管理/调试用:获取所有运单(不做用户过滤)
*/
async getAllOrders(): Promise<MockOrder[]> {
const { data, error } = await supa.from('platform_express_waybills')
.select('*, ml_orders(*)')
.order('created_at', { ascending: false })
.execute()
if (error != null) {
console.error('Fetch all orders error:', error)
return []
}
const list = data as Array<UTSJSONObject>
return this.mapDataToOrders(list)
}
/**
* 模拟各种物流场景 (生成生产测试数据)
*/
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<any>).length === 0) {
console.error('Scenario Error: Waybill not found', trackingNo)
return
}
const waybill = (waybills as Array<UTSJSONObject>)[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<UTSJSONObject>): 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<boolean> {
// 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<any>).length > 0) {
// 更新
const waybillRow = (existingWaybills as Array<UTSJSONObject>)[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<boolean> {
// 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<any>).length === 0) {
return false
}
const waybill = (waybills as Array<UTSJSONObject>)[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<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_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<UTSJSONObject> | 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<UTSJSONObject> | 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<MockWebhookLog[]> {
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<UTSJSONObject>
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<boolean> {
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()