diff --git a/pages/mall/delivery/doc/需求文档/README.md b/pages/mall/delivery/doc/需求文档/README.md
index 04242a59..01fb1629 100644
--- a/pages/mall/delivery/doc/需求文档/README.md
+++ b/pages/mall/delivery/doc/需求文档/README.md
@@ -65,4 +65,87 @@
- 联调/设计参考(不要用于生产直接落库):[express_tracking_mock_platform.sql](express_tracking_mock_platform.sql)
- 包含平台侧表 + `mock_*` 测试表(故障注入/回放用),仅用于联调与文档说明。
+测试/预发环境(不使用 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_ 前缀示例数据,用于页面联调与排障演示(可随时清理)。
+
+### Ubuntu 上的 Supabase(测试/预发)怎么执行
+
+目标:在 Supabase 上只使用“生产同款”三表进行联调,不创建/不依赖任何 `mock_*` 表。
+
+执行顺序(必须按顺序):
+1) 执行建表脚本:[express_tracking_platform_upgrade.sql](express_tracking_platform_upgrade.sql)
+2) 执行种子数据脚本:[seed_platform_express_test_data.sql](seed_platform_express_test_data.sql)
+
+方式 A:Supabase Dashboard(推荐)
+- 打开 Supabase 项目 -> SQL Editor
+- 分别粘贴并执行以上两个 SQL 文件内容(先建表、后种子)
+- 建议使用具备 DDL 权限的角色执行(通常 SQL Editor 以高权限执行)
+
+方式 B:Ubuntu 通过 psql 执行(适合自动化/脚本化)
+1) 安装客户端:`sudo apt-get update && sudo apt-get install -y postgresql-client`
+2) 从 Supabase 项目设置里复制连接串(Database -> Connection string),导出环境变量(示例):
+ `export DATABASE_URL='postgresql://USER:PASSWORD@HOST:6543/postgres?sslmode=require'`
+3) 在仓库根目录执行:
+ - `psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f pages/mall/delivery/doc/需求文档/express_tracking_platform_upgrade.sql`
+ - `psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f pages/mall/delivery/doc/需求文档/seed_platform_express_test_data.sql`
+
+清理测试数据:
+- 种子脚本底部自带清理 SQL(按 `tracking_no LIKE 'TEST_%'` 删除),需要时复制执行即可。
+
+### 如何“伪造第三方推送到数据库”(无需后端)
+
+如果你暂时没有 webhook 接收服务,但希望测试环境表现得像“第三方已经推送了轨迹”,可以直接写数据库:
+- 使用脚本 [simulate_third_party_to_db.sql](simulate_third_party_to_db.sql)
+ - 会同时写入:
+ - `platform_express_event_raw`(原始请求留痕/验签/排障)
+ - `platform_express_tracking_events`(时间线事件)
+ - `platform_express_waybills`(运单摘要)
+- 用法:在 Supabase SQL Editor 打开脚本,修改顶部【参数区】后执行整段。
+- 建议:运单号使用 `TEST_` 前缀,便于按脚本底部清理 SQL 一键删除。
+
+### 与数据库其他表是否“相通”(关联与校验)
+
+这套三表与平台业务表的关键“相通点”是:
+- `public.platform_express_waybills.order_id` 外键引用 `public.ml_orders(id)`(订单主表)。
+
+因此:
+- 如果你的 Supabase(测试/预发)数据库里已经部署了完整商城库(包含 `public.ml_orders`),那么这三表可以直接和订单表联查/联动。
+- 如果你的 Supabase 只是一个“独立的轨迹联调库”,没有 `public.ml_orders`,那么执行建表脚本时会因为外键依赖缺失而失败;此时建议先导入/执行商城主库迁移(让 `ml_orders` 存在),或临时改为仅使用 `order_no` 关联(不建外键),待接入完整主库后再补外键。
+
+在 Supabase SQL Editor 里可执行以下校验:
+
+1) 检查订单表是否存在:
+```sql
+select to_regclass('public.ml_orders') as ml_orders;
+```
+
+2) 检查外键是否创建成功(应该能看到 `platform_express_waybills_order_id_fkey` 或类似名称):
+```sql
+select
+ conname as fk_name,
+ pg_get_constraintdef(c.oid) as fk_def
+from pg_constraint c
+join pg_class t on t.oid = c.conrelid
+join pg_namespace n on n.oid = t.relnamespace
+where c.contype = 'f'
+ and n.nspname = 'public'
+ and t.relname = 'platform_express_waybills';
+```
+
+3) 联查示例(有真实订单时):
+```sql
+select
+ o.id as order_id,
+ o.order_no,
+ w.carrier,
+ w.tracking_no,
+ w.current_status_code,
+ w.last_synced_at
+from public.ml_orders o
+left join public.platform_express_waybills w on w.order_id = o.id
+order by o.created_at desc
+limit 20;
+```
+
建议下一步:在平台侧实现一个可切换的“Mock 数据源”开关(仅测试环境),并在 QA 用例中覆盖重复/乱序/验签失败等注入场景。
diff --git a/pages/mall/delivery/doc/需求文档/express_tracking_platform_upgrade.sql b/pages/mall/delivery/doc/需求文档/express_tracking_platform_upgrade.sql
index 3d8073a1..03966177 100644
--- a/pages/mall/delivery/doc/需求文档/express_tracking_platform_upgrade.sql
+++ b/pages/mall/delivery/doc/需求文档/express_tracking_platform_upgrade.sql
@@ -16,20 +16,20 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "btree_gin";
-- updated_at 维护函数:若主库已存在则不重复创建
-DO $$
+DO $do$
BEGIN
IF to_regprocedure('public.update_updated_at_column()') IS NULL THEN
CREATE OR REPLACE FUNCTION public.update_updated_at_column()
RETURNS TRIGGER
LANGUAGE plpgsql
- AS $$
+ AS $func$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
- $$;
+ $func$;
END IF;
-END $$;
+END $do$;
-- =====================================================================
-- A. 平台侧(platform):统一轨迹模型入库与查询
diff --git a/pages/mall/delivery/doc/需求文档/seed_platform_express_test_data.sql b/pages/mall/delivery/doc/需求文档/seed_platform_express_test_data.sql
new file mode 100644
index 00000000..3672cebc
--- /dev/null
+++ b/pages/mall/delivery/doc/需求文档/seed_platform_express_test_data.sql
@@ -0,0 +1,236 @@
+-- =====================================================================================
+-- 测试环境种子数据(仅写入 platform_express_* 三表)
+--
+-- 目的:在测试/预发环境使用“生产同款表结构”联调页面与接口,避免创建 mock_* 表。
+-- 适用:已执行 express_tracking_platform_upgrade.sql 后。
+--
+-- 注意:
+-- - 本文件会插入 tracking_no 以 TEST_ 前缀开头的示例数据。
+-- - 如需清理,可执行文末的清理 SQL。
+-- =====================================================================================
+
+BEGIN;
+
+-- 1) 创建/更新示例运单(若已存在则跳过)
+INSERT INTO public.platform_express_waybills (
+ order_id,
+ order_no,
+ carrier,
+ tracking_no,
+ source,
+ current_status_code,
+ current_status_text,
+ eta,
+ last_synced_at
+)
+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())
+ON CONFLICT (carrier, tracking_no) DO NOTHING;
+
+
+-- 2) 插入轨迹事件(幂等:按 (waybill_id, dedupe_key) 去重)
+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')
+)
+INSERT INTO public.platform_express_tracking_events (
+ waybill_id,
+ carrier,
+ tracking_no,
+ event_id,
+ event_time,
+ event_code,
+ event_text,
+ status_code,
+ node_name,
+ location,
+ description,
+ evidence_urls,
+ raw_payload,
+ received_at,
+ source,
+ dedupe_key
+)
+SELECT * FROM (
+ -- 运单 1:运输中
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_YD_20260206_0001') AS waybill_id,
+ 'YUNDA' AS carrier,
+ 'TEST_YD_20260206_0001' AS tracking_no,
+ 'test_e_1001' AS event_id,
+ NOW() - INTERVAL '12 hours' AS event_time,
+ 'PICKED' AS event_code,
+ '包裹已揽收' AS event_text,
+ 'ARRIVED_HUB' AS status_code,
+ '上海浦东集散中心' AS node_name,
+ '上海市 浦东新区' AS location,
+ NULL AS description,
+ '[]'::jsonb AS evidence_urls,
+ NULL::jsonb AS raw_payload,
+ NOW() AS received_at,
+ 'poll' AS source,
+ 'test_e_1001' AS dedupe_key
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_YD_20260206_0001'),
+ 'YUNDA',
+ 'TEST_YD_20260206_0001',
+ 'test_e_1002',
+ NOW() - INTERVAL '6 hours',
+ 'TRANSIT',
+ '快件离开【上海浦东集散中心】,已发往【杭州转运中心】',
+ 'IN_TRANSIT',
+ '上海浦东集散中心',
+ '上海市 浦东新区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'poll',
+ 'test_e_1002'
+
+ -- 运单 2:派送中
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_YT_20260206_0002'),
+ 'YTO',
+ 'TEST_YT_20260206_0002',
+ 'test_e_2001',
+ NOW() - INTERVAL '8 hours',
+ 'ARRIVAL',
+ '快件已到达【广州天河网点】',
+ 'IN_TRANSIT',
+ '广州天河网点',
+ '广州市 天河区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'webhook',
+ 'test_e_2001'
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_YT_20260206_0002'),
+ 'YTO',
+ 'TEST_YT_20260206_0002',
+ 'test_e_2002',
+ NOW() - INTERVAL '1 hours',
+ 'OUT_FOR_DELIVERY',
+ '派送员正在派件(测试数据)',
+ 'OUT_FOR_DELIVERY',
+ '广州天河网点',
+ '广州市 天河区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'webhook',
+ 'test_e_2002'
+
+ -- 运单 3:已签收
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_ZT_20260206_0003'),
+ 'ZTO',
+ 'TEST_ZT_20260206_0003',
+ 'test_e_3001',
+ NOW() - INTERVAL '2 days',
+ 'PICKED',
+ '包裹已揽收',
+ 'ARRIVED_HUB',
+ '北京朝阳网点',
+ '北京市 朝阳区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'poll',
+ 'test_e_3001'
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_ZT_20260206_0003'),
+ 'ZTO',
+ 'TEST_ZT_20260206_0003',
+ 'test_e_3002',
+ NOW() - INTERVAL '1 days',
+ 'TRANSIT',
+ '快件运输中',
+ 'IN_TRANSIT',
+ '北京朝阳网点',
+ '北京市 朝阳区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'poll',
+ 'test_e_3002'
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_ZT_20260206_0003'),
+ 'ZTO',
+ 'TEST_ZT_20260206_0003',
+ 'test_e_3003',
+ NOW() - INTERVAL '12 hours',
+ 'DELIVERED',
+ '您的快件已签收(测试数据)',
+ 'DELIVERED',
+ '北京朝阳网点',
+ '北京市 朝阳区',
+ NULL,
+ '["https://img-shop.gmugmu.com/mock/pod_sample.png"]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'webhook',
+ 'test_e_3003'
+) AS rows_to_insert
+WHERE rows_to_insert.waybill_id IS NOT NULL
+ON CONFLICT (waybill_id, dedupe_key) DO NOTHING;
+
+
+-- 3) 可选:插入原始接收留痕(用于演示验签/排障界面)
+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(),
+ 'webhook',
+ 'test_client',
+ 'YTO',
+ 'TEST_YT_20260206_0002',
+ TRUE,
+ 'test-signature',
+ EXTRACT(EPOCH FROM NOW())::text,
+ 'req_test_0001',
+ '127.0.0.1',
+ '{"content-type":"application/json"}'::jsonb,
+ '{"tracking_no":"TEST_YT_20260206_0002","status_code":"OUT_FOR_DELIVERY","event_text":"派送员正在派件(测试数据)"}'::jsonb,
+ NULL,
+ 'raw_test_0001'
+);
+
+COMMIT;
+
+-- =====================================================================================
+-- 清理(需要时手工执行)
+-- =====================================================================================
+-- BEGIN;
+-- DELETE FROM public.platform_express_event_raw WHERE tracking_no LIKE 'TEST_%';
+-- DELETE FROM public.platform_express_tracking_events WHERE tracking_no LIKE 'TEST_%';
+-- DELETE FROM public.platform_express_waybills WHERE tracking_no LIKE 'TEST_%';
+-- COMMIT;
diff --git a/pages/mall/delivery/doc/需求文档/simulate_third_party_to_db.sql b/pages/mall/delivery/doc/需求文档/simulate_third_party_to_db.sql
new file mode 100644
index 00000000..7ef01733
--- /dev/null
+++ b/pages/mall/delivery/doc/需求文档/simulate_third_party_to_db.sql
@@ -0,0 +1,191 @@
+-- =====================================================================================
+-- 模拟第三方“推送到平台”的数据(直接写入数据库)
+--
+-- 适用场景:
+-- - 你暂时没有后端 webhook 接收服务,但希望在测试/预发环境快速伪造第三方推送效果。
+-- - 通过写入:
+-- 1) platform_express_event_raw (原始请求留痕/验签结果/排障信息)
+-- 2) platform_express_tracking_events(解析后的轨迹事件,用于时间线展示)
+-- 3) platform_express_waybills(运单摘要)
+--
+-- 注意:
+-- - 这是“绕过后端解析与验签”的直写方案,仅用于测试/演示。
+-- - 生产环境不建议这样做。
+-- - 执行前请确保已跑过 express_tracking_platform_upgrade.sql 创建三表。
+--
+-- 使用方式:
+-- - Supabase SQL Editor:把下面的【参数区】内容改成你要的值,然后整段执行。
+-- =====================================================================================
+
+BEGIN;
+
+-- =========================
+-- 参数区(手工修改这里)
+-- =========================
+-- 承运商编码:建议与平台枚举一致(YTO/YUNDA/ZTO/STO...)
+-- 运单号:建议用 TEST_ 前缀,便于清理
+-- 事件码/状态码:按你的平台约定(IN_TRANSIT/OUT_FOR_DELIVERY/DELIVERED/EXCEPTION...)
+
+DO $$
+DECLARE
+ v_order_no TEXT := 'ORD_TEST_20260206099';
+ v_carrier TEXT := 'YTO';
+ v_tracking_no TEXT := 'TEST_YT_20260206_0099';
+
+ v_event_id TEXT := NULL; -- 可为空
+ v_event_time TIMESTAMPTZ := NOW();
+ v_event_code TEXT := 'OUT_FOR_DELIVERY';
+ v_event_text TEXT := '派送员正在派件(伪造推送,直写数据库)';
+ v_status_code TEXT := 'OUT_FOR_DELIVERY';
+ v_node_name TEXT := '广州天河网点';
+ v_location TEXT := '广州市 天河区';
+
+ v_source TEXT := 'webhook'; -- webhook/poll/manual
+ v_client_id TEXT := 'test_client';
+ v_signature_valid BOOLEAN := TRUE;
+ v_signature TEXT := 'fake-signature';
+ v_remote_ip INET := '127.0.0.1';
+
+ v_waybill_id UUID;
+ v_dedupe_key TEXT;
+BEGIN
+ -- 1) Upsert 运单主表(保证 waybill 存在)
+ INSERT INTO public.platform_express_waybills (
+ order_id,
+ order_no,
+ carrier,
+ tracking_no,
+ source,
+ current_status_code,
+ current_status_text,
+ last_synced_at
+ )
+ VALUES (
+ NULL,
+ v_order_no,
+ v_carrier,
+ v_tracking_no,
+ 'mock',
+ v_status_code,
+ CASE
+ WHEN v_status_code = 'PENDING' THEN '待发货'
+ WHEN v_status_code = 'SHIPPED' THEN '已发货'
+ WHEN v_status_code = 'IN_TRANSIT' THEN '运输中'
+ WHEN v_status_code = 'ARRIVED_HUB' THEN '到达网点'
+ WHEN v_status_code = 'OUT_FOR_DELIVERY' THEN '派送中'
+ WHEN v_status_code = 'DELIVERED' THEN '已签收'
+ WHEN v_status_code = 'EXCEPTION' THEN '异常'
+ ELSE v_status_code
+ END,
+ NOW()
+ )
+ ON CONFLICT (carrier, tracking_no)
+ DO UPDATE SET
+ order_no = COALESCE(EXCLUDED.order_no, public.platform_express_waybills.order_no),
+ current_status_code = EXCLUDED.current_status_code,
+ current_status_text = EXCLUDED.current_status_text,
+ last_synced_at = EXCLUDED.last_synced_at
+ RETURNING id INTO v_waybill_id;
+
+ -- 2) 计算幂等键(优先 event_id;否则用运单+事件码+事件时间)
+ v_dedupe_key := COALESCE(
+ v_event_id,
+ v_tracking_no || '|' || v_event_code || '|' || to_char(v_event_time, 'YYYY-MM-DD"T"HH24:MI:SSOF')
+ );
+
+ -- 3) 写入原始接收留痕(模拟第三方请求)
+ 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(),
+ v_source,
+ v_client_id,
+ v_carrier,
+ v_tracking_no,
+ v_signature_valid,
+ v_signature,
+ EXTRACT(EPOCH FROM NOW())::text,
+ 'req_fake_' || replace(v_dedupe_key, '|', '_'),
+ v_remote_ip,
+ '{"content-type":"application/json"}'::jsonb,
+ jsonb_build_object(
+ 'order_no', v_order_no,
+ 'carrier', v_carrier,
+ 'tracking_no', v_tracking_no,
+ 'event_id', v_event_id,
+ 'event_time', v_event_time,
+ 'event_code', v_event_code,
+ 'event_text', v_event_text,
+ 'status_code', v_status_code,
+ 'node_name', v_node_name,
+ 'location', v_location
+ ),
+ NULL,
+ 'raw|' || v_dedupe_key
+ );
+
+ -- 4) 写入解析后的轨迹事件(用于时间线展示)
+ INSERT INTO public.platform_express_tracking_events (
+ waybill_id,
+ carrier,
+ tracking_no,
+ event_id,
+ event_time,
+ event_code,
+ event_text,
+ status_code,
+ node_name,
+ location,
+ description,
+ evidence_urls,
+ raw_payload,
+ received_at,
+ source,
+ dedupe_key
+ )
+ VALUES (
+ v_waybill_id,
+ v_carrier,
+ v_tracking_no,
+ v_event_id,
+ v_event_time,
+ v_event_code,
+ v_event_text,
+ v_status_code,
+ v_node_name,
+ v_location,
+ NULL,
+ '[]'::jsonb,
+ NULL,
+ NOW(),
+ v_source,
+ v_dedupe_key
+ )
+ ON CONFLICT (waybill_id, dedupe_key) DO NOTHING;
+
+END $$;
+
+COMMIT;
+
+-- =====================================================================================
+-- 清理示例(需要时手工执行)
+-- =====================================================================================
+-- BEGIN;
+-- DELETE FROM public.platform_express_event_raw WHERE tracking_no LIKE 'TEST_%';
+-- DELETE FROM public.platform_express_tracking_events WHERE tracking_no LIKE 'TEST_%';
+-- DELETE FROM public.platform_express_waybills WHERE tracking_no LIKE 'TEST_%';
+-- COMMIT;
diff --git a/pages/mall/delivery/test/api-simulator.uvue b/pages/mall/delivery/test/api-simulator.uvue
index 4a18616f..cf3ef8fe 100644
--- a/pages/mall/delivery/test/api-simulator.uvue
+++ b/pages/mall/delivery/test/api-simulator.uvue
@@ -18,24 +18,24 @@
- 2. 构造回调数据 (JSON Payload)
+ 2. 构造圆通协议数据 (YTO Protocol)
- 运单号:
-
+ 物流单号 (mailNo):
+
- 快递公司:
-
+ 订单号 (txLogisticId):
+
- 物流状态:
+ 事件状态 (infoContent):
{{ currentStatusLabel }}
- 轨迹描述文字:
-
+ 轨迹描述 (remark):
+
@@ -64,61 +64,77 @@
const selectedOrderIndex = ref(-1)
const form = reactive({
- tracking_no: '',
- carrier: '',
- status_code: 'IN_TRANSIT',
- event_text: '快件已到达【XX转运中心】,准备发往下一站'
+ mailNo: '',
+ txLogisticId: '',
+ infoContent: 'SEND',
+ remark: '快件已到达【XX分拨中心】,准备发往下一站',
+ acceptTime: '',
+ carrier: '圆通速递'
})
const statusOptions = [
- { label: '在途中 (IN_TRANSIT)', value: 'IN_TRANSIT' },
- { label: '派送中 (OUT_FOR_DELIVERY)', value: 'OUT_FOR_DELIVERY' },
- { label: '已签收 (DELIVERED)', value: 'DELIVERED' },
- { label: '异常 (EXCEPTION)', value: 'EXCEPTION' }
+ { label: '揽收 (GOT)', value: 'GOT' },
+ { label: '运输中 (SEND)', value: 'SEND' },
+ { label: '派送中 (SENT)', value: 'SENT' },
+ { label: '已签收 (SIGNED)', value: 'SIGNED' },
+ { label: '异常 (FAILED)', value: 'FAILED' }
]
- const currentStatusLabel = computed(() => {
- const opt = statusOptions.find(o => o.value === form.status_code)
- return opt ? opt.label : '请选择'
+ const currentStatusLabel = computed((): string => {
+ const opt = statusOptions.find((o: UTSJSONObject): boolean => o['value'] === form.infoContent)
+ return (opt != null) ? opt['label'] as string : '请选择'
})
- const jsonString = computed(() => {
+ const jsonString = computed((): string => {
return JSON.stringify(form, null, 2)
})
function selectOrder(index: number) {
selectedOrderIndex.value = index
const order = shippedOrders.value[index]
- form.tracking_no = order.tracking_no
- form.carrier = order.carrier
+ form.mailNo = order.tracking_no
+ form.txLogisticId = order.order_no
+ form.carrier = order.carrier + '速递'
// 根据订单当前状态智能预设
if (order.status === 'SHIPPED') {
- form.status_code = 'IN_TRANSIT'
- form.event_text = '快件已揽收,正发往城市中心'
+ form.infoContent = 'SEND'
+ form.remark = '快件已到达北京分拨中心'
} else if (order.status === 'IN_TRANSIT') {
- form.status_code = 'OUT_FOR_DELIVERY'
- form.event_text = '派送员王师傅(13700008888)正在派件'
+ form.infoContent = 'SENT'
+ form.remark = '派送员王师傅(13700008888)正在派件'
}
}
function onStatusChange(e: UniPickerChangeEvent) {
const idx = e.detail.value as number
- form.status_code = statusOptions[idx].value as string
+ form.infoContent = statusOptions[idx].value
}
function sendWebhook() {
- if (!form.tracking_no) {
+ if (!form.mailNo) {
uni.showToast({ title: '请先填写运单号', icon: 'none' })
return
}
+ // 获取当前时间戳作为圆通要求的 acceptTime
+ const now = new Date()
+ const Y = now.getFullYear()
+ const M = (now.getMonth() + 1).toString().padStart(2, '0')
+ const D = now.getDate().toString().padStart(2, '0')
+ const h = now.getHours().toString().padStart(2, '0')
+ const m = now.getMinutes().toString().padStart(2, '0')
+ const s = now.getSeconds().toString().padStart(2, '0')
+ form.acceptTime = `${Y}-${M}-${D} ${h}:${m}:${s}`
+
// 执行模拟推送 (转换为普通对象以兼容 UTS)
const payload = {
- tracking_no: form.tracking_no,
- carrier: form.carrier,
- status_code: form.status_code,
- event_text: form.event_text
+ mailNo: form.mailNo,
+ txLogisticId: form.txLogisticId,
+ infoContent: form.infoContent,
+ remark: form.remark,
+ acceptTime: form.acceptTime,
+ carrier: form.carrier
} as UTSJSONObject
const success = mockService.pushWebhookData(payload)
diff --git a/pages/mall/delivery/test/consumer-logistics-detail.uvue b/pages/mall/delivery/test/consumer-logistics-detail.uvue
index f528ec3a..75181d33 100644
--- a/pages/mall/delivery/test/consumer-logistics-detail.uvue
+++ b/pages/mall/delivery/test/consumer-logistics-detail.uvue
@@ -135,7 +135,8 @@
},
getStatusText(status: string): string {
const maps = {
- 'SHIPPED': '运输中',
+ 'SHIPPED': '已发货',
+ 'IN_TRANSIT': '运输中',
'DELIVERED': '已签收',
'OUT_FOR_DELIVERY': '派送中',
'PENDING': '待揽收',
diff --git a/pages/mall/delivery/test/consumer-order-list.uvue b/pages/mall/delivery/test/consumer-order-list.uvue
index 5f81d2eb..d8117b92 100644
--- a/pages/mall/delivery/test/consumer-order-list.uvue
+++ b/pages/mall/delivery/test/consumer-order-list.uvue
@@ -71,7 +71,8 @@
},
getStatusText(status: string): string {
const maps = {
- 'SHIPPED': '运输中',
+ 'SHIPPED': '已发货',
+ 'IN_TRANSIT': '运输中',
'DELIVERED': '已签收',
'OUT_FOR_DELIVERY': '派送中',
'PENDING': '待揽收',
diff --git a/pages/mall/delivery/test/merchant-order-detail.uvue b/pages/mall/delivery/test/merchant-order-detail.uvue
index 18b1a548..0ad839f8 100644
--- a/pages/mall/delivery/test/merchant-order-detail.uvue
+++ b/pages/mall/delivery/test/merchant-order-detail.uvue
@@ -113,7 +113,14 @@
this.statusHistory = mockService.getMockTracking(this.orderNo)
},
getStatusText(status: string) : string {
- const maps = { 'PENDING': '待发货', 'SHIPPED': '待签收', 'DELIVERED': '已签收', 'EXCEPTION': '异常' }
+ const maps = {
+ 'PENDING': '待发货',
+ 'SHIPPED': '已发货',
+ 'IN_TRANSIT': '运输中',
+ 'DELIVERED': '已签收',
+ 'OUT_FOR_DELIVERY': '派送中',
+ 'EXCEPTION': '异常'
+ }
return (maps[status] != null) ? maps[status] : status
},
async refreshLogistics() {
diff --git a/pages/mall/delivery/test/merchant-order-list.uvue b/pages/mall/delivery/test/merchant-order-list.uvue
index 932fe6f5..4f5eebe9 100644
--- a/pages/mall/delivery/test/merchant-order-list.uvue
+++ b/pages/mall/delivery/test/merchant-order-list.uvue
@@ -54,7 +54,7 @@
选择承运商
-
+
{{ currentCarrier || '请选择' }}
@@ -83,8 +83,14 @@
orders: [] as MockOrder[],
showShipModal: false,
selectedOrder: null as MockOrder | null,
- carriers: ['YUNDA', 'YTO', 'ZTO', 'STO', 'SF'],
- currentCarrier: '',
+ carriers: [
+ { label: '韵达快递', value: '韵达' },
+ { label: '圆通速递', value: '圆通' },
+ { label: '中通快递', value: '中通' },
+ { label: '申通快递', value: '申通' },
+ { label: '顺丰速运', value: '顺丰' }
+ ],
+ currentCarrier: '韵达',
trackingNo: ''
}
},
@@ -117,6 +123,7 @@
const maps = {
'PENDING': '待发货',
'SHIPPED': '已发货',
+ 'IN_TRANSIT': '运输中',
'DELIVERED': '已签收',
'OUT_FOR_DELIVERY': '派送中',
'EXCEPTION': '包裹异常'
@@ -131,7 +138,7 @@
},
onCarrierChange(e: any) {
const index = e.detail.value as number
- this.currentCarrier = this.carriers[index]
+ this.currentCarrier = this.carriers[index].value
},
async confirmShip() {
if (!this.trackingNo) {
diff --git a/pages/mall/delivery/test/mock-service.uts b/pages/mall/delivery/test/mock-service.uts
index 63dfd37d..36e8f04e 100644
--- a/pages/mall/delivery/test/mock-service.uts
+++ b/pages/mall/delivery/test/mock-service.uts
@@ -436,14 +436,24 @@ class MockService {
}
/**
- * 核心功能:模拟第三方回调接口
- * 模拟外部物流平台向本系统发送轨迹更新 API
+ * 核心功能:模拟第三方回调接口 (适配圆通推送协议)
+ * 对应字段: txLogisticId(订单号), mailNo(票号), infoContent(状态), remark(描述)
*/
pushWebhookData(payload: UTSJSONObject): boolean {
- const tracking_no = payload['tracking_no'] as string
- const status_code = payload['status_code'] as string
- const event_text = payload['event_text'] as string
- const carrier = payload['carrier'] as string || '顺丰'
+ // 兼容圆通协议字段
+ 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()
@@ -451,7 +461,7 @@ class MockService {
time: this.formatDate(now).split(' ')[1],
carrier: carrier,
tracking_no: tracking_no,
- event_code: status_code,
+ event_code: yto_status, // 保留圆通原始代码
success: true,
result_text: '接收成功',
payload: payload
@@ -459,16 +469,16 @@ class MockService {
this.webhookLogs.unshift(log)
// 2. 更新系统内部轨迹
- const order = this.orders.find(o => o.tracking_no === tracking_no)
+ 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: 'wb_' + Date.now(),
- event_time: this.formatDate(now),
- event_code: status_code,
+ 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: []
@@ -479,7 +489,7 @@ class MockService {
}
log.success = false
- log.result_text = '未找到对应的运单号'
+ log.result_text = '未找到对应的运单或订单号'
return false
}
diff --git a/pages/mall/delivery/test/platform-tracking-query.uvue b/pages/mall/delivery/test/platform-tracking-query.uvue
index a2d32949..b09928bf 100644
--- a/pages/mall/delivery/test/platform-tracking-query.uvue
+++ b/pages/mall/delivery/test/platform-tracking-query.uvue
@@ -29,7 +29,7 @@
当前状态:
- {{ waybillInfo.status }}
+ {{ getStatusText(waybillInfo.status) }}
@@ -44,8 +44,8 @@
{{ event.event_text }}
- CODE: {{ event.event_code }}
- MAP: {{ event.status_code }}
+ 代码: {{ event.event_code }}
+ 映射状态: {{ getStatusMapping(event.status_code) }}
{{ JSON.stringify(event.raw_payload, null, 2) }}
@@ -116,6 +116,28 @@
const current = this.showRaw[index]
this.showRaw[index] = !current
},
+ getStatusText(status: string) : string {
+ const maps = {
+ 'PENDING': '待发货',
+ 'SHIPPED': '已发货',
+ 'IN_TRANSIT': '运输中',
+ 'DELIVERED': '已签收',
+ 'OUT_FOR_DELIVERY': '派送中',
+ 'EXCEPTION': '异常'
+ }
+ return (maps[status] != null) ? maps[status] : status
+ },
+ getStatusMapping(code: string) : string {
+ const maps = {
+ 'SHIPPED': '已揽收',
+ 'IN_TRANSIT': '运输中',
+ 'ARRIVED_HUB': '到达分拨中心',
+ 'OUT_FOR_DELIVERY': '正在派送',
+ 'DELIVERED': '已签收',
+ 'EXCEPTION': '包裹异常'
+ }
+ return (maps[code] != null) ? maps[code] : code
+ },
runScenario(type: string) {
if (this.waybillInfo == null) return
uni.showModal({
diff --git a/pages/mall/delivery/test/platform-webhook-logs.uvue b/pages/mall/delivery/test/platform-webhook-logs.uvue
index f20e3f4b..77db79ce 100644
--- a/pages/mall/delivery/test/platform-webhook-logs.uvue
+++ b/pages/mall/delivery/test/platform-webhook-logs.uvue
@@ -35,7 +35,7 @@
事件:
- {{ log.event_code }}
+ {{ getEventText(log.event_code) }}
状态:
@@ -84,6 +84,18 @@
showPayload.value = true
}
+ function getEventText(code: string) : string {
+ const maps = {
+ 'SHIPPED': '发货揽收',
+ 'IN_TRANSIT': '运输中',
+ 'ARRIVED_HUB': '中转入库',
+ 'OUT_FOR_DELIVERY': '派件中',
+ 'DELIVERED': '确认签收',
+ 'EXCEPTION': '异常上报'
+ }
+ return (maps[code] != null) ? maps[code] : code
+ }
+
function reProcess(log: MockWebhookLog) {
uni.showLoading({ title: '重送中...' })
setTimeout(() => {