创建数据库表格

This commit is contained in:
not-like-juvenile
2026-02-06 16:56:24 +08:00
parent 56ae71babf
commit a5e7afacec
12 changed files with 645 additions and 59 deletions

View File

@@ -65,4 +65,87 @@
- 联调/设计参考(不要用于生产直接落库):[express_tracking_mock_platform.sql](express_tracking_mock_platform.sql) - 联调/设计参考(不要用于生产直接落库):[express_tracking_mock_platform.sql](express_tracking_mock_platform.sql)
- 包含平台侧表 + `mock_*` 测试表(故障注入/回放用),仅用于联调与文档说明。 - 包含平台侧表 + `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)
方式 ASupabase Dashboard推荐
- 打开 Supabase 项目 -> SQL Editor
- 分别粘贴并执行以上两个 SQL 文件内容(先建表、后种子)
- 建议使用具备 DDL 权限的角色执行(通常 SQL Editor 以高权限执行)
方式 BUbuntu 通过 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 用例中覆盖重复/乱序/验签失败等注入场景。 建议下一步在平台侧实现一个可切换的“Mock 数据源”开关(仅测试环境),并在 QA 用例中覆盖重复/乱序/验签失败等注入场景。

View File

@@ -16,20 +16,20 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "btree_gin"; CREATE EXTENSION IF NOT EXISTS "btree_gin";
-- updated_at 维护函数:若主库已存在则不重复创建 -- updated_at 维护函数:若主库已存在则不重复创建
DO $$ DO $do$
BEGIN BEGIN
IF to_regprocedure('public.update_updated_at_column()') IS NULL THEN IF to_regprocedure('public.update_updated_at_column()') IS NULL THEN
CREATE OR REPLACE FUNCTION public.update_updated_at_column() CREATE OR REPLACE FUNCTION public.update_updated_at_column()
RETURNS TRIGGER RETURNS TRIGGER
LANGUAGE plpgsql LANGUAGE plpgsql
AS $$ AS $func$
BEGIN BEGIN
NEW.updated_at = NOW(); NEW.updated_at = NOW();
RETURN NEW; RETURN NEW;
END; END;
$$; $func$;
END IF; END IF;
END $$; END $do$;
-- ===================================================================== -- =====================================================================
-- A. 平台侧platform统一轨迹模型入库与查询 -- A. 平台侧platform统一轨迹模型入库与查询

View File

@@ -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;

View File

@@ -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;

View File

@@ -18,24 +18,24 @@
</view> </view>
<view class="section"> <view class="section">
<text class="section-title">2. 构造回调数据 (JSON Payload)</text> <text class="section-title">2. 构造圆通协议数据 (YTO Protocol)</text>
<view class="form-group"> <view class="form-group">
<text class="label">运单号:</text> <text class="label">物流单号 (mailNo):</text>
<input class="input" v-model="form.tracking_no" placeholder="请输入运单号" /> <input class="input" v-model="form.mailNo" placeholder="请输入运单号" />
</view> </view>
<view class="form-group"> <view class="form-group">
<text class="label">快递公司:</text> <text class="label">订单号 (txLogisticId):</text>
<input class="input" v-model="form.carrier" placeholder="请输入快递公司" /> <input class="input" v-model="form.txLogisticId" placeholder="请输入关联订单号" />
</view> </view>
<view class="form-group"> <view class="form-group">
<text class="label">物流状态:</text> <text class="label">事件状态 (infoContent):</text>
<picker :range="statusOptions" range-key="label" @change="onStatusChange"> <picker :range="statusOptions" range-key="label" @change="onStatusChange">
<view class="picker-val">{{ currentStatusLabel }}</view> <view class="picker-val">{{ currentStatusLabel }}</view>
</picker> </picker>
</view> </view>
<view class="form-group"> <view class="form-group">
<text class="label">轨迹描述文字:</text> <text class="label">轨迹描述 (remark):</text>
<textarea class="textarea" v-model="form.event_text" placeholder="描述当前的物流位置或状态..." /> <textarea class="textarea" v-model="form.remark" placeholder="描述当前的物流位置或状态..." />
</view> </view>
</view> </view>
@@ -64,61 +64,77 @@
const selectedOrderIndex = ref(-1) const selectedOrderIndex = ref(-1)
const form = reactive({ const form = reactive({
tracking_no: '', mailNo: '',
carrier: '', txLogisticId: '',
status_code: 'IN_TRANSIT', infoContent: 'SEND',
event_text: '快件已到达【XX转运中心】,准备发往下一站' remark: '快件已到达【XX分拨中心】,准备发往下一站',
acceptTime: '',
carrier: '圆通速递'
}) })
const statusOptions = [ const statusOptions = [
{ label: '在途中 (IN_TRANSIT)', value: 'IN_TRANSIT' }, { label: '揽收 (GOT)', value: 'GOT' },
{ label: '派送中 (OUT_FOR_DELIVERY)', value: 'OUT_FOR_DELIVERY' }, { label: '运输中 (SEND)', value: 'SEND' },
{ label: '已签收 (DELIVERED)', value: 'DELIVERED' }, { label: '派送中 (SENT)', value: 'SENT' },
{ label: '异常 (EXCEPTION)', value: 'EXCEPTION' } { label: '已签收 (SIGNED)', value: 'SIGNED' },
{ label: '异常 (FAILED)', value: 'FAILED' }
] ]
const currentStatusLabel = computed(() => { const currentStatusLabel = computed((): string => {
const opt = statusOptions.find(o => o.value === form.status_code) const opt = statusOptions.find((o: UTSJSONObject): boolean => o['value'] === form.infoContent)
return opt ? opt.label : '请选择' return (opt != null) ? opt['label'] as string : '请选择'
}) })
const jsonString = computed(() => { const jsonString = computed((): string => {
return JSON.stringify(form, null, 2) return JSON.stringify(form, null, 2)
}) })
function selectOrder(index: number) { function selectOrder(index: number) {
selectedOrderIndex.value = index selectedOrderIndex.value = index
const order = shippedOrders.value[index] const order = shippedOrders.value[index]
form.tracking_no = order.tracking_no form.mailNo = order.tracking_no
form.carrier = order.carrier form.txLogisticId = order.order_no
form.carrier = order.carrier + '速递'
// 根据订单当前状态智能预设 // 根据订单当前状态智能预设
if (order.status === 'SHIPPED') { if (order.status === 'SHIPPED') {
form.status_code = 'IN_TRANSIT' form.infoContent = 'SEND'
form.event_text = '快件已揽收,正发往城市中心' form.remark = '快件已到达北京分拨中心'
} else if (order.status === 'IN_TRANSIT') { } else if (order.status === 'IN_TRANSIT') {
form.status_code = 'OUT_FOR_DELIVERY' form.infoContent = 'SENT'
form.event_text = '派送员王师傅(13700008888)正在派件' form.remark = '派送员王师傅(13700008888)正在派件'
} }
} }
function onStatusChange(e: UniPickerChangeEvent) { function onStatusChange(e: UniPickerChangeEvent) {
const idx = e.detail.value as number const idx = e.detail.value as number
form.status_code = statusOptions[idx].value as string form.infoContent = statusOptions[idx].value
} }
function sendWebhook() { function sendWebhook() {
if (!form.tracking_no) { if (!form.mailNo) {
uni.showToast({ title: '请先填写运单号', icon: 'none' }) uni.showToast({ title: '请先填写运单号', icon: 'none' })
return 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) // 执行模拟推送 (转换为普通对象以兼容 UTS)
const payload = { const payload = {
tracking_no: form.tracking_no, mailNo: form.mailNo,
carrier: form.carrier, txLogisticId: form.txLogisticId,
status_code: form.status_code, infoContent: form.infoContent,
event_text: form.event_text remark: form.remark,
acceptTime: form.acceptTime,
carrier: form.carrier
} as UTSJSONObject } as UTSJSONObject
const success = mockService.pushWebhookData(payload) const success = mockService.pushWebhookData(payload)

View File

@@ -135,7 +135,8 @@
}, },
getStatusText(status: string): string { getStatusText(status: string): string {
const maps = { const maps = {
'SHIPPED': '运输中', 'SHIPPED': '已发货',
'IN_TRANSIT': '运输中',
'DELIVERED': '已签收', 'DELIVERED': '已签收',
'OUT_FOR_DELIVERY': '派送中', 'OUT_FOR_DELIVERY': '派送中',
'PENDING': '待揽收', 'PENDING': '待揽收',

View File

@@ -71,7 +71,8 @@
}, },
getStatusText(status: string): string { getStatusText(status: string): string {
const maps = { const maps = {
'SHIPPED': '运输中', 'SHIPPED': '已发货',
'IN_TRANSIT': '运输中',
'DELIVERED': '已签收', 'DELIVERED': '已签收',
'OUT_FOR_DELIVERY': '派送中', 'OUT_FOR_DELIVERY': '派送中',
'PENDING': '待揽收', 'PENDING': '待揽收',

View File

@@ -113,7 +113,14 @@
this.statusHistory = mockService.getMockTracking(this.orderNo) this.statusHistory = mockService.getMockTracking(this.orderNo)
}, },
getStatusText(status: string) : string { 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 return (maps[status] != null) ? maps[status] : status
}, },
async refreshLogistics() { async refreshLogistics() {

View File

@@ -54,7 +54,7 @@
<view class="modal-body"> <view class="modal-body">
<view class="form-item"> <view class="form-item">
<text class="label">选择承运商</text> <text class="label">选择承运商</text>
<picker :range="carriers" @change="onCarrierChange"> <picker :range="carriers" range-key="label" @change="onCarrierChange">
<view class="picker-val">{{ currentCarrier || '请选择' }}</view> <view class="picker-val">{{ currentCarrier || '请选择' }}</view>
</picker> </picker>
</view> </view>
@@ -83,8 +83,14 @@
orders: [] as MockOrder[], orders: [] as MockOrder[],
showShipModal: false, showShipModal: false,
selectedOrder: null as MockOrder | null, selectedOrder: null as MockOrder | null,
carriers: ['YUNDA', 'YTO', 'ZTO', 'STO', 'SF'], carriers: [
currentCarrier: '', { label: '韵达快递', value: '韵达' },
{ label: '圆通速递', value: '圆通' },
{ label: '中通快递', value: '中通' },
{ label: '申通快递', value: '申通' },
{ label: '顺丰速运', value: '顺丰' }
],
currentCarrier: '韵达',
trackingNo: '' trackingNo: ''
} }
}, },
@@ -117,6 +123,7 @@
const maps = { const maps = {
'PENDING': '待发货', 'PENDING': '待发货',
'SHIPPED': '已发货', 'SHIPPED': '已发货',
'IN_TRANSIT': '运输中',
'DELIVERED': '已签收', 'DELIVERED': '已签收',
'OUT_FOR_DELIVERY': '派送中', 'OUT_FOR_DELIVERY': '派送中',
'EXCEPTION': '包裹异常' 'EXCEPTION': '包裹异常'
@@ -131,7 +138,7 @@
}, },
onCarrierChange(e: any) { onCarrierChange(e: any) {
const index = e.detail.value as number const index = e.detail.value as number
this.currentCarrier = this.carriers[index] this.currentCarrier = this.carriers[index].value
}, },
async confirmShip() { async confirmShip() {
if (!this.trackingNo) { if (!this.trackingNo) {

View File

@@ -436,14 +436,24 @@ class MockService {
} }
/** /**
* 核心功能:模拟第三方回调接口 * 核心功能:模拟第三方回调接口 (适配圆通推送协议)
* 模拟外部物流平台向本系统发送轨迹更新 API * 对应字段: txLogisticId(订单号), mailNo(票号), infoContent(状态), remark(描述)
*/ */
pushWebhookData(payload: UTSJSONObject): boolean { pushWebhookData(payload: UTSJSONObject): boolean {
const tracking_no = payload['tracking_no'] as string // 兼容圆通协议字段
const status_code = payload['status_code'] as string const tracking_no = (payload['mailNo'] != null) ? payload['mailNo'] as string : (payload['tracking_no'] as string)
const event_text = payload['event_text'] as string const yto_status = (payload['infoContent'] != null) ? payload['infoContent'] as string : (payload['status_code'] as string)
const carrier = payload['carrier'] 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. 记录原始日志 // 1. 记录原始日志
const now = new Date() const now = new Date()
@@ -451,7 +461,7 @@ class MockService {
time: this.formatDate(now).split(' ')[1], time: this.formatDate(now).split(' ')[1],
carrier: carrier, carrier: carrier,
tracking_no: tracking_no, tracking_no: tracking_no,
event_code: status_code, event_code: yto_status, // 保留圆通原始代码
success: true, success: true,
result_text: '接收成功', result_text: '接收成功',
payload: payload payload: payload
@@ -459,16 +469,16 @@ class MockService {
this.webhookLogs.unshift(log) this.webhookLogs.unshift(log)
// 2. 更新系统内部轨迹 // 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 (order != null) {
if (!this.trackingHistory.has(order.order_no)) { if (!this.trackingHistory.has(order.order_no)) {
this.trackingHistory.set(order.order_no, []) this.trackingHistory.set(order.order_no, [])
} }
const history = this.trackingHistory.get(order.order_no)! const history = this.trackingHistory.get(order.order_no)!
history.unshift({ history.unshift({
event_id: 'wb_' + Date.now(), event_id: 'yto_' + Date.now(),
event_time: this.formatDate(now), event_time: payload['acceptTime'] as string || this.formatDate(now),
event_code: status_code, event_code: yto_status,
event_text: event_text, event_text: event_text,
status_code: status_code, status_code: status_code,
evidence_urls: [] evidence_urls: []
@@ -479,7 +489,7 @@ class MockService {
} }
log.success = false log.success = false
log.result_text = '未找到对应的运单号' log.result_text = '未找到对应的运单或订单号'
return false return false
} }

View File

@@ -29,7 +29,7 @@
</view> </view>
<view class="info-item"> <view class="info-item">
<text class="label">当前状态:</text> <text class="label">当前状态:</text>
<text class="value">{{ waybillInfo.status }}</text> <text class="value">{{ getStatusText(waybillInfo.status) }}</text>
</view> </view>
</view> </view>
</view> </view>
@@ -44,8 +44,8 @@
<view class="event-main-col"> <view class="event-main-col">
<text class="event-text">{{ event.event_text }}</text> <text class="event-text">{{ event.event_text }}</text>
<view class="event-meta"> <view class="event-meta">
<text class="meta-tag blue">CODE: {{ event.event_code }}</text> <text class="meta-tag blue">代码: {{ event.event_code }}</text>
<text class="meta-tag orange">MAP: {{ event.status_code }}</text> <text class="meta-tag orange">映射状态: {{ getStatusMapping(event.status_code) }}</text>
</view> </view>
<view class="raw-box" v-if="showRaw[index]"> <view class="raw-box" v-if="showRaw[index]">
<text class="raw-text">{{ JSON.stringify(event.raw_payload, null, 2) }}</text> <text class="raw-text">{{ JSON.stringify(event.raw_payload, null, 2) }}</text>
@@ -116,6 +116,28 @@
const current = this.showRaw[index] const current = this.showRaw[index]
this.showRaw[index] = !current 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) { runScenario(type: string) {
if (this.waybillInfo == null) return if (this.waybillInfo == null) return
uni.showModal({ uni.showModal({

View File

@@ -35,7 +35,7 @@
</view> </view>
<view class="log-row"> <view class="log-row">
<text class="log-label">事件:</text> <text class="log-label">事件:</text>
<text class="log-value">{{ log.event_code }}</text> <text class="log-value">{{ getEventText(log.event_code) }}</text>
</view> </view>
<view class="log-row"> <view class="log-row">
<text class="log-label">状态:</text> <text class="log-label">状态:</text>
@@ -84,6 +84,18 @@
showPayload.value = true 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) { function reProcess(log: MockWebhookLog) {
uni.showLoading({ title: '重送中...' }) uni.showLoading({ title: '重送中...' })
setTimeout(() => { setTimeout(() => {