507 lines
16 KiB
Plaintext
507 lines
16 KiB
Plaintext
/**
|
||
* 配送模块模拟数据服务 (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<string, MockTrackingEvent[]> = 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<boolean> {
|
||
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()
|