Files
medical-mall/pages/mall/delivery/doc/需求文档/推送与设备需求文档_含建表附录.md
2026-02-24 11:28:13 +08:00

277 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 推送与设备需求文档(含建表附录)
本文档为 `推送与设备需求文档.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