对接数据库,模拟第三方接入信息
This commit is contained in:
@@ -68,6 +68,7 @@
|
||||
测试/预发环境(不使用 mock_* 表也能联调):
|
||||
- 仍然执行 [express_tracking_platform_upgrade.sql](express_tracking_platform_upgrade.sql) 创建“生产同款”三表。
|
||||
- 再执行 [seed_platform_express_test_data.sql](seed_platform_express_test_data.sql) 向 `platform_express_*` 写入少量 TEST_ 前缀示例数据,用于页面联调与排障演示(可随时清理)。
|
||||
- 说明:当前脚本会插入 9 条 `TEST_` 运单(含 1 条 `PENDING/未发货`),并为每条运单插入 1~3 条不等的轨迹事件(幂等,可重复执行)。
|
||||
|
||||
### Ubuntu 上的 Supabase(测试/预发)怎么执行
|
||||
|
||||
@@ -93,6 +94,73 @@
|
||||
清理测试数据:
|
||||
- 种子脚本底部自带清理 SQL(按 `tracking_no LIKE 'TEST_%'` 删除),需要时复制执行即可。
|
||||
|
||||
### 执行成功后的校验 SQL(建议)
|
||||
|
||||
在 Supabase SQL Editor 执行以下查询,确认数据是否写入成功、事件是否按时间线可查:
|
||||
|
||||
1) 查看全部 TEST_ 运单(应看到 9 条):
|
||||
```sql
|
||||
select
|
||||
carrier,
|
||||
tracking_no,
|
||||
current_status_code,
|
||||
current_status_text,
|
||||
last_event_time,
|
||||
last_synced_at,
|
||||
created_at
|
||||
from public.platform_express_waybills
|
||||
where tracking_no like 'TEST_%'
|
||||
order by created_at desc;
|
||||
```
|
||||
|
||||
2) 按运单统计事件数量(确认每单都有事件):
|
||||
```sql
|
||||
select
|
||||
w.carrier,
|
||||
w.tracking_no,
|
||||
count(e.id) as event_count,
|
||||
min(e.event_time) as first_event_time,
|
||||
max(e.event_time) as last_event_time
|
||||
from public.platform_express_waybills w
|
||||
left join public.platform_express_tracking_events e
|
||||
on e.waybill_id = w.id
|
||||
where w.tracking_no like 'TEST_%'
|
||||
group by w.carrier, w.tracking_no
|
||||
order by last_event_time desc nulls last;
|
||||
```
|
||||
|
||||
3) 查看单个运单时间线(按 event_time 升序):
|
||||
```sql
|
||||
select
|
||||
event_time,
|
||||
event_code,
|
||||
event_text,
|
||||
status_code,
|
||||
node_name,
|
||||
node_location,
|
||||
source,
|
||||
dedupe_key
|
||||
from public.platform_express_tracking_events
|
||||
where tracking_no = 'TEST_YT_20260206_0002'
|
||||
order by event_time asc;
|
||||
```
|
||||
|
||||
4) 查看原始接收留痕(可选,用于排障/审计):
|
||||
```sql
|
||||
select
|
||||
received_at,
|
||||
source,
|
||||
carrier,
|
||||
tracking_no,
|
||||
signature_valid,
|
||||
request_id,
|
||||
dedupe_key
|
||||
from public.platform_express_event_raw
|
||||
where tracking_no like 'TEST_%'
|
||||
order by received_at desc
|
||||
limit 50;
|
||||
```
|
||||
|
||||
### 如何“伪造第三方推送到数据库”(无需后端)
|
||||
|
||||
如果你暂时没有 webhook 接收服务,但希望测试环境表现得像“第三方已经推送了轨迹”,可以直接写数据库:
|
||||
|
||||
@@ -26,7 +26,13 @@ INSERT INTO public.platform_express_waybills (
|
||||
VALUES
|
||||
(NULL, 'ORD_TEST_20260206001', 'YUNDA', 'TEST_YD_20260206_0001', 'mock', 'IN_TRANSIT', '运输中', NULL, NOW()),
|
||||
(NULL, 'ORD_TEST_20260206002', 'YTO', 'TEST_YT_20260206_0002', 'mock', 'OUT_FOR_DELIVERY', '派送中', NULL, NOW()),
|
||||
(NULL, 'ORD_TEST_20260206003', 'ZTO', 'TEST_ZT_20260206_0003', 'mock', 'DELIVERED', '已签收', NULL, NOW())
|
||||
(NULL, 'ORD_TEST_20260206003', 'ZTO', 'TEST_ZT_20260206_0003', 'mock', 'DELIVERED', '已签收', NULL, NOW()),
|
||||
(NULL, 'ORD_TEST_20260206004', 'STO', 'TEST_STO_20260206_0004', 'mock', 'EXCEPTION', '异常', NULL, NOW()),
|
||||
(NULL, 'ORD_TEST_20260206005', 'SF', 'TEST_SF_20260206_0005', 'mock', 'ARRIVED_HUB', '到达网点', NULL, NOW()),
|
||||
(NULL, 'ORD_TEST_20260206006', 'YUNDA', 'TEST_YD_20260206_0006', 'mock', 'SHIPPED', '已发货', NULL, NOW()),
|
||||
(NULL, 'ORD_TEST_20260206007', 'YTO', 'TEST_YT_20260206_0007', 'mock', 'IN_TRANSIT', '运输中', NULL, NOW()),
|
||||
(NULL, 'ORD_TEST_20260206008', 'ZTO', 'TEST_ZT_20260206_0008', 'mock', 'OUT_FOR_DELIVERY', '派送中', NULL, NOW()),
|
||||
(NULL, 'ORD_TEST_20260206009', 'YUNDA', 'TEST_YD_20260206_0009', 'mock', 'PENDING', '未发货', NULL, NOW())
|
||||
ON CONFLICT (carrier, tracking_no) DO NOTHING;
|
||||
|
||||
|
||||
@@ -34,7 +40,17 @@ ON CONFLICT (carrier, tracking_no) DO NOTHING;
|
||||
WITH w AS (
|
||||
SELECT id, carrier, tracking_no
|
||||
FROM public.platform_express_waybills
|
||||
WHERE tracking_no IN ('TEST_YD_20260206_0001', 'TEST_YT_20260206_0002', 'TEST_ZT_20260206_0003')
|
||||
WHERE tracking_no IN (
|
||||
'TEST_YD_20260206_0001',
|
||||
'TEST_YT_20260206_0002',
|
||||
'TEST_ZT_20260206_0003',
|
||||
'TEST_STO_20260206_0004',
|
||||
'TEST_SF_20260206_0005',
|
||||
'TEST_YD_20260206_0006',
|
||||
'TEST_YT_20260206_0007',
|
||||
'TEST_ZT_20260206_0008',
|
||||
'TEST_YD_20260206_0009'
|
||||
)
|
||||
)
|
||||
INSERT INTO public.platform_express_tracking_events (
|
||||
waybill_id,
|
||||
@@ -185,6 +201,198 @@ SELECT * FROM (
|
||||
NOW(),
|
||||
'webhook',
|
||||
'test_e_3003'
|
||||
|
||||
-- 运单 4:异常
|
||||
UNION ALL
|
||||
SELECT
|
||||
(SELECT id FROM w WHERE tracking_no = 'TEST_STO_20260206_0004'),
|
||||
'STO',
|
||||
'TEST_STO_20260206_0004',
|
||||
'test_e_4001',
|
||||
NOW() - INTERVAL '20 hours',
|
||||
'PICKED',
|
||||
'包裹已揽收',
|
||||
'ARRIVED_HUB',
|
||||
'深圳南山网点',
|
||||
'深圳市 南山区',
|
||||
NULL,
|
||||
'[]'::jsonb,
|
||||
NULL::jsonb,
|
||||
NOW(),
|
||||
'poll',
|
||||
'test_e_4001'
|
||||
UNION ALL
|
||||
SELECT
|
||||
(SELECT id FROM w WHERE tracking_no = 'TEST_STO_20260206_0004'),
|
||||
'STO',
|
||||
'TEST_STO_20260206_0004',
|
||||
'test_e_4002',
|
||||
NOW() - INTERVAL '2 hours',
|
||||
'EXCEPTION',
|
||||
'【包裹异常】收件地址不详,等待处理(测试数据)',
|
||||
'EXCEPTION',
|
||||
'深圳南山网点',
|
||||
'深圳市 南山区',
|
||||
NULL,
|
||||
'[]'::jsonb,
|
||||
NULL::jsonb,
|
||||
NOW(),
|
||||
'webhook',
|
||||
'test_e_4002'
|
||||
|
||||
-- 运单 5:到达网点
|
||||
UNION ALL
|
||||
SELECT
|
||||
(SELECT id FROM w WHERE tracking_no = 'TEST_SF_20260206_0005'),
|
||||
'SF',
|
||||
'TEST_SF_20260206_0005',
|
||||
'test_e_5001',
|
||||
NOW() - INTERVAL '10 hours',
|
||||
'PICKED',
|
||||
'包裹已揽收',
|
||||
'ARRIVED_HUB',
|
||||
'南京江宁集散中心',
|
||||
'南京市 江宁区',
|
||||
NULL,
|
||||
'[]'::jsonb,
|
||||
NULL::jsonb,
|
||||
NOW(),
|
||||
'poll',
|
||||
'test_e_5001'
|
||||
UNION ALL
|
||||
SELECT
|
||||
(SELECT id FROM w WHERE tracking_no = 'TEST_SF_20260206_0005'),
|
||||
'SF',
|
||||
'TEST_SF_20260206_0005',
|
||||
'test_e_5002',
|
||||
NOW() - INTERVAL '30 minutes',
|
||||
'ARRIVAL',
|
||||
'快件已到达【南京江宁网点】,等待派送(测试数据)',
|
||||
'ARRIVED_HUB',
|
||||
'南京江宁网点',
|
||||
'南京市 江宁区',
|
||||
NULL,
|
||||
'[]'::jsonb,
|
||||
NULL::jsonb,
|
||||
NOW(),
|
||||
'webhook',
|
||||
'test_e_5002'
|
||||
|
||||
-- 运单 6:已发货(待揽收)
|
||||
UNION ALL
|
||||
SELECT
|
||||
(SELECT id FROM w WHERE tracking_no = 'TEST_YD_20260206_0006'),
|
||||
'YUNDA',
|
||||
'TEST_YD_20260206_0006',
|
||||
'test_e_6001',
|
||||
NOW() - INTERVAL '5 hours',
|
||||
'SHIPPED',
|
||||
'商家已发货,等待快递公司揽收(测试数据)',
|
||||
'SHIPPED',
|
||||
NULL,
|
||||
'杭州市 余杭区',
|
||||
NULL,
|
||||
'[]'::jsonb,
|
||||
NULL::jsonb,
|
||||
NOW(),
|
||||
'manual',
|
||||
'test_e_6001'
|
||||
|
||||
-- 运单 7:运输中
|
||||
UNION ALL
|
||||
SELECT
|
||||
(SELECT id FROM w WHERE tracking_no = 'TEST_YT_20260206_0007'),
|
||||
'YTO',
|
||||
'TEST_YT_20260206_0007',
|
||||
'test_e_7001',
|
||||
NOW() - INTERVAL '18 hours',
|
||||
'PICKED',
|
||||
'包裹已揽收',
|
||||
'ARRIVED_HUB',
|
||||
'武汉江夏网点',
|
||||
'武汉市 江夏区',
|
||||
NULL,
|
||||
'[]'::jsonb,
|
||||
NULL::jsonb,
|
||||
NOW(),
|
||||
'poll',
|
||||
'test_e_7001'
|
||||
UNION ALL
|
||||
SELECT
|
||||
(SELECT id FROM w WHERE tracking_no = 'TEST_YT_20260206_0007'),
|
||||
'YTO',
|
||||
'TEST_YT_20260206_0007',
|
||||
'test_e_7002',
|
||||
NOW() - INTERVAL '7 hours',
|
||||
'TRANSIT',
|
||||
'快件离开【武汉江夏网点】,已发往【长沙转运中心】(测试数据)',
|
||||
'IN_TRANSIT',
|
||||
'武汉江夏网点',
|
||||
'武汉市 江夏区',
|
||||
NULL,
|
||||
'[]'::jsonb,
|
||||
NULL::jsonb,
|
||||
NOW(),
|
||||
'poll',
|
||||
'test_e_7002'
|
||||
|
||||
-- 运单 8:派送中
|
||||
UNION ALL
|
||||
SELECT
|
||||
(SELECT id FROM w WHERE tracking_no = 'TEST_ZT_20260206_0008'),
|
||||
'ZTO',
|
||||
'TEST_ZT_20260206_0008',
|
||||
'test_e_8001',
|
||||
NOW() - INTERVAL '9 hours',
|
||||
'ARRIVAL',
|
||||
'快件已到达【西安高新网点】(测试数据)',
|
||||
'IN_TRANSIT',
|
||||
'西安高新网点',
|
||||
'西安市 雁塔区',
|
||||
NULL,
|
||||
'[]'::jsonb,
|
||||
NULL::jsonb,
|
||||
NOW(),
|
||||
'poll',
|
||||
'test_e_8001'
|
||||
UNION ALL
|
||||
SELECT
|
||||
(SELECT id FROM w WHERE tracking_no = 'TEST_ZT_20260206_0008'),
|
||||
'ZTO',
|
||||
'TEST_ZT_20260206_0008',
|
||||
'test_e_8002',
|
||||
NOW() - INTERVAL '20 minutes',
|
||||
'OUT_FOR_DELIVERY',
|
||||
'派送员正在派件(测试数据)',
|
||||
'OUT_FOR_DELIVERY',
|
||||
'西安高新网点',
|
||||
'西安市 雁塔区',
|
||||
NULL,
|
||||
'[]'::jsonb,
|
||||
NULL::jsonb,
|
||||
NOW(),
|
||||
'webhook',
|
||||
'test_e_8002'
|
||||
|
||||
-- 运单 9:未发货(占位事件,便于前端展示)
|
||||
UNION ALL
|
||||
SELECT
|
||||
(SELECT id FROM w WHERE tracking_no = 'TEST_YD_20260206_0009'),
|
||||
'YUNDA',
|
||||
'TEST_YD_20260206_0009',
|
||||
'test_e_9001',
|
||||
NOW() - INTERVAL '3 hours',
|
||||
'PENDING',
|
||||
'商家待发货(测试数据)',
|
||||
'PENDING',
|
||||
NULL,
|
||||
'杭州市 余杭区',
|
||||
NULL,
|
||||
'[]'::jsonb,
|
||||
NULL::jsonb,
|
||||
NOW(),
|
||||
'manual',
|
||||
'test_e_9001'
|
||||
) AS rows_to_insert
|
||||
WHERE rows_to_insert.waybill_id IS NOT NULL
|
||||
ON CONFLICT (waybill_id, dedupe_key) DO NOTHING;
|
||||
@@ -224,6 +432,39 @@ VALUES (
|
||||
'raw_test_0001'
|
||||
);
|
||||
|
||||
INSERT INTO public.platform_express_event_raw (
|
||||
received_at,
|
||||
source,
|
||||
client_id,
|
||||
carrier,
|
||||
tracking_no,
|
||||
signature_valid,
|
||||
signature,
|
||||
ts_header,
|
||||
request_id,
|
||||
remote_ip,
|
||||
headers,
|
||||
body,
|
||||
parse_error,
|
||||
dedupe_key
|
||||
)
|
||||
VALUES (
|
||||
NOW(),
|
||||
'poll',
|
||||
'test_client',
|
||||
'STO',
|
||||
'TEST_STO_20260206_0004',
|
||||
NULL,
|
||||
NULL,
|
||||
EXTRACT(EPOCH FROM NOW())::text,
|
||||
'req_test_0002',
|
||||
'127.0.0.1',
|
||||
'{"content-type":"application/json"}'::jsonb,
|
||||
'{"tracking_no":"TEST_STO_20260206_0004","status_code":"EXCEPTION","event_text":"【包裹异常】收件地址不详,等待处理(测试数据)"}'::jsonb,
|
||||
NULL,
|
||||
'raw_test_0002'
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- =====================================================================================
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
- 平台接收 Mock Server 的 Webhook 推送(推荐)。
|
||||
- 平台亦可主动调用 Mock Server 查询轨迹(模拟轮询补偿)。
|
||||
|
||||
范围说明(避免误解):
|
||||
- 若要实现“电子面单/在线下单自动获取运单号”,需要对接第三方(快递公司直连或聚合平台)提供的下单/面单接口。
|
||||
- 该类“下单/面单”接口不属于本 Mock Server 接口规范范围;本文只覆盖轨迹事件的推送/查询与相关验签、幂等与字段口径。
|
||||
|
||||
## 四、接口定义(示例)
|
||||
|
||||
1) Mock Server -> 平台:事件推送(Webhook)
|
||||
|
||||
@@ -59,8 +59,8 @@
|
||||
与 Mock 的关系:
|
||||
- Mock 用于第三方未联通阶段的替代数据源与故障注入;生产环境默认关闭。
|
||||
|
||||
### 方案 B:商家自选配送(商家自找承运方 / 平台只做订单)
|
||||
适用:平台招商、品类/区域差异大、平台希望给商家“选择承运方”的灵活性。
|
||||
### 方案 B:商家自选配送(商家自选承运商并发货 / 平台负责运单绑定与轨迹展示)
|
||||
适用:平台招商、品类/区域差异大、平台希望给商家“选择承运方”的灵活性;平台不承担“实际配送执行”,但需要提供统一的运单绑定入口与轨迹展示底座。
|
||||
|
||||
重要说明(商家只是平台商户的常见情况):
|
||||
- 默认不要求商家自建系统或自行对接第三方 API。
|
||||
@@ -75,6 +75,30 @@
|
||||
- 在平台选择承运方并完成发货动作:提供/回填 `tracking_no` 与必要的订单关联信息(如 `order_no`)。
|
||||
- 若商家坚持使用“商家与第三方的独立合同/账号”,可向平台提交第三方对接所需材料,由平台统一配置与代接入(不要求商家自建回调服务)。
|
||||
|
||||
可选增强:商家在平台内“自助对接第三方”(无需商家开发)
|
||||
- 定位:可选增强能力,非一期必做。
|
||||
- 目标:让商家在平台后台完成第三方账号授权/配置,从而在平台内完成下单打单、获取运单号、查询/订阅轨迹;平台仍统一入库与展示。
|
||||
- 常见两种形态(可二选一,也可并存):
|
||||
1) 聚合平台授权开通(推荐优先):平台统一对接一家聚合服务商;商家在平台内完成开通/授权后即可使用多家快递能力。
|
||||
2) 商家自带第三方账号(BYO Account):商家已与某快递/聚合平台签约;在平台后台录入密钥或走 OAuth 授权,平台代为调用第三方 API。
|
||||
- 商家后台最小页面/流程建议:
|
||||
- 【物流渠道管理】:选择渠道类型(聚合/直连)、开启/停用
|
||||
- 【授权与密钥】:录入/更新 `appKey/appSecret/token` 或 OAuth 绑定;展示“最后一次连通性检测”结果
|
||||
- 【回调配置提示】:展示平台 Webhook 地址与白名单要求(若第三方需要配置回调)
|
||||
- 【测试与排障】:一键“连通性测试/拉取一条轨迹/模拟下单”,失败给出可读错误
|
||||
- 安全要求(必须):
|
||||
- 第三方密钥加密存储、最小权限、变更审计;仅商家管理员可配置。
|
||||
- 不在前端暴露密钥;平台服务端代调用第三方接口。
|
||||
- 轨迹/运单数据仍写入平台统一表结构与事件模型,避免不同渠道把差异带到前端。
|
||||
|
||||
是否需要做(决策建议):
|
||||
- 暂不需要(建议先不做)的场景:一期目标仅是“商家回填运单号 + 平台展示第三方轨迹关键节点”,且商家规模不大/对接资源有限。
|
||||
- 建议需要(做了收益明显)的场景:商家量大且运单号回填错误率高、客服投诉“查不到物流/不更新”多;或明确要上“电子面单/平台一键下单”。
|
||||
- 推荐分期:
|
||||
- Phase 1:回填运单号 + 轨迹接入与统一展示底座(本项目当前优先级)。
|
||||
- Phase 2:先接入 1 家聚合平台做统一下单/面单/轨迹(降低多家直连成本)。
|
||||
- Phase 3:再开放 BYO Account(商家自带账号)自助配置(安全与运维成本最高)。
|
||||
|
||||
优点:平台轻、商家灵活。
|
||||
风险/成本:体验碎片化风险大;如果不强制回传规范,平台客服/前端会被迫处理多样差异,长期维护成本更高。
|
||||
|
||||
@@ -85,6 +109,18 @@
|
||||
- `tracking_no`:运单号生成与回传方式(商家生成/第三方返回/平台生成)。
|
||||
- 订单关联信息:`order_no`(或平台侧可解析的业务单号),用于把轨迹绑定到订单详情页。
|
||||
|
||||
运单号(`tracking_no`)获取方式(两种常见落地,二选一或并存):
|
||||
1) 回填运单号(最小模式,推荐先上线):
|
||||
- 商家在线下/快递官方系统/聚合平台完成下单与交接,获得运单号。
|
||||
- 商家在平台后台“发货”时选择承运商并回填 `tracking_no`。
|
||||
- 平台不需要调用第三方“下单/面单”接口,只需后续接入轨迹(Webhook/轮询)用于展示。
|
||||
2) 电子面单 / 在线下单(增强模式,体验更好):
|
||||
- 平台(或平台集成的服务商)需要对接第三方提供的“下单/面单”接口,向第三方提交发货所需信息(收件人/地址/重量/件数等),并获得:运单号 `tracking_no` + 面单文件/面单号等。
|
||||
- 第三方可以是:
|
||||
- 直连快递公司接口(每家快递一套协议);或
|
||||
- 快递聚合平台接口(一套协议覆盖多家快递)。
|
||||
- 兜底策略建议:若第三方下单失败/超时,允许商家改为“手工回填运单号”完成发货闭环。
|
||||
|
||||
轨迹数据接入方式(平台统一接入,推荐第一种):
|
||||
1) 第三方 -> 平台 Webhook 推送(推荐):
|
||||
- 平台与第三方完成订阅/回调配置;第三方直接回调平台 Webhook。
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="back-link" @click="goBack">⬅ 返回</text>
|
||||
<text class="title">第三方物流 API 模拟发送器</text>
|
||||
<text class="subtitle">模拟外部物流平台向后端推送 Webhook 轨迹数据</text>
|
||||
</view>
|
||||
@@ -18,7 +19,7 @@
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<text class="section-title">2. 构造圆通协议数据 (YTO Protocol)</text>
|
||||
<text class="section-title">2. 构造协议数据 (YTO Protocol)</text>
|
||||
<view class="form-group">
|
||||
<text class="label">物流单号 (mailNo):</text>
|
||||
<input class="input" v-model="form.mailNo" placeholder="请输入运单号" />
|
||||
@@ -56,11 +57,26 @@
|
||||
|
||||
<script setup lang="uts">
|
||||
import { mockService, MockOrder } from './mock-service.uts'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
|
||||
const orders = ref([] as MockOrder[])
|
||||
const shippedOrders = computed((): MockOrder[] => {
|
||||
return mockService.getMockOrders().filter((o: MockOrder): boolean => o.status !== 'PENDING' && o.tracking_no !== '')
|
||||
return orders.value.filter((o: MockOrder): boolean => o.status !== 'PENDING' && o.tracking_no !== '')
|
||||
})
|
||||
|
||||
async function loadOrders() {
|
||||
const data = await mockService.getMockOrders()
|
||||
orders.value = data
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
loadOrders()
|
||||
})
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
const selectedOrderIndex = ref(-1)
|
||||
|
||||
const form = reactive({
|
||||
@@ -111,12 +127,23 @@
|
||||
form.infoContent = statusOptions[idx].value
|
||||
}
|
||||
|
||||
function sendWebhook() {
|
||||
async function sendWebhook() {
|
||||
if (!form.mailNo) {
|
||||
uni.showToast({ title: '请先填写运单号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 检查单号对应的订单是否已签收
|
||||
const targetOrder = orders.value.find((o: MockOrder): boolean => o.tracking_no === form.mailNo)
|
||||
if (targetOrder != null && targetOrder.status === 'DELIVERED') {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '该订单已显示已签收,无需继续推送物流动态。',
|
||||
showCancel: false
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前时间戳作为圆通要求的 acceptTime
|
||||
const now = new Date()
|
||||
const Y = now.getFullYear()
|
||||
@@ -137,11 +164,14 @@
|
||||
carrier: form.carrier
|
||||
} as UTSJSONObject
|
||||
|
||||
const success = mockService.pushWebhookData(payload)
|
||||
uni.showLoading({ title: '正在推送至数据库...' })
|
||||
const success = await mockService.pushWebhookData(payload)
|
||||
uni.hideLoading()
|
||||
|
||||
if (success) {
|
||||
uni.showToast({ title: 'API 发送成功!', icon: 'success' })
|
||||
// 可选:跳转到详情或日志预览
|
||||
// 成功后刷新列表,更新订单状态
|
||||
loadOrders()
|
||||
} else {
|
||||
uni.showModal({
|
||||
title: '发送失败',
|
||||
@@ -165,13 +195,22 @@
|
||||
|
||||
.header {
|
||||
margin-bottom: 25px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
font-size: 14px;
|
||||
color: #007aff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<view class="container">
|
||||
<!-- 头部状态栏 (沉浸式) -->
|
||||
<view class="status-header">
|
||||
<text class="back-link-white" @click="goBack">⬅ 返回</text>
|
||||
<view class="status-main">
|
||||
<text class="status-icon">{{ getStatusIcon(order.status) }}</text>
|
||||
<view class="status-text-wrap">
|
||||
@@ -40,17 +41,13 @@
|
||||
<view class="content">
|
||||
<view class="event-header">
|
||||
<view class="status-time-row">
|
||||
<text v-if="shouldShowStatus(index)" class="status-label" :class="{active: index === 0}">{{ getStatusLabel(event.status_code) }}</text>
|
||||
<text class="status-label" :class="{active: index === 0}">{{ getStatusLabel(event.status_code) }}</text>
|
||||
<text class="event-time" :class="{active: index === 0}">{{ event.event_time }}</text>
|
||||
</view>
|
||||
<view class="event-title-row">
|
||||
<text v-for="(seg, i) in splitText(event.event_text)" :key="i" class="event-title-text" :class="{ 'highlight': seg.isPhone, 'active': index === 0 }" @click="seg.isPhone ? makeCall(seg.raw as string) : null">{{ seg.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="event.location" class="location-info">
|
||||
<text class="loc-icon">📍</text>
|
||||
<text class="loc-text">{{ event.location }}</text>
|
||||
</view>
|
||||
<!-- 证据/签收图片 (POD) -->
|
||||
<view v-if="event.evidence_urls.length > 0" class="evidence-grid">
|
||||
<image v-for="(img, idx) in event.evidence_urls" :key="idx" :src="img" class="evidence-img" mode="aspectFill" @click="previewImage(img, event.evidence_urls)" />
|
||||
@@ -123,15 +120,19 @@
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
loadData() {
|
||||
const orders = mockService.getMockOrders()
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
async loadData() {
|
||||
const orders = await mockService.getMockOrders()
|
||||
const found = orders.find((o: MockOrder): boolean => o.order_no === this.orderNo)
|
||||
if (found != null) {
|
||||
// 确保对象引用更新以触发响应式
|
||||
this.order = { ...found } as MockOrder
|
||||
}
|
||||
// 确保数组引用更新
|
||||
this.statusHistory = [...mockService.getMockTracking(this.orderNo)]
|
||||
const tracking = await mockService.getMockTracking(this.orderNo)
|
||||
this.statusHistory = [...tracking]
|
||||
},
|
||||
getStatusText(status: string): string {
|
||||
const maps = {
|
||||
@@ -237,6 +238,12 @@
|
||||
padding: 60rpx 40rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
.back-link-white {
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 20rpx;
|
||||
display: block;
|
||||
}
|
||||
.status-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -428,7 +435,6 @@
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
.status-label.active {
|
||||
color: #007AFF;
|
||||
@@ -452,20 +458,6 @@
|
||||
.event-time.active {
|
||||
color: #007AFF;
|
||||
}
|
||||
.event-time.active {
|
||||
color: #666;
|
||||
}
|
||||
.location-info {
|
||||
margin-top: 12rpx;
|
||||
background-color: #f9fafb;
|
||||
padding: 10rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.loc-icon { font-size: 20rpx; }
|
||||
.loc-text { font-size: 22rpx; color: #666; }
|
||||
.evidence-grid {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="back-link" @click="goBack">⬅ 返回</text>
|
||||
<view class="header-main">
|
||||
<text class="title">我的快递</text>
|
||||
<text class="count">共 {{ orders.length }} 个包裹</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tabs">
|
||||
<view class="tab-item" :class="{active: currentTab === 'all'}" @click="currentTab = 'all'">全部</view>
|
||||
@@ -21,7 +24,7 @@
|
||||
<view class="card-body">
|
||||
<view class="pkg-icon">📦</view>
|
||||
<view class="pkg-info">
|
||||
<text class="latest-msg">{{ getLatestMsg(order.order_no) }}</text>
|
||||
<text class="latest-msg">{{ order.current_status_text || '包裹准备中' }}</text>
|
||||
<text class="order-meta">订单号: {{ order.order_no }}</text>
|
||||
</view>
|
||||
<text class="arrow">></text>
|
||||
@@ -66,8 +69,12 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadData() {
|
||||
this.orders = [...mockService.getMockOrders()]
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
async loadData() {
|
||||
const data = await mockService.getMockOrders()
|
||||
this.orders = [...data]
|
||||
},
|
||||
getStatusText(status: string): string {
|
||||
const maps = {
|
||||
@@ -80,17 +87,6 @@
|
||||
}
|
||||
return (maps[status] != null) ? maps[status] : '处理中'
|
||||
},
|
||||
getLatestMsg(orderNo: string): string {
|
||||
const history = mockService.getMockTracking(orderNo)
|
||||
if (history.length > 0) {
|
||||
const text = history[0].event_text
|
||||
// 消费者端列表页:脱敏处理手机号
|
||||
return text.replace(/(1[3-9]\d{9})/g, (m : string) : string => {
|
||||
return m.substring(0, 3) + '****' + m.substring(7)
|
||||
})
|
||||
}
|
||||
return '包裹准备中'
|
||||
},
|
||||
goDetail(orderNo: string) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/delivery/test/consumer-logistics-detail?order_no=${orderNo}`
|
||||
@@ -109,6 +105,20 @@
|
||||
.header {
|
||||
margin-bottom: 30rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.back-link {
|
||||
font-size: 28rpx;
|
||||
color: #007AFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
.header-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="back-link" @click="goBack">⬅ 返回</text>
|
||||
<text class="title">配送模块测试页面索引</text>
|
||||
</view>
|
||||
|
||||
@@ -58,6 +59,9 @@
|
||||
<script uts>
|
||||
export default {
|
||||
methods: {
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
goto(url: string) {
|
||||
uni.navigateTo({ url: url })
|
||||
}
|
||||
@@ -73,13 +77,20 @@
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 50rpx;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.back-link {
|
||||
font-size: 26rpx;
|
||||
color: #007AFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
display: block;
|
||||
}
|
||||
.desc {
|
||||
font-size: 24rpx;
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="back-link" @click="goBack">⬅ 返回</text>
|
||||
<text class="title">商家订单详情</text>
|
||||
</view>
|
||||
|
||||
<!-- 状态卡片 -->
|
||||
<view class="status-card">
|
||||
<text class="status-title">{{ getStatusText(order.status) }}</text>
|
||||
@@ -101,16 +106,19 @@
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
loadData() {
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
async loadData() {
|
||||
// 获取匹配的订单基本信息
|
||||
const allOrders = mockService.getMockOrders()
|
||||
const allOrders = await mockService.getMockOrders()
|
||||
const found = allOrders.find((o : MockOrder) : boolean => o.order_no === this.orderNo)
|
||||
if (found != null) {
|
||||
this.order = found
|
||||
}
|
||||
|
||||
// 获取轨迹
|
||||
this.statusHistory = mockService.getMockTracking(this.orderNo)
|
||||
this.statusHistory = await mockService.getMockTracking(this.orderNo)
|
||||
},
|
||||
getStatusText(status: string) : string {
|
||||
const maps = {
|
||||
@@ -158,6 +166,19 @@
|
||||
background-color: #f8f8f8;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
padding: 20rpx 10rpx;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.back-link {
|
||||
font-size: 28rpx;
|
||||
color: #007AFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
.status-card {
|
||||
background: linear-gradient(to right, #007AFF, #00C6FF);
|
||||
padding: 40rpx;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="back-link" @click="goBack">⬅ 返回</text>
|
||||
<text class="title">商家发货管理</text>
|
||||
</view>
|
||||
|
||||
@@ -84,13 +85,13 @@
|
||||
showShipModal: false,
|
||||
selectedOrder: null as MockOrder | null,
|
||||
carriers: [
|
||||
{ label: '韵达快递', value: '韵达' },
|
||||
{ label: '圆通速递', value: '圆通' },
|
||||
{ label: '中通快递', value: '中通' },
|
||||
{ label: '申通快递', value: '申通' },
|
||||
{ label: '顺丰速运', value: '顺丰' }
|
||||
{ label: '韵达快递', value: '韵达快递' },
|
||||
{ label: '圆通速递', value: '圆通速递' },
|
||||
{ label: '中通快递', value: '中通快递' },
|
||||
{ label: '申通快递', value: '申通快递' },
|
||||
{ label: '顺丰速运', value: '顺丰速运' }
|
||||
],
|
||||
currentCarrier: '韵达',
|
||||
currentCarrier: '韵达快递',
|
||||
trackingNo: ''
|
||||
}
|
||||
},
|
||||
@@ -112,9 +113,12 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadData() {
|
||||
// 使用展开运算符创建新数组引用,确保 Vue 响应式触发
|
||||
this.orders = [...mockService.getMockOrders()]
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
async loadData() {
|
||||
const data = await mockService.getMockOrders()
|
||||
this.orders = [...data]
|
||||
},
|
||||
setFilter(filter: string) {
|
||||
this.currentFilter = filter
|
||||
@@ -132,7 +136,7 @@
|
||||
},
|
||||
openShipModal(order: MockOrder) {
|
||||
this.selectedOrder = order
|
||||
this.currentCarrier = 'YUNDA'
|
||||
this.currentCarrier = '韵达快递'
|
||||
this.trackingNo = ''
|
||||
this.showShipModal = true
|
||||
},
|
||||
@@ -180,7 +184,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss">
|
||||
.container {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
@@ -194,6 +198,15 @@
|
||||
}
|
||||
.header {
|
||||
padding: 30rpx 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.back-link {
|
||||
font-size: 26rpx;
|
||||
color: #007AFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
/**
|
||||
* 配送模块模拟数据服务 (Mock Service)
|
||||
* 配送模块数据服务 (Express Service)
|
||||
* 依据: pages/mall/delivery/doc/需求文档/
|
||||
* 涵盖: 商家端订单、平台端轨迹、Webhook 日志与场景模拟
|
||||
* 功能: 对接 Supabase 生产表 (platform_express_*)
|
||||
*/
|
||||
|
||||
export interface MockOrder {
|
||||
id?: string
|
||||
order_no: string
|
||||
status: string
|
||||
created_at: string
|
||||
@@ -15,6 +18,7 @@ export interface MockOrder {
|
||||
carrier: string
|
||||
tracking_no: string
|
||||
last_synced_at?: string
|
||||
current_status_text?: string
|
||||
}
|
||||
|
||||
export interface MockTrackingEvent {
|
||||
@@ -39,466 +43,359 @@ export interface MockWebhookLog {
|
||||
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 配置项,支持页面间同步
|
||||
// 全局 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() {}
|
||||
|
||||
constructor() {
|
||||
this.initDefaultHistory()
|
||||
this.initDefaultLogs()
|
||||
async getMockOrders(): 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 orders error:', error)
|
||||
return []
|
||||
}
|
||||
|
||||
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
|
||||
const list = data as Array<UTSJSONObject>
|
||||
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<any>).length > 0) {
|
||||
ml = (mlData as Array<UTSJSONObject>)[0]
|
||||
} else if (!(mlData instanceof Array)) {
|
||||
ml = mlData 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: []
|
||||
}
|
||||
])
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
getAvailableCarriers(): MockCarrierOption[] {
|
||||
return [
|
||||
{ code: 'YUNDA', name: '韵达' },
|
||||
{ code: 'YTO', name: '圆通' },
|
||||
{ code: 'ZTO', name: '中通' },
|
||||
{ code: 'STO', name: '申通' }
|
||||
]
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
getMockOrders(): MockOrder[] {
|
||||
return this.orders
|
||||
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>)
|
||||
}
|
||||
|
||||
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: []
|
||||
// 如果按 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 order
|
||||
}
|
||||
return allEvents
|
||||
}
|
||||
|
||||
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)!
|
||||
// 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 []
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成符合消费者端高保真展示的物流轨迹
|
||||
* 模拟各种物流场景 (生成生产测试数据)
|
||||
*/
|
||||
generateFullProcess(id: string) {
|
||||
const order = this.orders.find(o => o.order_no === id || o.tracking_no === id)
|
||||
if (!order) 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()
|
||||
|
||||
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}`
|
||||
if (waybills == null || (waybills as Array<any>).length === 0) {
|
||||
console.error('Scenario Error: Waybill not found', trackingNo)
|
||||
return
|
||||
}
|
||||
|
||||
const tip = "【物流问题无需找商家或平台,请致电(95338)(专属热线:400-811-1111)更快解决】"
|
||||
const waybill = (waybills as Array<UTSJSONObject>)[0]
|
||||
const waybillId = waybill['id'] as string
|
||||
const carrier = waybill['carrier'] as string
|
||||
|
||||
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: '包裹正在等待揽收',
|
||||
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: waybill, error: wError } = await supa.from('platform_express_waybills').insert({
|
||||
order_id: orderId,
|
||||
order_no: orderNo,
|
||||
carrier: carrier,
|
||||
tracking_no: trackingNo,
|
||||
current_status_code: 'SHIPPED',
|
||||
current_status_text: '商家已发货',
|
||||
last_synced_at: new Date().toISOString()
|
||||
}).select().single().execute()
|
||||
|
||||
if (wError != null) {
|
||||
console.error('Bind shipment error:', wError)
|
||||
return false
|
||||
}
|
||||
|
||||
// 3. 产生初始轨迹
|
||||
await supa.from('platform_express_tracking_events').insert({
|
||||
waybill_id: waybill!['id'] as string,
|
||||
carrier: carrier,
|
||||
tracking_no: trackingNo,
|
||||
event_time: new Date().toISOString(),
|
||||
event_code: 'CREATED',
|
||||
event_text: '商家已发货,等待快递公司揽收',
|
||||
status_code: 'SHIPPED',
|
||||
evidence_urls: []
|
||||
}
|
||||
]
|
||||
dedupe_key: 'INIT_' + Date.now()
|
||||
}).execute()
|
||||
|
||||
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)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟从云端同步最新轨迹(使刷新按钮生效)
|
||||
* 核心功能:模拟第三方回调接口 (写库版)
|
||||
*/
|
||||
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 {
|
||||
// 兼容圆通协议字段
|
||||
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_id = payload['txLogisticId'] 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)
|
||||
|
||||
// 状态映射
|
||||
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],
|
||||
// 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 eventRes = await supa.from('platform_express_tracking_events').insert({
|
||||
waybill_id: final_id,
|
||||
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_time: payload['acceptTime'] as string || new Date().toISOString(),
|
||||
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)
|
||||
raw_payload: payload,
|
||||
dedupe_key: 'WEBHOOK_' + Date.now()
|
||||
}).execute()
|
||||
|
||||
if (eventRes.error != null) {
|
||||
console.error('Insert Event Error:', eventRes.error)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
log.success = false
|
||||
log.result_text = '未找到对应的运单或订单号'
|
||||
return false
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
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')
|
||||
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}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="back-link" @click="goBack">⬅ 返回</text>
|
||||
<text class="title">配送/对接配置中心</text>
|
||||
</view>
|
||||
|
||||
@@ -75,6 +76,9 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
toggleTestMode(e: any) {
|
||||
const val = e.detail.value as boolean
|
||||
this.isTestMode = val
|
||||
@@ -105,6 +109,15 @@
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 30rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.back-link {
|
||||
font-size: 26rpx;
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.title {
|
||||
font-size: 34rpx;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="back-link" @click="goBack">⬅ 返回</text>
|
||||
<text class="title">运单/轨迹查询 (运维排障)</text>
|
||||
</view>
|
||||
|
||||
@@ -87,14 +88,16 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
async doSearch() {
|
||||
if (!this.searchQuery) return
|
||||
uni.showLoading({ title: '查询中...' })
|
||||
|
||||
setTimeout(() => {
|
||||
this.hasSearched = true
|
||||
const tracking = mockService.getMockTracking(this.searchQuery)
|
||||
const orders = mockService.getMockOrders()
|
||||
const tracking = await mockService.getMockTracking(this.searchQuery)
|
||||
const orders = await mockService.getMockOrders()
|
||||
const order = orders.find((o: MockOrder): boolean => o.tracking_no === this.searchQuery || o.order_no === this.searchQuery)
|
||||
|
||||
if (order != null) {
|
||||
@@ -107,7 +110,6 @@
|
||||
|
||||
this.showRaw = new Array(this.events.length).fill(false)
|
||||
uni.hideLoading()
|
||||
}, 600)
|
||||
},
|
||||
formatTime(time: string) : string {
|
||||
return time
|
||||
@@ -138,16 +140,19 @@
|
||||
}
|
||||
return (maps[code] != null) ? maps[code] : code
|
||||
},
|
||||
runScenario(type: string) {
|
||||
async runScenario(type: string) {
|
||||
if (this.waybillInfo == null) return
|
||||
uni.showModal({
|
||||
title: '确认模拟',
|
||||
content: `是否对 ${this.waybillInfo!.tracking_no} 运行【${type}】场景模拟?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
mockService.runScenario(this.waybillInfo!.tracking_no, type)
|
||||
uni.showLoading({ title: '处理中...' })
|
||||
mockService.runScenario(this.waybillInfo!.tracking_no, type).then(() => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '已触发' })
|
||||
setTimeout(() => this.doSearch(), 1000)
|
||||
this.doSearch()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -163,8 +168,17 @@
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
.back-link {
|
||||
font-size: 28rpx;
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.title {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="back-link" @click="goBack">⬅ 返回</text>
|
||||
<text class="title">Webhook 接收日志</text>
|
||||
<text class="subtitle">监控推送到达、验签与入库状态</text>
|
||||
</view>
|
||||
@@ -76,9 +77,20 @@
|
||||
const currentPayload = ref('')
|
||||
|
||||
onShow(() => {
|
||||
logs.value = mockService.getMockWebhookLogs()
|
||||
loadLogs()
|
||||
})
|
||||
|
||||
async function loadLogs() {
|
||||
uni.showLoading({ title: '加载中...' })
|
||||
const data = await mockService.getMockWebhookLogs()
|
||||
logs.value = [...data]
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function viewPayload(log: MockWebhookLog) {
|
||||
currentPayload.value = JSON.stringify(log.payload, null, 2)
|
||||
showPayload.value = true
|
||||
@@ -115,6 +127,15 @@
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 30rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.back-link {
|
||||
font-size: 26rpx;
|
||||
color: #3498db;
|
||||
cursor: pointer;
|
||||
}
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
|
||||
Reference in New Issue
Block a user