Files
medical-mall/server/消息推送文档/DELIVERY_WEBHOOK_PUSH_SERVER_OVERVIEW.md
2026-03-16 14:58:00 +08:00

24 KiB
Raw Permalink Blame History

物流 Webhook 接收器 & Push Server推送后台总览

面向读者:需要理解“物流事件如何入库、如何触发消息、如何推送到 App”的后端/运维/联调同学。

本文只做总结性说明;细节请跳转到对应子文档与源码。


1. 这两套后台分别解决什么问题?

Webhook 接收器webhook-receiver

一句话:把第三方/承运方推过来的“物流轨迹事件”接进来,完成验签(可选)、留痕,并把事件写入 Supabase 的 platform_express_* 表。

它关注的是:

  • Webhook 请求能否稳定接收(可公网暴露、可控验签)
  • 原始 payload 是否可追溯(用于审计/排障)
  • 轨迹事件是否能归一化写入(供业务查询、消息生成)
  • 运单摘要状态是否能更新(用于页面“当前状态”展示)

Push Serverpush-server

一句话:维护“账号 ↔ 设备 CID”的映射并提供“消息入队 + 消费下发”能力,把通知可靠地投递到云函数(由云函数完成 uni-push2 真实下发)。

它关注的是:

  • 设备 CID 的注册/解绑与存储(本地 JSON 回退 + Supabase 表)
  • 生成推送任务(写入 express_notifications
  • 消费推送任务并调用 CLOUD_FUNC_URL(重试/回写状态)

1.1 用什么做的(技术栈/依赖)

这两套服务都是“轻量 Node.js HTTP 服务”,共同特点:

  • 运行时Node.js建议 18+;你当前环境是 Node.js 22 也可以)
  • HTTP 框架Express
  • JSON 解析body-parser
  • 与 Supabase 通信:直接调用 PostgREST REST API$SUPA_URL/rest/v1/...),通过 apikey(可选 Bearer鉴权
  • 配置加载:复用 server/load-config.js,支持 .env / .json / CONFIG_FILE 指定

差异点:

  • webhook-receiver额外用 crypto 做可选 HMAC 验签;主要写 platform_express_* 三表
  • push-server额外包含“消费者轮询 + 重试退避 + 调用云函数”的逻辑;本地 JSON 作为设备表回退(server/data/push_devices.json

2. 整体链路(从物流事件到手机通知)

建议把链路理解为 3 段:

  1. 事件入库段Webhook 接收器负责)
  • 第三方回调 → webhook-receiver → 写入/更新:
    • platform_express_event_raw(原始留痕)
    • platform_express_tracking_events(归一化事件事实表)
    • platform_express_waybills(运单摘要 current_status_*
  1. 消息生成段(通常是“事件处理器/任务/触发器”,不在这两个服务里)
  • 触发器把 platform_express_tracking_events 的关键状态事件写入队列表 notify_queue
  • 常驻 notify-worker 消费 notify_queue,按推送策略(关键状态、去噪、幂等)生成“消息中心记录/推送任务”
  • 写入 express_notificationspush-server consumer 会复用该表进行下发)

相关实现(仓库内):

  • 迁移脚本:pages/mall/delivery/doc/需求文档/20260309_add_notify_queue_and_trigger.sql
  • 常驻 workerserver/notify-worker.js
  1. 推送下发段Push Server + 云函数负责)
  • push-server consumer 读取 express_notifications 的待处理记录
  • 找到目标设备(push_devices 中的活跃 CID
  • 对每个 CID POST 到 CLOUD_FUNC_URL
  • 云函数内部调用 uni-push2 下发 → App 收到通知

一个简化示意图:

flowchart LR
  Third[第三方/承运方] -->|POST webhook| WH[webhook-receiver :7201]
  WH --> RAW[(platform_express_event_raw)]
  WH --> EV[(platform_express_tracking_events)]
  WH --> WB[(platform_express_waybills)]

  EV --> TRG[DB Trigger]
  TRG --> Q[(notify_queue)]
  Q --> W[notify-worker]
  W --> N[(express_notifications)]

  App[App 客户端] -->|注册CID| PS[push-server :7301]
  PS --> D[(push_devices)]

  PS -->|轮询待发通知| N
  PS -->|POST| CF[CLOUD_FUNC_URL 云函数]
  CF --> UP[uni-push2]
  UP --> App

参考(更完整的业务口径与隐私约束):

  • pages/mall/delivery/doc/需求文档/物流消息推送方案_用户端与商家端.md
  • pages/mall/delivery/doc/需求文档/推送与设备需求文档.md

3. Webhook 接收器webhook-receiver

3.1 位置与入口

  • 代码:pages/mall/delivery/webhook-server/webhook-receiver.js
  • 说明文档:pages/mall/delivery/webhook-server/README.md

3.2 对外接口

  • POST /webhook/express/status
    • 作用:接收轨迹变更回调
    • 返回:
      • 成功:{ ok: true }
      • 运单不存在(不算系统错误):{ ok: false, message: 'waybill not found' }HTTP 200
      • Supabase 鉴权失败等HTTP 502 + { ok:false, message:'...' }
  • GET /health:健康检查

3.3 写库行为(核心表)

接收器在一次 webhook 调用中做三件事(顺序上:先留痕,再归一化,再更新摘要):

  1. 原始留痕:插入 platform_express_event_raw
  • 保存carrier、tracking_no、body原始 payload、received_at、signature_valid 等
  1. 定位运单:在 platform_express_waybills 中查找对应记录
  • 优先按 tracking_no,其次按 order_no
  • 找不到时返回 { ok:false, message:'waybill not found' }
  1. 写入事件 & 更新运单摘要
  • 将第三方的状态字段映射到平台统一的 status_code(如 OUT_FOR_DELIVERY/DELIVERED/EXCEPTION/...
  • PATCH platform_express_waybills:更新 current_status_code/current_status_text/last_synced_at
  • INSERT platform_express_tracking_events:写入归一化事件事实数据(包含 raw_payload、dedupe_key 等)

当前实现的 dedupe_key 使用稳定哈希(基于 tracking_no / carrier / status_code / event_time / event_code / event_text 的截断组合),用于保证 webhook 重复回调不会重复写入事件表,从而避免重复入队/重复通知。

3.4 验签(可选)

  • 配置 WEBHOOK_SECRET 后会校验:
    • X-TimestampX-Signature
    • 签名算法:HMAC-SHA256(secret, rawBodyText + timestamp),输出 hex务必使用原始请求体文本参与计算)
  • 默认行为:验签失败不会直接拒绝入库(会在 raw 表记录 signature_valid=false)。
  • 可选严格模式:设置 WEBHOOK_REJECT_INVALID_SIGNATURE=true 后,验签失败将直接返回 HTTP 401。

3.5 配置与启动方式

接收器会先通过 server/load-config.js 把配置注入 process.env,优先级是:

  1. 系统环境变量
  2. CONFIG_FILE/CONFIG_PATH 指定的文件
  3. 接收器同目录的 webhook.config.json
  4. server/.envserver/config.jsonserver/config.json.example

常用环境变量:

  • SUPA_URL(必需)
  • SUPA_KEY(必需,建议使用 service_role仅后端使用
  • PORT(默认 7201
  • SUPA_USE_BEARER(默认 false
  • WEBHOOK_SECRET(可选)

4. Push Serverpush-server

4.1 位置与入口

  • 代码:server/push-server.js
  • 说明文档(偏“可运行/可运维”):
    • server/PUSH_SERVER_README.md
    • server/README.md

4.2 核心能力

  1. 设备 CID 管理
  • POST /api/v1/push/register:注册/更新设备(写本地 JSON并尝试 upsert 到 push_devices
  • POST /api/v1/push/unregister:解绑/置 inactive
  • GET /api/v1/push/devices:列出设备(优先 Supabase
  1. 推送下发(云函数模式)
  • POST /api/v1/push/send
    • 直接按 cidsuser_id 发送(对每个 CID 调用 CLOUD_FUNC_URL
  1. 通知入队 + 消费者轮询下发
  • POST /api/v1/notifications:写入 express_notifications(排队)
  • consumer可选启用
    • 定时轮询 express_notifications 的待处理记录
    • 取到记录后查询目标 CID 列表
    • 逐个调用 CLOUD_FUNC_URL,并回写状态/错误/重试次数

备注本仓库当前为“仅云函数模式”push-server 自身不直接对接 uni-push真实推送逻辑在云函数里。

4.3 数据依赖Supabase 表)

  • public.push_devices
    • 存储 cid ↔ user_id/merchant_idis_active
  • public.express_notifications
    • 存储待发通知、状态、重试信息等(用于消息中心/推送队列)

字段语义(重要,避免联调误判):

  • status_code:物流/业务状态(例如 OUT_FOR_DELIVERY/DELIVERED/EXCEPTION),由 webhook→tracking_events→notify-worker 生成。
  • send_status:投递处理状态(null=待发送,processing/retrying/success/failed/no-targets),由 push-server consumer 读写。

迁移脚本:

  • 新环节(入队+workerpages/mall/delivery/doc/需求文档/20260309_add_notify_queue_and_trigger.sql
  • 字段拆分consumer 必需):pages/mall/delivery/doc/需求文档/20260309_add_express_notifications_send_status.sql

迁移脚本参考:pages/mall/delivery/doc/需求文档/20260224_add_push_devices_and_notifications.sql

4.4 关键配置env

  • PORT:默认 7301
  • SUPA_URLSUPA_KEY/SERVICE_ROLE_KEY
  • SUPA_USE_BEARER:同 webhook-receiver默认只发 apikey

消费者相关:

  • ENABLE_CONSUMER=true(或 CONSUMER_ENABLED=true
  • CONSUMER_POLL_MS:轮询间隔(默认 2000
  • CLOUD_FUNC_URL:云函数 HTTP invoke 地址(必需)
  • PUSH_TOKEN:可选鉴权透传给云函数
  • 重试配置:MAX_RETRIES / RETRY_INITIAL_MS / RETRY_FACTOR / RETRY_MAX_MS

4.4.1 云函数过期/URL 变更时,改哪里?

现象云函数链接过期或更换后push-server 下发会出现 HTTP 4xx/5xx 或网络错误(日志里通常能看到调用 CLOUD_FUNC_URL 失败),并导致 express_notifications.send_status 长期处于 retrying/failed

需要修改的配置键:

  • CLOUD_FUNC_URL:云函数 HTTP invoke 地址push-server consumer 与 /api/v1/push/send 都依赖它)
  • (如云函数鉴权也变化)PUSH_TOKEN

配置可能存在于以下位置(按优先级理解即可):

  1. 系统环境变量(临时验证/单次启动最方便)
  2. 显式配置文件:通过 CONFIG_FILE/CONFIG_PATH 指定的 .json.env
  3. 默认配置文件server/config.jsonserver/load-config.js 会读取并注入 env

最常用改法(推荐):修改 server/config.json 里的 CLOUD_FUNC_URL,然后重启 push-server。

PowerShell 快速验证(不改文件,先确认新 URL 可用):

$env:CLOUD_FUNC_URL='https://new-cloudfunc.example/invoke'
node server/push-server.js

改完必须做的动作:

  • 重启 push-serverCLOUD_FUNC_URL 在进程启动时读取,改配置后不重启不会生效。

CI/自动化(如果你用了 smoke test

  • GitHub Actions / CI 里若有云函数 smoke test通常还需要同步更新 Secrets例如 CLOUD_FUNC_URL)。

最小 smoke test确认云函数通不通

curl.exe -sS -X POST "$env:CLOUD_FUNC_URL" -H "Content-Type: application/json" -d "{\"token\":\"$env:PUSH_TOKEN\",\"push_clientid\":\"<DEVICE_CID>\",\"title\":\"ping\",\"content\":\"pong\",\"payload\":{}}"

4.5 常见问题(定位方向)

  • Supabase 报 PGRST301 / 401通常是 Bearer/JWT_SECRET 不匹配导致,优先只用 apikeySUPA_USE_BEARER=false)。
  • /api/v1/push/send 返回 push_clientid required:云函数侧没有正确解析请求体(常见于 HTTP 触发器把 body 放在 event.body)。
  • 中文变 ????Windows PowerShell 5.1 发送 JSON 编码问题,按 server/PUSH_SERVER_README.md 的 UTF-8 字节方式发送。
  • 消费者不工作:检查 ENABLE_CONSUMERCLOUD_FUNC_URL 是否配置;再查 express_notifications 是否有待处理记录。

4.6 notify-worker消息生成入队

4.6.1 作用(它负责哪一段)

一句话:从 notify_queue 消费“需要生成消息的事件”,把它转换成 express_notifications(消息中心记录/推送任务),供 push-server consumer 后续下发。

它关注的是:

  • 把事件变成消息:把“物流事件”落成“可按 user/merchant 查询的消息记录”。
  • 收件人映射:根据运单 → 订单,解析出 user_id / merchant_id
  • 幂等入队:对同一事件避免重复生成(靠稳定的 message_id upsert

边界notify-worker 不负责推送下发;真正调用 CLOUD_FUNC_URL 的是 push-server consumer。

4.6.2 位置与入口

  • 代码:server/notify-worker.js
  • 说明文档(更详细):server/NOTIFY_WORKER_README.md

4.6.3 核心行为(做了什么)

  1. 拉取待处理队列:notify_queue?processed_at=is.null(按 created_at 升序,批量处理)。
  2. 解析业务上下文:
  • platform_express_waybillsorder_id/order_no, tracking_no, carrier
  • ml_ordersuser_id, merchant_id
  1. 为每个受众写消息:
  • user_id 存在:写 aud='user'
  • merchant_id 存在:写 aud='merchant'
  • 写入方式upsert 到 express_notificationson_conflict=message_id),避免重复。
  1. 回写队列处理结果:把 notify_queue.processed_at 置上,并写 process_status/last_errorqueued/skipped/failed

4.6.4 数据依赖(读/写哪些表)

  • 读取:notify_queueplatform_express_waybillsml_orders
  • 写入:express_notifications

关键字段口径:

  • notify_queue.dedupe_key:应尽量稳定(同一事件重复到达也一致),否则会造成重复消息。
  • express_notifications.message_id:脚本按 aud|waybill_id|dedupe_key 计算 hash 生成,用于幂等。

4.6.5 配置与运行(最小要点)

配置加载与 webhook/push-server 一致(复用 server/load-config.js),并支持同目录 server/notify-worker.config.json

常用环境变量:

  • SUPA_URL(必需)
  • SERVICE_ROLE_KEYSUPA_KEY(必需,推荐 service_role
  • SUPA_USE_BEARER(可选,默认 false
  • NOTIFY_WORKER_POLL_MS(默认 2000
  • NOTIFY_WORKER_BATCH_SIZE(默认 20
  • RUN_ONCE=true(只跑一轮便退出,适合验证)

启动示例PowerShell

node server/notify-worker.js

4.6.6 常见问题(定位方向)

  • 队列一直堆积:看 notify-worker.err.log/控制台错误;重点检查 SUPA_URL/SERVICE_ROLE_KEY 是否正确、表/字段是否存在。
  • 都是 skipped通常是运单找不到订单platform_express_waybillsorder_id/order_noml_orders 查不到)。
  • 重复消息:检查上游写入 notify_queue.dedupe_key 是否稳定(避免 Date.now() 这种随机值)。
  • RLS/403优先使用 SERVICE_ROLE_KEY,并确保 PostgREST 有权限访问相关表。

5. 给联调/运维的“最小心智模型”

  • Webhook-receiver 只负责:接收 & 入库platform_express_*)。
  • Push-server 只负责:设备表 & 通知队列 & 调用云函数push_devices/express_notifications)。
  • 中间那段“什么时候该推、推给谁、推什么文案/脱敏后字段”通常在事件处理器/业务服务里实现(或数据库触发器 + 消费者)。

把这三段拆开,排障会非常快:

  1. webhook 是否收到?(看 webhook-receiver 日志 / raw 表)
  2. tracking_events 是否写入?(看事件表)
  3. notifications 是否生成?(看 express_notifications
  4. push-server 是否消费?(看 push-server 日志 / 重试字段)
  5. 云函数是否下发成功?(看云函数日志 / uni-push 返回)

5.1 一页摘要

业务价值(为什么要做)

  • 提升履约感知:物流关键节点及时触达(用户端/商家端),减少客服咨询与退款/纠纷风险。
  • 可追溯Webhook 原始留痕 + 事件事实表 + 消息队列表,出了问题能定位“卡在哪一段”。
  • 解耦与可演进Webhook 入库、消息生成、推送下发三段解耦,后续可替换推送通道/触发方式而不重写全链路。

范围边界(做了什么 / 没做什么)

  • 已覆盖:第三方物流事件接收入库;设备 CID 注册与管理;通知入队与消费;调用云函数(由云函数实际对接 uni-push2
  • 不在本两服务内:消息生成策略(哪些事件要推、文案/脱敏、收件人映射、幂等 dedupe_key 规则)通常由“事件处理器/业务服务/任务”负责。

外部依赖(上线前必须明确)

  • Supabase/Postgres:两服务都依赖 REST 读写(需要稳定网络、正确的 key 权限、相关表已迁移)。
  • 云函数push-server 依赖 CLOUD_FUNC_URL 可用;云函数内部需能成功调用 uni-push2。
  • 第三方/承运方Webhook 接入参数、验签口径、回调重试策略需要对齐。

风险与控制点

  • 密钥风险service_role key 属高权限,必须只在服务器环境变量中使用;严禁进入前端/日志/截图。
  • 隐私合规raw 表与 payload 可能含敏感字段,必须限制读取权限;推送内容只允许“脱敏/清洗后的摘要”。
  • 可靠性Webhook 可重复/乱序;需要稳定的幂等策略(事件 dedupe_key、消息 dedupe_key避免重复消息与状态回退。
  • 可用性push-server consumer 失败要可重试、可观测、可告警;云函数故障不应影响主业务写库。

运维成本(需要投入多少人/怎么管)

  • 部署形态:两个常驻 Node 进程(端口默认 webhook 7201、push 7301建议用进程守护Windows 服务 / pm2 / docker / supervisor 任选其一)。
  • 日志与排障:至少保留 7~30 天日志;能按 request_id/message_id/tracking_no 追踪。

建议监控指标(可用来验收与周报)

  • Webhook每分钟请求量、2xx 比例、验签失败率、写库失败率、waybill not found 比例。
  • 推送待处理队列长度pending/retrying 数)、每分钟消费量、成功率、平均重试次数、失败 Top 原因HTTP 非 2xx / 超时 / 云函数业务 errCode
  • 端到端:从事件入库到推送送达的 P50/P95 延迟(分钟级即可)。

责任分工(出现问题找谁)

  • webhook-receiver对“第三方回调接入、验签、入库”负责。
  • 事件处理器/业务服务:对“消息生成规则、脱敏文案、收件人映射、幂等策略”负责。
  • push-server + 云函数:对“队列消费、重试、推送通道调用成功率”负责。

上线检查清单(最小版)

  1. 表结构已迁移(platform_express_*push_devicesexpress_notifications)。
  2. 密钥仅在服务器环境变量,日志不打印敏感值。
  3. webhook /health 正常;能写 raw 与 events运单状态能更新。
  4. push-server /health 正常能注册设备能入队consumer 能调用云函数并回写状态。
  5. 监控与告警已配置(至少:连续失败、队列堆积、云函数不可达)。

5.2 联调/运维速记2026-03-10

下面是本仓库在“webhook → 入库 → 入队 → notify-worker 生成通知 → push-server consumer 调云函数推送到手机”端到端跑通时,最容易踩到的坑与对应结论(只写要点;细节见排障文档)。

更详细的排障与 SQL 清单:docs/DELIVERY_E2E_TROUBLESHOOTING_20260310.md

5.2.1 最常见的故障与根因(按出现频率)

  1. notify-worker 报 “order not found for waybill” 但数据库明明有订单
  • 典型根因:ml_orders 开了 RLSPostgREST 用 apikey 查询会返回空数组(看起来像“没数据”)。
  • 处理建议:生产上用 SERVICE_ROLE_KEY 或服务端 JWT + 正确策略;联调阶段可以临时放开/调整策略以确认链路。
  1. express_notifications upsert 报 42P10ON CONFLICT 找不到唯一约束)
  • 典型根因表上是“部分唯一索引partial unique index而 PostgREST 的 on_conflict=message_id 只能匹配“普通 UNIQUE 约束/索引”。
  • 处理建议:确保存在普通唯一索引:UNIQUE(message_id)(不要带 WHERE 条件)。
  1. push-server 启动失败:EADDRINUSE / 7301 端口被占用
  • 典型根因:旧进程未退出、或端口被其它程序占用。
  • 处理建议:先释放端口再启动;一键启动脚本会自动尝试清理 7201/7301。
  1. 日志显示云函数调用 success但手机不弹通知
  • 典型根因:下发的 content/body 为空或过短,在某些系统/机型上会被折叠为“不展示”。
  • 处理建议:确保推送请求里至少有稳定的 title + content(本仓库已在 consumer 侧做了 content 兜底)。
  1. send_status=no-targets(找不到目标设备)
  • 典型根因:push_devices 里没有对应 user_id/merchant_id 的活跃 cid(未注册/已注销/绑定字段为空)。
  • 处理建议:先用 App 触发注册接口写入设备表;若要商家端收推送,必须正确绑定 merchant_id

5.2.2 Windows 下一键启动/停止(后台常驻)

为方便“可运维 + 可自启动”,仓库提供了 PowerShell 脚本把 3 个常驻进程统一拉起/停掉:

  • 启动(会尽量释放 7201/7301并把进程 PID 写入 runtime 文件):
powershell -ExecutionPolicy Bypass -File .\server\scripts\start-delivery-backend.ps1
  • 停止(按 PID 文件停止进程):
powershell -ExecutionPolicy Bypass -File .\server\scripts\stop-delivery-backend.ps1
  • PID 文件位置(便于运维查进程/做自启接入):
    • server/.runtime/delivery-backend.pids.json

说明:脚本按自身路径定位仓库 root因此可以在任意工作目录执行并会按 server/load-config.js 的规则加载配置(最常见是 server/config.json)。CLOUD_FUNC_URL 等配置变更后需重启进程才会生效。

脚本兼容性说明Windows PowerShell 5.1

  • start-delivery-backend.ps1 会把 PID 文件写成 UTF-8 无 BOM,避免 ConvertFrom-Json 在 5.1 下因 BOM/编码细节解析失败。
  • stop-delivery-backend.ps1 会:
    • 优先按 PID 文件停止进程,并删除 PID 文件;
    • 解析异常时也会兜底尝试释放端口7201/7301避免“端口占用导致下次启动失败”。

如果不停止会怎么样

  • 常见现象:下次启动报 EADDRINUSE7201/7301 端口被旧进程占用)。
  • 风险:重复启动多个 notify-worker/consumer可能造成重复消费、重复写消息/推送,排障会非常混乱。
  • 风险:旧进程仍在用旧配置/旧代码,你改了配置但看起来“不生效”。

5.2.3 快速复现(和联调一致的 E2E

  1. 先启动后台(见上节)。
  2. 触发 webhook 测试请求:
node .\pages\mall\delivery\webhook-server\test-send.js
  1. 观察链路:
  • webhook-receiver 日志是否收到请求;若返回 { ok:false, message:'waybill not found' },先补齐 platform_express_waybills 对应运单。
  • notify-worker 是否把队列变成 express_notifications
  • push-server consumer 是否把 send_status 推进到 success(或 no-targets/retrying/failed)。

6. 进一步阅读(从“总览”到“可落地”)

  • 数据流详细流转图谱与分析:server/消息推送文档/DELIVERY_NOTIFICATION_DATA_FLOW.md
  • webhook-receiverpages/mall/delivery/webhook-server/README.md
  • push-server运行与变更记录server/PUSH_SERVER_README.md
  • notify-worker消息生成入队server/NOTIFY_WORKER_README.md
  • push-server消费者与云函数约定server/README.md
  • 配置加载器:server/load-config.js
  • 业务方案与隐私口径:
    • pages/mall/delivery/doc/需求文档/物流消息推送方案_用户端与商家端.md
    • pages/mall/delivery/doc/需求文档/推送与设备需求文档.md

7. 安全提醒(强烈建议)

  • SUPA_KEY/service_role key 只允许在后端/服务器环境使用,严禁下发到前端
  • 如需把 push-server 的发送/入队接口对外开放建议至少加一层鉴权Bearer token、IP 白名单或内网访问)。
  • platform_express_event_raw.body/raw_payload 可能包含敏感信息,生产上建议严格控制读取权限并记录审计。