修改过时文档,优化文档内容
This commit is contained in:
@@ -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 天未上报)。
|
||||
-- =====================================================================================
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
101
pages/mall/delivery/doc/需求文档(现用)/db/消息推送后台_数据库表结构需求文档.md
Normal file
101
pages/mall/delivery/doc/需求文档(现用)/db/消息推送后台_数据库表结构需求文档.md
Normal 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. 表职责方向
|
||||
保存第一手的物流商(如快递100)Webhook 推送过来的原生 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. 把源头触发器挂载到新的交易表中即可。此五表闭环具备极佳的**水平扩展性**与**极低的中间件运维成本**。
|
||||
@@ -0,0 +1,85 @@
|
||||
-- =====================================================================================
|
||||
-- RPC: notify-worker safe recipients lookup (RLS-safe)
|
||||
--
|
||||
-- 背景:
|
||||
-- - public.ml_orders 已开启 RLS,PostgREST 在未携带可解码 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) 读取请求头 token(PostgREST 会把 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;
|
||||
Reference in New Issue
Block a user