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

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