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 }} - 轨迹描述文字: -