修改过时文档,优化文档内容

This commit is contained in:
not-like-juvenile
2026-03-17 11:06:26 +08:00
parent dac730474b
commit f33efe6ec8
45 changed files with 162 additions and 236 deletions

View File

@@ -0,0 +1,46 @@
# 消息推送与物流发货后台 —— 需求与架构文档总览
> **更新日期**2026年03月16日
> **核心职责**管理商城发货物流对接、Webhook状态更新接受、基于消息队列模型的异步应用内推送UniPush 2.0)。
本目录包含了本系统从“接收物流消息”到“完成终端设备 APP 推送”的全核心链路设计与需求规范。我们采用了**免 Redis 的高可靠去中心化架构(基于 PostgreSQL Transactional Outbox 模式)**。
## 🎯 必读核心文档(从 0 到 1 快速理解本套系统)
1. 📂 **数据库表结构设计(全系统基石)**
- [消息推送后台_数据库表结构需求文档.md](./db/消息推送后台_数据库表结构需求文档.md) *(核心!详述了从快递节点到通知排队流转经过的 5 张关键表)*
2. 🔄 **数据流向与演进架构**
-`server/消息推送文档/DELIVERY_NOTIFICATION_DATA_FLOW.md` *(全局架构数据泳道流转图与为什么不使用 Redis 的技术选型解说)*
3. 📦 **业务推送机制文档**
- [物流消息推送方案_用户端与商家端.md](物流消息推送方案_用户端与商家端.md) *(阐述推送业务交互形态及场景)*
- [推送与设备需求文档_含建表附录.md](推送与设备需求文档_含建表附录.md) *(阐述设备绑定及队列下发业务目标)*
---
## 📚 目录体系详解
### 1. 核心架构与数据库执行脚本 (`db/` 目录)
如果需要在全新环境部署此推送系统,请**按顺序执行**以下表结构 SQL 迁移脚本:
- `express_tracking_platform_upgrade.sql`: 运单主表、源事件表(`platform_express_event_raw`/`tracking_events`)的创建。
- `20260224_add_push_devices_and_notifications.sql`: 客户端推送标记路由表(`push_devices`)与最终推送记录大表(`express_notifications`)创建。
- `20260309_add_notify_queue_and_trigger.sql`: **极度重要!**创建异步缓冲外箱表 `notify_queue` 和连接事件与处理器的 PostgreSQL 防丢触发器 `event_to_queue_trigger`
- `20260309_add_express_notifications_send_status.sql`: 添加投递状态控制。
- `20260310_fix_express_notifications_on_conflict_message_id.sql`: 去重逻辑修复。
### 2. 前端与联调试配规范文档
本块文档聚焦于多端(客户端 UniApp 与 管理后台端)关于物流模块的展现与对接规范:
- **客户端获取 CID 核心技术栈**[uni-push2_安卓联调与取CID说明.md](uni-push2_安卓联调与取CID说明.md)(指导客户端童鞋如果提取各厂手机独立推送凭证)
- **后台页面结构**[后台页面设计说明.md](后台页面设计说明.md)
- **数据流与展示字段**[前端字段清单.md](前端字段清单.md)
- **前后端快递状态映射**[状态映射表.md](状态映射表.md) (如:快递参数如何转为本系统枚举类型代码)
### 3. 测试与沙盘联调 (Mock 专属)
因接入第三方如快递100需签约本系统也内置了一套模拟联调能力。
- [接口规范.md](接口规范.md)
- SQL 数据灌入脚本:`simulate_third_party_to_db.sql` / `seed_platform_express_test_data.sql` / `express_tracking_mock_platform.sql`
### 4. (历史/备份)早期草案与建议档案
以下文档作为架构迭代的历史依据:
- `推送与设备需求文档.md`(原始未涵盖表结构的草案)
- `配送模块需求文档.md`(旧三通一达体系梳理)
- `生产表说明_platform_express.md` / `数据库对比与修改建议.md` / `缺口与待补充清单.md`

View File

@@ -0,0 +1,200 @@
-- =====================================================================================
-- Add push device table and express_notifications table
-- 目的:保存每个用户/商家设备的推送 CID并记录由轨迹事件触发的通知消息
-- 创建日期2026-02-24
-- 注意:尽量与仓库中其它迁移风格保持一致;该脚本对已存在对象做 IF NOT EXISTS 检查。
-- =====================================================================================
BEGIN;
-- 依赖扩展(若已存在则忽略)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- updated_at 自动维护函数(若仓库已有同名函数则不重复创建)
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 $func$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$func$;
END IF;
END $do$;
-- =====================================================
-- A. push_devices设备注册/绑定表
-- =====================================================
CREATE TABLE IF NOT EXISTS public.push_devices (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- 关联主体user / merchant使用具体字段便于查询与权限控制。
user_id UUID NULL,
merchant_id UUID NULL,
-- 设备推送客户端标识CID由推送 SDK 提供
cid VARCHAR(255) NOT NULL,
-- 平台/渠道android / ios / web / huawei / xiaomi
platform VARCHAR(32) NOT NULL DEFAULT 'android',
-- 应用标识(用于区分不同打包的 AppID / 环境)
appid VARCHAR(128) NOT NULL DEFAULT 'default',
-- 设备是否激活(登录/解绑/失效时置 false
is_active BOOLEAN NOT NULL DEFAULT TRUE,
-- 设备最后上报时间(用于清理失效设备)
last_seen_at TIMESTAMP WITH TIME ZONE NULL,
-- 注册来源(可选,用于审计)
registration_source VARCHAR(64) NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- updated_at 触发器
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_trigger
WHERE tgname = 'trigger_push_devices_updated_at'
) THEN
CREATE TRIGGER trigger_push_devices_updated_at
BEFORE UPDATE ON public.push_devices
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
END IF;
END $$;
-- 唯一约束:同一 appid 下 cid 唯一
CREATE UNIQUE INDEX IF NOT EXISTS ux_push_devices_appid_cid
ON public.push_devices(appid, cid);
-- 索引:按 user_id/merchant_id 查询是常用路径
CREATE INDEX IF NOT EXISTS idx_push_devices_user_id ON public.push_devices(user_id);
CREATE INDEX IF NOT EXISTS idx_push_devices_merchant_id ON public.push_devices(merchant_id);
CREATE INDEX IF NOT EXISTS idx_push_devices_is_active ON public.push_devices(is_active);
-- =====================================================
-- B. express_notifications物流相关推送消息中心表
-- 用途:保存由轨迹事件触发的消息记录(用于消息中心、未读计数与幂等)
-- =====================================================
CREATE TABLE IF NOT EXISTS public.express_notifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- 接收范围user / merchant
aud VARCHAR(16) NOT NULL,
recipient_id UUID NOT NULL,
-- 业务关联(可选):订单/运单
order_id UUID NULL,
waybill_id UUID NULL,
-- 运单与承运方冗余字段,便于快速查询
tracking_no VARCHAR(64) NULL,
carrier VARCHAR(32) NULL,
-- 推送消息唯一 id可由服务端生成并返回给客户端
message_id VARCHAR(128) NULL,
-- 事件摘要(下发给客户端的安全/脱敏文案)
event_text_safe TEXT NULL,
status_code VARCHAR(32) NULL,
retry_count INTEGER NOT NULL DEFAULT 0,
last_error TEXT NULL,
next_attempt_at TIMESTAMP WITH TIME ZONE NULL,
event_time TIMESTAMP WITH TIME ZONE NULL,
-- 透传/审计用 payload不包含敏感字段raw_payload 请勿透传给客户端)
payload JSONB NULL,
-- 已读时间null 表示未读)
read_at TIMESTAMP WITH TIME ZONE NULL,
-- 幂等键(确保同一事件只生成一条消息)
dedupe_key VARCHAR(256) NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- updated_at 触发器
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_trigger
WHERE tgname = 'trigger_express_notifications_updated_at'
) THEN
CREATE TRIGGER trigger_express_notifications_updated_at
BEFORE UPDATE ON public.express_notifications
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
END IF;
END $$;
-- 索引与约束
CREATE INDEX IF NOT EXISTS idx_express_notifications_aud_recipient ON public.express_notifications(aud, recipient_id);
CREATE INDEX IF NOT EXISTS idx_express_notifications_order_id ON public.express_notifications(order_id);
CREATE INDEX IF NOT EXISTS idx_express_notifications_waybill_id ON public.express_notifications(waybill_id);
CREATE INDEX IF NOT EXISTS idx_express_notifications_created_at ON public.express_notifications(created_at);
CREATE INDEX IF NOT EXISTS idx_express_notifications_read_at ON public.express_notifications(read_at);
-- 若使用 message_id 做幂等(外部系统/队列),则建立唯一索引
-- 注意这里不能使用“部分唯一索引WHERE message_id IS NOT NULL
-- 否则 PostgREST 的 upsert `?on_conflict=message_id` 会触发 42P10
-- "there is no unique or exclusion constraint matching the ON CONFLICT specification"
-- 普通 UNIQUE INDEX 仍允许多个 NULL符合历史兼容
CREATE UNIQUE INDEX IF NOT EXISTS ux_express_notifications_message_id
ON public.express_notifications(message_id);
-- =====================================================
-- C. 兼容性与外键(若目标表存在则添加外键约束;若不存在则保留字段)
-- 说明:为避免迁移失败,我们在这里尝试添加外键,但使用 DO $$ ... EXCEPTION 来忽略不存在表的情况。
-- =====================================================
DO $$
BEGIN
-- 尝试为 push_devices.user_id 添加外键(如果 ak_users 表存在)
IF EXISTS (SELECT 1 FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid WHERE c.relname = 'ak_users' AND n.nspname = 'public') THEN
BEGIN
ALTER TABLE public.push_devices
ADD CONSTRAINT fk_push_devices_user FOREIGN KEY (user_id) REFERENCES public.ak_users(id) ON DELETE SET NULL;
EXCEPTION WHEN duplicate_object THEN
-- 已存在约束则忽略
NULL;
END;
END IF;
-- 尝试为 express_notifications.order_id 添加外键(如果 ml_orders 表存在)
IF EXISTS (SELECT 1 FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid WHERE c.relname = 'ml_orders' AND n.nspname = 'public') THEN
BEGIN
ALTER TABLE public.express_notifications
ADD CONSTRAINT fk_express_notifications_order FOREIGN KEY (order_id) REFERENCES public.ml_orders(id) ON DELETE SET NULL;
EXCEPTION WHEN duplicate_object THEN
NULL;
END;
END IF;
-- 尝试为 express_notifications.waybill_id 添加外键(如果 platform_express_waybills 表存在)
IF EXISTS (SELECT 1 FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid WHERE c.relname = 'platform_express_waybills' AND n.nspname = 'public') THEN
BEGIN
ALTER TABLE public.express_notifications
ADD CONSTRAINT fk_express_notifications_waybill FOREIGN KEY (waybill_id) REFERENCES public.platform_express_waybills(id) ON DELETE SET NULL;
EXCEPTION WHEN duplicate_object THEN
NULL;
END;
END IF;
END $$;
COMMIT;
-- =====================================================================================
-- 使用说明(简要)
-- 1) `push_devices` 存储设备 CID 与所属主体user_id / merchant_id提供活跃检测与解绑能力。
-- 2) `express_notifications` 存储消息中心记录;生成通知时写入该表並异步向 `push_devices` 中匹配设备发送推送。
-- 3) 推荐在应用层实现:当用户登出或解绑设备时将 `push_devices.is_active` 置 false。
-- 4) 可按需要添加清理任务:清理 long-inactive 的 `push_devices`(例如 180 天未上报)。
-- =====================================================================================

View File

@@ -0,0 +1,26 @@
-- =====================================================================================
-- Add send_status to express_notifications
--
-- 目的区分“物流状态status_code”与“投递处理状态send_status”。
-- - status_code业务/物流状态SHIPPED/OUT_FOR_DELIVERY/...
-- - send_status投递状态null=待发送, processing, retrying, success, failed, no-targets
--
-- 创建日期2026-03-09
-- =====================================================================================
BEGIN;
ALTER TABLE public.express_notifications
ADD COLUMN IF NOT EXISTS send_status VARCHAR(32) NULL;
CREATE INDEX IF NOT EXISTS idx_express_notifications_send_status
ON public.express_notifications(send_status);
-- 兼容旧实现:历史上 push-server consumer 使用 status_code 存投递状态。
-- 迁移后 consumer 改读写 send_status为避免把旧的 success/failed 等记录当成 pending 再次推送,做一次安全回填。
UPDATE public.express_notifications
SET send_status = status_code
WHERE send_status IS NULL
AND status_code IN ('processing', 'retrying', 'success', 'failed', 'no-targets');
COMMIT;

View File

@@ -0,0 +1,102 @@
-- =====================================================================================
-- notify_queue + trigger: platform_express_tracking_events -> notify_queue
--
-- 目的:把“轨迹事件入库”和“消息生成/推送”解耦。
-- - Webhook/轮询/手工写入 tracking_events 后,由触发器把关键事件入队到 notify_queue。
-- - 常驻 worker 消费 notify_queue生成 express_notifications消息中心/推送任务)。
-- - push-server consumer 轮询 express_notifications 并调用 CLOUD_FUNC_URL 进行实际下发。
--
-- 创建日期2026-03-09
-- =====================================================================================
BEGIN;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- 队列表:仅做“轻量入队”,避免触发器做外部 IO
CREATE TABLE IF NOT EXISTS public.notify_queue (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
waybill_id UUID NOT NULL,
carrier VARCHAR(32) NULL,
tracking_no VARCHAR(64) NULL,
event_id VARCHAR(128) NULL,
status_code VARCHAR(32) NOT NULL,
event_time TIMESTAMP WITH TIME ZONE NOT NULL,
event_text TEXT NULL,
source VARCHAR(16) NULL,
-- 与 platform_express_tracking_events 对齐的幂等键
dedupe_key VARCHAR(256) NOT NULL,
raw_payload JSONB NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
processed_at TIMESTAMP WITH TIME ZONE NULL,
process_status VARCHAR(32) NULL,
last_error TEXT NULL,
CONSTRAINT uk_notify_queue_dedupe UNIQUE (waybill_id, dedupe_key)
);
CREATE INDEX IF NOT EXISTS idx_notify_queue_processed_at ON public.notify_queue(processed_at);
CREATE INDEX IF NOT EXISTS idx_notify_queue_created_at ON public.notify_queue(created_at);
CREATE INDEX IF NOT EXISTS idx_notify_queue_status_code ON public.notify_queue(status_code);
-- 触发器函数:入队关键状态事件
CREATE OR REPLACE FUNCTION public.notify_new_tracking_event()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $func$
DECLARE
should_enqueue BOOLEAN := FALSE;
BEGIN
-- 推送策略MVP只对关键状态入队
IF NEW.status_code IN ('SHIPPED','OUT_FOR_DELIVERY','READY_FOR_PICKUP','DELIVERED','EXCEPTION','RETURNED') THEN
should_enqueue := TRUE;
END IF;
IF should_enqueue THEN
INSERT INTO public.notify_queue(
waybill_id,
carrier,
tracking_no,
event_id,
status_code,
event_time,
event_text,
source,
dedupe_key,
raw_payload
) VALUES (
NEW.waybill_id,
NEW.carrier,
NEW.tracking_no,
NEW.event_id,
NEW.status_code,
NEW.event_time,
NEW.event_text,
NEW.source,
NEW.dedupe_key,
NEW.raw_payload
)
ON CONFLICT (waybill_id, dedupe_key) DO NOTHING;
END IF;
RETURN NEW;
END;
$func$;
-- 触发器tracking_events 写入后入队
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_trigger WHERE tgname = 'trigger_notify_new_tracking_event'
) THEN
CREATE TRIGGER trigger_notify_new_tracking_event
AFTER INSERT ON public.platform_express_tracking_events
FOR EACH ROW EXECUTE FUNCTION public.notify_new_tracking_event();
END IF;
END $$;
COMMIT;

View File

@@ -0,0 +1,30 @@
-- =====================================================================================
-- Fix express_notifications upsert conflict target (message_id)
-- 目的:修复 notify-worker / push-server 在 upsert 时遇到的 42P10
-- "there is no unique or exclusion constraint matching the ON CONFLICT specification"
-- 原因历史迁移可能创建了部分唯一索引WHERE message_id IS NOT NULL
-- PostgREST 的 upsert `?on_conflict=message_id` 无法匹配该索引。
-- 方案:改为普通 UNIQUE INDEX (message_id)。Postgres UNIQUE 允许多条 NULL兼容旧数据。
-- 创建日期2026-03-10
-- =====================================================================================
BEGIN;
-- 1) 预检查:是否存在重复的非空 message_id若存在创建唯一索引会失败
-- 如有返回结果,请先人工去重后再继续执行后续语句。
-- 示例去重策略:保留最新 created_at其它行将 message_id 置为 NULL 或删除重复行。
--
-- SELECT message_id, COUNT(*)
-- FROM public.express_notifications
-- WHERE message_id IS NOT NULL
-- GROUP BY message_id
-- HAVING COUNT(*) > 1;
-- 2) 删除历史“部分唯一索引”(如果存在)
DROP INDEX IF EXISTS public.ux_express_notifications_message_id;
-- 3) 创建普通唯一索引,让 `on_conflict=message_id` 正常工作
CREATE UNIQUE INDEX IF NOT EXISTS ux_express_notifications_message_id
ON public.express_notifications(message_id);
COMMIT;

View File

@@ -0,0 +1,174 @@
-- =====================================================================================
-- 第三方快递轨迹(平台侧)表结构升级 (PostgreSQL + Supabase)
-- 用途:
-- - 引入第三方承运方运单与轨迹事件的统一入库模型
-- - 支撑商家端/用户端/平台后台的同源轨迹展示
-- 说明:
-- - 仅创建 platform 侧三张表platform_express_waybills / platform_express_tracking_events / platform_express_event_raw
-- - 不包含 mock_* 测试表
-- - 不涉及自营骑手表ml_delivery_*
-- =====================================================================================
BEGIN;
-- 依赖扩展(项目主库已使用 uuid-ossp这里重复声明是安全的
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "btree_gin";
-- updated_at 维护函数:若主库已存在则不重复创建
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 $func$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$func$;
END IF;
END $do$;
-- =====================================================================
-- A. 平台侧platform统一轨迹模型入库与查询
-- =====================================================================
-- 运单主表:按 (carrier, tracking_no) 唯一
CREATE TABLE IF NOT EXISTS public.platform_express_waybills (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- 关联订单推荐关联主键order_no 作为展示/兜底)
order_id UUID NULL REFERENCES public.ml_orders(id) ON DELETE SET NULL,
order_no VARCHAR(64) NULL,
carrier VARCHAR(32) NOT NULL, -- YUNDA/YTO/ZTO/STO/KDN...
tracking_no VARCHAR(64) NOT NULL,
source VARCHAR(16) NOT NULL DEFAULT 'mock', -- mock/carrier/aggregator
current_status_code VARCHAR(32) NOT NULL DEFAULT 'SHIPPED',
current_status_text TEXT NULL,
eta TIMESTAMP WITH TIME ZONE NULL,
last_synced_at TIMESTAMP WITH TIME ZONE NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT uk_platform_express_waybill UNIQUE (carrier, tracking_no)
);
-- updated_at 触发器
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_trigger
WHERE tgname = 'trigger_platform_express_waybills_updated_at'
) THEN
CREATE TRIGGER trigger_platform_express_waybills_updated_at
BEFORE UPDATE ON public.platform_express_waybills
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
END IF;
END $$;
-- 索引(按订单查运单是主查询路径)
CREATE INDEX IF NOT EXISTS idx_platform_express_waybills_order_id
ON public.platform_express_waybills(order_id);
CREATE INDEX IF NOT EXISTS idx_platform_express_waybills_order_no
ON public.platform_express_waybills(order_no);
CREATE INDEX IF NOT EXISTS idx_platform_express_waybills_tracking_no
ON public.platform_express_waybills(tracking_no);
CREATE INDEX IF NOT EXISTS idx_platform_express_waybills_status
ON public.platform_express_waybills(current_status_code);
-- 轨迹事件表:前端时间线/告警/统计的主数据来源
CREATE TABLE IF NOT EXISTS public.platform_express_tracking_events (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
waybill_id UUID NOT NULL REFERENCES public.platform_express_waybills(id) ON DELETE CASCADE,
carrier VARCHAR(32) NOT NULL,
tracking_no VARCHAR(64) NOT NULL,
-- 第三方事件唯一标识(可能缺失)
event_id VARCHAR(128) NULL,
event_time TIMESTAMP WITH TIME ZONE NOT NULL,
event_code VARCHAR(64) NOT NULL,
event_text TEXT NOT NULL,
-- 平台统一状态
status_code VARCHAR(32) NOT NULL,
node_name VARCHAR(128) NULL,
location TEXT NULL,
description TEXT NULL,
evidence_urls JSONB NOT NULL DEFAULT '[]'::jsonb,
-- 原始回文(用于审计/排障)
raw_payload JSONB NULL,
-- 接收侧信息
received_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
source VARCHAR(16) NOT NULL DEFAULT 'webhook', -- webhook/poll/manual
-- 幂等去重键:优先 event_id缺失时用 tracking_no+event_code+event_time(+可选字段) 构造
dedupe_key VARCHAR(256) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT uk_platform_express_event_dedupe UNIQUE (waybill_id, dedupe_key)
);
CREATE INDEX IF NOT EXISTS idx_platform_express_events_waybill_time
ON public.platform_express_tracking_events(waybill_id, event_time);
CREATE INDEX IF NOT EXISTS idx_platform_express_events_tracking_time
ON public.platform_express_tracking_events(tracking_no, event_time);
CREATE INDEX IF NOT EXISTS idx_platform_express_events_status
ON public.platform_express_tracking_events(status_code);
CREATE INDEX IF NOT EXISTS idx_platform_express_events_received_at
ON public.platform_express_tracking_events(received_at);
CREATE INDEX IF NOT EXISTS gin_platform_express_events_raw_payload
ON public.platform_express_tracking_events USING gin (raw_payload);
-- 原始接收表:记录每一次 webhook/轮询原文、验签结果与解析错误
CREATE TABLE IF NOT EXISTS public.platform_express_event_raw (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
received_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
source VARCHAR(16) NOT NULL DEFAULT 'webhook',
-- 请求侧标识
client_id VARCHAR(64) NULL,
carrier VARCHAR(32) NULL,
tracking_no VARCHAR(64) NULL,
-- 安全审计
signature_valid BOOLEAN NULL,
signature TEXT NULL,
ts_header TEXT NULL,
request_id VARCHAR(64) NULL,
remote_ip INET NULL,
headers JSONB NULL,
body JSONB NULL,
parse_error TEXT NULL,
-- 去重辅助(可选):用于识别完全重复的原始请求
dedupe_key VARCHAR(256) NULL
);
CREATE INDEX IF NOT EXISTS idx_platform_express_raw_received_at
ON public.platform_express_event_raw(received_at);
CREATE INDEX IF NOT EXISTS idx_platform_express_raw_tracking_no
ON public.platform_express_event_raw(tracking_no);
CREATE INDEX IF NOT EXISTS idx_platform_express_raw_signature_valid
ON public.platform_express_event_raw(signature_valid);
CREATE INDEX IF NOT EXISTS gin_platform_express_raw_body
ON public.platform_express_event_raw USING gin (body);
COMMIT;

View File

@@ -0,0 +1,101 @@
# 消息推送后台 —— 核心数据库表结构需求文档
> **创建日期**2026年03月16日
> **模块归属**:商城发货与物流推送后台
> **涉及架构**Node.js Worker + Supabase (PostgreSQL) + UniPush 2.0
为支撑高可用、防丢失、可追溯的异步消息推送架构本消息推送系统在底层Supabase/PostgreSQL摒弃了传统的 Redis 中间件依赖直接利用关系型数据库的横表与触发器机制Transactional Outbox 事务外箱模式)完成了高并发状态下的物流事件缓冲与分发。
本需求文档详细列出了参与推送闭环的 **5 张核心数据表** 的设计规范、字段说明与职责定位,是后端/DBA 开发与日常客诉运维排障的唯一真理依据。
---
## 整体库表流转全景
消息在数据库中的物理游走路径为:
**`平台接收原生钩子`** $\rightarrow$ **`轨迹业务表入库`** $\rightarrow$ (触发器) $\rightarrow$ **`任务队列缓冲表`** $\rightarrow$ (合并订单/用户数据) $\rightarrow$ **`最终下发通知表`** $\rightarrow$ **`匹配设备表CID推送`**
---
## 一、 原始通信日志表 (`platform_express_event_raw`)
### 1. 表职责方向
保存第一手的物流商如快递100Webhook 推送过来的原生 JSON 报文。不做任何业务过滤。
- **核心价值**:排查“某单是不是底下的快递公司就没给我们发回调?”的第一现场。如果这里没有记录,绝不可能是本系统的 Bug。
### 2. 核心字段设计 (逻辑概述)
* `id` (UUID): 唯一自增主键。
* `raw_payload` (JSONB): 快递100推过来的纯原生报文。
* `created_at` (Timestamp): 平台接收到外网请求的确切服务器时间。
---
## 二、 规范化轨迹主表 (`platform_express_tracking_events` / `waybills`)
### 1. 表职责方向
用于商城订单域查询使用的结构化物流跟踪事件表。接收服务将 `event_raw` 里的 JSON 解包后,转化为本商城统一的枚举状态。
- **机制特点**:这张表在发生 `INSERT` (即产生新轨迹节点) 时,会触发 PostgreSQL 内置触发器 `event_to_queue_trigger`
### 2. 核心字段要求
* `waybill_id` (UUID): 关联的运单主键。
* `status_code` (VARCHAR): 规范化的签收/派件枚举状态码。
* `event_text` (TEXT): 具体的物流节点描述文本(例:“北京市【朝阳区】,您的快递已由门卫代签收”)。
---
## 三、 异步缓冲队列表 (`notify_queue`)
### 1. 表职责方向
承接业务量暴增时的**削峰填谷**与**断电防丢**任务(本质是实现基于 DB 的 Message Queue
- **流程规范**:触发器把满足条件(如需要给用户弹推送的节点)的事件主键,极速插入本表待命。后台的 `notify-worker.js` (消费者) 轮询查表消费。
### 2. 核心字段定义
* `id` (UUID): 队列任务唯一标识。
* `waybill_id` (UUID): 运单号Worker 读取后利用它去关联查询出订单里的 `user_id` 和商品信息。
* `status_code` (VARCHAR): 通知级别状态。
* **`dedupe_key` (VARCHAR)**: 防重幂等键。防止因为相同事件重复回调产生两条推送。
* **`process_status` (VARCHAR)**: 最核心的状态机!
* `pending`: 等待 Node.js Worker 消费。
* `processing`: Worker 消费中(乐观锁占用)。
* `processed`: 拼接业务数据完成,已成功放入推送大表,完美结束。
* `failed`: 服务异常或订单找不到,抛弃处理。
* `last_error` (TEXT): 消费报错时的堆栈。
---
## 四、 最终下发通知表 (`express_notifications`)
### 1. 表职责方向
面向客户端推送与“站内信展示”的实体记录表。这里存放的是已经完成业务数据组装(谁买的、买的啥、什么物流状态),只需调用第三方厂商接口往外发的纯粹“信件”内容。
- **流程规范**`push-server.js` 轮询此表进行真实推送。
### 2. 核心字段定义
* `id` (UUID): 消息的主键ID。
* `recipient_id` (UUID): 接收用户的标识 (商城买家ID)。
* `event_text_safe` (TEXT): 已经组装好且脱敏的安全话术(例如:“[张*]您的Nike球鞋正在派送中”
* **`send_status / push_status` (VARCHAR)**: 推送服务通道状态机。
* `pending`: 已产生信件,排队等待网络 POST 请发给 UniPush 云函数。
* `delivered`: 云函数返回 200且厂家确认通道收录。
* `failed`: 网络异常或设备CID已注销失效。
* `provider_response` (JSONB): 如果推送失败,此处会存储华为/小米等厂商或推服务产生的物理失败原因。
---
## 五、 设备凭证绑定表 (`push_devices`)
### 1. 表职责方向
提供从“商城系统 `user_id`” 到“手机厂家原生通道标识 `cid`” 的 KV 路由映射能力。
- **业务场景**:用户 App 在启动或大版本更新时,前端会通过 `uni.getPushClientId` 获取本设备的厂商识别码,通过接口上报并 UPSERT 写入此表。
### 2. 核心字段定义
* `id` (UUID): 映射记录主键。
* `user_id` (UUID): 系统业务逻辑里的用户身份标识。
* **`cid` (VARCHAR)**: UniPush 2.0 分配在设备当前生命周期的绝对推送识别码(至关重要,一旦清空缓存/重装系统可能会变动,必须持续覆盖更新)。
* `updated_at` (Timestamp): 用以判断该设备CID的活跃新鲜度。如果一个CID超过1年未更新导致 `push_status` 频繁 `failed`,后台可将其标记为离线沉默用户放弃网络调用。
---
## 附:业务表拓展准则 (Guidelines)
如果未来需要将这套机制从物流推送拓展至**“拼团成功通知”**或**“特价秒杀通知”**,只需要:
1. 复用 `express_notifications` 表,或者建立同层级类似的 `system_notifications` 表,保留 `push_status``recipient_id`
2. 复用 `push_devices` 路由查找。
3. 把源头触发器挂载到新的交易表中即可。此五表闭环具备极佳的**水平扩展性**与**极低的中间件运维成本**。

View File

@@ -0,0 +1,85 @@
-- =====================================================================================
-- RPC: notify-worker safe recipients lookup (RLS-safe)
--
-- 背景:
-- - public.ml_orders 已开启 RLSPostgREST 在未携带可解码 JWT 时auth.uid() 为 NULL
-- 直接 SELECT 会被策略过滤为 0 行,导致 notify-worker 报 “order not found for waybill”。
-- - 在一些自托管场景中Authorization: Bearer <service_role JWT> 可能因 JWT_SECRET 不一致被 PostgREST 拒绝401 PGRST301
--
-- 方案:
-- - 提供 SECURITY DEFINER 的 RPC只返回订单的收件人映射user_id / merchant_id
-- - 通过请求头 x-notify-worker-token 做显式鉴权(避免把表全局 SELECT 放开)。
--
-- 使用:
-- - notify-worker 调用 POST /rest/v1/rpc/notify_get_order_recipients
-- 并携带 header: x-notify-worker-token: <NOTIFY_WORKER_RPC_TOKEN>
-- =====================================================================================
BEGIN;
CREATE OR REPLACE FUNCTION public.notify_get_order_recipients(
p_order_id UUID DEFAULT NULL,
p_order_no TEXT DEFAULT NULL
)
RETURNS TABLE (
id UUID,
order_no VARCHAR,
user_id UUID,
merchant_id UUID
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $func$
DECLARE
headers_json JSON;
token TEXT;
expected_token TEXT;
BEGIN
-- 1) 读取请求头 tokenPostgREST 会把 headers 放入 GUC request.headers
expected_token := current_setting('app.notify_worker_token', true);
headers_json := NULLIF(current_setting('request.headers', true), '')::json;
IF headers_json IS NOT NULL THEN
token := headers_json->>'x-notify-worker-token';
END IF;
IF expected_token IS NULL OR expected_token = '' THEN
RAISE EXCEPTION 'server misconfigured: app.notify_worker_token is not set';
END IF;
IF token IS NULL OR token <> expected_token THEN
RAISE EXCEPTION 'permission denied: invalid x-notify-worker-token';
END IF;
-- 2) 参数校验
IF (p_order_id IS NULL OR p_order_id::text = '') AND (p_order_no IS NULL OR btrim(p_order_no) = '') THEN
RAISE EXCEPTION 'p_order_id or p_order_no must be provided';
END IF;
-- 3) 返回映射SECURITY DEFINER 可绕过 RLS只返回最小必要字段
RETURN QUERY
SELECT o.id, o.order_no, o.user_id, o.merchant_id
FROM public.ml_orders o
WHERE (p_order_id IS NOT NULL AND o.id = p_order_id)
OR (p_order_no IS NOT NULL AND o.order_no = p_order_no)
LIMIT 1;
END;
$func$;
-- 默认收紧:撤销 PUBLIC按需授予 anon/authenticated/service_role 执行权限。
REVOKE ALL ON FUNCTION public.notify_get_order_recipients(UUID, TEXT) FROM PUBLIC;
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN
GRANT EXECUTE ON FUNCTION public.notify_get_order_recipients(UUID, TEXT) TO anon;
END IF;
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN
GRANT EXECUTE ON FUNCTION public.notify_get_order_recipients(UUID, TEXT) TO authenticated;
END IF;
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN
GRANT EXECUTE ON FUNCTION public.notify_get_order_recipients(UUID, TEXT) TO service_role;
END IF;
END $$;
COMMIT;

View File

@@ -0,0 +1,298 @@
-- 物流轨迹(第三方/Mock数据库设计PostgreSQL / Supabase 兼容)
-- 重要说明(生产环境请勿直接执行本文件):
-- - 本文件包含平台侧表 + mock_* 测试表(用于联调、故障注入与回放)。
-- - 生产环境请执行迁移脚本mall_sql/migrations/express_tracking_platform_upgrade.sql
-- 该脚本仅创建平台侧 platform_express_* 三张表,并与主库 UUID/updated_at 规范对齐。
-- 目标:
-- 1) 平台侧:统一接收不同第三方事件 -> 幂等去重/乱序入库 -> 给前端提供稳定时间线
-- 2) Mock 承运方侧:可持久化运单/事件/场景/推送日志,支持故障注入与回放
-- 说明本文件仅提供表结构与索引建议RLS/权限策略按项目实际补充。
BEGIN;
-- Supabase / Postgres 常用扩展
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- 通用 updated_at 维护函数(如项目已有同名函数,可忽略重复定义)
CREATE OR REPLACE FUNCTION public.set_updated_at()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$;
-- =====================================================================
-- A. 平台侧platform统一轨迹模型入库与查询
-- =====================================================================
CREATE TABLE IF NOT EXISTS platform_express_waybills (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
-- 可选:若平台订单已存在,可关联
order_id uuid NULL,
order_no varchar(64) NULL,
carrier varchar(32) NOT NULL, -- YUNDA/YTO/ZTO/STO/KDN...
tracking_no varchar(64) NOT NULL,
source varchar(16) NOT NULL DEFAULT 'mock', -- mock/carrier/aggregator
current_status_code varchar(32) NOT NULL DEFAULT 'SHIPPED',
current_status_text text NULL,
eta timestamptz NULL,
last_synced_at timestamptz NULL,
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT uk_platform_waybill UNIQUE (carrier, tracking_no)
);
CREATE TRIGGER trg_platform_waybills_updated_at
BEFORE UPDATE ON platform_express_waybills
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
CREATE INDEX IF NOT EXISTS idx_platform_waybills_order_no ON platform_express_waybills(order_no);
CREATE INDEX IF NOT EXISTS idx_platform_waybills_tracking_no ON platform_express_waybills(tracking_no);
CREATE INDEX IF NOT EXISTS idx_platform_waybills_status ON platform_express_waybills(current_status_code);
-- 统一事件表:前端时间线/告警/统计的主数据来源
CREATE TABLE IF NOT EXISTS platform_express_tracking_events (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
waybill_id uuid NOT NULL REFERENCES platform_express_waybills(id) ON DELETE CASCADE,
carrier varchar(32) NOT NULL,
tracking_no varchar(64) NOT NULL,
-- 第三方事件唯一标识(可能缺失)
event_id varchar(128) NULL,
event_time timestamptz NOT NULL,
event_code varchar(64) NOT NULL,
event_text text NOT NULL,
status_code varchar(32) NOT NULL, -- 平台统一状态
node_name varchar(128) NULL,
location text NULL,
description text NULL,
evidence_urls jsonb NOT NULL DEFAULT '[]'::jsonb,
-- 原始回文(用于审计/排障;如担心体积可移到 raw 表或只存引用)
raw_payload jsonb NULL,
-- 接收侧信息
received_at timestamptz NOT NULL DEFAULT NOW(),
source varchar(16) NOT NULL DEFAULT 'webhook', -- webhook/poll/manual
-- 幂等去重键:优先 event_id缺失时用 tracking_no+event_code+event_time(+可选字段) 构造
dedupe_key varchar(256) NOT NULL,
created_at timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT uk_platform_event_dedupe UNIQUE (waybill_id, dedupe_key)
);
CREATE INDEX IF NOT EXISTS idx_platform_events_waybill_time ON platform_express_tracking_events(waybill_id, event_time);
CREATE INDEX IF NOT EXISTS idx_platform_events_tracking_time ON platform_express_tracking_events(tracking_no, event_time);
CREATE INDEX IF NOT EXISTS idx_platform_events_status ON platform_express_tracking_events(status_code);
CREATE INDEX IF NOT EXISTS idx_platform_events_received_at ON platform_express_tracking_events(received_at);
CREATE INDEX IF NOT EXISTS gin_platform_events_raw_payload ON platform_express_tracking_events USING gin (raw_payload);
-- 原始接收表:记录每一次 webhook/轮询原文、验签结果与解析错误
CREATE TABLE IF NOT EXISTS platform_express_event_raw (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
received_at timestamptz NOT NULL DEFAULT NOW(),
source varchar(16) NOT NULL DEFAULT 'webhook',
-- 请求侧标识
client_id varchar(64) NULL,
carrier varchar(32) NULL,
tracking_no varchar(64) NULL,
-- 安全审计
signature_valid boolean NULL,
signature text NULL,
ts_header text NULL,
request_id varchar(64) NULL,
remote_ip inet NULL,
headers jsonb NULL,
body jsonb NULL,
parse_error text NULL,
-- 去重辅助(可选):用于识别完全重复的原始请求
dedupe_key varchar(256) NULL
);
CREATE INDEX IF NOT EXISTS idx_platform_raw_received_at ON platform_express_event_raw(received_at);
CREATE INDEX IF NOT EXISTS idx_platform_raw_tracking_no ON platform_express_event_raw(tracking_no);
CREATE INDEX IF NOT EXISTS idx_platform_raw_signature_valid ON platform_express_event_raw(signature_valid);
CREATE INDEX IF NOT EXISTS gin_platform_raw_body ON platform_express_event_raw USING gin (body);
-- =====================================================================
-- B. Mock 承运方侧mock运单/事件/场景/推送日志
-- =====================================================================
CREATE TABLE IF NOT EXISTS mock_carrier_config (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name varchar(64) NOT NULL DEFAULT 'default',
target_webhook_url text NULL,
client_id varchar(64) NULL,
secret text NULL,
default_carrier varchar(32) NOT NULL DEFAULT 'YUNDA',
is_active boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT uk_mock_config_name UNIQUE (name)
);
CREATE TRIGGER trg_mock_carrier_config_updated_at
BEFORE UPDATE ON mock_carrier_config
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
CREATE TABLE IF NOT EXISTS mock_waybills (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
carrier varchar(32) NOT NULL,
tracking_no varchar(64) NOT NULL,
order_no varchar(64) NULL,
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT uk_mock_waybill UNIQUE (carrier, tracking_no)
);
CREATE TRIGGER trg_mock_waybills_updated_at
BEFORE UPDATE ON mock_waybills
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
CREATE TABLE IF NOT EXISTS mock_tracking_events (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
waybill_id uuid NOT NULL REFERENCES mock_waybills(id) ON DELETE CASCADE,
carrier varchar(32) NOT NULL,
tracking_no varchar(64) NOT NULL,
event_id varchar(128) NULL,
event_time timestamptz NOT NULL,
event_code varchar(64) NOT NULL,
event_text text NOT NULL,
node_name varchar(128) NULL,
location text NULL,
description text NULL,
evidence_urls jsonb NOT NULL DEFAULT '[]'::jsonb,
raw_payload jsonb NULL,
-- 生成幂等键,便于 Mock 内部也能去重
dedupe_key varchar(256) NOT NULL,
created_at timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT uk_mock_event_dedupe UNIQUE (waybill_id, dedupe_key)
);
CREATE INDEX IF NOT EXISTS idx_mock_events_waybill_time ON mock_tracking_events(waybill_id, event_time);
-- 场景定义:标准签收/拒收退回/地址异常等
CREATE TABLE IF NOT EXISTS mock_scenarios (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
scenario_code varchar(64) NOT NULL,
scenario_name varchar(128) NOT NULL,
description text NULL,
is_active boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT uk_mock_scenario_code UNIQUE (scenario_code)
);
CREATE TRIGGER trg_mock_scenarios_updated_at
BEFORE UPDATE ON mock_scenarios
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
-- 场景步骤:相对时间偏移 + 事件内容
CREATE TABLE IF NOT EXISTS mock_scenario_steps (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
scenario_id uuid NOT NULL REFERENCES mock_scenarios(id) ON DELETE CASCADE,
step_no int NOT NULL,
offset_seconds int NOT NULL DEFAULT 0,
event_code varchar(64) NOT NULL,
event_text text NOT NULL,
status_code varchar(32) NULL,
node_name varchar(128) NULL,
location text NULL,
description text NULL,
evidence_urls jsonb NOT NULL DEFAULT '[]'::jsonb,
created_at timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT uk_mock_scenario_step UNIQUE (scenario_id, step_no)
);
-- 场景运行记录:便于复现/回放
CREATE TABLE IF NOT EXISTS mock_scenario_runs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
waybill_id uuid NOT NULL REFERENCES mock_waybills(id) ON DELETE CASCADE,
scenario_id uuid NOT NULL REFERENCES mock_scenarios(id),
inject jsonb NOT NULL DEFAULT '{}'::jsonb, -- delay_ms/duplicate/out_of_order/bad_signature/timestamp_skew_seconds/drop_fields...
pushed boolean NOT NULL DEFAULT false,
started_at timestamptz NOT NULL DEFAULT NOW(),
finished_at timestamptz NULL
);
CREATE INDEX IF NOT EXISTS idx_mock_runs_waybill ON mock_scenario_runs(waybill_id, started_at);
-- 推送日志:记录每次 push 的请求/响应摘要
CREATE TABLE IF NOT EXISTS mock_push_logs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
waybill_id uuid NOT NULL REFERENCES mock_waybills(id) ON DELETE CASCADE,
event_id varchar(128) NULL,
dedupe_key varchar(256) NULL,
target_url text NULL,
request_headers jsonb NULL,
request_body jsonb NULL,
response_status int NULL,
response_body text NULL,
error_message text NULL,
created_at timestamptz NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_mock_push_waybill ON mock_push_logs(waybill_id, created_at);
COMMIT;

View File

@@ -0,0 +1,539 @@
-- =====================================================================================
-- 测试环境种子数据(仅写入 platform_express_* 三表)
--
-- 目的:在测试/预发环境使用“生产同款表结构”联调页面与接口,避免创建 mock_* 表。
-- 适用:已执行 express_tracking_platform_upgrade.sql 后。
--
-- 注意:
-- - 本文件会插入示例数据:大多数 tracking_no 以 TEST_ 前缀开头;另有 1 条 ORDER_PLACED 示例运单号为空。
-- - 如需清理,可执行文末的清理 SQL。
-- =====================================================================================
BEGIN;
-- 兼容旧版本种子数据:曾使用 TEST_YT_20260206_0010 作为“已下单”示例
-- 现要求:已下单运单号为空;因此先清理旧 tracking_no 相关记录,避免重复数据。
-- DELETE FROM public.platform_express_tracking_events WHERE tracking_no = 'TEST_YT_20260206_0010';
-- DELETE FROM public.platform_express_event_raw WHERE tracking_no = 'TEST_YT_20260206_0010';
-- DELETE FROM public.platform_express_waybills WHERE carrier = 'YTO' AND tracking_no = 'TEST_YT_20260206_0010' AND order_no = 'ORD_TEST_20260206010';
-- 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()),
(NULL, 'ORD_TEST_20260206004', 'STO', 'TEST_STO_20260206_0004', 'mock', 'EXCEPTION', '异常', NULL, NOW()),
(NULL, 'ORD_TEST_20260206005', 'SF', 'TEST_SF_20260206_0005', 'mock', 'IN_TRANSIT', '运输中', 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_20260206010', 'YTO', '', 'mock', 'ORDER_PLACED', '已下单', NULL, NOW()),
(NULL, 'ORD_TEST_20260206011', 'ZTO', 'TEST_ZT_20260206_0011', 'mock', 'READY_FOR_PICKUP', '待取件', 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',
'TEST_STO_20260206_0004',
'TEST_SF_20260206_0005',
'TEST_YD_20260206_0006',
'TEST_YT_20260206_0007',
'TEST_ZT_20260206_0008',
'',
'TEST_ZT_20260206_0011'
)
)
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,
'IN_TRANSIT' 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',
'包裹已揽收',
'IN_TRANSIT',
'北京朝阳网点',
'北京市 朝阳区',
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'
-- 运单 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',
'包裹已揽收',
'IN_TRANSIT',
'深圳南山网点',
'深圳市 南山区',
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',
'包裹已揽收',
'IN_TRANSIT',
'南京江宁集散中心',
'南京市 江宁区',
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',
'运输中(测试数据)',
'IN_TRANSIT',
'南京江宁网点',
'南京市 江宁区',
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',
'包裹已揽收',
'IN_TRANSIT',
'武汉江夏网点',
'武汉市 江夏区',
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 = ''),
'YTO',
'',
'test_e_10001',
NOW() - INTERVAL '1 days',
'ORDER_PLACED',
'订单已下单(测试数据)',
'ORDER_PLACED',
NULL,
NULL,
NULL,
'[]'::jsonb,
NULL::jsonb,
NOW(),
'manual',
'test_e_10001'
-- 运单 10待取件到驿站/自提柜)
UNION ALL
SELECT
(SELECT id FROM w WHERE tracking_no = 'TEST_ZT_20260206_0011'),
'ZTO',
'TEST_ZT_20260206_0011',
'test_e_11001',
NOW() - INTERVAL '3 hours',
'READY_FOR_PICKUP',
'快件已到达【杭州余杭菜鸟驿站】,请凭取件码取件(测试数据)',
'READY_FOR_PICKUP',
'杭州余杭菜鸟驿站',
'杭州市 余杭区',
NULL,
'[]'::jsonb,
NULL::jsonb,
NOW(),
'webhook',
'test_e_11001'
) 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'
);
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'
);
-- ORDER_PLACED 示例:允许 tracking_no 为空,但 raw body 中保留 order_no 便于排障/审计筛选
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',
NULL,
TRUE,
'test-signature',
EXTRACT(EPOCH FROM NOW())::text,
'req_test_0003',
'127.0.0.1',
'{"content-type":"application/json"}'::jsonb,
'{"order_no":"ORD_TEST_20260206010","tracking_no":"","status_code":"ORDER_PLACED","event_text":"已下单(测试数据)"}'::jsonb,
NULL,
'raw_test_0003'
);
COMMIT;
-- =====================================================================================
-- 清理(需要时手工执行)
-- =====================================================================================
-- BEGIN;
-- DELETE FROM public.platform_express_event_raw WHERE tracking_no LIKE 'TEST_%' OR (body->>'order_no') = 'ORD_TEST_20260206010';
-- DELETE FROM public.platform_express_tracking_events WHERE tracking_no LIKE 'TEST_%' OR status_code = 'ORDER_PLACED';
-- DELETE FROM public.platform_express_waybills WHERE tracking_no LIKE 'TEST_%' OR order_no = 'ORD_TEST_20260206010';
-- COMMIT;

View File

@@ -0,0 +1,199 @@
-- =====================================================================================
-- 模拟第三方“推送到平台”的数据(直接写入数据库)
--
-- 适用场景:
-- - 你暂时没有后端 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_status_code_normalized TEXT;
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
-- 兼容旧值:不再使用 ARRIVED_HUB统一归类为 IN_TRANSIT运输中
v_status_code_normalized := CASE
WHEN v_status_code = 'ARRIVED_HUB' THEN 'IN_TRANSIT'
WHEN v_status_code = 'PENDING' THEN 'ORDER_PLACED'
ELSE v_status_code
END;
-- 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_normalized,
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_normalized,
'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_normalized,
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

@@ -0,0 +1,124 @@
# uni-push2 安卓联调与取 CID 说明
## 1. 目的与范围
本文档用于记录本项目在 **uni-app xUVUE + UTS** 下,对 DCloud **统一推送 uni-push2** 的安卓端最小化接入与验证流程,目标是完成闭环:
1) 真机运行应用
2) 获取 CIDclientId
3) 在 uni-push2 控制台使用“指定 CID”推送
4) 设备收到推送(通知栏/透传回调)
> 说明:本文聚焦“能跑通验证链路”的最小方案;厂商通道(小米/华为等)的离线推送配置属于后续增强。
## 2. 关键结论(先看这个)
- **调试基座 ≠ 你的应用**:在 HBuilderX 使用“运行到 Android 设备(标准基座)”拿到的 CID归属是“基座应用”因此在你的 uni-push2 应用里按 CID 推送会提示“CID 不存在或不属于此应用”。
- 要让 CID 归属正确,必须使用 **云打包/本地打包** 生成的 **你自己的 APK**,安装到手机后再获取 CID。
- 不依赖控制台日志获取 CID为了避免无法抓取 logcat 的情况,已实现 **把 CID 写入本地存储**,并在 minimal 页面 **直接展示 + 点击复制**
## 3. 环境说明
- 框架uni-app xUVUE + UTS
- 平台Android 真机(红米/小米系统 MIUI/HyperOS 等)
- 推送DCloud uni-push2
## 4. 客户端实现(最小可用)
### 4.1 App 启动初始化与监听
在应用启动时:
- 调用 `uni.onPushMessage` 监听推送回调(用于验证是否收到推送/透传)
- 调用 `uni.getPushClientId` 获取 CID
- 将 CID 输出日志,同时写入本地存储 `uni_push2_cid`
涉及文件:
- `App.uvue`
注意事项UTS 语法/类型限制):
- UTS 对 `Any`/`Any?` 较严格,不能随意使用 TS 写法(如 `(x as any)`
-`ret.cid` 这类对 `Any` 的直接属性访问可能编译失败,需要用 `UTSJSONObject` 或字符串兜底
### 4.2 minimal 页面展示 CID 并一键复制
为了不依赖 HBuilderX 控制台/adb
- 从本地存储读取 `uni_push2_cid`
- 页面展示 CID
- 点击 CID 触发 `uni.setClipboardData` 复制
涉及文件:
- `pages/minimal.uvue`
## 5. 打包与安装(验证闭环必需)
### 5.1 云打包
- 在 HBuilderX 执行 Android 云端打包
- 打包成功后会得到下载链接
下载次数限制(常见问题):
- 云端下载链接可能存在短时间“下载次数超限”
- 规避策略:
- 尽量 **只下载一次并备份**(网盘/U盘/微信文件传输助手)
- 必要时更换网络/IP例如手机热点
- 仍受限时,重新云打包生成新下载链接
### 5.2 手机安装与启动
- 建议卸载旧的调试基座或旧包,避免混淆
- 通过系统安装 APK
- 桌面无图标时MIUI/HyperOS 常见):
- 在系统“应用列表/管理应用”中搜索应用名进入详情页打开
- 或在应用抽屉里搜索应用名,长按拖到桌面
## 6. 获取 CID 的两种方式
### 6.1 推荐:从 minimal 页面直接复制(无需日志)
步骤:
1) 安装云打包 APK
2) 打开应用进入 Minimal Test Page
3) 等待 1030 秒(必要时重启一次 App
4) 页面显示 CID点击即可复制
### 6.2 备选adb logcat 抓日志(需要电脑装 adb
- 电脑安装 Android SDK Platform-Tools
- 开启手机 USB 调试并授权
- 使用 `adb logcat` 过滤关键词(如:`uni-push2``clientId``cid`
> 说明:不建议通过 HBuilderX “运行到 Android 设备标准基座”来取日志因为会回到基座环境CID 归属不正确。
## 7. 控制台推送验证步骤
1) 确认使用的 uni-push2 控制台账号/应用,与打包 APK 对应的 AppID 一致
2) 在控制台选择“消息推送/测试推送 → 指定 CID”
3) 粘贴从 minimal 页面复制的 CID
4) 发送通知消息
如果提示“CID 不存在或不属于此应用”,按以下顺序排查:
- 是否仍在使用调试基座的 CID需要用云打包 APK 重新取 CID
- 是否账号/应用选错AppID 不一致)
- 是否刚安装/首次启动CID 同步可能有延迟,等 13 分钟再试)
## 8. 厂商通道(小米等)说明(可选)
- 厂商通道用于离线推送/更稳定通知
- 若云打包勾选/启用小米推送模块但未配置参数,可能触发校验提示
- 最小验证路径:先确保能完成“在线推送/控制台指定 CID”闭环再按需要补齐厂商通道
## 9. 本次相关改动点(便于回溯)
- `App.uvue`
- 初始化 `uni.onPushMessage`
- 获取 CID 并写入 `uni.setStorageSync('uni_push2_cid', cid)`
- `pages/minimal.uvue`
- 读取 `uni.getStorageSync('uni_push2_cid')`
- 展示 CID点击复制
- 修复 UTS 的 `Any?` 类型推断问题(避免强制标注 `any`
---
如需把“推送触达率/离线推送(小米/华为/OPPO/vivo/通知样式与点击跳转”纳入需求范围,请在本文基础上补充对应厂商后台开通、证书参数与多通道策略。

View File

@@ -0,0 +1,75 @@
# 前端字段清单(订单详情页物流时间线|安卓 & Web
说明:本清单用于平台前端展示“物流关键节点时间线”。数据来源为平台后端消费 Mock Server或未来真实承运方的事件后形成的统一模型。
本文定位:只定义“平台后端 -> 客户端”的展示字段契约与规则Mock Server 的接口与字段以 `接口规范.md` 为准。
关联文档:
- `配送模块需求文档.md`:目标/范围/验收与故障注入
- `接口规范.md`:统一事件模型、平台接收 Webhook、Mock 控制面 API
- `状态映射表.md`event_code -> status_code 映射建议
适用端:
- 安卓端App重点是用户体验弱网/图片预览/快速刷新)。
- Web 端H5/PC重点是排障效率筛选/搜索/原始事件折叠展示)。
1. 订单详情delivery block
- `order_no`:订单号
- `tracking_no` / `waybill_no`:运单号(可能为空)
- `carrier`承运商标识YUNDA/YTO/KDN 等)
- `status`平台统一状态码ORDER_PLACED/SHIPPED/IN_TRANSIT/OUT_FOR_DELIVERY/READY_FOR_PICKUP/DELIVERED/EXCEPTION/RETURNED
- `status_history`:事件数组,元素结构 `{event_id, event_time, event_code, event_text, status_code, node_name, location, description, evidence_urls, source}`
- `eta`预计到达时间ISO8601若承运方或 ETA 服务提供)
- `pod`:签收凭证数组,元素 `{type: photo|signature, url, timestamp, remark}`
- `receiver_name`:收件人姓名(仅客服可查看完整)
- `receiver_masked_phone`:脱敏手机号(前端展示)
- `receiver_address`:收货地址(可按需隐藏部分字段)
- `last_synced_at`:最后同步时间(平台本地)
- `source`来源mock / carrier / aggregator用于 UI 标注与排障
2. 管理/调试视图字段(可选)
- `mock_enabled`:是否开启 mock 数据
- `mock_carrier`:当前模拟承运方
- `webhook_last_result`:最近一次 webhook 推送结果摘要(仅调试页面)
3. 行为与按钮映射(平台侧)
- 刷新物流 -> GET /api/v1/delivery/express/track平台查询自身已入库事件或平台发起轮询
- 上报异常 -> POST /api/v1/delivery/express/report-exception
(联调阶段可选)平台侧调试按钮(调用 Mock Server 控制面 API
- 运行标准场景 -> POST /mock/v1/waybills/{tracking_no}/run-scenario
- 触发推送 -> POST /mock/v1/waybills/{tracking_no}/push
4. 展示规则 & 隐私
- 所有时间使用 ISO8601含时区
- 手机号必须脱敏(如 138****8000完整手机号仅在客服/运维界面显示并记录访问审计。
- 证据 URL 应带鉴权或短期有效,前端需支持预览与下载。
4.1 同源数据,不同端展示颗粒度
- 同一运单的轨迹事件(`event_code/event_text/status_code/node_name/location`)必须同源,保证事实与状态口径一致;不同端仅做“展示层过滤/脱敏”,不做口径改写。
- 商家端B 端):只需展示大致进度(到达某地/中转/派送/签收/异常)与最新节点摘要;不展示配送员/快递员手机号等个人敏感信息。
- 消费者端C 端):可展示更详细的节点文案;若提供“联系配送”能力,建议用按钮/虚拟号/中转号承载,而不是把第三方 `event_text` 中的手机号原文直接展示或通过消息推送透传。
4.2 `event_text` 使用建议(避免直接透传敏感信息)
- `event_text` 属于第三方原文,可能包含手机号、网点内部信息等;建议前端展示使用“清洗后的文案”。
- 最小实现:
- 商家端优先展示 `status_code + node_name/location` 组合出的摘要;必要时展示清洗后的 `event_text`
- 消费者端可展示清洗后的 `event_text`;仅在 `OUT_FOR_DELIVERY` 等末端节点按需提供脱敏联系方式。
- 口径要求:除末端状态(派送中/待取件/已签收/异常/退回等)外,中转/到达/发往下一站等统一展示为“运输中”,不展示“到达××网点/分拨中心”等中间节点状态文案。
5. 安卓端适配要点
- 缓存:将最近一次 `status_history` 缓存在本地(仅该用户的订单范围),离线可展示并提示“数据可能不是最新”。
- 图片POD/证据图片支持大图预览与失败重试;弱网下优先加载文本节点。
- 刷新:进入页面自动拉取;下拉刷新;显示 `last_synced_at`
6. Web 端适配要点
- 调试信息:`raw_payload` 默认折叠,仅客服/运维可展开查看。
- 筛选搜索:按 `status_code`、时间范围、承运方 `carrier` 过滤;支持按 `tracking_no` 搜索。
- 安全:避免在前端日志中打印 `raw_payload` 与敏感字段。
7. 兼容性
- 前端组件可接收 `raw_payload`(仅供客服/运维查看)并折叠显示。
- 时间线组件需支持展开单条事件详情、证据大图预览与来源标注(承运方/聚合方)。
---
(以上为前端字段与契约建议,实际字段名可与后端协商最终确认)

View File

@@ -0,0 +1,194 @@
# 后台页面设计说明(配送模块|平台对接第三方轨迹)
本文定位:
- 定义“后台应该有什么样的页面”与每个页面的最小展示/操作/权限要求。
- 本文服务于平台系统的一部分:订单详情页物流区块 + 客服/履约/对接运维排障。
口径说明:本目录文档作为实现与验收唯一口径;如页面形态与本文不一致,以本文更新为准。
关联文档:
- `配送模块需求文档.md`:范围/目标/验收与总体定位
- `接口规范.md`Webhook/控制面接口与入库建议
- `前端字段清单.md`:订单详情页物流区块字段契约(安卓/Web
- `状态映射表.md`event_code/event_text -> status_code 映射与一致性治理
- `缺口与待补充清单.md`:需补齐的验收/场景/权限矩阵等
---
## 1. 角色与权限(页面设计前提)
角色建议:
- 普通用户(消费者端):仅查看自己订单物流(不在本后台范围内)
- 商家(商户后台):发货与查看订单物流(不看 raw_payload
- 客服:查看订单物流、辅助处理异常(有限查看 raw_payload需审计
- 履约/运营:看指标、看异常趋势、做配置(按权限)
- 对接运维:看 webhook 日志、验签结果、配置密钥与回调、排障(可看 raw_payload需审计
强约束:
- `raw_payload`、完整手机号/地址、签收凭证POD属于敏感信息默认折叠、按角色授权、访问需审计。
### 1.1 页面拆分原则 & 权限控制原则(避免跑偏)
结论:页面是否拆分不只由“权限”决定;更主要由“工作流/信息密度/风险”决定,权限用于控制同一页面内的可见字段与可操作按钮。
页面拆分建议(什么时候需要单独页面/专区):
- 工作流不同:商家做发货/运单绑定;客服做工单/异常处理;运维做对接配置/排障。
- 信息密度不同:面向商家的页面应短而清晰;面向运维的页面需要更细的日志与诊断信息。
- 风险不同:密钥/证书、回调配置、`raw_payload` 查看等高敏能力建议放在“对接运维专区”或独立页面,降低误操作与暴露面。
权限控制建议(同一页面内怎么按角色展示):
- 字段级:例如 `raw_payload`、完整手机号/地址、POD 仅授权角色可见;默认折叠。
- 操作级:例如“触发回查/补采”“修改接入配置/密钥”“启用 Mock 开关”等仅运维可操作。
- 审计级:对敏感字段访问与关键配置变更必须记录审计。
推荐做法:
- 信息架构先按人群拆两条导航线(商家后台 vs 平台后台)。
- 在每个页面内部再做字段/按钮的权限控制(同页不同角色看到的内容不同)。
---
## 2. 信息架构IA与导航建议
建议将后台拆成两条导航线:
A. 商家后台(商户侧)
- 订单
- 订单列表
- 订单详情(含物流区块 + 发货/运单绑定)
- 设置(可选)
- 默认承运方/发货偏好
B. 平台后台(客服/运维侧)
- 订单
- 订单查询
- 订单详情(含物流区块)
- 物流对接
- 运单/轨迹查询(排障)
- Webhook 接收日志
- 接入配置中心(承运方/聚合配置)
- 监控与告警
- 状态映射治理(可选增强)
---
## 3. 商家后台页面(最小集)
### 3.1 订单列表页(商家)
目的:商家快速定位需要发货/查看物流的订单。
最小展示:
- `order_no`、下单时间、收件人(脱敏)、订单状态、发货状态
- 物流摘要(若已发货):`carrier``tracking_no`、当前 `status_code`
最小操作:
- 查看订单详情
- 发货(跳转/弹窗进入“承运方选择 + 运单号绑定”)
### 3.2 订单详情页(商家,含物流区块)
目的:完成发货动作,并查看物流时间线。
物流区块展示(以 `前端字段清单.md` 为准):
- `carrier``tracking_no``status``status_history``last_synced_at`
操作:
- 发货:选择承运方 + 录入/回填 `tracking_no`
- 刷新物流:调用平台统一查询(不直连第三方)
禁止/限制:
- 不展示 `raw_payload`
- 收件信息按平台规则脱敏
---
## 4. 平台后台页面(客服/履约/对接运维)
### 4.1 订单查询页(平台)
目的:客服/履约按订单维度查找并进入详情处理。
筛选:
- `order_no``tracking_no``carrier``status_code`、时间范围
展示:
- 订单摘要 + 物流摘要(当前状态、最后同步时间)
### 4.2 订单详情页(平台,含物流区块加强版)
目的:在订单上下文内查看完整物流与异常。
展示:
- 物流区块:同商家侧字段 + 事件来源标注mock/承运方/聚合)
- 异常标记:长时间无更新、签收失败、退回等(按规则)
- POD可选签收凭证预览按权限
操作:
- 刷新/回查(可选增强):触发平台轮询补偿或第三方回查
- 备注/工单关联(可选):记录客服处理信息
### 4.3 运单/轨迹查询页(平台排障核心)
目的:脱离订单也能按运单快速排障。
筛选:
- `tracking_no``carrier``status_code`、时间范围、来源mock/承运方/聚合)
展示:
- 事件时间线(入库后统一模型)
- 幂等/乱序结果提示(例如:去重命中次数、最新事件时间)
操作:
- 复制对接诊断信息(不含敏感字段)
### 4.4 Webhook 接收日志页(平台对接运维)
目的:定位“回调未到/验签失败/重复推送/入库失败”。
筛选:
- 时间范围、`carrier``tracking_no`、验签结果、HTTP 状态、处理结果
展示:
- 请求摘要:`X-Client-Id``X-Timestamp`、签名校验结果
- 处理摘要:解析成功/失败原因、幂等键、入库结果
- 原始回文:`raw_payload` 折叠展示(仅授权角色可见,且记录访问审计)
### 4.5 接入配置中心(平台对接运维)
目的:管理承运方/聚合接入与回调配置。
配置项:
- 承运方列表与状态(启用/停用)
- 验签密钥/证书、公网 IP 白名单(如需要)
- Webhook 目标地址与环境(测试/预发/生产)
- 轮询补偿策略:频率、阈值(多久无更新触发)、熔断
- 测试环境 Mock 开关(默认关闭,需显式开启)
### 4.6 监控与告警页(平台履约/运维)
目的:按承运方/接口维度观察质量。
指标建议:
- webhook 接收成功率、验签失败率、平均同步延迟
- 超过阈值未更新的运单数
- 第三方接口错误率/超时率(轮询补偿场景)
### 4.7 状态映射治理页(可选增强)
目的:让 `event_code -> status_code` 可治理、可追溯。
展示:
- 当前映射表、变更记录、(可选)映射版本
---
## 5. MVP最小可上线范围
商家后台:
- 订单列表 + 订单详情(含发货与物流区块)
平台后台:
- 运单/轨迹查询
- Webhook 接收日志(含验签/去重/入库结果)
- 接入配置中心(至少支持配置承运方/密钥/回调目标)
- 监控与告警(至少有基础指标与列表)
---
## 6. 验收要点(页面层)
- 订单详情页物流区块字段与展示规则符合 `前端字段清单.md`
- 平台后台能从运单号定位到:事件时间线 + webhook 接收记录 + 验签结果 + 入库结果
- `raw_payload` 默认不可见/折叠,且按权限展示并有审计
- Mock 开关默认关闭,且仅在测试环境允许启用

View File

@@ -0,0 +1,516 @@
# 配送对接接口规范(模拟三通一达后台 / Mock 承运方 Server
本文档用于定义“模拟承运方后台服务Mock Server”的接口与行为供平台在未接入真实承运方前完成联调与测试。
本文定位:
- 规定 Mock Server 与平台后端之间的交互Webhook 推送 + 控制面 API
- 规定平台侧建议的统一事件模型(用于入库、时间线展示、监控告警)。
关联文档:
- `配送模块需求文档.md`:目标/范围/验收与必须的故障注入
- `状态映射表.md`状态码映射建议Mock event_code -> 平台 status_code
- `前端字段清单.md`:安卓/Web 时间线展示字段契约
包含两类接口:
1) Mock Server -> 平台Webhook 事件推送(模拟承运方回调)
2) 平台/测试工具 -> Mock Server控制面 API创建运单、追加事件、运行场景、触发推送、查询轨迹
## 统一约定
- 时间格式ISO 8601示例"2026-02-05T10:30:00+08:00"
- 所有接口请求/响应均为 JSONContent-Type: application/json
- 写操作建议携带 `request_id` 用于幂等与排查
- 所有第三方回调应保存 `raw_payload` 用于审计
补充说明(安卓端 & Web 端使用场景):
- 本文主要规范 Mock Server 与平台后端的交互;安卓/Web 客户端不应直接调用 Mock Server。
- Web 端联调时如需浏览器直连平台接口,应由平台网关配置 CORS仅测试环境白名单避免将 Mock Server 暴露给公网。
## 一、目标与约束
- 目标:用可控数据模拟关键配送节点与异常场景,覆盖验签、幂等、乱序/重复、延迟等测试。
- 约束:本规范服务于测试/预发布环境,不用于生产承运方直连。
## 二、核心数据模型(建议统一存储字段)
- `carrier`承运方标识例如YUNDA、YTO、KDN
- `tracking_no`:运单号
- `event_id`第三方事件唯一ID用于去重
- `event_time`事件时间ISO8601
- `event_code`:第三方原始事件码(尽量不改写,用于审计与一致性对齐)
- `event_text`:第三方原始事件文案(前端时间线默认展示)
- `status_code`平台统一状态ORDER_PLACED, SHIPPED, IN_TRANSIT, OUT_FOR_DELIVERY, READY_FOR_PICKUP, DELIVERED, EXCEPTION, RETURNED
- `node_name`:节点名称(中转站/网点/城市)
- `location`:节点位置描述(城市/网点地址,注意隐私)
- `description`:事件详细描述
- `evidence_urls`:证据照片/签名链接数组
- `estimated_arrival`ETA若承运方提供可选
- `raw_payload`:原始第三方 JSON审计
- `last_synced_at`:本地同步时间
## 三、接入模式
- 平台接收 Mock Server 的 Webhook 推送(推荐)。
- 平台亦可主动调用 Mock Server 查询轨迹(模拟轮询补偿)。
范围说明(避免误解):
- 若要实现“电子面单/在线下单自动获取运单号”,需要对接第三方(快递公司直连或聚合平台)提供的下单/面单接口。
- 该类“下单/面单”接口不属于本 Mock Server 接口规范范围;本文只覆盖轨迹事件的推送/查询与相关验签、幂等与字段口径。
## 四、接口定义(示例)
1) Mock Server -> 平台事件推送Webhook
- URL: POST /webhook/express/status
- 描述: 承运方/聚合方主动推送运单事件(单条或批量事件)。平台应校验签名并返回 HTTP 200 表示成功接收。
- HTTP 头部(建议):
- `Content-Type: application/json`
- `X-Client-Id`: 承运方或聚合方ID
- `X-Timestamp`: 推送时间戳(防重放)
- `X-Signature`: HMAC-SHA256(body + X-Timestamp) 使用双方共享 secret
- 回调示例(单事件):
{
"tracking_no":"YD123456789",
"carrier":"YUNDA",
"event_id":"e_20260205_0001",
"event_code":"TRANSIT",
"event_text":"运输中",
"event_time":"2026-02-05T14:32:00+08:00",
"node_name":"北京分拨中心",
"location":"北京市朝阳区XXX",
"evidence_urls":[],
"raw_payload":{/* 原始承运方 JSON */}
}
处理要求:
- 平台校验 `X-Signature``X-Timestamp`;若验签失败或时间差过大返回 4xx。若成功返回 HTTP 200。接收后异步完成映射与入库。
- 幂等:基于 `event_id``tracking_no+event_code+event_time` 去重。
2) 平台 -> Mock Server轨迹主动拉取模拟轮询
- URL: GET /mock/v1/track?tracking_no={tracking_no}&carrier={carrier}
- 描述: 平台主动查询单运单轨迹,返回事件数组。
- 响应示例:
{
"tracking_no":"YD123456789",
"carrier":"YUNDA",
"events":[
{"event_id":"e1","event_code":"PICKED","event_text":"已揽收","event_time":"2026-02-04T18:00:00+08:00","node_name":"门店揽收网点","location":"北京市顺义"},
{"event_id":"e2","event_code":"ARRIVAL","event_text":"运输中","event_time":"2026-02-05T14:32:00+08:00","node_name":"北京分拨中心","location":"北京市朝阳区"}
]
}
3) 平台:异常上报 API用户/客服触发,平台侧,不属于 Mock Server
- URL: POST /api/v1/delivery/express/report-exception
- 说明: 该接口为平台自身能力Mock Server 不强制实现。
- 请求示例:
{
"tracking_no":"YD123456789",
"reported_by":"user",
"report_type":"damaged",
"description":"包裹外包装破损,有压痕",
"evidence_urls":["https://.../img1.jpg"]
}
- 平台可选地回传给真实承运方(若未来接入)并记录回传结果。
4) 平台/测试工具 -> Mock Server控制面 API建议实现
4.1 配置回调目标
- URL: POST /mock/v1/config
- 描述: 设置 Mock Server 推送到平台的 Webhook 目标地址与验签参数。
- 请求示例:
{
"target_webhook_url":"https://api.yourplatform.com/webhook/express/status",
"client_id":"carrier_mock_yunda",
"secret":"shared_secret",
"default_carrier":"YUNDA"
}
4.2 创建运单
- URL: POST /mock/v1/waybills
- 描述: 创建一个 mock 运单,可由调用方指定或由服务生成。
- 请求示例:
{
"carrier":"YUNDA",
"tracking_no":"YD123456789",
"order_no":"202602050001"
}
- 响应示例:
{
"carrier":"YUNDA",
"tracking_no":"YD123456789",
"created_at":"2026-02-05T14:00:00+08:00"
}
4.3 追加事件(不推送,仅入库)
- URL: POST /mock/v1/waybills/{tracking_no}/events
- 请求示例:
{
"event_id":"e_20260205_0002",
"event_code":"TRANSIT",
"event_text":"运输中",
"event_time":"2026-02-05T14:32:00+08:00",
"node_name":"北京分拨中心",
"location":"北京市朝阳区",
"evidence_urls":[]
}
4.4 运行预置场景(生成一组事件并可选立即推送)
- URL: POST /mock/v1/waybills/{tracking_no}/run-scenario
- 请求示例:
{
"scenario":"standard_delivered",
"push":true,
"inject":{
"delay_ms":300,
"duplicate":0,
"out_of_order":false,
"bad_signature":false,
"timestamp_skew_seconds":0
}
}
4.5 触发推送(把该运单事件推到平台)
- URL: POST /mock/v1/waybills/{tracking_no}/push
- 请求示例:
{
"mode":"all",
"inject":{
"delay_ms":0,
"duplicate":1,
"out_of_order":true
}
}
4.6 查询运单轨迹Mock Server 自身查询接口)
- URL: GET /mock/v1/waybills/{tracking_no}/track
- 响应示例:
{
"carrier":"YUNDA",
"tracking_no":"YD123456789",
"events":[
{"event_id":"e1","event_code":"PICKED","event_text":"已揽收","event_time":"2026-02-04T18:00:00+08:00"}
]
}
4.7 健康检查
- URL: GET /mock/v1/health
- 响应示例:
{"status":"ok"}
## 五、字段映射与状态对照(示例)
- 平台统一状态 `OUT_FOR_DELIVERY` 映射关系示例:
- 韵达:承运方 `派送中` -> 平台 `OUT_FOR_DELIVERY`
- 聚合:原始 `deliver` -> 平台 `OUT_FOR_DELIVERY`
状态一致性策略(尽量保持平台与第三方一致):
- 一致性定义:平台保存的 `event_code/event_text``raw_payload` 应与第三方可查询到的轨迹语义一致;平台对外展示的 `status_code` 必须由映射表确定性生成(同一承运方同一 `event_code` 在同一版本映射下得到同一 `status_code`)。
- 字段保真:`event_code/event_text` 仅做透传与脱敏展示,不建议“为了统一文案”而改写原文;统一展示口径使用 `status_code` 的标签/颜色/筛选实现。
- 映射表治理:映射表变更需要版本化(例如 `mapping_version`)并走发布流程;避免在生产环境频繁调整导致历史轨迹“同码不同态”。如必须调整,建议补充回放/回填策略以保证历史一致。
- 回查纠偏(可选增强):平台定期或按需对“关键单/争议单”触发第三方轨迹回查;若发现缺失事件则补采入库;若发现语义冲突,优先保留原始事件并追加“平台侧纠偏事件/备注”,避免静默覆盖造成审计对不上。
## 六、验签与安全
- 建议使用 HMAC-SHA256 签名,签名字段为 `body + X-Timestamp`,服务端使用共享 `secret` 校验。
- 防重放:校验 `X-Timestamp` 与当前时间差(例如 < 5 分钟),并保存最近 N 个 `X-Signature``event_id` 用于去重。
## 七、幂等与重试策略
- webhook承运方可能重试多次平台必须基于 `event_id` 或 (`tracking_no`+`event_code`+`event_time`) 去重。
- 主动调用承运方接口失败时采用指数退避重试;关键操作(打单)建议入队异步重试并人工告警。
入库层(幂等去重、乱序处理)建议:
- 幂等目标:同一条事件无论推送/重试多少次,最终数据库只保留一条(或同一条被安全更新),避免重复入库造成时间线膨胀。
- 幂等键优先级:
- 优先使用第三方提供的 `event_id`
- 若缺少 `event_id`,使用兜底组合键:`tracking_no + event_code + event_time`(必要时可补充 `node_name/location` 以降低碰撞)。
- 数据库约束:建议在事件表上建立唯一约束(例如 `carrier + tracking_no + event_id``tracking_no + dedupe_key`),写入采用 Upsert/Insert-Ignore应用层无需“先查再写”来保证并发安全。
- 乱序处理:允许事件乱序写入(以免晚到的历史节点被丢弃);查询展示时按 `event_time` 排序生成时间线。
- 状态不回退(平台侧可选规则):当先收到终态(如 `DELIVERED`)后又收到更早时间的在途事件时,不应将订单状态从终态回退;可采用“以最新 `event_time` 事件计算 current_status”或“按状态流转等级仅允许前进”的策略。
## 八、展示与 UI 要求(与前端对接点)
- 时间线按 `event_time` 展示,标注 `carrier` 来源与 `last_synced_at`
- 节点可展开查看 `description``node_name``location``evidence_urls`(签收照片/回单)。若无证据则隐藏预览。
- 提供“查看第三方原文”链接,展示 `raw_payload`(仅客服/运维可见)。
平台对客户端返回建议(安卓/Web
- 客户端请求应面向平台统一接口(例如:`GET /api/v1/delivery/express/track?order_no=...`),由平台返回统一的 `status_history`
- 响应中建议包含:`carrier``tracking_no``status``status_history``last_synced_at`,以及可选的 `eta`
- `raw_payload` 不建议下发给普通用户端;仅在 Web 客服/运维界面按权限下发并记录审计。
## 九、监控与告警
- 指标webhook 接收成功率、平均同步时延、同步失败率、超过阈值未更新的运单数。
- 告警:承运方 5xx 增加、webhook 验签失败率异常、单运单长时间无更新。
## 十、示例 cURLWebhook 验证场景模拟)
```
curl -X POST https://api.yourplatform.com/webhook/express/status \
-H "Content-Type: application/json" \
-H "X-Client-Id: carrier_yunda" \
-H "X-Timestamp: 2026-02-05T14:32:00+08:00" \
-H "X-Signature: <hmac-signature>" \
-d '{"tracking_no":"YD123456789","event_id":"e_20260205_0001","event_code":"TRANSIT","event_time":"2026-02-05T14:32:00+08:00","event_text":"运输中"}'
```
## 十一、日志与保留策略
- 建议保留 `raw_payload` 与入库审计日志至少 30 天以便纠纷处理审计记录包括接收时间、IP、头部信息与处理结果。
## 十二、兼容与扩展建议
- 建议实现承运商 Adapter 层:承运方差异在 Adapter 层转换为统一模型,便于后续扩展新承运方或替换聚合方。
- 若需签收照片或更高粒度信息,应优先与承运方签订企业级合同或直连,并在 UI 端明确注明凭证来源。
### 十二点一、平台后端适配架构(推荐实现方式)
目标:当不同第三方(直连承运方/聚合平台API 结构、鉴权方式、事件码不同的时候,平台侧**不修改核心业务与数据库结构**即可接入。
核心原则:
- **统一领域模型入库**:第三方差异在入库前完成映射;数据库保存统一事件字段 + `raw_payload`
- **Adapter 可插拔**:每家第三方实现一个 Adapter验签/解析/查询/映射),核心服务只处理统一模型。
- **接入入口两条线**Webhook推送与 Poller轮询补偿最终都走同一条 `EventIngestService`
模块划分(建议):
- `WebhookController`:接收回调、路由到对应 Adapter、快速返回 200业务异步处理
- `CarrierAdapter`(每家一个):验签、解析、字段映射、状态映射、(可选)轨迹查询。
- `AdapterRegistry/Router`:按 `carrier``X-Client-Id` 选择 Adapter支持灰度与多配置
- `EventIngestService`:幂等去重、乱序处理策略、统一入库、写审计日志。
- `TrackQueryService`:给安卓/Web 提供统一查询接口(只读),不透出第三方差异。
- `PollerJob`:定时对“长时间无更新”的运单做轮询补偿(可按承运方频率配置)。
幂等与乱序建议(平台侧):
- 幂等键优先级:`event_id`(优先) > `tracking_no + event_code + event_time`(兜底)。
- 入库:允许乱序写入;查询展示时按 `event_time` 排序。
- 兼容缺字段:缺少 `event_id` 时必须使用兜底组合键;缺少 `location/node_name` 时 UI 降级仅展示 `event_text`
### 架构图Mermaid
```mermaid
flowchart LR
subgraph Clients[客户端]
A[安卓 App] -->|GET 物流时间线| PAPI[平台 API]
W[Web/H5/PC] -->|GET 物流时间线| PAPI
end
subgraph Platform[平台后端]
PAPI --> TQS[TrackQueryService\n统一查询]
TQS --> DB[(Logistics DB\nWaybill + TrackingEvent)]
WH[WebhookController\n接收回调] --> REG[AdapterRegistry/Router\n按carrier或X-Client-Id路由]
REG --> AD1[YUNDA Adapter]
REG --> AD2[YTO Adapter]
REG --> AD3[Aggregator Adapter\n(快递鸟/快递100)]
AD1 --> ING[EventIngestService\n幂等/映射/入库]
AD2 --> ING
AD3 --> ING
ING --> DB
PJ[PollerJob\n轮询补偿] --> REG
PJ -->|queryTrack| AD1
PJ -->|queryTrack| AD2
PJ -->|queryTrack| AD3
end
subgraph ThirdParty[第三方/Mock]
MS[Mock Server 或真实承运方/聚合] -->|Webhook 推送| WH
end
```
### 回调时序图Webhook 推送 -> 入库 -> 客户端展示)
```mermaid
sequenceDiagram
participant TP as Mock/第三方
participant WH as WebhookController
participant REG as AdapterRegistry
participant AD as CarrierAdapter
participant ING as EventIngestService
participant DB as LogisticsDB
participant API as 平台查询API
participant APP as 安卓/Web
TP->>WH: POST /webhook/express/status (headers + body)
WH->>REG: resolveAdapter(carrier/X-Client-Id)
REG->>AD: getAdapter()
WH->>AD: verify + parseWebhook(rawBody)
AD-->>WH: NormalizedEvent[]
WH-->>TP: 200 OK (快速返回)
WH->>ING: ingest(NormalizedEvent[])
ING->>DB: upsert events (幂等去重)
APP->>API: GET /api/v1/delivery/express/track?order_no=...
API->>DB: query events (order_no/tracking_no)
DB-->>API: events ordered by event_time
API-->>APP: timeLine(status_history + last_synced_at)
```
备注Mermaid 图在不支持的 Markdown 渲染器中会降级为代码块,不影响内容阅读。
## 十三、附录:不同第三方 API 形态示例(用于理解差异)
说明:以下为“常见形态示例”,用于帮助团队理解不同第三方接口在鉴权、字段与能力上的差异。
- 不保证与任一承运方/聚合平台的官方文档 100% 一致;对接时必须以官方文档、沙箱与实际回文为准。
- 推荐做法:在平台侧实现 Adapter/Mapper把第三方差异映射为本文定义的统一事件模型再入库。
### 13.1 聚合平台常见形态如快递100/快递鸟类)
典型能力:
- 轨迹查询 API传入 `company_code + tracking_no`(有些场景需要收件人手机号后四位)
- 订阅/推送:先订阅,后续以 Webhook 回调推送事件
- 鉴权:常见 `appKey + sign`MD5/HMAC或 Token
示例:轨迹查询(示例)
```http
POST https://api.aggregator.example.com/v1/track/query
Content-Type: application/json
{
"company_code": "yunda",
"tracking_no": "430123456789",
"phone_last4": "8000",
"nonce": "n_123",
"timestamp": 1738735200,
"sign": "md5_or_hmac_signature"
}
```
示例响应(示例)
```json
{
"success": true,
"tracking_no": "430123456789",
"company_code": "yunda",
"state": "in_transit",
"events": [
{ "time": "2026-02-05T10:12:00+08:00", "context": "已揽收", "location": "深圳市" },
{ "time": "2026-02-05T22:01:00+08:00", "context": "到达广州分拨中心", "location": "广州市" }
]
}
```
差异点:
- `state` / `status` 枚举不统一;事件字段可能是 `context/desc``time/timestamp`
- 不同快递公司可能要求手机号参与查询或订阅验证。
### 13.2 承运方直连常见形态(企业接口)
典型能力:
- 轨迹查询、签收回单POD查询可能拆成多个接口
- 鉴权更严格HMAC、证书、IP 白名单或 OAuth2
示例:轨迹查询(示例)
```http
POST https://open.carrier.example.com/v2/route/query
Content-Type: application/json
X-Client-Id: your_client_id
X-Timestamp: 2026-02-05T10:30:00+08:00
X-Signature: hmac_sha256(body + timestamp)
{
"tracking_no": "SF1234567890",
"include_pod": true
}
```
示例响应(示例)
```json
{
"tracking_no": "SF1234567890",
"routes": [
{ "code": "PICKED", "desc": "已揽收", "time": "2026-02-05T09:10:00+08:00", "station": "深圳XX营业点" },
{ "code": "TRANSIT", "desc": "运输中", "time": "2026-02-05T21:45:00+08:00", "station": "广州分拨中心" }
],
"pod": { "signed": false, "photo_url": null }
}
```
差异点:
- 字段更细网点编码、操作员等但不统一POD 能力通常受权限/合同影响。
### 13.3 Webhook 推送常见形态(订阅后回调)
典型能力:
- 订阅后由第三方主动推送轨迹事件到平台
- 具备重试机制,因此平台必须做幂等去重、乱序处理
示例:回调事件(示例,字段名因第三方而异)
```json
{
"tracking_no": "430123456789",
"carrier": "YUNDA",
"event_id": "evt_0001",
"event_time": "2026-02-05T22:01:00+08:00",
"event_text": "到达广州分拨中心",
"location": "广州市",
"extra": { "pod_photo": null }
}
```
接入建议:
- 无论第三方字段如何变化,平台入库前统一映射到本文“核心数据模型”,并保留 `raw_payload`
- 平台用 `event_id`(优先)或组合键(`tracking_no+event_code+event_time`)实现幂等。
### 13.4 圆通YTO物流轨迹查询接口官方文档摘要
文档入口:
- https://open.yto.net.cn/interfaceDocument/menu251/submenu258
定位:该文档描述“根据圆通运单号查询物流轨迹”的接口形态,典型属于“平台主动查询(轮询)”模式。
关键交互要点(按官方文档整理,具体以控制台配置与最新文档为准):
- 传输HTTPSPOST。
- 报文结构:请求体包含 `timestamp``param``format``sign`
- `param`:以 JSON/XML 字符串承载业务参数;轨迹查询场景下包含圆通运单号字段(示例为 `NUMBER`,一次查询一个单号)。
签名规则(按文档描述抽象):
- 参与签名的明文:`param + method + v`(其中 `method``v` 来自控制台为该接口生成的配置)。
- 将上述明文与客户密钥(`secret`)拼接后做 MD5再对 MD5 的字节结果进行 Base64 编码,得到 `sign`
- 伪公式:`sign = base64(md5((param + method + v) + secret))`
响应字段(按文档列举的 JSON 返回字段抽象):
- 运单号:`waybill_No`
- 走件时间:`upload_Time`yyyy-MM-dd HH:mm:ss
- 物流状态码:`infoContent`(示例枚举包括 GOT/ARRIVAL/DEPARTURE/SENT_SCAN/SIGNED 等)
- 物流信息文案:`processInfo`
- 城市/区县:`city``district`(可选)
- 重量:`weight`(可选)
平台侧映射建议(把第三方差异收敛到统一模型):
- 时间:`upload_Time` -> 平台 `event_time`
- 文案:`processInfo` -> 平台 `event_text`(可另存 `description`
- 状态:`infoContent` -> 平台 `event_code`(保留原码),再映射到平台统一 `status_code`(见 `状态映射表.md`
`infoContent` 到平台统一 `status_code` 的建议映射(示例):
- GOT已揽收-> IN_TRANSIT
- ARRIVAL已收入/到达)-> IN_TRANSIT中转/到达节点统一视为运输中)
- DEPARTURE已发出/离开节点)-> IN_TRANSIT
- SENT_SCAN派件-> OUT_FOR_DELIVERY
- INBOUND自提柜入柜-> IN_TRANSIT或按业务定义为 OUT_FOR_DELIVERY
- SIGNED签收成功-> DELIVERED
- FAILED签收失败-> EXCEPTION
- TMS_RETURN退回-> RETURNED
落库建议:
- 将圆通原始返回(整个数组或单条对象)保存到 `raw_payload`,便于客服/运维对照圆通官网。
- 幂等去重:若第三方无稳定 `event_id`,可用组合键 `tracking_no + infoContent + upload_Time` 生成 `dedupe_key`
### 13.5 韵达YUNDA开放平台官方文档入口与调研清单
文档入口:
- https://open.yundaex.com/api/apiDoc
说明:韵达开放平台文档页面存在较多交互式内容(需登录/控制台配置后才能看到每个接口的 `method/v/测试地址` 等关键信息)。因此本节先固化“对接时必须确认的要点清单”,避免对接过程中遗漏。
从官方文档导航可见的能力分类(用于判断覆盖范围):
- API 鉴权说明
- 电子面单、散件下单
- 物流轨迹
- 售后服务、国际件、基础服务等
韵达轨迹对接需要在控制台确认/落盘的信息(建议形成《承运方接入配置表》):
- 鉴权方式签名算法HMAC/MD5 等)、参与签名字段、编码/排序规则、是否包含时间戳与 nonce。
- 接口要素:轨迹查询接口 URL、`method``v`(如平台/第三方采用“method+version+param”体系
- 订阅/回调能力:是否支持轨迹订阅与回调推送、回调重试策略、回调验签字段。
- 返回字段:事件时间字段、事件码/状态字段、事件文案字段、地点字段(城市/网点)与可选 POD 能力。
平台侧落地方式保持不变:
- 无论韵达返回结构如何,统一通过 Adapter 映射为平台 `TrackingEvent` 领域模型入库,并保留 `raw_payload`
Mock 控制面通用错误码(示例)
- 40001: invalid_parameter
- 40002: missing_required_field
- 40901: duplicate_request
- 50001: internal_error
可选增强(非本期必需):
- 生成 OpenAPI 文档(控制面 API + Webhook 示例)
- 补齐“字段必填矩阵/容错矩阵”(配合 `drop_fields` 故障注入)

View File

@@ -0,0 +1,111 @@
# 推送与设备需求文档
本文档定义平台端基于 `push_devices``express_notifications` 的推送能力需求,覆盖设备 CID 的采集/管理、物流事件驱动的消息生成与送达、隐私与验收标准。
关联文档:
- 配送模块需求文档.md
- 接口规范.md
- 前端字段清单.md
- 状态映射表.md
- mall_sql/migrations/20260224_add_push_devices_and_notifications.sql
## 一、目标
- 保存每个用户/商家设备的推送 CID支持多设备、多平台、多 appid
- 在轨迹事件触发时,生成消息中心记录并向对应活跃设备下发推送(通知/透传)。
- 提供消息幂等、未读/已读管理与审计能力,且严格控制敏感字段透传。
## 二、范围
- 包含:设备注册/解绑、设备活跃检测、消息表写入、消息去重、对外推送触发接口与消息中心 API。
- 不包含:具体第三方推送厂商通道的离线能力(小米/华为等厂商通道配置为后续任务)。
## 三、术语
- CIDPush 平台分配的 client id。
- 设备:一台移动设备或浏览器实例,用 `cid + appid` 唯一标识。
- 消息Notification由轨迹事件驱动的逻辑消息写入 `express_notifications` 并可同步到消息中心与 Push 通道。
## 四、数据表与字段(概要)
说明:详细 DDL 请参考迁移脚本:`mall_sql/migrations/20260224_add_push_devices_and_notifications.sql`
- `push_devices`
- `id, user_id, merchant_id, cid, platform, appid, is_active, last_seen_at, registration_source, created_at, updated_at`
- 约束:同一 `appid``cid` 唯一;按 `user_id/merchant_id` 建索引。
- `express_notifications`
- `id, aud, recipient_id, order_id, waybill_id, tracking_no, carrier, message_id, event_text_safe, status_code, event_time, payload, read_at, dedupe_key, created_at, updated_at`
- 幂等:`message_id` 唯一(若存在)或 `dedupe_key` 在生成逻辑层保证不重复写入。
## 五、REST API 设计(建议)
API 端点为示例,具体实现放在平台后端网关下。
1) 设备管理
- `POST /api/v1/push/register` — 注册/更新设备
请求体:`{user_id?, merchant_id?, cid, platform, appid, registration_source}`
返回:设备记录
- `POST /api/v1/push/unregister` — 解绑设备
请求体:`{cid, appid, user_id?}`
- `GET /api/v1/push/devices?user_id=...` — 列出某用户活跃设备
2) 消息与消息中心
- `POST /api/v1/notifications/express/create` — 由后端服务写入消息并触发推送(通常由 EventProcessor 调用)
请求体示例:
```json
{
"aud":"user",
"recipient_id":"<user_uuid>",
"order_id":"<order_uuid>",
"waybill_id":"<waybill_uuid>",
"tracking_no":"YT123...",
"carrier":"YTO",
"message_id":"msg_...",
"event_text_safe":"包裹正在派送中,派送员预计今日到达",
"status_code":"OUT_FOR_DELIVERY",
"event_time":"2026-02-24T10:00:00+08:00",
"payload": {"deeplink": {"path":"/pages/order/detail","query":{"order_no":"ORD_...","tab":"logistics"}}},
"dedupe_key":"waybill|event_id_or_composite"
}
```
- `GET /api/v1/notifications?aud=user&recipient_id=...` — 列表
- `POST /api/v1/notifications/read` — 标记已读(按 message_id 或 recipient_id+order_id 批量)
3) 运维/调试
- `GET /api/v1/push/stats` — 推送成功率/失败率/未读数(用于监控)
## 六、消息生成与推送流程
1) 当 `platform_express_tracking_events` 写入新事件且满足推送策略(见下),平台事件处理器调用 `express_notifications.create` 写入消息表(使用 `dedupe_key` 保证幂等)。
2) 写入成功后,异步任务查询 `push_devices`(按 `aud/recipient_id/appid/is_active`)获取目标 `cid` 列表并调用推送通道uni-push2 或后端统一推送服务)。
3) 推送返回结果写入日志并根据结果更新统计;若设备失效则将 `push_devices.is_active` 置 false或记录探测失败
推送策略建议MVP只在状态级变化或关键状态产生消息`SHIPPED/OUT_FOR_DELIVERY/READY_FOR_PICKUP/DELIVERED/EXCEPTION/RETURNED`。
## 七、隐私与安全
- 不通过 push 透传 `raw_payload`、完整手机号、签名、密钥等敏感信息。
- `event_text_safe` 为由平台清洗/脱敏后的文案,任何发送给客户端的文案必须经过该字段。
- 设备绑定时不得将密钥写入前端CID 由客户端 SDK 提供,后端仅关联 `user_id`/`merchant_id`。
- 审计:对 `push_devices` 的创建/更新/读取 `raw_payload` 访问均需记录审计日志(平台侧实现)。
## 八、非功能需求
- 可用性:消息写入及触发应具备重试机制,推送通道建议异步队列化处理。
- 可观测性:记录每条消息的 `created_at`, `sent_status`, `sent_at`, `response_code`(推送日志由推送通道记录)。
- 可扩展性:支持按 `appid` 区分不同应用/环境,便于多包管理。
## 九、验收标准
1) 能成功注册设备并查询到对应 `cid`(按 `user_id`)。
2) 插入一条关键轨迹事件后:在 `express_notifications` 中新增消息且按活跃设备下发推送(模拟推送通道验证)。
3) 相同事件重复到达不会产生重复消息(由 `dedupe_key` 或 `message_id` 保证)。
4) 推送 payload 不包含敏感信息(检查 `payload` 字段)。
## 十、迁移与回滚建议
- 在执行迁移脚本前,先在测试库运行并检查索引与触发器。
- 若生产已有其它命名冲突,请先手工评估 `push_devices` 与 `express_notifications` 字段与现有表的兼容性。
## 十一、参考
- `pages/mall/delivery/doc/需求文档/配送模块需求文档.md`
- `pages/mall/delivery/doc/需求文档/接口规范.md`
- `mall_sql/migrations/20260224_add_push_devices_and_notifications.sql`
---
作者:自动生成(可手动微调)
日期2026-02-24

View File

@@ -0,0 +1,276 @@
# 推送与设备需求文档(含建表附录)
本文档为 `推送与设备需求文档.md` 的拷贝,并在末尾追加 `express_tracking_platform_upgrade.sql` 的建表示例,便于 DBA / 后端直接参考与执行。
---
(以下为需求文档原文)
# 推送与设备需求文档
本文档定义平台端基于 `push_devices``express_notifications` 的推送能力需求,覆盖设备 CID 的采集/管理、物流事件驱动的消息生成与送达、隐私与验收标准。
关联文档:
- 配送模块需求文档.md
- 接口规范.md
- 前端字段清单.md
- 状态映射表.md
- mall_sql/migrations/20260224_add_push_devices_and_notifications.sql
## 一、目标
- 保存每个用户/商家设备的推送 CID支持多设备、多平台、多 appid
- 在轨迹事件触发时,生成消息中心记录并向对应活跃设备下发推送(通知/透传)。
- 提供消息幂等、未读/已读管理与审计能力,且严格控制敏感字段透传。
## 二、范围
- 包含:设备注册/解绑、设备活跃检测、消息表写入、消息去重、对外推送触发接口与消息中心 API。
- 不包含:具体第三方推送厂商通道的离线能力(小米/华为等厂商通道配置为后续任务)。
## 三、术语
- CIDPush 平台分配的 client id。
- 设备:一台移动设备或浏览器实例,用 `cid + appid` 唯一标识。
- 消息Notification由轨迹事件驱动的逻辑消息写入 `express_notifications` 并可同步到消息中心与 Push 通道。
## 四、数据表与字段(概要)
说明:详细 DDL 请参考迁移脚本:`mall_sql/migrations/20260224_add_push_devices_and_notifications.sql`
- `push_devices`
- `id, user_id, merchant_id, cid, platform, appid, is_active, last_seen_at, registration_source, created_at, updated_at`
- 约束:同一 `appid``cid` 唯一;按 `user_id/merchant_id` 建索引。
- `express_notifications`
- `id, aud, recipient_id, order_id, waybill_id, tracking_no, carrier, message_id, event_text_safe, status_code, event_time, payload, read_at, dedupe_key, created_at, updated_at`
- 幂等:`message_id` 唯一(若存在)或 `dedupe_key` 在生成逻辑层保证不重复写入。
## 五、REST API 设计(建议)
API 端点为示例,具体实现放在平台后端网关下。
1) 设备管理
- `POST /api/v1/push/register` — 注册/更新设备
请求体:`{user_id?, merchant_id?, cid, platform, appid, registration_source}`
返回:设备记录
- `POST /api/v1/push/unregister` — 解绑设备
请求体:`{cid, appid, user_id?}`
- `GET /api/v1/push/devices?user_id=...` — 列出某用户活跃设备
2) 消息与消息中心
- `POST /api/v1/notifications/express/create` — 由后端服务写入消息并触发推送(通常由 EventProcessor 调用)
请求体示例:
```json
{
"aud":"user",
"recipient_id":"<user_uuid>",
"order_id":"<order_uuid>",
"waybill_id":"<waybill_uuid>",
"tracking_no":"YT123...",
"carrier":"YTO",
"message_id":"msg_...",
"event_text_safe":"包裹正在派送中,派送员预计今日到达",
"status_code":"OUT_FOR_DELIVERY",
"event_time":"2026-02-24T10:00:00+08:00",
"payload": {"deeplink": {"path":"/pages/order/detail","query":{"order_no":"ORD_...","tab":"logistics"}}},
"dedupe_key":"waybill|event_id_or_composite"
}
```
- `GET /api/v1/notifications?aud=user&recipient_id=...` — 列表
- `POST /api/v1/notifications/read` — 标记已读(按 message_id 或 recipient_id+order_id 批量)
3) 运维/调试
- `GET /api/v1/push/stats` — 推送成功率/失败率/未读数(用于监控)
## 六、消息生成与推送流程
1) 当 `platform_express_tracking_events` 写入新事件且满足推送策略(见下),平台事件处理器调用 `express_notifications.create` 写入消息表(使用 `dedupe_key` 保证幂等)。
2) 写入成功后,异步任务查询 `push_devices`(按 `aud/recipient_id/appid/is_active`)获取目标 `cid` 列表并调用推送通道uni-push2 或后端统一推送服务)。
3) 推送返回结果写入日志并根据结果更新统计;若设备失效则将 `push_devices.is_active` 置 false或记录探测失败
推送策略建议MVP只在状态级变化或关键状态产生消息`SHIPPED/OUT_FOR_DELIVERY/READY_FOR_PICKUP/DELIVERED/EXCEPTION/RETURNED`。
## 七、隐私与安全
- 不通过 push 透传 `raw_payload`、完整手机号、签名、密钥等敏感信息。
- `event_text_safe` 为由平台清洗/脱敏后的文案,任何发送给客户端的文案必须经过该字段。
- 设备绑定时不得将密钥写入前端CID 由客户端 SDK 提供,后端仅关联 `user_id`/`merchant_id`。
- 审计:对 `push_devices` 的创建/更新/读取 `raw_payload` 访问均需记录审计日志(平台侧实现)。
## 八、非功能需求
- 可用性:消息写入及触发应具备重试机制,推送通道建议异步队列化处理。
- 可观测性:记录每条消息的 `created_at`, `sent_status`, `sent_at`, `response_code`(推送日志由推送通道记录)。
- 可扩展性:支持按 `appid` 区分不同应用/环境,便于多包管理。
## 九、验收标准
1) 能成功注册设备并查询到对应 `cid`(按 `user_id`)。
2) 插入一条关键轨迹事件后:在 `express_notifications` 中新增消息且按活跃设备下发推送(模拟推送通道验证)。
3) 相同事件重复到达不会产生重复消息(由 `dedupe_key` 或 `message_id` 保证)。
4) 推送 payload 不包含敏感信息(检查 `payload` 字段)。
## 十、迁移与回滚建议
- 在执行迁移脚本前,先在测试库运行并检查索引与触发器。
- 若生产已有其它命名冲突,请先手工评估 `push_devices` 与 `express_notifications` 字段与现有表的兼容性。
## 十一、参考
- `pages/mall/delivery/doc/需求文档/配送模块需求文档.md`
- `pages/mall/delivery/doc/需求文档/接口规范.md`
- `mall_sql/migrations/20260224_add_push_devices_and_notifications.sql`
---
(以下为与目录中其他文档的对齐要点、触发器/队列与视图示例)
## 与目录中其他文档的对齐要点
为保证与 `配送模块需求文档.md`、`接口规范.md`、`前端字段清单.md`、`状态映射表.md` 一致,本文档做如下约定并实现兼容:
- 事件模型:消息生成使用 `platform_express_tracking_events` 的归一化字段(`event_id/event_time/event_code/event_text/status_code/waybill_id`)。
- 状态口径:使用 `状态映射表.md` 中定义的 `ORDER_PLACED/SHIPPED/IN_TRANSIT/OUT_FOR_DELIVERY/READY_FOR_PICKUP/DELIVERED/EXCEPTION/RETURNED` 作为 `express_notifications.status_code` 值域。
- 字段契约:客户端仅消费 `express_notifications.event_text_safe`、`status_code`、`event_time`、`payload.deeplink`;原始 `raw_payload` 不对客户端下发(仅后台审计可见)。
- Webhook 与控制面:推送触发入口由事件处理器(或 Mock Server 控制面)调用 `POST /api/v1/notifications/express/create` 写入消息表并触发推送。
## 自动化:事件入库 -> 消息生成(触发器示例)
建议在应用层以异步任务方式实现;以下为数据库触发器/队列化的参考实现思路Postgres + pg_notify 或写入中间队列表):
1) 触发器:在 `platform_express_tracking_events` 插入后触发 `notify_new_tracking_event`,把 `waybill_id/event_time/status_code/event_id` 写入 `notify_queue`(或使用 `pg_notify` 发消息)。
2) 后台消费者订阅 `notify_queue`(或 LISTEN/NOTIFY根据 `推送策略` 和 `recipient` 映射生成 `express_notifications` 并把实际推送任务入列(调用推送服务)。
示例 SQL简化仅供参考
```sql
-- 中间队列表(示例)
CREATE TABLE IF NOT EXISTS public.notify_queue (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
waybill_id uuid NOT NULL,
event_id varchar(128) NULL,
status_code varchar(32) NULL,
event_time timestamptz NULL,
processed boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now()
);
-- 触发器函数:插入 notify_queue
CREATE OR REPLACE FUNCTION public.trg_platform_event_after_insert()
RETURNS trigger AS $$
BEGIN
INSERT INTO public.notify_queue(waybill_id,event_id,status_code,event_time)
VALUES (NEW.waybill_id, NEW.event_id, NEW.status_code, NEW.event_time);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 建触发器(若已存在请在应用层采用幂等部署)
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trigger_platform_events_after_insert') THEN
CREATE TRIGGER trigger_platform_events_after_insert
AFTER INSERT ON public.platform_express_tracking_events
FOR EACH ROW
EXECUTE FUNCTION public.trg_platform_event_after_insert();
END IF;
END $$;
```
说明:生产环境推荐把业务逻辑放在后台消费者(可重试、可观测),数据库触发器仅负责把事件推入可靠队列。
## 视图与未读计数(示例)
为方便前端读取未读数与消息列表,建议创建以下视图或物化视图:
1) 用户消息视图(按 user
```sql
CREATE OR REPLACE VIEW public.vw_express_notifications_for_user AS
SELECT n.*
FROM public.express_notifications n
WHERE n.aud = 'user';
```
2) 未读计数物化视图(按用户/商家)
```sql
CREATE MATERIALIZED VIEW IF NOT EXISTS public.mv_unread_notifications_count AS
SELECT aud, recipient_id, count(*) FILTER (WHERE read_at IS NULL) AS unread_count
FROM public.express_notifications
GROUP BY aud, recipient_id;
-- 刷新策略:可按计划任务或在写入消息时触发快速更新
```
## 示例:从事件到推送的完整查询链
- 查找某 `order_id` 的活跃设备:
```sql
SELECT p.* FROM public.push_devices p
WHERE p.user_id = (SELECT o.user_id FROM public.ml_orders o WHERE o.id = '<order_uuid>')
AND p.appid = 'default' AND p.is_active = true;
```
- 创建消息并返回 message_id 的示例(业务层用 INSERT ... RETURNING
```sql
INSERT INTO public.express_notifications (aud, recipient_id, order_id, waybill_id, tracking_no, carrier, message_id, event_text_safe, status_code, event_time, payload, dedupe_key)
VALUES ('user', '<user_uuid>', '<order_uuid>', '<waybill_uuid>', 'TEST_123', 'YTO', 'msg_sync_123', '包裹正在派送中', 'OUT_FOR_DELIVERY', now(), jsonb_build_object('deeplink', jsonb_build_object('path','/pages/order/detail','query',jsonb_build_object('order_no','ORD_...'))), 'dedupe_...' )
RETURNING id;
```
## 与现有文档的补充修改建议(供你选择采纳)
- 在 `接口规范.md` 中增加一节:`消息中心/推送契约`,描述 `express_notifications` 的字段与 `POST /api/v1/notifications/express/create` 示例。
- 在 `前端字段清单.md` 中明确 `event_text_safe` 的清洗规则与允许的占位符(例如 `{carrier}`、`{eta}`)。
- 在 `状态映射表.md` 中加入 `推送触发级别` 一栏,列出哪些 `status_code` 会触发推送(默认为关键状态)。
## 下一步
我可以:
- 将上述触发器与视图 SQL 附加到迁移目录作为可选脚本(例如:`mall_sql/migrations/20260224_notifications_trigger_and_views.sql`),并生成 psql/PowerShell 执行命令;或
- 只生成后台消费者示例Node.js + supabase 或 pg 客户端),负责从 `notify_queue` 消费并调用推送通道。
请告诉我要先做哪一项。
(以下为附录:平台侧建表示例摘录)
```sql
-- 第三方快递轨迹(平台侧)表结构升级 (PostgreSQL + Supabase)
-- 用途: 引入第三方承运方运单与轨迹事件的统一入库模型
BEGIN;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "btree_gin";
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 $func$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$func$;
END IF;
END $do$;
CREATE TABLE IF NOT EXISTS public.platform_express_waybills (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
order_id UUID NULL REFERENCES public.ml_orders(id) ON DELETE SET NULL,
order_no VARCHAR(64) NULL,
carrier VARCHAR(32) NOT NULL,
tracking_no VARCHAR(64) NOT NULL,
source VARCHAR(16) NOT NULL DEFAULT 'mock',
current_status_code VARCHAR(32) NOT NULL DEFAULT 'SHIPPED',
current_status_text TEXT NULL,
eta TIMESTAMP WITH TIME ZONE NULL,
last_synced_at TIMESTAMP WITH TIME ZONE NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT uk_platform_express_waybill UNIQUE (carrier, tracking_no)
);
-- 跳过部分
COMMIT;
```
---
作者:自动生成(可手动微调)
日期2026-02-24

View File

@@ -0,0 +1,196 @@
# 配送端(自营骑手)表 vs 第三方快递轨迹表:对比与数据库修改建议
本文目标:
- 整理“原先配送端(自营骑手/同城配送)”使用的核心数据表。
- 对比“现在第三方快递配送(韵达/圆通等)”的轨迹/运单数据表。
- 给出建议的数据库改造方案:如何在不破坏订单主表的前提下,引入第三方快递轨迹能力,并(可选)逐步弃用原先配送端表的依赖。
明确决策(当前结论):
- 仅支持第三方快递轨迹(承运方运单 + 轨迹时间线)。
- 不做自营骑手端/同城配送任务流;`ml_delivery_drivers`/`ml_delivery_tasks` 视为历史遗留表,不再作为新能力的依赖与写入目标。
> 说明:本对比只讨论“第三方快递轨迹/运单”与“自营骑手任务”两套模型如何在数据库层共存/迁移;二者是不同业务域,不建议强行复用同一张表。
---
## 1. 原先配送端(自营骑手)核心表(现状)
数据来源:主库 DDL`mall_sql/schemas/complete_mall_database.sql`)与旧配送端文档(`pages/mall/delivery/doc/old/*`)。
### 1.1 `ml_delivery_drivers`(配送员信息表)
用途:记录配送员(骑手)档案与实时工作状态;用于配送端登录后的“是否可接单/是否在线”。
关键字段(节选):
- `id`:配送员 ID
- `user_id`:关联 `ak_users.id`
- `real_name``id_card``driver_license`
- `vehicle_type``vehicle_number`
- `service_areas`JSONB
- `work_status`1 在线 / 2 忙碌 / 3 离线
- `current_lat` / `current_lng`
- `rating_avg` / `rating_count` / `order_count`
- `status`1 正常 / 2 暂停 / 3 离职
### 1.2 `ml_delivery_tasks`(配送任务表,配送端“状态真源”)
用途:描述“一个订单被分配给某个骑手并经历接单→取货→配送→送达”的任务流。
关键字段(节选):
- 关联:
- `order_id`UUID**唯一**FK 到 `ml_orders.id`
- `driver_id`FK 到 `ml_delivery_drivers.id`,可为空表示未接单)
- 地址与费用:
- `pickup_address`JSONB取货地址
- `delivery_address`JSONB配送地址
- `distance``estimated_time``delivery_fee`
- 任务状态:`status`int
- 1 待接单
- 2 已接单
- 3 取货中
- 4 配送中
- 5 已送达
- 6 配送失败
- 时间戳:`assigned_at` / `picked_at` / `delivered_at`
- 其他:`delivery_code`(取货码)、`remark``failure_reason`
> 注意:旧页面/旧文档里常出现 `accepted_at`、`pickup_time`、`delivered_time` 等命名,和主库 DDL 的 `assigned_at/picked_at/delivered_at` 存在差异;如果你要继续使用自营配送链路,建议在代码层做统一字段映射或做一次字段对齐迁移。
### 1.3 与订单表的关系(旧模型的关键耦合点)
- `ml_delivery_tasks.order_id` 强依赖 `ml_orders.id`,并且 **1 个订单只能有 1 条配送任务**`order_id UNIQUE`)。
- 旧实现里常尝试把 `ml_delivery_tasks.status` 同步到 `ml_orders.order_status`,容易造成口径不一致(旧文档也提到了这种冲突风险)。
---
## 2. 现在第三方快递配送(轨迹/运单)表(目标模型)
数据来源:`pages/mall/delivery/doc/需求文档/express_tracking_mock_platform.sql`
> 这套表的定位不是“骑手任务流”,而是“第三方承运的运单 + 轨迹事件时间线”。
### 2.1 `platform_express_waybills`(平台侧:运单主表)
用途:一条运单(一个 `tracking_no`)的聚合信息;用于订单详情页展示“承运方/运单号/当前状态/最后同步时间”。
关键字段(节选):
- 关联:`order_id`(可选)、`order_no`(可选)
- 运单:`carrier``tracking_no``source`
- 聚合状态:`current_status_code``current_status_text`
- 时间:`eta``last_synced_at`
- 约束:`UNIQUE (carrier, tracking_no)`
特点:
- 同一个订单允许对应多条运单(拆包裹/分批发货)——因为 `order_id` 不唯一。
### 2.2 `platform_express_tracking_events`(平台侧:轨迹事件表)
用途:时间线的主数据来源;用于前端展示、告警统计与排障。
关键字段(节选):
- 关联:`waybill_id`FK 到 `platform_express_waybills.id`
- 事件:`event_id`(第三方可能缺失)、`event_time``event_code``event_text`
- 平台统一状态:`status_code`
- 节点:`node_name``location``description`
- 证据:`evidence_urls`jsonb
- 原始回文:`raw_payload`jsonb可用于审计/排障)
- 幂等:`dedupe_key` + `UNIQUE (waybill_id, dedupe_key)`
### 2.3 `platform_express_event_raw`(平台侧:原始接收表)
用途:记录 webhook/轮询的原始请求、验签与解析错误,便于排障与审计。
### 2.4 `mock_*`Mock 承运方侧表)
这部分只用于联调/回归/故障注入,不建议进入生产主库的核心业务 schema可以放在独立 schema 或测试库)。
---
## 3. 两套模型的核心差异(为什么不建议“复用一张表”)
| 维度 | 自营骑手(旧配送端) | 第三方快递(新模型) |
|---|---|---|
| 业务对象 | 任务(骑手接单、取货、送达) | 运单(承运方扫描轨迹) |
| 与订单关系 | 1 订单 = 1 任务(`order_id UNIQUE` | 1 订单 = N 运单(拆包裹常见) |
| 状态来源 | 平台自己驱动状态机(按钮/操作) | 第三方事件驱动webhook/轮询) |
| 数据颗粒度 | 少量节点(接单/取货/送达) | 高频节点(到站/发车/分拣/派送/签收…) |
| 关键字段 | `driver_id`、地址 JSON、配送费、距离 | `carrier``tracking_no`、事件时间线、幂等去重 |
| 风险点 | 并发抢单、任务状态与订单状态不同步 | 乱序/重复事件、验签、防重放、脱敏 |
结论:
- 旧表(`ml_delivery_*`)适合“自营骑手/同城配送”。
- 新表(`platform_express_*`)适合“第三方快递轨迹”。
- 两者可以共存,但不要强行把第三方轨迹塞进 `ml_delivery_tasks`
---
## 4. 推荐的数据库修改方案(兼容、可渐进)
### 4.1 方案 1推荐新增第三方快递表不改旧配送表
适用:你们当前要做第三方快递展示,且已明确“不做自营骑手”。
做法:
1) 在主库新增(或通过 migration 引入)三张平台侧表:
- `platform_express_waybills`
- `platform_express_tracking_events`
- `platform_express_event_raw`
2) 商家发货时写入/绑定:创建 `platform_express_waybills` 行(填 `order_id``order_no``carrier``tracking_no`)。
3) 第三方回调/轮询入库:写 `platform_express_tracking_events`,并更新 `platform_express_waybills.current_status_*``last_synced_at`
建议的小幅增强(强烈建议做):
-`platform_express_waybills(order_id)` 增加索引(便于按订单查运单)。
-`platform_express_waybills.order_id` 建外键到 `ml_orders(id)`(如果你们能保证订单一定存在)。
### 4.2 方案 2逐步“弃用旧配送端表”的依赖当你们全面第三方快递时
适用:你们不再提供自营骑手配送,或者想把骑手端做成独立项目。
做法(建议按阶段,不要硬删表):
- 阶段 A停止业务写入 `ml_delivery_tasks`(页面/接口不再创建任务)。
- 阶段 B订单详情页的物流展示只依赖 `platform_express_*`
- 阶段 C`ml_delivery_tasks` 标为 legacy只读保留一段时间最终再评估是否归档/删除。
为什么不建议立刻删:
- 历史数据、结算对账、纠纷复盘可能仍需要旧数据。
在“不做自营骑手”的前提下,最小要求:
- 新增第三方快递表后,业务代码只写入/读取 `platform_express_*`,不要再创建/更新 `ml_delivery_tasks`
- 旧表保留只读(或仅用于历史查询/清理脚本),后续再评估归档策略。
### 4.3 如果两种配送方式要并存(同城骑手 + 快递)
建议用“履约类型”做分流,而不是混表:
- 同城/自营:继续用 `ml_delivery_tasks`(任务流)
- 快递:用 `platform_express_*`(运单轨迹)
- 订单详情页按订单的履约类型选择展示模块(或同时展示多个包裹)
---
## 5. 迁移/改库执行清单(建议写成 migration
### 5.1 建表落库
-`express_tracking_mock_platform.sql` 的 A 部分platform整理为一份 migration放到 `mall_sql/migrations/`),避免:
- 重复创建 `set_updated_at()`(项目若已有同名函数)
-`mock_*` 测试表混进生产 schema
### 5.2 对接 `ml_orders`
- 建议:
- `platform_express_waybills.order_id REFERENCES public.ml_orders(id)`
- `CREATE INDEX ... ON platform_express_waybills(order_id)`
- 不建议:
- 把运单号/承运方直接塞进 `ml_orders`(会导致一单多包裹很难处理)
### 5.3 数据回填(可选)
如果你们历史上已经保存过“承运方/运单号”在别处(例如订单扩展表/发货日志),可做一次性回填:
- 按订单生成 waybill 行
- 再按运单触发一次轨迹拉取
### 5.4 权限与隐私
- 买家/商家/客服看到的轨迹必须“同源”,但展示要按角色脱敏;敏感字段(如手机号、原始回文)按文档约束控制。
---
## 6. 你接下来要怎么改数据库(最短路线)
如果你现在的目标是:商家可选承运方 + 录入运单号 + 用户能看轨迹时间线。
最短路线:
1) 在主库落 `platform_express_waybills` / `platform_express_tracking_events` / `platform_express_event_raw`
2) 发货绑定时写 `platform_express_waybills(order_id, order_no, carrier, tracking_no)`
3) 第三方事件入库写 `platform_express_tracking_events`
4) 订单详情页按 `order_id` 查 waybills再查 events 渲染时间线
---
(如你确认要我进一步把“建议的小幅增强”直接写成 SQL migration 文件,我可以在 `mall_sql/migrations/` 里新增一份只包含 platform 表的 migration并确保不引入 `mock_*` 表。)

View File

@@ -0,0 +1,234 @@
# 物流消息推送方案(用户端 + 商家端)
日期2026-02-11
## 0. 文档定位
本方案用于把“第三方快递轨迹platform_express_*”与移动端推送uni-push2结合
- **用户端C 端)**:推送订单物流节点更新,并在消息中心可回溯、可标记已读。
- **商家端B 端)**:推送与发货/履约强相关的物流节点更新(重点关注异常/签收等),并在商家消息中心可回溯。
约束与口径继承自本目录既有文档:
- 统一事件模型、字段契约、状态映射:见接口与字段相关文档。
- 敏感信息raw_payload、完整手机号/地址、POD 等)必须按角色授权,**不得通过推送明文下发**。
## 1. 背景与目标
### 1.1 背景
配送模块的事实数据源是第三方轨迹事件流Webhook/轮询接收后入库到 `platform_express_*` 三表。
### 1.2 目标
1) **及时性**:当运单产生关键节点时,用户/商家能收到提醒。
2) **一致性**:推送只是“变更通知”,最终展示以平台查询接口返回的 `status_history` 为准。
3) **可追溯**:所有推送对应的“物流消息”可在消息中心列表查询,并可已读/未读统计。
4) **合规与隐私**:推送 payload 与通知文案不包含敏感信息,不透传 raw。
### 1.3 不在本方案范围
- 自营骑手/同城配送任务(`ml_delivery_*`)的接单/导航/送达链路。
- 厂商通道(小米/华为等)离线能力细节(可在 uni-push2 联调文档基础上扩展)。
## 2. 数据源与现有模型复用
### 2.1 事实数据表(生产口径)
- `platform_express_waybills`运单摘要current_status_*、last_synced_at
- `platform_express_tracking_events`轨迹事件事实表event_time/event_text/status_code 等)。
- `platform_express_event_raw`:原始留痕与验签审计(仅运维/客服可见)。
结论:
- **消息生成以 `platform_express_tracking_events` 的新增事件为触发**。
- **通知展示以 `platform_express_waybills.current_status_code/text` + 最新事件摘要为基础**。
### 2.2 统一状态码(必须遵循)
使用平台统一 `status_code`
`ORDER_PLACED / SHIPPED / IN_TRANSIT / OUT_FOR_DELIVERY / READY_FOR_PICKUP / DELIVERED / EXCEPTION / RETURNED`
映射规则来自“状态映射表”,平台内部应保证确定性。
## 3. 总体架构(事件驱动 + 推送唤醒刷新)
### 3.1 核心原则
- **Push ≠ 数据源**Push 仅承载“发生了什么/在哪里看”,不承载完整时间线。
- **App 打开详情页时以接口/DB查询为准**:避免推送丢失、乱序导致前端状态不一致。
### 3.2 推荐链路
1) 第三方回调/轮询到达平台Webhook handler / polling job
2) 平台:验签、防重放、幂等去重、乱序入库
3) 平台:基于“新插入事件”计算是否需要生成物流消息(对 C/B 端分别判断)
4) 平台:写入“物流消息表”(用于消息中心/未读)
5) 平台:按收件人设备 CID 列表调用 uni-push2 发送通知/透传
6) 客户端:收到推送 -> 更新消息中心未读角标/缓存 -> 用户点击进入订单/物流详情 -> 拉取最新时间线
## 4. 推送场景清单C 端 + B 端)
### 4.1 场景分级(建议最小可用)
为了避免 IN_TRANSIT 等高频节点造成骚扰MVP 建议:
- **只对“状态级别变化”或“关键状态”推送**,并允许后续配置化扩展。
### 4.2 用户端C 端)推送场景
对单个订单/运单,建议推送:
- `SHIPPED`:商家已发货/已绑定运单
- `OUT_FOR_DELIVERY`:派送中
- `READY_FOR_PICKUP`:待取件
- `DELIVERED`:已签收
- `EXCEPTION`:异常(破损/拒收/地址不详等)
- `RETURNED`:退回/退件
可选(非必须、后续再做):
- `IN_TRANSIT`:仅在“跨天/关键节点”或“长时间无更新后恢复更新”时推送。
### 4.3 商家端B 端)推送场景
商家端更关注履约风险与闭环结果,建议推送:
- `SHIPPED`:发货成功(用于确认运单绑定成功/进入履约)
- `DELIVERED`:已签收(用于完成售后风险关闭)
- `EXCEPTION`:异常(优先级最高)
- `RETURNED`:退回/退件
可选(按业务需要):
- `OUT_FOR_DELIVERY`:派送中(对高价值订单可能有意义)
### 4.4 同一事件对不同端的文案差异
- 同一事件事实字段同源event_time/event_text/status_code
- **C 端**文案可更贴近用户体验(避免网点内部信息、避免电话明文)。
- **B 端**文案以进度/风险为主(异常/退回明确提示)。
## 5. 消息生成规则(幂等 + 去噪)
### 5.1 生成触发点
在平台完成 `platform_express_tracking_events` 插入成功后触发(建议异步队列/任务)。
### 5.2 幂等与去重
事件表已有 `(waybill_id, dedupe_key)` 唯一约束;消息层建议再做一层幂等:
- 推荐幂等键:`recipient_scope + recipient_id + waybill_id + status_code + event_time`
- 若第三方提供 `event_id`,可用:`recipient_scope + recipient_id + waybill_id + event_id`
保证:同一条事件重试/重复推送不会造成消息中心重复。
### 5.3 去噪MVP
- 默认仅对 4.2/4.3 列出的状态集合生成消息。
- 同状态多事件:只推“最新 event_time”对应的一条或按节点去重
## 6. Push payload 规范(建议)
### 6.1 设计目标
- **足够定位**:能让客户端知道要刷新哪个订单/运单、点击去哪里。
- **不泄露敏感**:不含手机号/地址/raw_payload。
- **可审计**:含平台侧 message_id / request_id。
### 6.2 字段建议(透传/通知通用)
```json
{
"biz": "express",
"aud": "user",
"message_id": "msg_xxx",
"order_no": "ORD_2026...",
"carrier": "YTO",
"tracking_no": "YT123...",
"status_code": "OUT_FOR_DELIVERY",
"event_time": "2026-02-05T14:32:00+08:00",
"event_text": "快件正在派送中",
"deeplink": {
"path": "/pages/order/detail",
"query": {"order_no":"ORD_2026...", "tab":"logistics"}
}
}
```
说明:
- `event_text` 推荐使用“清洗后的文案”(避免把第三方原文中的手机号明文透传)。
- `aud`:区分用户端/商家端,有利于同一 App 多角色或同一推送通道多消费方。
### 6.3 通知栏标题/摘要建议(示例)
- 用户端:
- 标题:`物流更新` / `订单物流更新`
- 内容:`【派送中】快件正在派送中`
- 商家端:
- 标题:`订单履约提醒`
- 内容:`【异常】包裹地址不详正在退回`(异常优先级最高)
## 7. 消息中心与未读(服务端为准)
### 7.1 为什么需要消息中心
- Push 可能丢失/被系统拦截;消息中心提供可回溯记录。
- 多设备登录需要统一未读。
### 7.2 建议的数据模型(平台侧新增)
建议新增两类表(命名仅示例):
1) 设备注册表 `push_devices`
- `id`
- `user_id` / `merchant_id`(二选一或用统一 subject
- `cid`
- `platform`android/ios/web
- `appid`(用于区分环境)
- `last_seen_at``is_active`
2) 物流消息表 `express_notifications`
- `id`message_id
- `aud`user/merchant
- `recipient_id`
- `order_id/order_no``waybill_id``tracking_no``carrier`
- `status_code``event_time``event_text_safe`
- `read_at`(或拆到 read 表做多端多设备)
- `created_at`
- 幂等键unique
### 7.3 API建议
- 设备注册:`POST /api/v1/push/register`(登录后上报 CID绑定到账号
- 设备解绑:`POST /api/v1/push/unregister`
- 消息列表:`GET /api/v1/notifications?biz=express&aud=user|merchant&page=...`
- 标记已读:`POST /api/v1/notifications/read`(按 message_id 或按订单批量)
- 未读数:`GET /api/v1/notifications/unread-count?biz=express&aud=...`
## 8. 点击跳转deeplink
### 8.1 跳转原则
- 点击通知后进入“订单详情-物流区块”或“物流详情页”。
- 页面打开后必须调用平台查询接口刷新 `status_history`
### 8.2 跳转目标建议
- 用户端:订单详情页(展示物流时间线)
- 商家端:订单详情页(展示物流时间线 + 发货信息)
- 平台后台:不通过 App push 跳转(后台有自己的排障页)
## 9. 安全、权限与隐私
### 9.1 收件人权限校验
- 用户端:仅能收到/查看自己订单的物流消息。
- 商家端:仅能收到/查看自己店铺订单的物流消息。
服务端生成消息时必须校验:`order_no -> 订单归属 -> recipient`
### 9.2 敏感字段控制(强约束)
- 禁止通过 push payload/通知文案下发完整手机号、完整地址、raw_payload、签名/密钥。
- `raw_payload` 仅允许客服/运维在后台折叠查看,并记录审计。
### 9.3 CID 绑定安全
- CID 必须在**登录态**下上报并绑定到账号。
- 更换设备/卸载重装CID 可能变化,需更新绑定。
- 退出登录时建议解绑或标记设备 inactive。
## 10. 失败与补偿
- 推送失败CID 失效/不属于应用):标记设备 inactive 并等待下次登录刷新。
- Webhook 入库成功但推送失败:不影响事实数据;消息中心仍可查询到记录(若已写入消息表)。
- 客户端未收到 push用户进入订单详情时仍能通过查询接口看到最新轨迹。
## 11. 验收清单(与联调口径一致)
1) 插入一条轨迹事件(模拟 webhook
- waybill `current_status_code/text` 更新
- events 表新增且幂等
- 消息中心新增 1 条(不重复)
- 目标端(用户/商家)收到推送
2) 乱序/重复推送:
- 事件表不重复、消息中心不重复
- 当前状态不回退(按平台选定算法)
3) 隐私校验:
- push 文案与 payload 无手机号明文/无 raw
4) 点击跳转:
- 落到订单/物流详情页并刷新出最新时间线
---
## 12. 与本目录文档的对应关系(索引)
- 统一模型与 Webhook 约束:接口规范
- 状态映射:状态映射表
- 展示字段与隐私规则:前端字段清单
- 生产表结构与幂等/乱序生产表说明_platform_express
- 移动端 push 联调与 CID 归属uni-push2 安卓联调与取 CID 说明

View File

@@ -0,0 +1,71 @@
# 状态映射表(第三方/Mock -> 平台统一状态)
本表用于将第三方(或 Mock Server产生的 `event_code/event_text` 映射为平台统一 `status_code`,以保证前端时间线与告警规则可复用。
关联文档:
- `接口规范.md`:统一事件模型与接入模式
- `前端字段清单.md`时间线字段契约status_history/status_code
平台统一状态(建议,尽量少且稳定):
- ORDER_PLACED已下单平台侧订单状态通常不是第三方快递事件
- SHIPPED已发货已绑定运单/待揽收)
- IN_TRANSIT运输中含“中转中/发往下一站/分拨/到达节点/在途”等)
- OUT_FOR_DELIVERY派送中
- READY_FOR_PICKUP待取件到驿站/自提柜等,等待收件人取件)
- DELIVERED已签收含本人签收/代收点签收)
- EXCEPTION异常地址不详、拒收、破损、丢件、派送失败等
- RETURNED退回/退件
说明:
- `ORDER_PLACED` 通常来自平台订单系统(下单成功),不一定存在 `tracking_no`;在“统一时间线展示”场景下可作为平台生成事件出现在物流时间线里。
- 第三方事件原文(`event_text`)尽量保留;平台只用 `status_code` 做标签/筛选/告警。
第三方/Mock 事件码(建议) -> 平台统一状态(示例映射)
- ORDERED / CREATED -> SHIPPED若该事件表示“快递单已创建/已出库”,而非平台下单)
- PICKED / COLLECTED -> IN_TRANSIT已揽收后进入物流网络
- ARRIVED_HUB / ARRIVAL -> IN_TRANSIT中转/到达节点统一视为运输中)
- DEPARTED_HUB / TRANSIT / IN_TRANSIT -> IN_TRANSIT
- ARRIVED_DEST_CITY -> IN_TRANSIT
- OUT_FOR_DELIVERY -> OUT_FOR_DELIVERY
- AT_PICKUP_POINT / READY_FOR_PICKUP / DELIVERED_TO_PICKUP -> READY_FOR_PICKUP
- SIGNED / DELIVERED -> DELIVERED
- FAILED_DELIVERY / REJECTED / ADDRESS_INVALID / DAMAGED / LOST -> EXCEPTION
- RETURNED / RETURNING -> RETURNED
平台生成事件(用于统一时间线,可选) -> 平台统一状态
- ORDER_PLACED -> ORDER_PLACED
- MERCHANT_CONFIRMED_SHIPMENT / SHIPPED -> SHIPPED
示例(用于 UI 文案)
- IN_TRANSIT运输中
- OUT_FOR_DELIVERY快件正在派送中
- SIGNED客户已签收可带 POD 图片)
注意事项
- 平台入库请保留原始 `event_text``raw_payload`,前端默认展示 `event_text`,状态标签使用 `status_code`
- Mock Server 应支持故障注入(重复/乱序/延迟/验签失败),平台状态映射必须在幂等入库后执行。
为尽量保证事件状态保持一致(基础要求):
- `event_code/event_text` 视为第三方“事实记录”,尽量不改写;统一口径(标签、颜色、筛选)交由 `status_code` 承担。
- 映射必须确定性:同一承运方、同一 `event_code` 在同一套映射规则下应得到同一 `status_code`
备选方案:映射版本化(`mapping_version`)(可选增强)
为什么要做:
- 防止“状态漂移”:映射规则一旦调整,历史订单的已入库事件如果重新计算 `status_code`,可能出现“昨天运输中,今天变异常/又变回运输中”,引发用户与客服争议。
- 提升可审计性:当出现纠纷或对账问题时,需要能回答“当时按哪一版规则映射出来的”。
- 降低排障成本:多承运方/多渠道(直连+聚合)并存时,版本信息能快速定位是“第三方事件变了”还是“平台映射变了”。
什么时候启用:
- 状态映射会随运营/产品频繁调整,且希望历史展示稳定、可解释。
- 客服/售后对轨迹状态一致性有明确 SLA 或合规/审计要求。
怎么落地(从轻到重):
- 轻量做法(推荐起步):将映射表作为配置/代码资产管理,使用 Git tag/发布号作为 `mapping_version`,并在发布记录中保留变更原因与生效时间。
- 加强做法:在事件入库时把 `mapping_version` 一并存入事件记录(或运单聚合记录),对外查询接口可返回该版本用于排查。
- 变更策略建议:
- 小改动优先“仅影响未来事件”(不回刷历史)。
- 如必须修正历史映射,采用回放/回填任务,并对已对外展示的结果做备注或审计记录,避免静默改写。
代价与取舍:
- 会引入配置治理与发布流程的管理成本;若当前映射很稳定、承运方少、无强审计诉求,可以先不启用该方案。

View File

@@ -0,0 +1,148 @@
# 生产表说明platform_express_*(第三方快递轨迹平台侧)
本文档对应生产迁移脚本:`pages/mall/delivery/doc/需求文档/express_tracking_platform_upgrade.sql`
## 1. 设计目标与边界
- 目标:为第三方承运(韵达/圆通/中通等)提供统一的“运单 + 轨迹事件 + 原始回调留痕”入库模型,支撑用户端/商家端/平台后台同源展示。
- 边界:
- 仅覆盖第三方快递轨迹,不包含自营骑手/配送任务(`ml_delivery_*`)。
- 生产仅使用 `platform_express_*` 三表;不包含任何 `mock_*` 测试表。
## 2. 表关系概览
- `platform_express_waybills`(运单主表)
- 1 条运单对应 N 条轨迹事件(`platform_express_tracking_events`)。
- `platform_express_tracking_events`(轨迹事件事实表)
- 时间线展示、状态判断、告警统计的主来源。
- 对同一运单使用 `(waybill_id, dedupe_key)` 做幂等去重。
- `platform_express_event_raw`(原始接收留痕表)
- 记录每一次 webhook/轮询/人工补录的原始请求/响应内容与验签结果、解析错误,用于审计与排障。
## 3. 字段说明
### 3.1 `public.platform_express_waybills`(运单主表)
用途:
-`(carrier, tracking_no)` 为唯一键管理“运单实体”,可选关联订单;承载“当前状态/预计到达/最近同步时间”等运单级摘要信息。
约束/触发器/索引:
- 唯一约束:`uk_platform_express_waybill (carrier, tracking_no)`
- 外键:`order_id -> public.ml_orders(id)``ON DELETE SET NULL`
- `updated_at` 触发器:`trigger_platform_express_waybills_updated_at`UPDATE 时自动刷新)
- 常用索引:
- `idx_platform_express_waybills_order_id (order_id)`
- `idx_platform_express_waybills_order_no (order_no)`
- `idx_platform_express_waybills_tracking_no (tracking_no)`
- `idx_platform_express_waybills_status (current_status_code)`
字段表:
| 字段 | 类型 | 可空 | 默认/约束 | 含义 |
|---|---|---:|---|---|
| id | UUID | 否 | PK`uuid_generate_v4()` | 运单主键 |
| order_id | UUID | 是 | FK -> `ml_orders(id)`,删除置空 | 关联订单主键(推荐) |
| order_no | VARCHAR(64) | 是 | | 订单号(展示/兜底,用于无法拿到 `order_id` 的场景) |
| carrier | VARCHAR(32) | 否 | 参与唯一约束 | 承运方编码(如 `YUNDA`/`YTO`/`ZTO` 等;也可接入聚合方编码) |
| tracking_no | VARCHAR(64) | 否 | 参与唯一约束 | 运单号/快递单号 |
| source | VARCHAR(16) | 否 | 默认 `'mock'` | 运单数据来源:`mock`/`carrier`/`aggregator` 等 |
| current_status_code | VARCHAR(32) | 否 | 默认 `'SHIPPED'` | 运单当前平台状态(用于列表页/摘要),通常由最新事件映射得到 |
| current_status_text | TEXT | 是 | | 运单当前状态文本(可为最新事件文本的“清洗版”或平台自定义) |
| eta | TIMESTAMPTZ | 是 | | 预计送达时间(可选;来自承运方/聚合方或平台预测) |
| last_synced_at | TIMESTAMPTZ | 是 | | 最近一次与承运方/聚合方同步时间(用于健康度与补偿轮询判断) |
| created_at | TIMESTAMPTZ | 否 | 默认 `NOW()` | 创建时间 |
| updated_at | TIMESTAMPTZ | 否 | 默认 `NOW()`;触发器维护 | 更新时间UPDATE 自动刷新) |
### 3.2 `public.platform_express_tracking_events`(轨迹事件表)
用途:
- 存储每一个轨迹节点(揽收/到站/运输中/派送/签收/异常等),作为时间线展示与状态机判断的事实来源。
- 保留第三方原文(`event_code`/`event_text`/`raw_payload`),同时写入平台统一状态(`status_code`)。
- 通过 `dedupe_key` 实现幂等:重复回调/轮询不会重复插入。
约束/索引:
- 外键:`waybill_id -> platform_express_waybills(id)``ON DELETE CASCADE`
- 唯一约束:`uk_platform_express_event_dedupe (waybill_id, dedupe_key)`
- 常用索引:
- `idx_platform_express_events_waybill_time (waybill_id, event_time)`(按运单查时间线)
- `idx_platform_express_events_tracking_time (tracking_no, event_time)`(按运单号查)
- `idx_platform_express_events_status (status_code)`(按状态统计/筛选)
- `idx_platform_express_events_received_at (received_at)`(按接收时间排障)
- `gin_platform_express_events_raw_payload``raw_payload` JSONB GIN便于排障检索
字段表:
| 字段 | 类型 | 可空 | 默认/约束 | 含义 |
|---|---|---:|---|---|
| id | UUID | 否 | PK`uuid_generate_v4()` | 事件主键 |
| waybill_id | UUID | 否 | FK级联删除 | 所属运单 |
| carrier | VARCHAR(32) | 否 | | 承运方编码(冗余字段,便于查询/对账) |
| tracking_no | VARCHAR(64) | 否 | | 运单号(冗余字段,便于查询/对账) |
| event_id | VARCHAR(128) | 是 | | 第三方事件唯一 ID有些承运方会提供优先用于幂等 |
| event_time | TIMESTAMPTZ | 否 | | 事件发生时间(时间线排序的主依据) |
| event_code | VARCHAR(64) | 否 | | 第三方事件码/节点码(保留原值) |
| event_text | TEXT | 否 | | 第三方事件文本(保留原值;展示前可做脱敏/清洗) |
| status_code | VARCHAR(32) | 否 | | 平台统一状态码(由 `event_code/event_text` 规则映射得出) |
| node_name | VARCHAR(128) | 是 | | 节点/网点名称如“xx转运中心” |
| location | TEXT | 是 | | 地理位置文本(如省市区/网点地址;不建议存精确经纬度,除非明确需求) |
| description | TEXT | 是 | | 平台补充说明(如“疑似退回件,已通知商家”) |
| evidence_urls | JSONB | 否 | 默认 `[]` | 证据/附件 URL 列表(签收图、面单、异常凭证等) |
| raw_payload | JSONB | 是 | | 第三方原始事件载荷(用于审计/排障;注意权限控制) |
| received_at | TIMESTAMPTZ | 否 | 默认 `NOW()` | 平台接收时间(用于排查延迟/乱序) |
| source | VARCHAR(16) | 否 | 默认 `'webhook'` | 数据进入平台的方式:`webhook`/`poll`/`manual` |
| dedupe_key | VARCHAR(256) | 否 | 参与唯一约束 | 幂等去重键(建议:有 `event_id``event_id`;否则构造 `tracking_no|event_code|event_time` 并按需要拼接 `node/location` |
| created_at | TIMESTAMPTZ | 否 | 默认 `NOW()` | 入库时间 |
幂等与乱序建议:
- 幂等:插入事件时以 `(waybill_id, dedupe_key)` 唯一约束兜底;遇到冲突可 `ON CONFLICT DO NOTHING`
- 乱序:允许旧事件晚到入库;前端展示按 `event_time` 排序。
- 运单摘要(`platform_express_waybills.current_status_*`)建议由“最新事件(按 event_time映射结果”更新避免用 `received_at` 作为最新依据。
### 3.3 `public.platform_express_event_raw`(原始接收留痕表)
用途:
- 记录每一次 webhook 回调或轮询结果的“原始内容 + 安全审计 + 解析结果”。
- 用于:验签问题定位、第三方字段变更回溯、解析失败重放、争议审计。
索引:
- `idx_platform_express_raw_received_at (received_at)`
- `idx_platform_express_raw_tracking_no (tracking_no)`
- `idx_platform_express_raw_signature_valid (signature_valid)`
- `gin_platform_express_raw_body``body` JSONB GIN便于按字段排障检索
字段表:
| 字段 | 类型 | 可空 | 默认/约束 | 含义 |
|---|---|---:|---|---|
| id | UUID | 否 | PK`uuid_generate_v4()` | 原始接收记录主键 |
| received_at | TIMESTAMPTZ | 否 | 默认 `NOW()` | 平台收到请求/响应的时间 |
| source | VARCHAR(16) | 否 | 默认 `'webhook'` | 来源:`webhook`/`poll`/`manual` |
| client_id | VARCHAR(64) | 是 | | 调用方/渠道标识(如回调应用 ID、聚合方商户号等 |
| carrier | VARCHAR(32) | 是 | | 承运方编码(可从路由/请求体解析;可能为空) |
| tracking_no | VARCHAR(64) | 是 | | 运单号(可能为空:如批量回调或解析失败) |
| signature_valid | BOOLEAN | 是 | | 验签是否通过(解析失败或未验签可为空) |
| signature | TEXT | 是 | | 签名原文(如 header 签名) |
| ts_header | TEXT | 是 | | 时间戳 header用于防重放字段名取决于对接规范 |
| request_id | VARCHAR(64) | 是 | | 请求追踪 ID平台生成或对方传入 |
| remote_ip | INET | 是 | | 来源 IP用于安全审计/黑白名单) |
| headers | JSONB | 是 | | 请求头(建议按需过滤敏感头再落库) |
| body | JSONB | 是 | | 原始请求体/响应体(用于回溯与重放) |
| parse_error | TEXT | 是 | | 解析错误信息(成功解析则为空) |
| dedupe_key | VARCHAR(256) | 是 | | 原始请求去重辅助键(可选:如 `hash(headers+body)` |
权限与合规建议(实现侧):
- `raw_payload``platform_express_event_raw.body/headers` 可能包含敏感信息(手机号、地址、签名等),建议仅后台/运维具备读取权限,并记录审计日志。
## 4. 常见查询与写入路径(建议)
- 发货/绑定运单:
- Upsert `platform_express_waybills`(按 `(carrier, tracking_no)`);有订单关联时写入 `order_id/order_no`
- 接收回调/轮询:
- 先插入 `platform_express_event_raw` 做留痕(无论解析成功与否)。
- 解析出事件后写入 `platform_express_tracking_events`;用 `dedupe_key` 做幂等。
- 视需要更新 `platform_express_waybills.current_status_*``last_synced_at`
- 订单详情页展示:
- 通过 `order_id``platform_express_waybills`
- 再按 `waybill_id``platform_express_tracking_events`,按 `event_time` 升序组装时间线。

View File

@@ -0,0 +1,82 @@
# 缺口与待补充清单(配送模块|第三方轨迹对接 + Mock 联调)
本文定位:
- 用“交付物清单”的方式回答:当前这套配送对接文档还缺什么、为什么缺、补了带来什么收益。
- 不改变现有方案(统一事件模型 + Adapter + 幂等乱序入库 + 前端统一契约),只补齐落地时最容易卡住的材料。
关联文档:
- `README.md`:阅读入口与联调路径
- `配送模块需求文档.md`:目标/范围/验收口径
- `接口规范.md`Webhook + 控制面 API + 入库建议
- `状态映射表.md`event_code/event_text -> status_code + 一致性治理(含备选方案)
- `前端字段清单.md`:安卓/Web 展示契约
---
## P0建议本期补齐影响联调与验收能否顺利推进
1) 联调/验收 Checklist平台侧 + Mock 侧)
- 为什么要补现在“验收标准”是目标描述但缺少可勾选的操作性检查项QA/对接会反复口头对齐。
- 补齐收益:把“验签/幂等/乱序/重复/延迟/缺字段”等要求落到可执行项,减少漏测。
- 建议产物:一份表格/列表(可以后续直接贴到测试用例/测试报告里)。
2) 场景库清单scenario catalog+ 每个场景的事件序列期望
- 为什么要补:当前只提了 `standard_delivered` 等示例名,但没有定义“这个场景到底要生成哪些关键节点、每个节点对应哪些 event_code/status_code、时间间隔怎样”。
- 补齐收益:前端可稳定对照 UI后端可稳定断言幂等与排序QA 可复现回归。
- 建议产物:
- 场景列表(标准签收/拒收退回/地址异常/破损/丢失/长时间无更新/签收失败后成功等)
- 每个场景:事件数、关键事件码、是否包含 POD、是否包含异常、是否需要乱序/重复/延迟注入。
3) Webhook 接收契约的“ACK/重试”约定(平台与第三方的边界)
- 为什么要补:目前只写“成功返回 200”但缺少“平台什么时候返回 2xx、什么时候 4xx/5xx、第三方是否重试、重试间隔与上限”的共识。
- 补齐收益:避免线上/联调出现“平台处理慢导致第三方重推爆量”“平台 4xx 被第三方当作可重试”之类问题。
- 建议产物:在 `接口规范.md` 增补一小节:
- 验签失败/缺字段/无法解析 -> 4xx不建议重试
- 平台内部暂时不可用 -> 5xx允许重试
- 成功接收但异步入库失败 -> 2xx 还是 5xx 的取舍(建议:接收成功就 2xx入库失败走内部告警与补偿队列
4) 承运方接入配置表模板Carrier Integration Profile
- 为什么要补:你已经发现韵达/圆通等第三方细节差异大,缺少一个“把关键信息落盘”的标准表格,后续每接一家都要重新摸索。
- 补齐收益:缩短接新承运方周期;把差异收敛到 Adapter/配置层;减少口头信息丢失。
- 建议字段(模板):
- `carrier`、接入模式webhook/polling/both
- 鉴权类型与签名算法、参与签名字段、是否含 timestamp/nonce
- 轨迹查询接口URL/method/v/字段示例)、回调接口(回调验签字段/重试策略)
- 是否提供稳定 `event_id`、是否要求手机号后四位
- 时间格式/时区、地点粒度
- 状态码枚举来源与映射版本
---
## P1建议尽快补齐影响长期维护与一致性/审计能力)
1) 权限矩阵与审计要求(谁能看 raw_payload / POD / 全量地址手机号)
- 原因:`raw_payload` 可包含敏感字段Web 客服/运维能力需要最小权限与审计。
- 产物:角色(普通用户/商家/客服/运维/管理员)× 字段raw/POD/手机号/地址)的可见性矩阵 + 审计日志要求。
2) 错误码与返回体统一(控制面 API
- 原因:目前 `接口规范.md` 仅列举了示例错误码,但缺少标准响应结构。
- 产物:统一响应:`{success, code, message, request_id, data}`;并约定 HTTP 状态码与业务码的对应。
3) OpenAPI可选
- 原因:接口多了以后靠 Markdown 容易漂移。
- 产物:控制面 API + Webhook payload 的 OpenAPI至少给 Mock 侧)。
4) “状态不回退/一致性”策略的明确算法(平台侧)
- 原因:`接口规范.md` 提了可选规则,但未选型;当出现终态后补到更早事件时,当前状态如何计算需要一口径。
- 产物:选定一种算法并写清:以 `event_time` 最新为准 / 状态等级只进不退 / 双轨展示按时间current_status 不回退)。
---
## P2可后补锦上添花
- DB 字典与 ER 图(便于新人理解 schema
- 场景导入导出JSON格式规范
- 对接灰度/熔断/降级策略的配置说明
---
## 建议补充顺序(最省时间)
1) 先补 P0-1Checklist+ P0-2场景库清单——直接提升联调效率
2) 再补 P0-3ACK/重试约定)——避免重复推送与误判
3) 再补 P0-4承运方接入配置表模板——为后续接真实承运方铺路
4) 最后补 P1 项——为线上长期治理做准备

View File

@@ -0,0 +1,317 @@
# 配送模块需求文档(模拟三通一达后台 / Mock 承运方 Server
本文定位:定义 Mock 承运方 Server 的目标、范围、流程、故障注入与验收标准;接口与字段细节以 `接口规范.md``前端字段清单.md` 为准。
口径说明:本目录文档作为联调、验收与实现的唯一口径;如与口头沟通或临时讨论结论不一致,以本文档及其关联文档(接口/字段/映射)为准,并通过文档更新同步。
关联文档:
- `README.md`:阅读入口与联调路径
- `接口规范.md`Webhook 推送规范 + Mock 控制面 API
- `状态映射表.md`event_code/event_text -> 平台 status_code
- `前端字段清单.md`:安卓/Web 时间线字段契约
## 1. 背景
当前配送链路依赖第三方承运方(常见为“三通一达”等快递公司或聚合平台)。在真实承运方接口尚未签约/联调完成前,需要一个**可控的模拟承运方后台服务Mock Server**用于开发与测试平台侧的Webhook 接收、验签、幂等入库、时间线展示、异常处理与监控告警。
系统定位与边界:
- 本模块属于平台系统的一部分,最终呈现在“订单详情页”的物流区块,并同时服务客服/履约/对接排障。
- 平台端需要查看订单的履约详情(至少包括:`order_no``tracking_no``carrier`、物流时间线 `status_history`、异常与处理记录)。
- 但本需求不覆盖商品明细、支付、营销等订单业务域的展示与流程;这里聚焦“物流轨迹对接、入库与展示”。
多端归属与页面边界(统一口径):
- 本需求产出的“物流能力”不是一个独立的骑手/配送员端 App而是作为同一套物流模块能力分别嵌入到三个端按权限展示不同颗粒度
1) 消费者端C 端):订单详情的物流时间线/物流详情(只读为主,展示 `status_code` + 轨迹文案;可在“派送中”等末端环节提供“联系配送”能力,但涉及手机号等敏感信息需脱敏/虚拟号策略,且仅对收件人可见;不展示 `raw_payload`)。
2) 商家端B 端):发货与运单绑定(选择承运方、录入/回填 `tracking_no`+ 查看物流概览(以“到达某地/中转/派送/签收/异常”等大致进度为主;不展示配送员手机号等个人敏感信息;不包含对接配置与敏感调试信息)。
3) 管理端(平台后台:客服/履约/对接运维):订单/运单查询、Webhook 接收日志、接入配置、(可选)监控告警与审计能力。
- 三端展示字段口径以统一事件模型与前端字段契约为准(见 `接口规范.md``前端字段清单.md`),避免出现“不同端展示不一致/状态口径不一致”。
- 若未来要做“自营配送/同城骑手端”(接单、导航、到店取货、送达签收等),需单独立项与另写需求文档,不在本需求范围内。
敏感信息展示差异(必须遵循):
- 轨迹事件以同一份“事实数据源”入库与治理(`event_code/event_text/status_code` 等同源),但不同端的展示需按角色过滤与脱敏。
- 配送员/快递员手机号:
- 商家端:不展示。
- 消费者端:仅在末端环节(如 `OUT_FOR_DELIVERY`)按需提供,并必须脱敏(如 138****8000或采用虚拟号/中转号方案;避免把第三方 `event_text` 原文中的手机号明文直接透传到推送消息或页面。
## 2. 目标
- 适用阶段:主要用于真实第三方承运方未签约/未联调/不可控时的替代数据源;即便后续已接入真实承运方,也保留用于回归测试、故障注入与排障复现(生产环境默认关闭)。
- 提供一个可配置的 Mock 承运方服务,模拟多承运方(圆通/韵达/中通/申通)轨迹事件与签收凭证。
- 支持将轨迹事件按既定规范推送到平台 Webhook模拟承运方 -> 平台)。
- 支持主动查询轨迹(平台轮询场景)与“预置场景脚本”快速生成整条物流生命周期。
- 支持故障注入:延迟、重复推送、乱序、签名错误、时间戳偏移、缺字段等,用于验证平台鲁棒性。
## 2.1 配送合作模式(不同解决方案)
说明:以下方案是“商城履约/配送合作关系”的产品与工程取舍;无论选哪一种,只要前端要展示稳定一致的物流时间线,平台都建议建设统一的轨迹模型与查询接口(见 `接口规范.md` 与数据库 Schema。本 Mock 体系用于在第三方未就绪或不可控时,替代外部系统快速联调与做故障注入回归。
### 方案 A平台统一配送平台与第三方合作 / 统采统接)
适用:希望提供强一致的履约体验、统一客服口径、统一监控与统计;平台愿意承担对接与运维成本。
平台职责:
- 统一签约/选择承运方或聚合平台;提供平台侧发货能力(下单、订阅/回调、轨迹查询与回单能力按合同)。
- 平台统一事件模型入库(幂等去重、乱序处理、状态不回退),对客户端输出统一 `status_history`
商家职责:
- 在平台内选择配送方案并发货;不需要自行对接承运方轨迹接口(或仅提供必要发货信息)。
优点:体验一致,平台可观测性强;扩展新承运方主要改 Adapter不影响前端契约。
风险/成本:平台对接与运维成本高;履约兜底责任更集中在平台侧。
与 Mock 的关系:
- Mock 用于第三方未联通阶段的替代数据源与故障注入;生产环境默认关闭。
### 方案 B商家自选配送商家自选承运商并发货 / 平台负责运单绑定与轨迹展示)
适用:平台招商、品类/区域差异大、平台希望给商家“选择承运方”的灵活性;平台不承担“实际配送执行”,但需要提供统一的运单绑定入口与轨迹展示底座。
重要说明(商家只是平台商户的常见情况):
- 默认不要求商家自建系统或自行对接第三方 API。
- “商家自选”指商家在平台提供的承运方/聚合平台范围内进行选择,第三方对接、验签、幂等、入库与前端展示由平台统一承担。
平台职责(建议至少做到“统一展示底座”):
- 提供统一轨迹查询接口给前端;平台内部仍建议使用统一事件模型入库。
- 平台负责与第三方承运方/聚合平台对接:订阅/回调Webhook或轨迹查询轮询补偿、验签、防重放、幂等去重、乱序处理与统一入库。
- 平台提供“发货与运单绑定”的入口(承运方选择、运单号录入/回填/打单),把差异收敛在平台 Adapter/映射规则,避免前端直接适配多家第三方。
商家职责:
- 商家在平台的订单列表/订单详情页点击“发货/绑定运单”,选择承运方并录入/回填 `tracking_no``order_no` 由平台订单上下文自动带入,不要求商家手工输入订单号。
- `ORDER_PLACED`(已下单)阶段允许 `tracking_no` 为空(前端展示“暂无运单号”);完成发货绑定后进入 `SHIPPED`,此时必须具备 `carrier + tracking_no`
- 若商家坚持使用“商家与第三方的独立合同/账号”,可向平台提交第三方对接所需材料,由平台统一配置与代接入(不要求商家自建回调服务)。
可选增强:商家在平台内“自助对接第三方”(无需商家开发)
- 定位:可选增强能力,非一期必做。
- 目标:让商家在平台后台完成第三方账号授权/配置,从而在平台内完成下单打单、获取运单号、查询/订阅轨迹;平台仍统一入库与展示。
- 常见两种形态(可二选一,也可并存):
1) 聚合平台授权开通(推荐优先):平台统一对接一家聚合服务商;商家在平台内完成开通/授权后即可使用多家快递能力。
2) 商家自带第三方账号BYO Account商家已与某快递/聚合平台签约;在平台后台录入密钥或走 OAuth 授权,平台代为调用第三方 API。
- 商家后台最小页面/流程建议:
- 【物流渠道管理】:选择渠道类型(聚合/直连)、开启/停用
- 【授权与密钥】:录入/更新 `appKey/appSecret/token` 或 OAuth 绑定;展示“最后一次连通性检测”结果
- 【回调配置提示】:展示平台 Webhook 地址与白名单要求(若第三方需要配置回调)
- 【测试与排障】:一键“连通性测试/拉取一条轨迹/模拟下单”,失败给出可读错误
- 安全要求(必须):
- 第三方密钥加密存储、最小权限、变更审计;仅商家管理员可配置。
- 不在前端暴露密钥;平台服务端代调用第三方接口。
- 轨迹/运单数据仍写入平台统一表结构与事件模型,避免不同渠道把差异带到前端。
是否需要做(决策建议):
- 暂不需要(建议先不做)的场景:一期目标仅是“商家回填运单号 + 平台展示第三方轨迹关键节点”,且商家规模不大/对接资源有限。
- 建议需要(做了收益明显)的场景:商家量大且运单号回填错误率高、客服投诉“查不到物流/不更新”多;或明确要上“电子面单/平台一键下单”。
- 推荐分期:
- Phase 1回填运单号 + 轨迹接入与统一展示底座(本项目当前优先级)。
- Phase 2先接入 1 家聚合平台做统一下单/面单/轨迹(降低多家直连成本)。
- Phase 3再开放 BYO Account商家自带账号自助配置安全与运维成本最高
优点:相对方案 A 平台责任更轻、商家更灵活。
风险/成本:体验碎片化风险大;如果不强制回传规范,平台客服/前端会被迫处理多样差异,长期维护成本更高。
#### 商家自选配送需要提供的内容(接入清单)
基础信息(发货侧最小闭环):
- `carrier`:承运方标识(可为直连承运方或聚合平台,如 YUNDA/YTO/ZTO/STO/KDN 等)。
- `tracking_no`:运单号生成与回传方式(商家生成/第三方返回/平台生成)。
- 订单关联信息:`order_no`(或平台侧可解析的业务单号),用于把轨迹绑定到订单详情页;通常由平台在“订单发货/绑定运单”操作中自动带入,不要求商家手工录入。
运单号(`tracking_no`)获取方式(两种常见落地,二选一或并存):
1) 回填运单号(最小模式,推荐先上线):
- 商家在线下/快递官方系统/聚合平台完成下单与交接,获得运单号。
- 商家在平台后台“发货/绑定运单”时选择承运商并回填 `tracking_no`(订单号无需手填,平台自动关联当前订单)。
- 平台不需要调用第三方“下单/面单”接口只需后续接入轨迹Webhook/轮询)用于展示。
2) 电子面单 / 在线下单(增强模式,体验更好):
- 平台(或平台集成的服务商)需要对接第三方提供的“下单/面单”接口,向第三方提交发货所需信息(收件人/地址/重量/件数等),并获得:运单号 `tracking_no` + 面单文件/面单号等。
- 第三方可以是:
- 直连快递公司接口(每家快递一套协议);或
- 快递聚合平台接口(一套协议覆盖多家快递)。
- 兜底策略建议:若第三方下单失败/超时,允许商家改为“手工回填运单号”完成发货闭环。
轨迹数据接入方式(平台统一接入,推荐第一种):
1) 第三方 -> 平台 Webhook 推送(推荐):
- 平台与第三方完成订阅/回调配置;第三方直接回调平台 Webhook。
- 平台统一完成:验签、防重放、字段映射、状态映射、幂等去重、乱序入库。
2) 平台 -> 第三方 轮询拉取(可选补偿):
- 平台按承运方策略轮询“长时间无更新”的运单,补齐轨迹。
- 适用于第三方不稳定、回调丢失或仅提供查询能力的场景,但平台运维成本更高。
(高级模式,可选,适用于有自建系统的大商家)
- 商家系统 -> 平台:商家按平台统一事件模型回传轨迹事件;平台仍执行幂等/乱序入库与审计。
对接材料(按商家选择的接入方式提供):
- 若商家仅在“平台已接入承运方范围内自选”,通常无需商家提供第三方接口材料。
- 若商家使用“独立合同/账号”并要求平台代接入,则需要商家提供或协助申请:
- 鉴权信息:`appKey/appSecret``token`、证书、公网 IP 白名单等(因第三方而异)。
- 回调能力:是否支持订阅 + Webhook以及重试策略、签名算法、回调白名单
- 能力说明:是否提供 `event_id`、是否提供 POD/签收凭证、是否需要手机号后四位参与查询等。
映射配置(用于消除差异化,平台侧必须落库为配置或代码映射):
- 事件码/文案 -> 平台 `status_code` 的映射(至少覆盖:揽收/在途/派送中/签收/异常/退回)。
- 幂等去重策略:优先 `event_id`;缺失时使用组合键(见 `接口规范.md` 的入库层建议)。
#### 平台统一配送状态(`status_code`,必须遵循)
说明:平台内部与对外输出统一使用同一套 `status_code`三端C/B/管理端)按需展示,不得随意改写状态口径。
状态枚举(当前统一口径):
- `ORDER_PLACED`:已下单(平台侧订单已创建;通常不是第三方快递事件;此阶段允许 `tracking_no` 为空)
- `SHIPPED`:已发货(已绑定运单/待揽收)
- `IN_TRANSIT`:运输中(所有中转/分拨/到达节点统一归类为运输中)
- `OUT_FOR_DELIVERY`:派送中
- `READY_FOR_PICKUP`:待取件(驿站/自提柜等)
- `DELIVERED`:已签收
- `EXCEPTION`:异常
- `RETURNED`:退回/退件
展示规范:
- 为保证用户体验,应真实展示第三方回传的物流轨迹文案(如“到达北京分拨中心”、“离开上海网点”等),不做脱敏处理。
- 前端时间线展示 `event_text` 原文,状态标签对应 `status_code`
说明:本项目不维护“发货前”的配送状态;在未绑定运单阶段属于订单域状态,不纳入物流轨迹状态枚举。
补充约束(与页面展示相关):
-`tracking_no` 为空时,前端仅展示订单的配送状态为 `ORDER_PLACED` 与“暂无运单号”,不展示可查询的第三方物流时间线;待商家完成“发货/绑定运单”后,再展示轨迹时间线。
#### 差异化来源与平台收敛策略
差异化来源(商家自选必然存在):
- 鉴权差异Token/HMAC/证书/IP 白名单等。
- 字段差异:时间格式、地点粒度、是否有 `event_id`、异常字段命名等。
- 状态差异:同一状态不同叫法/码值,甚至“派送/签收/退回”的边界不同。
- 交互差异:仅查询(轮询)/订阅回调Webhook/两者并存;重试与乱序程度不同。
平台收敛策略(避免差异扩散到前端与数据库):
- 前端只消费平台统一查询接口与统一字段(见 `前端字段清单.md`),不得直接依赖第三方字段。
- 平台统一入库模型与幂等/乱序规则(见 `接口规范.md`),第三方差异仅在 Adapter/配置映射层处理。
- 分级能力与降级:
- 达标(可回传轨迹或可稳定拉取 + 通过验收)-> 展示完整时间线 + 可做告警统计。
- 不达标(仅能提供运单号或字段缺失严重)-> 降级为“仅展示运单号/跳转外部查询”,不承诺完整时间线。
验收建议(商家自配准入):
- 必测:重复推送、乱序、延迟、验签失败、缺字段(对应本 Mock 的故障注入项)。
- 通过后才允许全量展示/告警统计;未通过仅允许降级展示。
与 Mock 的关系:
- Mock 可作为“平台对接与回归”的验收工具:在第三方未就绪或不可控时,先用 Mock 场景验证幂等/乱序/缺字段容错等;接入真实第三方后也可用于故障注入回归。
### 方案 C混合模式平台默认合作 + 允许商家自配)【推荐】
适用:既要平台可控的默认体验,又要给大商家/特殊品类保留弹性。
规则建议:
- 默认:商家走平台合作承运方(覆盖大多数订单,保证体验一致)。
- 例外:允许商家自配,但必须满足平台回传规范(至少事件时间/文案/状态映射可得);否则只提供降级展示能力。
- 前端:永远只消费平台统一查询接口,不直接依赖第三方字段。
落地建议(最小闭环):
- 平台统一事件表与查询 API 先上线Mock 先跑通联调与回归。
- Phase 1接 1 家聚合或 1 家主力承运方(平台统一配送)。
- Phase 2开放商家自配按准入与验收清单接入
## 3. 范围
### 3.1 包含
- Mock Server 对外提供控制面 API创建/查询运单、追加事件、运行场景、触发推送、配置回调目标。
- Mock Server 向平台推送 Webhook按统一事件模型、支持 HMAC 验签头。
- 事件与场景数据本地持久化(最小可用可内存;建议支持文件落盘,便于复现)。
### 3.2 不包含
- 真实承运方业务能力(计费、真实网点、真实 POD 获取)
- 配送员实时定位与地图
- 平台侧业务实现(本需求仅定义 Mock Server 的接口与行为)
## 4. 目标用户
- 平台后端开发:联调 Webhook、验签、幂等与入库。
- 平台前端开发:使用固定运单/事件数据验证时间线展示。
- QA 测试:用故障注入覆盖异常链路与回归。
- 运维/对接:用于排查“回调未到/重复到/乱序到”等问题。
## 4.1 使用端(主要投放端)
- 安卓端App订单详情页展示物流时间线、签收凭证预览、异常上报。
- Web 端H5/PC 管理后台或用户 Web同样展示物流时间线提供更完整的原始事件/审计信息(按权限)。
## 4.2 平台后台展示建议(客服/履约/对接运维最小集)
说明:本节定义“平台侧管理后台”在物流模块上建议具备的最小展示与排障能力(不等同于完整订单后台)。
1) 订单详情页(物流区块)
- 基础信息:`order_no``carrier``tracking_no`、当前 `status_code``last_synced_at`
- 物流时间线:`status_history`(按 `event_time` 排序,展示 `event_text`,标签使用 `status_code`
- 异常提示:长时间无更新、签收失败、退回等(按 `status_code` 与规则触发)
2) 运单/轨迹查询页(排障入口)
- 支持按 `tracking_no/order_no/carrier/status_code` 查询
- 展示最近 N 条事件与来源mock/承运方/聚合),便于快速定位
3) 对接日志与审计(运维/对接用)
- Webhook 接收日志:验签结果、去重命中、入库结果/错误原因(按权限)
- 原始回文:`raw_payload` 折叠展示(仅客服/运维可见,记录访问审计)
4) 配置中心(对接运维用)
- 承运方/聚合平台配置、Webhook 目标与密钥/证书、轮询补偿开关与频率
- 测试环境 Mock 开关(默认关闭,需显式开启)
(可选增强)一致性回查/纠偏入口:按需触发第三方回查,补采缺失事件并保留审计记录。
## 4.3 商家后台展示建议(商户侧最小集)
说明:商家只是平台商户时,建议仅提供“发货与运单绑定 + 物流查看”的最小能力。
- 发货:选择承运方、录入/回填 `tracking_no`、发货确认
- 订单详情(物流区块):查看时间线与当前状态(不展示 `raw_payload`
端侧差异化要求:
- 安卓端需支持弱网/后台切换恢复后的快速刷新;证据图片需支持点击预览/下载。
- Web 端需支持表格/筛选/搜索便于客服排障对调试信息raw_payload默认折叠。
## 5. 核心概念
- 承运方carrierYUNDA / YTO / ZTO / STO可扩展
- 运单tracking_no可由 Mock Server 生成或由调用方指定
- 事件event包含 event_id、event_time、event_code、event_text、node_name、location、evidence_urls 等
- 场景scenario一组按时间顺序生成的事件脚本如“标准签收”“拒收退回”“地址异常”
## 6. 关键流程
1) 配置推送目标:设置平台 Webhook URL、`client_id``secret`
2) 创建运单:选择 carrier生成/指定 tracking_no。
3) 运行场景Mock Server 生成一系列关键节点事件并入库。
4) 触发推送Mock Server 按顺序(或按乱序/延迟策略)将事件推送到平台。
5) 平台侧验证:校验验签、去重、入库、前端展示与告警。
## 7. 功能需求
### 7.1 控制面 APIMock Server
- 配置:设置 webhook 目标、密钥、默认承运方。
- 运单:创建/查询/列表。
- 事件:追加/查询/清空;支持批量追加。
- 场景:运行预置脚本(标准流程/异常流程)。
- 推送:立即推送指定运单的全部/增量事件;支持模拟重试与重复。
### 7.2 Webhook 推送Mock -> 平台)
- 请求头包含 `X-Client-Id``X-Timestamp``X-Signature`
- body 结构遵循统一事件模型(见 `接口规范.md`)。
- 幂等:对同一事件可重复推送,平台需以 `event_id` 去重。
### 7.3 故障注入(必须)
- `delay_ms`:推送延迟
- `duplicate`:重复推送次数
- `out_of_order`:乱序推送
- `bad_signature`:签名错误
- `timestamp_skew_seconds`:时间戳偏移
- `drop_fields`:缺字段(用于校验平台必填校验与容错)
## 8. 非功能需求
- 易用性:一条命令启动;提供最少的配置即可发送事件。
- 可复现:场景运行应可导出/导入(或提供固定随机种子)。
- 可观测:每次推送记录 request/response 摘要与 request_id。
- 安全:仅用于内网/测试环境;支持简单 Token 或 IP 白名单(可选)。
端侧体验要求(安卓/Web
- 刷新策略:页面进入时拉取一次平台侧轨迹;用户可手动“刷新物流”;显示 `last_synced_at`
- 离线降级:无网络时展示上次缓存的时间线并提示“网络不可用”。
- 权限与隐私:手机号脱敏;配送员/快递员手机号仅对收件人可见且需脱敏/虚拟号策略(商家端不展示);`raw_payload` 仅客服/运维可见并记录审计;不展示配送员精确地址/定位。
## 9. 验收标准
- 能创建运单并运行“标准签收”场景,向平台推送至少 6 个关键节点(揽收/到达中转/在途/到达目的地/派送中/签收)。
- 能注入重复与乱序事件,平台侧仍能正确去重并按时间线展示。
- 能注入验签失败事件,平台侧能拒绝并记录告警。
## 10. 文档与交付物
- `接口规范.md`Mock Server 控制面 API + Webhook 推送规范
- `状态映射表.md`:事件码/原文到平台统一状态映射建议
- `前端字段清单.md`:前端展示字段契约(供时间线组件使用)
- 数据库 Schema`mall_sql/schemas/express_tracking_mock_platform.sql`(平台统一轨迹入库 + Mock 承运方持久化表)
## 11. 待补充项(备忘与下一步)
为保证联调、验收与后续接真实承运方不走弯路,建议按优先级补齐:
- `缺口与待补充清单.md`