# 订单发货与物流通知：全链路数据流转分析文档

本文档详细描述了本商城系统中，用户订单在发货阶段，从底层物流提供商（快递100）触发Webhook回掉，一直到最终向用户设备下发App内推送消息的全量数据流转过程。

---

## 整体数据流图谱

整个数据运转被精心设计为异步且解耦的数个阶段，以确保在高并发回调或网络异常时，数据不丢失、不阻塞。

```mermaid
sequenceDiagram
    participant Kuaidi100 as 快递100 (Webhook)
    participant Receiver as Webhook Receiver
    participant Supabase as Supabase (PostgreSQL)
    participant DBTrigger as 数据库触发器
    participant NotifyWorker as Notify Worker
    participant PushServer as Push Server
    participant UniPush as 云函数/UniPush
    participant UserApp as 用户App

    %% 第一阶段：外部数据接入与存储
    Kuaidi100->>Receiver: HTTP POST 物流状态更新
    Note over Kuaidi100,Receiver: 验证签名(sign)，解析参数
    Receiver->>Supabase: 插入/更新 platform_express_tracking_events
    Supabase-->>Receiver: 存入成功返回
    Receiver-->>Kuaidi100: 200 OK 返回给快递平台

    %% 第二阶段：事件触发与入队
    Supabase->>DBTrigger: 触发器: event_to_queue_trigger
    Note over DBTrigger: 监听到 tracking_events 写入/更新<br>（条件检查：特定物流状态等）
    DBTrigger->>Supabase: 插入新任务至 notify_queue 表

    %% 第三阶段：队列消费与通知生成
    loop 轮询读取队列 (10秒/次)
        NotifyWorker->>Supabase: 捞取 status='pending' 的任务
        NotifyWorker->>Supabase: 分析业务数据 (查ml_orders等)
        Note over NotifyWorker: 数据转换、格式化模板内容
        NotifyWorker->>Supabase: 将生成的推送记录写入 express_notifications 表
        NotifyWorker->>Supabase: 更新 notify_queue.status 为 'processed'
    end

    %% 第四阶段：消息分发与远端推送
    loop 轮询待推送通知 (10秒/次)
        PushServer->>Supabase: 捞取 push_status='pending' 的记录
        PushServer->>Supabase: 结合 push_devices 表查找用户cid
        PushServer->>UniPush: HTTP POST (附带云函数URL & 推送体)
        Note over PushServer,UniPush: 触发 unicloud 上的云函数
        UniPush->>UserApp: 发放手机系统推送通知 (通道下发)
        UniPush-->>PushServer: 返回推送结果 (成功/失败)
        PushServer->>Supabase: 更新 express_notifications 推送状态 (delivered/failed)
    end
```

---

## 阶段一：外部回调接入与原始数据落库

### 1. Webhook 请求接收
- **服务组件**：`server/webhook-receiver.js` ( 基于 Node.js + Express )
- **触发源头**：快递100平台（订阅回调业务）。
- **行为过程**：
  - 接收 HTTP POST 请求（默认端口 3001，路径 `POST /webhook/kuaidi100`）。
  - **参数验证**：解析 `param` 数据和 `sign` 签名，比对系统中配置的 secret 确保数据安全，非假冒请求。
  - **源头日志留痕**：原始的 JSON 报文会被原封不动地记录在 **`platform_express_event_raw`** 表中。这是追踪客诉（如“没收到通知”时排查是否丢件）的**第一查证现场**。
  - **数据提取**：解包得到快递单号、最新物流轨迹节点、物流状态编码（如：在途、派件、签收等）。

### 2. 落入业务核心库
- 表目标：主要涉及 `platform_express_tracking_events`（物流事件记录）或 `platform_express_waybills`（运单主表）。
- **处理要求**：将接收到的非结构化或外部结构化数据，转译为本系统认可的数据类型，并 Upsert（更新或插入）入库。至此，外部调用阶段结束，返回 `200 { "result": true, "returnCode": "200", "message": "成功" }` 予以确认，这确保了外部平台不再重复推送同一条消息。

---

## 阶段二：数据库内部事件流转（触发器驱动）

为了让核心写入逻辑与后续繁琐的通知逻辑解耦，系统利用了 PostgreSQL 强大的触发器（Trigger）能力。

### 1. 触发器监听
- **关联脚本**：`20260309_add_notify_queue_and_trigger.sql` (位于 `pages/mall/delivery/doc/需求文档/`)
- 当 `platform_express_tracking_events` 发生 `INSERT` (或特定 `UPDATE`) 时，触发底层 SQL 逻辑。

### 2. 队列填充 (`notify_queue`)
- 触发器脚本 `event_to_queue_trigger` 负责初步筛选。它可能会排除一些毫无业务通知价值的冗余轨迹节点，只抓取关键节点（例如：“正在派发”、“已签收”）。
- 把关联的业务主键（如：运单号、关联订单 ID）、事件类型，作为 JSON payload，以极快的速度 `INSERT` 进入 `notify_queue` 表，设定初始状态为 `pending`。
- **优点**：Webhook API 的响应时间不再受后续通知逻辑（查询用户组、查找文案模板）的拖累。

---

## 阶段三：通知生成流水线（Notify Worker 消费任务）

这是业务逻辑最密集的部分，将纯粹的“发生了一件事”转化为具体的“应该发给谁、发什么内容”的推送任务数据。

### 1. 任务抓取与消费
- **服务组件**：`server/notify-worker.js` ( 独立的 Node.js 守护进程 )
- Worker 在后台持续运行，定期（例如每 10 秒）扫描 `notify_queue` 中处于 `pending` 状态的任务项。

### 2. 业务溯源与数据拼接
- Worker 锁定任务后，从 Payload 提取运单 ID / 订单 ID。
- **关联查询**：向数据库发起查询，结合 `ml_orders` (订单表)、可能的用户属性表、商品主数据表，查询出：买家的 `user_id`，商品名称摘要等内容。
- **模版渲染**：例如，将“订单 + 运单号处于签收状态”渲染为中文文本：“您购买的商品【xxx等】已由用户签收”。

### 3. 持久化至推送队代表（加工日志留存）
- 组装完毕后，Worker 将成型的通知指令，插入到真实的通知表 `express_notifications` 中，其包括目标 `user_id`、具体的 `title`、`body`。
- 把该条新建的记录标记为 `push_status = 'pending'`。
- 最后，将 `notify_queue` 中的原始任务记录状态更新为 `processed`。如果中间报错（例如解析运单或匹配订单失败），则会标记为 **`failed`** 并带有 **`error_msg`**，此表充当了**加工阶段的完整状态日志**。

---

## 阶段四：物理推送与结果反馈（Push Server 分发）

该阶段将上阶段准备好的具体通知内容，通过第三方通道真正发送到用户设备。

### 1. 推送目标检索
- **服务组件**：`server/push-server.js` ( 基于 Node.js + Express )
- Push Server 定期轮询 `express_notifications` 中 `push_status = 'pending'` 的新通知项。

### 2. 补齐通道标识(CID)
- 系统根据需通知的目标 `user_id`，去 `push_devices`（Client ID 表） 中查找此用户最后活跃设备的绑定的 `cid`。如果没有找到合适的 `cid`，可能直接将该通知记为 `failed` 或转为站内信处理。

### 3. 请求云函数下发 (DCloud / uniCloud 侧)
- **关联配置**：`server/config.json` 的 `CLOUD_FUNC_URL`。
- Push Server 封装目标 `cid` 列表、文章标题 (title)、摘要主体 (body)、可能的附加跳转数据（payload 数据），构建一个 HTTP POST 载荷。
- 通过网络请求抛给特定的 **DCloud 阿里云/腾讯云空间** 上部署的 **uniCloud 云函数服务**。
- **核心下发通道技术栈**：因为本商城前端使用的是 uni-app x 架构，该云函数服务内包装了 DCloud 官方提供的 **UniPush 2.0 管理端 SDK**。由其直接对接个推（Getui）和包括华米OV（华为、小米、OPPO、vivo）以及苹果（APNs）在内的各大手机硬件厂商原生的**离线下发通道**，以保证 App 无论是否在后台驻留都能收到消息。

### 4. 结果更新与下发日志跟踪
- 网络请求完成后，Push Server 会接受到云函数关于各客户端接收/下发的汇报。
- 该结果会作为**下发日志**回写至对应的 `express_notifications` 记录中：
  - 如果成功，`push_status` 变为 **`delivered`**。
  - 如果失败，`push_status` 变为 **`failed`**，且将具体的错误文本（例如 `"invalid cid"`，无效的通道ID）存入 **`provider_response`** 字段中。这直接揭示了**为什么下发失败**。
- **至此，一条由快递公司产生的外部轨迹更新，完美结束了内部的长尾运转，闭环成了用户手机屏幕上的一次震动提醒。**

---

## 业务溯源与客诉排查指南

本架构天然具备完整的数据追溯能力，所有生命周期均在数据库中永久留痕。在处理诸如“用户投诉没有收到签收推送”的问题时，运维人员可按照以下步骤依序溯源：

1. **查源头是否丢件 (`platform_express_event_raw`)**：根据快递单号检索此表，确认底层物流商是否真的推送了回调数据。如果没有记录，说明是快递公司没发/漏发。
2. **查系统是否消费失败 (`notify_queue`)**：根据单号检索，看是否存在对应记录。如果在，看 `status` 字段。如果是 `pending`，说明 `notify-worker` 服务挂了没消费；如果是 `failed`，请查看 `error_msg`。
3. **查终端是否下发失败 (`express_notifications`)**：如果前面都正常，看此表的 `push_status`。如果是 `pending` 说明云函数未执行/Push Server离线；如果是 `failed`，直接查看 `provider_response` 确认厂商阻断或CID失效的具体原因。

---

## 架构演进与为何不使用 Redis 的说明

针对很多大厂常见的推送后台中大量使用 Redis 的现状，本系统出于以下考量采取了**“去中间件化”的轻量级 PostgreSQL 强依赖架构**：

1. **队列平替**：大厂常将 `notify_queue` 的待推消息放在 Redis（如 List/Stream）。我们直接利用 Supabase PostgreSQL 的实体表 `notify_queue` 结合 10 秒级定时轮询 (Poll) 实现了队列。
   - **优势**：数据强一致性，发生断电/宕机等灾难时消息绝对不丢；极大地节约了项目运维成本，不需要单独起 Redis 实例或关心 RDB/AOF 策略。
   - **容限**：在单日处理数万至十万级流转的状态下，这类轮询对 PostgreSQL 的 IO 压力微乎其微。只有当出现“秒杀级别数十万并发群发”时，才需演进到 Redis 消息队列。
2. **设备缓存查找平替**：查找 `userId -> cid` 虽然是典型的 KV 场景，但通过 Supabase 的索引表直接查询依然能保证极低的延迟响应。

这套设计对于我们十万级规模商城是性价比极高、免维护且极其稳壮的 Enterprise-Ready （企业级可用）的轻量化方案。

---

## 系统容量预期与未来性能优化指南

随着业务长期运行和单量增长，本套无中间件架构在某些极限边界上可能会暴露出其物理瓶颈。为了防患于未然，现将已知的极限边缘与预案说明如下，供未来的后端维护者参考：

### 1. 历史数据爆炸与日志表清理（DB 膨胀约束）
由于本系统利用 `platform_express_event_raw`、`notify_queue` 和 `express_notifications` 等实体表承担了主要的业务流转与日志溯源功能，它们是“只增不减”的。
- **隐患**：随着每天单量的累积，单表数据可轻易超过百万条，导致查询缓慢、索引膨胀或耗尽 Supabase 数据库的物理容量。
- **未来演进四大方案（由简到强建议）**：
  1. **方案一：轻量级定时硬清理（起步期适用）**：在 Supabase 面板开启 `pg_cron`，每晚跑定时 SQL 脚本，直接 `DELETE` 掉 60 天前的历史记录。优点是零门槛实施，缺点是追溯期短且引发表碎片。
  2. **方案二：冷热数据分离归档（小型商城标配）**：创建同构归档表（如 `notify_queue_archive`），每晚将旧数据从热表迁移至归档表再删热表。确保生产环境极快的同时，运维能查陈年老账。
  3. **方案三：声明式表分区 Table Partitioning（高配优雅解）**：将主表按月改造成分区表层级（如 `queue_2026_01`）。业务代码零修改，删数据只需直接 `DROP TABLE` 具体月表，毫秒级释放巨大闲置空间，完全无磁盘碎片。
  4. **方案四：日志流外部卸载（大厂海量终极解）**：挂载监听 Supabase Realtime Webhook 或者 PostgreSQL 的逻辑复制流（Logical Replication），将所有的日志插入和变化发往第三方 Elasticsearch 或云厂商极其廉价的 OSS 对象存储桶里长存。主事务数据库里发完件立马删空，只留运行中的活跃数据。

### 2. 避免队列发生“毒药死循环” (Poison Message)
- **隐患**：本纯 DB 驱动的队列体系未完全建立如同专业死信队列（Dead Letter Queue）那般的重试计数和隔离。如果在运行 `notify-worker` 的拼接数据逻辑中，有一条“破损订单数据”触发了未能 Catch 住的致命错(Fatal Error) 使得 Node.js 进程崩塌，进程通过守护脚本重启后，这个任务依然是 `pending`。Worker 重新抓取再次崩溃，就会形成堵死同批其他订单消息的“毒药效应”。
- **未来预案/开发原则**：在 `notify-worker.js` 中新增、修改关于订单及商品表的复杂关联解析代码时，**必须将对单条业务的独立 Try-Catch 包裹进最内层的批次 `map` 中**，失败了直接给它标记为 `failed` 并写入 `error_msg` 抛弃，**绝不能**向外层抛出导致整个批次断裂。

### 3. 排队性能上限设置 (Scale-up)
- **隐患**：当前我们提供的起服动作 (`start-delivery-backend.ps1`) 均为单线程执行（一个 Node 进程）。一分钟只能通过轮询处理规定上限条数的推送，若双十一爆发发货，可能会导致发信严重滞后（排队几小时后才收到）。
- **未来预案**：
  1. 第一个手段是修改配置：适当调短轮询间隙时间 (`POLL_MS`)，或加大每次查询限制 (`BATCH_SIZE`)。
  2. 第二个手段是多实例并发：如果要开多个 Worker 同时跑以消化消息波峰。在做这个扩展前，必须保证通过修改 SQL 将获取队列的查询改造为 `SELECT ... FOR UPDATE SKIP LOCKED`。以此实现单条消息在数据库级别的悲观跨行抢占，防止多个 Worker 获取并重复发送同一个通知。
  
本设计在每个阶段都支持“断层续接”：
1. 若 Webhook receiver 崩溃，外部快递100会按照自己的退避算法发起重试，保证事件能抵达网络库。
2. 若 Notify worker 崩溃，数据库里的 `notify_queue` 会静静堆积 pending 数据，一旦恢复，Worker 会把积压数据逐步消化。
3. 若 Push server 网络故障或 云函数URL失效，`express_notifications` 依然保留 `pending`，待网络或配置恢复（如更新 `CLOUD_FUNC_URL`）后自动补偿发送。
