修改消息后台的启动和停止文件并整理文档

This commit is contained in:
not-like-juvenile
2026-03-11 16:42:33 +08:00
parent e67016a6f4
commit 9cc6dcc2a6
19 changed files with 327 additions and 375 deletions

View File

@@ -0,0 +1,257 @@
# 物流 Webhook → 通知入库 → 推送到手机联调问题记录2026-03-10
本文记录本次将链路跑通时遇到的阻塞点、根因与修复方式,便于后续环境复现与排障。
目标链路:
1) webhook-receiver 接收物流回调并落库
2) DB/服务侧写入 `notify_queue`
3) `server/notify-worker.js` 消费 `notify_queue`,生成 `express_notifications`
4) `server/push-server.js` 的 consumer 轮询 `express_notifications`,调用 `CLOUD_FUNC_URL` 推送到设备 `cid`
---
## 1. 症状notify-worker 报 `order not found for waybill`
**现象**
- `notify_queue.process_status=skipped``failed`
- `last_error=order not found for waybill`
**根因**
- `public.ml_orders` 启用了 RLS。
- 后端通过 PostgREST 仅使用 `apikey`(或无法解码的 Bearer访问时`auth.uid()` 为空RLS policy 不成立,导致对 `ml_orders` 的查询返回 `200 []`(看起来像“无数据”,实为被行级安全过滤)。
**验证方法**
- 对比SQL Editor 里能查到订单,但经 `/rest/v1/ml_orders` 查不到。
**临时修复(用于快速跑通链路)**
```sql
ALTER TABLE public.ml_orders DISABLE ROW LEVEL SECURITY;
```
> 备注:你当前观察到“把安全策略关上就可以进行推送”,就是因为 notify-worker 能重新读到 `ml_orders.user_id/merchant_id` 以解析收件人。
**恢复建议(生产化方向)**
- 不建议长期关闭 RLS。
- 推荐做法(本仓库已支持):
- 保持全局 `SUPA_USE_BEARER=false`(避免影响其它服务/自托管网关 JWT 配置不一致带来的 `PGRST301`
- 仅对 notify-worker 开启 Bearer`server/config.json` 设置 `NOTIFY_WORKER_SUPA_USE_BEARER=true`
- 确保 `SUPA_KEY`/`SERVICE_ROLE_KEY` 是可用的 service_role keyJWT
- 备选:通过 RPC/安全视图完成必要查询;避免依赖 `DISABLE RLS`
---
## 2. 症状:写入 `express_notifications` 时报 `42P10`
**现象**
- `notify_queue.process_status=failed`
- `last_error` 包含:
- `there is no unique or exclusion constraint matching the ON CONFLICT specification`
**根因**
- notify-worker / push-server 使用 PostgREST upsert
- `POST /rest/v1/express_notifications?on_conflict=message_id`
- 但数据库侧 `express_notifications(message_id)` 只有“部分唯一索引”(例如 `WHERE message_id IS NOT NULL`),无法匹配 `ON CONFLICT(message_id)`,触发 `42P10`
**修复(改为普通唯一索引)**
- 执行脚本:
- `pages/mall/delivery/doc/需求文档/20260310_fix_express_notifications_on_conflict_message_id.sql`
核心 SQL摘要
```sql
DROP INDEX IF EXISTS public.ux_express_notifications_message_id;
CREATE UNIQUE INDEX IF NOT EXISTS ux_express_notifications_message_id
ON public.express_notifications(message_id);
```
**修复后验证**
- `notify_queue` 最新记录不再失败;`express_notifications` 能正常生成/更新。
---
## 3. 症状push-server 启动失败 `EADDRINUSE 0.0.0.0:7301`
**现象**
- `node server/push-server.js` 直接退出
- 提示端口占用
**根因**
- 7301 端口已有 node 进程占用(重复启动或遗留进程)
**排查/处理Windows**
```powershell
netstat -ano | Select-String ":7301 "
Get-Process -Id <PID>
Stop-Process -Id <PID> -Force
```
---
## 4. 症状PGRST301JWT 无法解码)导致访问异常
**现象**
- PostgREST 返回 `PGRST301` 或“JWT cannot be decoded/verified”
**根因**
- 在自托管/网关配置不一致时,手动发送 `Authorization: Bearer <SUPA_KEY>` 可能触发 JWT 解码失败。
- 本仓库对 Supabase REST 调用默认只发 `apikey`,仅在显式 `SUPA_USE_BEARER=true` 时才附加 Bearer。
**处理建议**
- 保持 `SUPA_USE_BEARER=false`(默认)
- 使用可用的 `apikey/service_role key` 走 Kong key-auth
相关实现:
- `server/push-server.js``supaFetch` 逻辑
---
## 4.1 症状:`503 PGRST002`PostgREST schema cache / 数据库不可用)
**现象**
- webhook-receiver 返回 `502 { ok:false, message:'... HTTP 503 {"code":"PGRST002" ... }' }`
- 或 worker 日志持续刷:
- `HTTP 503 {"code":"PGRST002","message":"Could not query the database for the schema cache. Retrying."}`
**根因**
- 这不是 RLS 问题。
- 这是 **PostgREST 无法连接到 Postgres**(或数据库正在重启/不可用/连接池耗尽/磁盘满等),因此无法构建/刷新 schema cache。
**快速验证**
```powershell
# 直连 REST 读一条数据(只要能返回 200/空数组就说明 REST 已恢复)
$cfg=Get-Content .\server\config.json -Raw | ConvertFrom-Json
$u=($cfg.SUPA_URL.TrimEnd('/')) + '/rest/v1/platform_express_waybills?select=id&limit=1'
Invoke-WebRequest -UseBasicParsing -Uri $u -Headers @{ apikey=$cfg.SUPA_KEY; Accept='application/json' } -Method GET
```
**处理建议(按优先级)**
1) 在 Supabase 部署机器上确认 Postgres 是否正常能否连接、磁盘是否满、CPU/内存是否打满)。
2) 重启 PostgREST/rest 服务(多数情况下会自动恢复 schema cache
3) 如使用 docker-compose优先重启 db 与 rest容器名因环境不同可能不同
4) 待 REST 恢复后再重跑本仓库的 3 个 Node 后台(或等待它们自动恢复)。
---
## 5. 症状:数据库显示 `send_status=success`,但手机没弹通知
**现象**
- `express_notifications.send_status=success`
- 但手机端没有系统通知/横幅
**常见原因**
1) 推送内容为空(部分通道/客户端不会展示空内容通知)
2) 客户端处于前台/透传模式:消息到达但不自动弹系统通知(需要客户端本地展示)
3) 手机系统通知权限/渠道被关闭、省电策略限制
**本次定位与修复**
- notify-worker 生成的 `express_notifications` 通常只有 `event_text_safe`可作为标题body/content 可能为空。
- push-server consumer 调云函数时原先传入的 `content` 可能为空,导致“云函数返回成功,但不展示”。
- 已在 `server/push-server.js` 增加兜底:当 body/content 为空时,使用 title/event_text_safe 作为 content。
验证方法(直接调用云函数对 CID 推送)
- 观察云函数返回中是否包含类似 `successed_online`;并对比手机是否实际展示。
---
## 6. 症状merchant 侧 `send_status=no-targets`
**现象**
- `express_notifications.aud=merchant` 的记录 `send_status=no-targets`
- `last_error=no active devices`
**根因**
- 设备绑定表 `push_devices` 仅存在 `user_id` 绑定,没有 `merchant_id` 绑定。
**处理建议**
- 让商家端也调用注册接口写入 `merchant_id` 维度的设备(或插入对应记录)。
- push-server consumer 对 merchant 的设备查询路径是:
- `push_devices?merchant_id=eq.<recipient_id>&is_active=eq.true`
---
## 7. 端到端自检清单(最短路径)
1) 触发 webhook 测试:
- `node pages/mall/delivery/webhook-server/test-send.js`
2) 查队列:
- `notify_queue` 最新应为 `process_status=queued``last_error=null`
3) 查通知:
- `express_notifications` 最新应生成 `aud=user``aud=merchant` 两条
- `aud=user` 通常应推进到 `send_status=success`
4) 查设备:
- `push_devices` 中对应 `user_id`/`merchant_id` 必须存在 `is_active=true``cid`
---
## 7.1 Windows 一键启动三服务(建议)
为了保证与联调时一致的效果,推荐用脚本一次性启动 3 个后台进程(并落日志):
1) 启动(会默认释放 7201/7301 端口占用):
- `powershell -ExecutionPolicy Bypass -File .\server\scripts\start-delivery-backend.ps1`
2) 触发与联调一致的 webhook 测试(打到本机 7201
- `node .\pages\mall\delivery\webhook-server\test-send.js`
3) 停止:
- `powershell -ExecutionPolicy Bypass -File .\server\scripts\stop-delivery-backend.ps1`
日志文件默认在仓库根目录:
- `webhook-receiver.log/.err.log`
- `notify-worker.log/.err.log`
- `push-server.log/.err.log`
PID 文件(用于停止/排查进程)在:
- `server/.runtime/delivery-backend.pids.json`
查看当前是否仍在运行(示例):
- `Get-Content .\server\.runtime\delivery-backend.pids.json`
- `Get-Process -Id <PID>`
注意:`test-send.js` 默认请求 `http://localhost:7201/webhook/express/status`,所以必须先启动 webhook-receiver。
### 7.1.1 脚本行为与常见坑PowerShell 5.1
- PID 文件由 `start-delivery-backend.ps1` 写入,采用 **UTF-8 无 BOM**,避免 PS 5.1 下 `ConvertFrom-Json` 因 BOM/编码细节解析失败。
- `stop-delivery-backend.ps1` 会优先按 PID 文件停止进程,并在解析/停止异常时兜底尝试释放端口7201/7301
如果你不先停止就重复启动,通常会出现:
- `EADDRINUSE`(端口占用导致启动失败)
- 多实例 worker/consumer 并发运行,导致重复消费、重复写入、重复推送(排障难度陡增)
如果 `test-send.js` 返回 `{ ok: false, message: 'waybill not found' }`
- 说明数据库里没有 `platform_express_waybills.tracking_no = 'TEST_YT_20260206_0007'` 的运单记录。
- 处理方式:先在 `platform_express_waybills` 补一条对应 tracking_no 的运单(并关联到你的 `ml_orders`),再重试发送。
---
## 8. 常用 SQL / 操作片段
### 8.1 重跑某条队列记录(把 failed/skipped 重置为可再次消费)
```sql
UPDATE public.notify_queue
SET processed_at = NULL,
process_status = NULL,
last_error = NULL
WHERE id = '<notify_queue.id>';
```
### 8.2 检查 message_id 是否存在重复(创建唯一索引前)
```sql
SELECT message_id, COUNT(*)
FROM public.express_notifications
WHERE message_id IS NOT NULL
GROUP BY message_id
HAVING COUNT(*) > 1;
```
### 8.3 查看 `push_devices` 是否有绑定
```sql
SELECT *
FROM public.push_devices
WHERE user_id = '<user_id>' OR merchant_id = '<merchant_id>'
ORDER BY updated_at DESC
LIMIT 20;
```

View File

@@ -0,0 +1,458 @@
# 物流 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_*
2) **消息生成段**(通常是“事件处理器/任务/触发器”,不在这两个服务里)
- 触发器把 `platform_express_tracking_events` 的关键状态事件写入队列表 `notify_queue`
- 常驻 `notify-worker` 消费 `notify_queue`,按推送策略(关键状态、去噪、幂等)生成“消息中心记录/推送任务”
- 写入 `express_notifications`push-server consumer 会复用该表进行下发)
相关实现(仓库内):
- 迁移脚本:`pages/mall/delivery/doc/需求文档/20260309_add_notify_queue_and_trigger.sql`
- 常驻 worker`server/notify-worker.js`
3) **推送下发段**Push Server + 云函数负责)
- push-server consumer 读取 `express_notifications` 的待处理记录
- 找到目标设备(`push_devices` 中的活跃 CID
- 对每个 CID POST 到 `CLOUD_FUNC_URL`
- 云函数内部调用 uni-push2 下发 → App 收到通知
一个简化示意图:
```mermaid
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 等
2) **定位运单**:在 `platform_express_waybills` 中查找对应记录
- 优先按 `tracking_no`,其次按 `order_no`
- 找不到时返回 `{ ok:false, message:'waybill not found' }`
3) **写入事件 & 更新运单摘要**
- 将第三方的状态字段映射到平台统一的 `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` 使用 `WEBHOOK_ + Date.now()`,更偏向“留痕与追溯”;如需严格幂等(重复回调不重复入库),建议按文档口径构造稳定 dedupe_key例如 `tracking_no|event_code|event_time`)。
### 3.4 验签(可选)
- 配置 `WEBHOOK_SECRET` 后会校验:
- `X-Timestamp``X-Signature`
- 签名算法:`HMAC-SHA256(secret, bodyText + timestamp)`,输出 hex
- 目前验签失败不会直接拒绝入库(会在 raw 表记录 `signature_valid=false`),可按需要升级为“验签失败直接 4xx”。
### 3.5 配置与启动方式
接收器会先通过 `server/load-config.js` 把配置注入 `process.env`,优先级是:
1) 系统环境变量
2) `CONFIG_FILE/CONFIG_PATH` 指定的文件
3) 接收器同目录的 `webhook.config.json`
4) `server/.env``server/config.json``server/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
2) **推送下发(云函数模式)**
- `POST /api/v1/push/send`
- 直接按 `cids``user_id` 发送(对每个 CID 调用 `CLOUD_FUNC_URL`
3) **通知入队 + 消费者轮询下发**
- `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_id``is_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 读写。
迁移脚本:
- 新环节(入队+worker`pages/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_URL``SUPA_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.json``server/load-config.js` 会读取并注入 env
最常用改法(推荐):修改 `server/config.json` 里的 `CLOUD_FUNC_URL`,然后重启 push-server。
PowerShell 快速验证(不改文件,先确认新 URL 可用):
```powershell
$env:CLOUD_FUNC_URL='https://new-cloudfunc.example/invoke'
node server/push-server.js
```
改完必须做的动作:
- **重启 push-server**`CLOUD_FUNC_URL` 在进程启动时读取,改配置后不重启不会生效。
CI/自动化(如果你用了 smoke test
- GitHub Actions / CI 里若有云函数 smoke test通常还需要同步更新 Secrets例如 `CLOUD_FUNC_URL`)。
最小 smoke test确认云函数通不通
```powershell
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 不匹配导致,优先只用 `apikey``SUPA_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_CONSUMER``CLOUD_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_waybills``order_id/order_no, tracking_no, carrier`
-`ml_orders``user_id, merchant_id`
3) 为每个受众写消息:
-`user_id` 存在:写 `aud='user'`
-`merchant_id` 存在:写 `aud='merchant'`
- 写入方式upsert 到 `express_notifications``on_conflict=message_id`),避免重复。
4) 回写队列处理结果:把 `notify_queue.processed_at` 置上,并写 `process_status/last_error`queued/skipped/failed
### 4.6.4 数据依赖(读/写哪些表)
- 读取:`notify_queue``platform_express_waybills``ml_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_KEY``SUPA_KEY`(必需,推荐 service_role
- `SUPA_USE_BEARER`(可选,默认 false
- `NOTIFY_WORKER_POLL_MS`(默认 2000
- `NOTIFY_WORKER_BATCH_SIZE`(默认 20
- `RUN_ONCE=true`(只跑一轮便退出,适合验证)
启动示例PowerShell
```powershell
node server/notify-worker.js
```
### 4.6.6 常见问题(定位方向)
- 队列一直堆积:看 `notify-worker.err.log`/控制台错误;重点检查 `SUPA_URL/SERVICE_ROLE_KEY` 是否正确、表/字段是否存在。
- 都是 skipped通常是运单找不到订单`platform_express_waybills``order_id/order_no``ml_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_devices``express_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 + 正确策略;联调阶段可以临时放开/调整策略以确认链路。
2) **写 `express_notifications` upsert 报 `42P10`ON CONFLICT 找不到唯一约束)**
- 典型根因表上是“部分唯一索引partial unique index而 PostgREST 的 `on_conflict=message_id` 只能匹配“普通 UNIQUE 约束/索引”。
- 处理建议:确保存在普通唯一索引:`UNIQUE(message_id)`(不要带 WHERE 条件)。
3) **push-server 启动失败:`EADDRINUSE` / 7301 端口被占用**
- 典型根因:旧进程未退出、或端口被其它程序占用。
- 处理建议:先释放端口再启动;一键启动脚本会自动尝试清理 7201/7301。
4) **日志显示云函数调用 success但手机不弹通知**
- 典型根因:下发的 `content/body` 为空或过短,在某些系统/机型上会被折叠为“不展示”。
- 处理建议:确保推送请求里至少有稳定的 `title` + `content`(本仓库已在 consumer 侧做了 content 兜底)。
5) **`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
powershell -ExecutionPolicy Bypass -File .\server\scripts\start-delivery-backend.ps1
```
- 停止(按 PID 文件停止进程):
```powershell
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避免“端口占用导致下次启动失败”。
**如果不停止会怎么样**
- 常见现象:下次启动报 `EADDRINUSE`7201/7301 端口被旧进程占用)。
- 风险:重复启动多个 `notify-worker`/consumer可能造成重复消费、重复写消息/推送,排障会非常混乱。
- 风险:旧进程仍在用旧配置/旧代码,你改了配置但看起来“不生效”。
### 5.2.3 快速复现(和联调一致的 E2E
1) 先启动后台(见上节)。
2) 触发 webhook 测试请求:
```powershell
node .\pages\mall\delivery\webhook-server\test-send.js
```
3) 观察链路:
- 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. 进一步阅读(从“总览”到“可落地”)
- webhook-receiver`pages/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` 可能包含敏感信息,生产上建议严格控制读取权限并记录审计。

View File

@@ -0,0 +1,80 @@
**部署工作流(打包 → 上传/发布 → smoketest**
- **目的**: 描述如何把本地 `uniCloud` 云函数项目自动化打包、上传并发布为可供后端调用的云函数;说明两种上传方式(直接调用上传 API / 使用 `unicloud` CLI与 smoketest 验证流程;给出 CI 集成与常见故障排查建议。
- **目录结构(示例)**:
- `uniCloud-alipay/cloudfunctions/<funcName>/` - 云函数源码目录(含 `index.js``package.json` 等)
- `server/tools/deploy-cloudfunc.js` - 打包并上传/发布的脚本
- `server/dist/` - 输出的 zip 文件目录
1) 打包
- 使用脚本 `server/tools/deploy-cloudfunc.js` 的默认行为:把指定云函数目录压缩为 zip例如 `dist/testUnipush2.zip`)。
- 本地快速命令:
```bash
# 只打包(不上传)
node server/tools/deploy-cloudfunc.js --dir path/to/your/cloudfunctions/func
```
2) 上传 / 发布(两种方式,任选其一)
- A. 使用提供商“上传/发布”HTTP API推荐用于无 CLI 的环境)
- 要求:提供上传 URLCLOUD_UPLOAD_URL 或 `--upload`)、必要的 tokenCLOUD_UPLOAD_TOKEN和可能的额外表单字段例如 appId
- 流程:脚本把 zip 作为 `multipart/form-data` POST 到上传端点;若上传端点返回仅是“已接收”而非“已发布”,还需调用发布/部署 APICLOUD_DEPLOY_API 或 `--deployApi`)。
- 注意:不要把 zip 发到函数的 invoke调用URL例如 `/test`)。如果你把文件 POST 到 invoke URL平台会把请求当作函数执行并返回业务错误例如 `{"errCode":400,"errMsg":"push_clientid required"}`)。脚本内已添加检测,当响应看起来像 invoke 返回会立即报错并提示。
- B. 使用 `unicloud` CLI简单且与 HBuilder 官方流程一致)
- 前提:机器/CI 能安装并执行 `unicloud`,并已登录或有可用凭证。常用命令示例:
```bash
unicloud upload -p <SPACE_ID> -f <FUNCTION_NAME> ./uniCloud-alipay/cloudfunctions/<FUNCTION_NAME>
```
- 优点:不需要逆向抓包,等同于控制台发布,适合 CI。缺点需在 CI 中安装 CLI 并处理登录。
- 建议:在脚本中添加 CLI fallback当未提供 `--upload` 时尝试调用 `unicloud upload`(脚本可接收 `--spaceId` 或环境变量 `CLOUD_UNICLOUD_SPACEID`)。
3) smoketest发布后验证
- 在上传/发布成功后,应通过函数的 invoke URL 做一次简单调用,验证函数行为(例如发送测试推送或返回 expected response。脚本支持传入 `--invokeUrl` / `--funcInvokeUrl` 与 `--testCid` 做自动验证。
- 若 smoketest 失败:检查是否为 invoke URL 被误用为 upload URL、是否发布尚未完成、或权限/鉴权token问题。
4) CI 集成GitHub Actions 示例思路)
- 步骤:
1. Checkout 代码
2. 安装依赖(若使用脚本中的 archiver/form-data/node-fetch
3. 若使用 CLI安装 `unicloud` 并通过 secrets 登录(或在 runner 上提前配置 service account
4. 运行 `node server/tools/deploy-cloudfunc.js --upload ${{ secrets.CLOUD_UPLOAD_URL }} --invokeUrl ${{ secrets.CLOUD_FUNC_URL }} --testCid ${{ secrets.TEST_DEVICE_CID }}` 或运行 CLI fallback
5. 检查返回码并在失败时标记 Job 失败
5) 必要环境变量与参数
- CLOUD_UPLOAD_URL / --upload : 上传 API URL非函数 invoke URL
- CLOUD_UPLOAD_TOKEN : 上传时使用的 Bearer token或其它鉴权
- CLOUD_DEPLOY_API / --deployApi : 若上传后需单独调用发布 API可传入
- CLOUD_FUNC_URL / --invokeUrl : 发布后的函数调用 URL用来做 smoketest
- TEST_DEVICE_CID / --testCid : 用于 push smoketest 的测试设备 CID
- CLOUD_UNICLOUD_SPACEID / --spaceId : 若使用 `unicloud` CLI 上传则为服务空间 ID
6) 常见问题与排查步骤
- 问:脚本返回 `Only absolute URLs are supported` 或 fetch 报错
- 答:通常因为 `--upload` 使用但没有提供值;请改为 `--upload <URL>` 或设置 `CLOUD_UPLOAD_URL`。
- 问:上传后看到 `{"errCode":400,"errMsg":"push_clientid required"}`
- 答:说明你把 zip POST 到了函数的 invoke URL它把请求当作函数调用处理。请提供正确的上传/发布 API 或使用 `unicloud upload` CLI 来发布。
- 排查建议:
- 在 HBuilderX 的发布流程中打开 Network 面板,执行一次发布并抓取对应请求(记录 URL、method、form 字段、headers把它贴到脚本配置中或发给维护者。
- 在 PowerShell 或 Linux 上用 `curl -v` 验证上传端点返回内容,注意响应结构是否像“发布成功”而非函数执行结果。
7) 安全建议
- 把上传/发布的 token、spaceId、testCid 等放在 CI secrets 或受保护的环境变量中,避免写入代码库。
- 脚本中的自动部署入口应加鉴权(例如 `DEPLOY_BEARER`),不要在公网上暴露未经保护的上传接口。
8) 下一步(可选)
- 我可以把 `deploy-cloudfunc.js` 增加 `unicloud` CLI fallback检测 `--spaceId` 或 `CLOUD_UNICLOUD_SPACEID` 并在没有 `--upload` 时调用 CLI也可直接把你抓到的 HBuilder 上传请求模式接入脚本以实现完全自动化。请告知你偏好哪个方案。
-----------------
文件位置server/DEPLOY_WORKFLOW.md

View File

@@ -0,0 +1,145 @@
# notify-worker物流事件 -> 消息入队)说明
面向读者:后端/运维/联调同学。
一句话:`notify-worker` 是常驻 worker用于从 `notify_queue` 消费“新物流事件通知”并生成upsert`express_notifications`,让后续的 `push-server` consumer 去实际下发推送。
---
## 1. 它解决什么问题?
- **解耦**Webhook 入库只保证事实表(`platform_express_*`)一致;消息生成用 worker 异步做,避免在 webhook 请求里做过多业务计算。
- **可追溯/可补偿**:队列表 `notify_queue` 和消息表 `express_notifications` 让每次处理结果可落库(成功/跳过/失败原因)。
- **幂等**:对同一事件(同一 `dedupe_key`)写入消息时,使用稳定的 `message_id` 进行 upsert避免重复生成消息。
在整体链路中的位置:
1) webhook-receiver 写入 `platform_express_tracking_events`
2) 事件被推入 `notify_queue`(由触发器/任务/上游逻辑实现)
3) **notify-worker 消费 `notify_queue` → 写入 `express_notifications`**
4) push-server consumer 轮询 `express_notifications` → 调用 `CLOUD_FUNC_URL` 下发
---
## 2. 用什么做的(技术栈/依赖)
- **运行时**Node.js建议 18+,以确保 `fetch` 可用;旧版本会回退到 `node-fetch`
- **通信方式**:直接调用 Supabase PostgREST REST API`$SUPA_URL/rest/v1/...`
- **鉴权**:默认仅发送 `apikey` header支持 worker 专用开关 `NOTIFY_WORKER_SUPA_USE_BEARER=true`(或 `WORKER_SUPA_USE_BEARER=true`)来附加 `Authorization: Bearer ...`(否则会回退到全局 `SUPA_USE_BEARER`)。
- **核心依赖**
- `crypto`:计算稳定的 `message_id`SHA-256
- `fetch` / `node-fetch`HTTP 调用 Supabase REST
它不是一个 HTTP Server不监听端口而是一个“循环轮询的后台进程”。
---
## 3. 输入/输出(读写哪些表)
### 3.1 输入:`notify_queue`
`notify-worker` 会拉取:
- `processed_at IS NULL` 的记录
-`created_at ASC` 排序
- 每次最多 `BATCH_SIZE`
并在处理完成后回写:
- `processed_at`(处理时间)
- `process_status``queued` / `skipped` / `failed`
- `last_error`(失败/跳过原因,截断保存)
> 说明:字段命名以当前脚本实现为准,你的表结构需要包含这些列。
### 3.2 查询依赖
为把“物流事件”映射到收件人user/merchant脚本会查询
- `platform_express_waybills`:取 `order_id/order_no, carrier, tracking_no`
- `ml_orders`:取 `user_id, merchant_id`
### 3.3 输出:`express_notifications`
对每个收件人写入 1 条通知(同一事件会写 user 与 merchant 两条,若都存在):
- `aud``user``merchant`
- `recipient_id`
- `order_id / waybill_id / tracking_no / carrier`
- `message_id`:稳定哈希生成(`aud|waybill_id|dedupe_key`
- `dedupe_key``waybill_id|aud|<queue.dedupe_key>`(截断到 256
- `event_text_safe`:当前实现直接复用队列行的 `event_text`
- `status_code / event_time / payload`
写入方式是 **upsert**`POST express_notifications?on_conflict=message_id`,并使用 `resolution=merge-duplicates`
注意:数据库侧必须有 **普通** 唯一索引/约束 `express_notifications(message_id)`;不要用 `WHERE message_id IS NOT NULL` 的部分唯一索引,否则会触发 `42P10`
若你已遇到该错误,执行修复脚本:`pages/mall/delivery/doc/需求文档/20260310_fix_express_notifications_on_conflict_message_id.sql`
---
## 4. 配置与启动
### 4.1 配置加载优先级
脚本会复用 `server/load-config.js`,加载顺序(只填充未设置的 env
1) 系统环境变量
2) `CONFIG_FILE` / `CONFIG_PATH` 指定的 `.env``.json`
3) 同目录 `server/notify-worker.config.json`
4) `server/.env` / `server/config.json` / `server/config.json.example`
### 4.2 关键环境变量
- `SUPA_URL`(必需)
- `SERVICE_ROLE_KEY``SUPA_KEY`(必需,推荐 service_role
- `NOTIFY_WORKER_SUPA_USE_BEARER`(可选,默认 false仅对 notify-worker 生效,若为 `true` 则发送 `Authorization: Bearer <SUPA_KEY>`
- `SUPA_USE_BEARER`(可选,默认 false全局开关会影响其它服务一般不建议在自托管 JWT_SECRET 不一致时开启)
worker 行为:
- `NOTIFY_WORKER_POLL_MS`(默认 2000
- `NOTIFY_WORKER_BATCH_SIZE`(默认 20
- `RUN_ONCE=true`(只跑一轮就退出,适合手动验证/CI
### 4.3 启动命令
在仓库根目录:
```powershell
# 常驻运行
node server/notify-worker.js
# 只跑一轮(用于验证)
$env:RUN_ONCE='true'
node server/notify-worker.js
```
如果你的配置放在 JSON
```powershell
$env:CONFIG_FILE=(Resolve-Path .\server\notify-worker.config.json)
node server/notify-worker.js
```
---
## 5. 管理者“一页摘要”(给非一线同学)
### 业务价值
- 让“物流事件入库”与“消息生成/推送”解耦,降低 webhook 链路复杂度和故障扩散。
- 通过队列表落库,可审计、可重放、可定位卡点。
### 边界
- notify-worker **只负责生成消息入队**`express_notifications`),不负责实际推送下发。
- 推送通道成功率与重试投递由 push-server consumer + 云函数负责。
### 关键依赖
- Supabase/Postgres 表:`notify_queue``platform_express_waybills``ml_orders``express_notifications`
- 上游必须保证:有机制把新事件推入 `notify_queue`(触发器/任务/服务均可)
### 主要风险与控制点
- **幂等/重复消息**:依赖 `dedupe_key` 的稳定性;上游写队列时应构造稳定的 dedupe_key不要用 Date.now 这种随机值)。
- **权限/密钥**:需要 `service_role` 读写多表,必须只放服务器环境变量。
- **数据合规**`event_text_safe` 应为清洗/脱敏后的文案;若上游传入的是原文,需在生成前做脱敏。
### 建议监控指标
- `notify_queue` 未处理数量(`processed_at IS NULL`)是否持续增长
- 每分钟处理量、`process_status=failed` 比例与 Top `last_error`
- 从队列入库到消息入队的延迟created_at → processed_at
---
## 6. 进一步阅读
- push-server投递消费者`server/PUSH_SERVER_README.md`
- 总览Webhook + worker + push`docs/DELIVERY_WEBHOOK_PUSH_SERVER_OVERVIEW.md`
- webhook-receiver事件入库`pages/mall/delivery/webhook-server/README.md`

View File

@@ -0,0 +1,408 @@
**Push Server - 使用与变更说明**
简要说明
- 该文档记录对 `server/push-server.js` 的修改、运行所需的环境变量、表结构依赖、以及如何把 Supabase 的 cid 与通知通过云函数(`CLOUD_FUNC_URL`)下发的端到端操作步骤。
组件补充(新增)
- `server/notify-worker.js`:常驻 worker`notify_queue` 生成/写入 `express_notifications`(消息入队)。
- 说明文档:`server/NOTIFY_WORKER_README.md`(包含技术栈/依赖、配置项、监控与管理者摘要)。
变更要点(代码修改摘要)
- supaFetch: 默认仅发送 `apikey`;仅当 `SUPA_USE_BEARER=true` 时才发送 `Authorization: Bearer`。用于避免自托管 Supabase/Kong 场景下因 JWT_SECRET 不一致触发 `PGRST301`
- 新增 endpoint `/api/v1/notifications`:将通知写入 `express_notifications`(排队);由消费者(轮询)读取待处理记录并 POST 到 `CLOUD_FUNC_URL`
- 仅云函数模式:`server/push-server.js` 已禁用 UNIPUSH/代理分支,发送只走 `CLOUD_FUNC_URL`
<!--
历史能力(已注释/不再使用):
- uni-push adapter sendToUniPush(targets, notification, payload)
- PUSH_PROXY_URL / PUSH_PROXY_TOKEN 转发
-->
新增/修改的接口(简要)
- GET `/health` — 健康检查。
- POST `/api/v1/push/register` — 注册/更新设备;会写本地 `server/data/push_devices.json`,并尝试 upsert 到 Supabase `push_devices` 表(如果配置了 SUPA_URL + SERVICE_ROLE_KEY
- POST `/api/v1/push/unregister` — 注销设备(本地并尝试同步 Supabase
- GET `/api/v1/push/devices` — 列出设备(优先从 Supabase 获取)。
- POST `/api/v1/push/send` — 直接按 `cids``user_id` 发送推送(仅云函数:对每个 cid POST 到 `CLOUD_FUNC_URL`)。
- POST `/api/v1/notifications` — 将通知写入 `express_notifications`(排队);实际下发由消费者完成。
依赖的数据库表(必须存在)
- `public.push_devices`:用于存储设备 cid、user_id/merchant_id、is_active 等(见仓库迁移脚本 `20260224_add_push_devices_and_notifications.sql`)。
- `public.express_notifications`:用于保存通知记录与状态(见迁移脚本)。
字段语义(从 2026-03-09 起,重要变更)
- `status_code`:物流/业务状态(如 `OUT_FOR_DELIVERY/DELIVERED/...`),由 `notify-worker` 写入。
- `send_status`:投递处理状态(`null`=待发送,`processing/retrying/success/failed/no-targets`),由 push-server consumer 读写。
为什么需要这个变更:引入 `notify-worker` 后,`status_code` 不再适合作为“投递状态”,否则 consumer 会捞不到记录(或把物流状态误当投递状态)。
迁移脚本(必需执行一次):
- `pages/mall/delivery/doc/需求文档/20260309_add_express_notifications_send_status.sql`
消息生成(可选但推荐)
- `public.notify_queue`:轨迹事件入队表(由 DB trigger 写入)。
- 迁移脚本:`pages/mall/delivery/doc/需求文档/20260309_add_notify_queue_and_trigger.sql`(创建 `notify_queue` + `AFTER INSERT` trigger
- 常驻消费者:`server/notify-worker.js`(从 `notify_queue` 生成 `express_notifications`push-server consumer 再负责下发)。
关键环境变量(示例与说明)
- SUPA_URL — Supabase RESTPostgREST地址内部建议 `http://rest:3000`)。
- SERVICE_ROLE_KEY 或 SUPA_KEY — 用作 `apikey` 向 PostgREST 请求(不要把明文放到 Authorization 除非该值确为 JWT
- SUPA_USE_BEARER — (可选) 若为 `true` 则强制发送 Authorization: Bearer <SUPA_KEY>。
- ENABLE_CONSUMER / CONSUMER_ENABLED — 启用消费者轮询(从 express_notifications 读取待处理记录)。
- CONSUMER_POLL_MS — 轮询间隔(毫秒)。
- CLOUD_FUNC_URL — 云函数外网调用 URL每个目标 cid 会对该 URL 发 POST
- PUSH_TOKEN — (可选) 云函数鉴权 token会在 POST body 的 token 字段中透传)。
- PUSH_SERVER_PORT — (可选) push-server 监听端口,默认 7301推荐用这个便于与 webhook-receiver 共享同一份 `server/config.json`)。
<!--
UNI_PUSH_URL / UNI_PUSH_APPID / UNI_PUSH_SECRET / PUSH_PROXY_URL / PUSH_PROXY_TOKEN仅在非云函数模式下使用当前已不再使用。
-->
运行与测试(本地示例)
1) 安装依赖并启动:
```bash
cd server
npm install express body-parser cors node-fetch
SUPA_URL='http://rest:3000' SERVICE_ROLE_KEY='PASTE_SERVICE_ROLE_KEY' node push-server.js
```
2) 健康检查:
```bash
curl http://localhost:7301/health
# 返回 {"ok":true}
```
3) 注册设备(后端会写本地并尝试写 Supabase
```bash
curl -X POST http://localhost:7301/api/v1/push/register \
-H 'Content-Type: application/json' \
-d '{"cid":"test-cid-1","user_id":"<USER_UUID>","platform":"android"}'
```
4) 按 user 发通知(写入 express_notifications 并触发推送):
```bash
curl -X POST http://localhost:7301/api/v1/notifications \
-H 'Content-Type: application/json' \
-d '{"aud":"user","recipient_id":"<USER_UUID>","notification":{"title":"测试","body":"uni-push 测试"}}'
```
> 说明(仅云函数模式):`/api/v1/notifications` 仅写入 `express_notifications`(排队)。实际下发由消费者轮询后对 `CLOUD_FUNC_URL` 执行 POST。
端到端验证(推荐)
-----------------
前置条件
- `CLOUD_FUNC_URL` 可访问且返回 2xx否则会被标记为 failed/retrying
- 目标用户在 `push_devices` 中存在至少一个 `is_active=true` 的设备(否则会被标记为 no-targets
PowerShell 示例Windows
```powershell
# 1) 注册设备(写入 push_devices
Invoke-RestMethod -Uri 'http://localhost:7301/api/v1/push/register' -Method POST -ContentType 'application/json' -Body (@{
cid='CID_TEST_001'
user_id='a8e3a568-fc1f-4237-bcc5-5722e2fca0a3'
platform='android'
} | ConvertTo-Json)
# 2) 写入通知(排队)
$body = @{
aud='user'
recipient_id='a8e3a568-fc1f-4237-bcc5-5722e2fca0a3'
notification=@{ title='测试'; body='hello' }
payload=@{ order_id='123' }
} | ConvertTo-Json -Depth 6
Invoke-RestMethod -Uri 'http://localhost:7301/api/v1/notifications' -Method POST -ContentType 'application/json' -Body $body
# 3) 等待 2 秒CONSUMER_POLL_MS 默认 2000观察 push-server 控制台日志:应出现对 CLOUD_FUNC_URL 的 POST
```
如何确认处理结果
- 在 Supabase 中查询最近记录:
```sql
select id, message_id, send_status, status_code, retry_count, last_error, updated_at
from public.express_notifications
order by created_at desc
limit 10;
```
5) 直接按 cid 发(跳过 DB
```bash
curl -X POST http://localhost:7301/api/v1/push/send \
-H 'Content-Type: application/json' \
-d '{"cids":["test-cid-1"],"notification":{"title":"hi","body":"msg"}}'
```
UNIPUSH 集成注意事项
<!--
仅云函数模式:此服务不再直接对接 UNIPUSH请在云函数内部完成 UNIPUSH2 调用与鉴权。
-->
故障与排查要点
- 如果 Supabase 报 401 或 PGRST301不要把明文 service key 作为 Bearer使用 `apikey` header或生成并使用与 `PGRST_JWT_SECRET` 匹配的 JWT。可通过 `docker inspect` 检查容器 env 中的 `PGRST_JWT_SECRET`
- 如果 `/rest/v1/push_devices` 返回 404确认表名在 `public` schema 中并加载,或调整请求前缀。
- 查看 push-server 控制台输出中的 `supaFetch` warn 和 proxy 响应体以获取具体错误信息。
- 如果 `/api/v1/push/send` 返回 HTTP 200 但 body 为 `{ errCode: 400, errMsg: "push_clientid required" }`
- 含义:云函数没有拿到 `push_clientid`,因此没有真正执行 uni-push 下发。
- 常见原因:`CLOUD_FUNC_URL` 指向 HTTP 触发云函数cloudbasefunction.cn 等),请求体通常在 `event.body`(字符串)中。
- 云函数侧最小兼容写法:先把 `event.body` 解析为 JSON再从解析结果解构 `push_clientid/title/content`
- 如果手机通知标题/内容变成 `????`
- 含义中文在发出请求前已被错误编码Windows PowerShell 5.1 常见)。
- 解决:用 UTF-8 字节发送 JSON并设置 `application/json; charset=utf-8`)。示例:
```powershell
$bodyObj = @{
cids = @('YOUR_DEVICE_CID')
notification = @{ title = '测试标题'; body = '测试内容' }
payload = @{ order_id = '123' }
}
$json = $bodyObj | ConvertTo-Json -Depth 10
$bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
Invoke-RestMethod -Method Post -Uri 'http://127.0.0.1:7301/api/v1/push/send' `
-ContentType 'application/json; charset=utf-8' `
-Body $bytes | ConvertTo-Json -Depth 20
```
- 备注:也可用 `curl.exe` 发送 JSON避免 PowerShell 的编码差异。
- 如果你在云函数里加了“回显 recv 并提前 return”用于排查乱码
- 排查完成后务必移除该提前 return否则云函数不会执行 `uniPush.sendMessage()`,手机将收不到通知。
后续建议(可选实现)
-`express_notifications` 增加 `attempts``error``sent_at` 字段以支持重试与错误记录;可实现后台 worker 或 pg_notify+listener 做可靠投递与重试。
-`/api/v1/push/send``/api/v1/notifications` 添加管理员鉴权(例如 `PUSH_ADMIN_KEY`)以限制谁能发送通知。
文件位置
- 文档:[server/PUSH_SERVER_README.md](server/PUSH_SERVER_README.md)
- 代码:[server/push-server.js](server/push-server.js)
自动化部署(可选)
- 本仓库新增了一个打包并可选上传云函数的脚本:`server/tools/deploy-cloudfunc.js`,会把
`uniCloud-alipay/cloudfunctions/testUnipush2` 打包到 `server/dist/testUnipush2.zip`
- 使用前请在 `server` 目录安装依赖:
```bash
cd server
npm install archiver node-fetch form-data
```
- 本地打包(只生成 zip
```bash
node server/tools/deploy-cloudfunc.js
```
- 如果你有云平台的上传 URL可直接上传脚本会读取 `CLOUD_UPLOAD_URL` 与 `CLOUD_UPLOAD_TOKEN` 环境变量,或使用 `--upload <URL>` 参数):
```bash
export CLOUD_UPLOAD_URL='https://your-cloud-upload-api'
export CLOUD_UPLOAD_TOKEN='xxxxx'
node server/tools/deploy-cloudfunc.js --upload
```
- 我也添加了一个 GitHub Actions 模板(.github/workflows/deploy-cloudfunc.yml可在仓库的 `main` 分支 push 时自动打包并上传(需要在 Actions Secrets 中配置 `CLOUD_UPLOAD_URL` 与 `CLOUD_UPLOAD_TOKEN`)。
CI / 自动化 说明
- 已提供 GitHub Actions workflow: [.github/workflows/deploy-cloudfunc.yml](.github/workflows/deploy-cloudfunc.yml)。支持可选步骤:上传 -> 触发部署 API -> 调用云函数做 smoke test。
- 推荐在仓库 Secrets 中添加(根据你云厂商调整):
- `CLOUD_UPLOAD_URL` : 上传 zip 的 HTTP endpointupload 接口)
- `CLOUD_UPLOAD_TOKEN` : 上传接口的 Bearer token
- `CLOUD_DEPLOY_API` : (可选)触发云函数部署/发布的 API
- `CLOUD_DEPLOY_TOKEN` : (可选)部署 API 的 token
- `CLOUD_FUNC_URL` : (可选)已部署云函数的外网 URLCI 将对其执行一次 smoke 测试
- `PUSH_TOKEN` : (可选)云函数鉴权 token用于 smoke 测试
- `TEST_DEVICE_CID` : (可选)用于 CI smoke 测试的设备 CID
- 我也添加了本地 helper 脚本:`server/tools/ci-deploy.ps1`,可在本地执行相同流程(打包 -> 上传 -> 触发 -> 调用)。
如果需要,我可以:
- 把 adapter 的请求体精确匹配你现有成功的 uni-push curl请把 curl 发来);或
- 为通知添加重试/记录字段并实现简单重试机制。
自动部署成功示例
-----------------
下面是一个你实际执行并确认成功的示例,便于复制到文档或在团队内复现:
- 启用自动部署并设置 tokenPowerShell 示例):
```powershell
$env:AUTO_DEPLOY_ON_START='true'
$env:DEPLOY_BEARER='your-secret' # 请使用真实随机 token 暂无
$env:CLOUD_UPLOAD_URL='https://env-00jy5x5oy9zd.dev-hz.cloudbasefunction.cn/test'
$env:CLOUD_UPLOAD_TOKEN='upload-token' # 如果上传需要鉴权
$env:AUTO_DEPLOY_ARGS='--upload' # 可选:将 --upload 传给 deploy 脚本
node push-server.js
```
- 预期控制台输出(已在你的环境中观察到):
```
Push server listening on http://0.0.0.0:7301
ENV: CLOUD_FUNC_URL configured? true
ENV: ENABLE_CONSUMER= true
Auto-deploy: spawning node D:\...\server\tools\deploy-cloudfunc.js
[auto-deploy stdout] 打包目录: D:\...\uniCloud-alipay\cloudfunctions\testUnipush2
[auto-deploy stdout] 打包完成 -> D:\...\server\dist\testUnipush2.zip (1324 bytes)
[auto-deploy stdout] 上传到: https://env-00jy5x5oy9zd.dev-hz.cloudbasefunction.cn/test
[auto-deploy stdout] 上传响应: 200 {"data":{...},"errCode":0,"errMsg":"success"}
Auto-deploy process exited with code=0 signal=null
```
- 说明:
- `errCode:0` / `errMsg: "success"` 表示云平台已接收并下发(上线)该云函数包。具体字段会随云厂商略有不同,请以返回的 `errCode` / `errMsg` 或 `status` 为准。
- 若你需要进一步验证云函数可用性,请用 `curl` 或 `Invoke-RestMethod` 调用返回的云函数 URL 做一次 smoke test见上文的“直接调用云函数smoke test”示例
安全提醒:自动部署具有上传并触发发布的能力,请仅在受控环境或 CI 中启用,并把 `DEPLOY_BEARER` / `CLOUD_UPLOAD_TOKEN` 等密钥放入 CI Secrets 或受限的环境变量中。
快速上手(最小环境变量)
- 如果你只是想快速上传并通过云函数下发一次推送,最小只需设置两个环境变量并运行打包脚本:
PowerShell 示例:
```powershell
$env:CLOUD_UPLOAD_URL='https://env-00jy5x5oy9zd.dev-hz.cloudbasefunction.cn/test'
$env:UNI_PUSH_APPID='__UNI__9462CA7'
node tools\deploy-cloudfunc.js --upload
```
说明:设置 `CLOUD_UPLOAD_URL` 与 `UNI_PUSH_APPID` 后,运行 `deploy-cloudfunc.js` 会把 `uniCloud-alipay/cloudfunctions/testUnipush2` 打包并上传到该地址,上传成功后云平台会返回已下发/上线的结果,从而完成推送通路的测试。
**用户整合指南(一步到位)**
下面把之前讨论过的流程与命令整合成一个可复用的清单,便于在本地或 CI 中自动化执行:
- 先决条件:在 `server` 目录下有 `tools/deploy-cloudfunc.js`,并且已安装依赖 `archiver node-fetch form-data`。
1) 本地打包并上传(最小)
PowerShell
```powershell
cd D:\叶\桌面文件\hfkj_mall\mall\server
npm install archiver node-fetch form-data
$env:CLOUD_UPLOAD_URL='https://env-00jy5x5oy9zd.dev-hz.cloudbasefunction.cn/test'
$env:UNI_PUSH_APPID='__UNI__9462CA7'
node tools\deploy-cloudfunc.js --upload
```
说明:这两条环境变量(`CLOUD_UPLOAD_URL` + `UNI_PUSH_APPID`)在多数场景下已足够完成上传并让云平台下发推送;上传成功时脚本会打印上传后的 URL 与平台响应(检查返回的 `errCode` / `data` 字段以确认)。
2) 直接调用云函数smoke test
PowerShell使用 `curl.exe` 保持与 Linux curl 行为一致):
```powershell
curl.exe -X POST "https://env-.../test" `
-H "Content-Type: application/json" `
-d "{\"token\":\"<PUSH_TOKEN>\",\"push_clientid\":\"<DEVICE_CID>\",\"title\":\"测试\",\"content\":\"hi\",\"payload\":{}}"
```
或用 PowerShell 原生:
```powershell
$body = @{ token = '<PUSH_TOKEN>'; push_clientid = '<DEVICE_CID>'; title='测试'; content='hi'; payload=@{} } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri 'https://env-.../test' -ContentType 'application/json' -Body $body
```
3) 本地辅助脚本(整合流程)
- 已添加 `server/tools/ci-deploy.ps1`,支持:打包、上传、触发部署 API、调用云函数做 smoke test。用法示例
```powershell
# 只打包
.\server\tools\ci-deploy.ps1 -Pack
# 打包并上传并触发部署
.\server\tools\ci-deploy.ps1 -Pack -UploadUrl 'https://your-upload' -UploadToken 'token' -DeployApi 'https://your-deploy-api' -DeployToken 'token'
# 打包并上传然后调用云函数做 smoke test
.\server\tools\ci-deploy.ps1 -Pack -UploadUrl 'https://your-upload' -UploadToken 'token' -FuncInvokeUrl 'https://env-.../test' -PushToken 'xxx' -TestCid 'device-cid'
```
4) CIGitHub Actions
- workflow: `.github/workflows/deploy-cloudfunc.yml` 已包含:打包 -> 上传 ->(可选)触发部署 API ->(可选)调用云函数做 smoke test。
- 建议在 Actions Secrets 中添加:
- `CLOUD_UPLOAD_URL`(必需用于上传)
- `CLOUD_UPLOAD_TOKEN`(上传 token
- `CLOUD_DEPLOY_API` / `CLOUD_DEPLOY_TOKEN`(可选,用于触发厂商部署)
- `CLOUD_FUNC_URL` / `PUSH_TOKEN` / `TEST_DEVICE_CID`(可选,用于 smoke test
5) 后端直接单推(如果你愿意绕过云函数)
- 启动 `push-server`(示例):
```powershell
cd D:\叶\桌面文件\hfkj_mall\mall\server
npm install
SUPA_URL='http://rest:3000' SERVICE_ROLE_KEY='PASTE_SERVICE_ROLE_KEY' node push-server.js
```
- 直接按 CID 发送(跳过 DB
```powershell
curl.exe -X POST "http://localhost:7301/api/v1/push/send" -H "Content-Type: application/json" -d "{\"cids\":[\"<DEVICE_CID>\"],\"notification\":{\"title\":\"hi\",\"body\":\"msg\"}}"
```
注意与安全建议
- `uniCloud.uploadFile` 可以把 zip 存到云端存储(返回 `fileID`),但不会自动部署为云函数;若要自动部署需要调用云厂商的部署/发布 API或使用厂商控制台/CLI
- 保管好 `CLOUD_UPLOAD_TOKEN`、`CLOUD_DEPLOY_TOKEN`、`SERVICE_ROLE_KEY` 等机密,放到 CI Secrets 或安全环境变量中,不要硬编码在仓库。
- 上传后若要确认运行状态,请查看云平台函数控制台或调用厂商的状态/日志接口CI 的 smoke test 能快速验证下发路径是否通顺。
如果你希望,我可以把上面示例中的 `curl` 调用替换成你厂商的部署 API 或把 `ci-deploy.ps1` 改为更严格的错误处理与日志输出。
部署服务文件说明
----------------
以下两个文件用于让后端通过一个 HTTP 接口自动完成云函数的打包、上传、触发部署与可选的 smoke-test
- `server/tools/deploy-cloudfunc-service.js`
- 目的封装打包zip、上传、触发部署 API、调用云函数smoke-test的通用函数便于从后端代码调用或在 CI 中复用。
- 主要导出:`deployCloudFunction(options)`,接收的 `options` 支持字段:
- `uploadUrl`(必需或由环境变量提供):上传 zip 的 HTTP endpoint。
- `uploadToken`:上传接口的 Bearer token可选
- `uniAppId`:可附加到上传请求的 appId可选
- `deployApi` / `deployToken`:如果需要调用厂商的部署/发布 API可指定可选
- `funcInvokeUrl` / `pushToken` / `testCid`:若提供,将在上传/部署后调用该云函数进行 smoke-test可选
- 返回值:一个对象,包含 `packed`(打包信息)、`uploaded`(上传响应)、`deployed`(触发部署 API 的响应)和 `invoked`smoke-test 调用响应)四部分,便于在后端记录和判断各个阶段的结果。
- `server/routes/deploy.js`
- 目的:提供一个 Express 路由 `/api/v1/deploy-cloudfunc`,让后端或 CI 通过一次 HTTP POST 调用完成上面封装的流程。
- 请求体示例JSON
```json
{
"uploadUrl":"https://your-upload-endpoint",
"uploadToken":"<UPLOAD_TOKEN>",
"uniAppId":"__UNI__9462CA7",
"deployApi":"https://your-deploy-api",
"deployToken":"<DEPLOY_TOKEN>",
"funcInvokeUrl":"https://env-.../test",
"pushToken":"<PUSH_TOKEN>",
"testCid":"<DEVICE_CID>"
}
```
- 行为:路由优先使用请求体中的字段;若缺失则回落使用环境变量(例如 `CLOUD_UPLOAD_URL`、`CLOUD_UPLOAD_TOKEN`、`UNI_PUSH_APPID`、`CLOUD_DEPLOY_API`、`CLOUD_DEPLOY_TOKEN`、`CLOUD_FUNC_URL`、`PUSH_TOKEN`、`TEST_DEVICE_CID`)。
- 返回JSON 格式,包含 `result` 对象(见 `deployCloudFunction` 的返回结构)。
安全与接入建议
- 强烈建议只在内网或受保护的管理接口上暴露该路由,或在请求中加入鉴权头(例如 `Authorization: Bearer <ADMIN_TOKEN>`)并在服务端验证。此接口具有上传并触发部署的能力,若未保护将导致安全风险。
- 如果你需要,我可以:
- 把路由自动挂载到 `server/push-server.js`(并添加简单的 token 验证);或
- 增强 `deploy-cloudfunc-service.js` 的错误日志与重试策略,或把上传/部署调用改写成你云厂商的精确 API 参数格式。
异步 Consumer已实现说明
---------------------------------
- 目的:从数据库 `express_notifications` 拉取待发送的消息pending解耦写入与下发流程保证可重试、可审计与可运维。
- 实现位置:`server/push-server.js`(已新增 consumer 逻辑)。
- 启用:设置环境变量 `ENABLE_CONSUMER=true`(或 `CONSUMER_ENABLED=true`),可选配置轮询间隔 `CONSUMER_POLL_MS`(默认 2000 ms
- 轮询频率说明:默认 `CONSUMER_POLL_MS` 为 `2000`(即每 2000 毫秒2 秒轮询一次)。若需调整频率,请设置环境变量 `CONSUMER_POLL_MS` 为毫秒数。例如将间隔改为 5 秒:
```powershell
$env:ENABLE_CONSUMER="true"
$env:CONSUMER_POLL_MS="5000"
node push-server.js
```
- 关键环境变量:`SUPA_URL`、`SUPA_KEY`Supabase REST、`CLOUD_FUNC_URL`(云函数 invoke URL、`PUSH_TOKEN`(云函数鉴权)。
- 行为摘要:
- 轮询 `express_notifications`status_code IS NULL并选取记录
- 通过带过滤的 PATCH 抢占(将 `status_code` 设为 `processing`)以避免并发重复处理;
- 查询目标设备(`push_devices`),对每个 `cid` 构造 event 并 POST 到 `CLOUD_FUNC_URL`;若未配置 `CLOUD_FUNC_URL` 则本次处理将失败并写入错误原因;
- 根据调用结果回写 `express_notifications.status_code` 为 `success` / `failed` / `no-targets`。
- 限制与扩展点:当前 consumer 依赖 Supabase REST尚未在 DB 中新增 `retry_count`/`last_error` 字段(建议在迁移中加入以支持指数退避与重试);为保证高可用建议配合 `FOR UPDATE SKIP LOCKED` 或 Supabase Realtime 优化并发策略。
文档实践
---------------------------------
- 变更策略:今后每次由自动化工具或我生成/修改后端代码时,将同步把该变更的作用、配置项与启用方法记录到本文件(`server/PUSH_SERVER_README.md`)或对应 workflow 文档中,确保运行/运维人员能直接查阅到最新说明。

View File

@@ -0,0 +1,124 @@
**推送工作流说明Push Queue + 后端消费者 + 云函数 invoke**
概述
- 目的是将数据库中产生的待推送消息可靠地传递到云函数进行实际推送(云函数使用 `uniPush`)。
- 思路:业务写库 → 写入 `push_queue`(持久化队列)→ `push-server` 或消费者监听队列 → 构造 `event` 并调用云函数 invoke URL → 云函数从 `event` 中读取参数并发送推送。
优点
- 可靠:持久化队列可重试、审计与人工干预。
- 解耦:业务写库与实际推送解耦,降低失败传播风险。
- 可扩展:支持多 worker 并发消费与并发控制。
组件与职责
- `push_queue`(数据库表):保存待推送任务与状态;保证持久化与可重试。
- `push-server`:消费者或 HTTP 接口,负责从队列读取任务、锁定、调用云函数,并更新任务状态。
- 云函数(已上传并发布一次):例如 `uniCloud` 云函数 `testUnipush2`,接收 `event` 并调用 `uniPush.sendMessage` 完成推送。
Schema 示例Postgres / Supabase
```sql
CREATE TABLE push_queue (
id serial PRIMARY KEY,
push_clientid varchar NOT NULL,
title text,
content text,
payload jsonb DEFAULT '{}'::jsonb,
status varchar(16) DEFAULT 'pending', -- pending, processing, success, failed
retry_count int DEFAULT 0,
last_error text,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);
CREATE INDEX ON push_queue (status);
```
写入队列(业务侧)
- 在业务事务中,除了写业务表外,同时插入一条 `push_queue` 记录(保证一致性)。
- 如果业务不想同步写队列,也可由异步任务或触发器生成。
消费者(`push-server`)行为说明
- 1) 轮询或订阅:通过 Supabase Realtime、Postgres LISTEN/NOTIFY、或定时轮询读取 `status='pending'` 的记录。
- 2) 锁定任务:在读取后将该记录设为 `processing`(或用数据库事务/乐观锁),防止重复消费。
- 3) 构造 event
{
token: <PUSH_TOKEN>,
push_clientid: <push_clientid>,
title: <title>,
content: <content>,
payload: <payload>
}
- 4) 调用云函数POST JSON 到云函数 invoke URL或使用云 SDK
- 5) 根据响应更新:
- 成功:`status='success'`,记录返回结果。
- 失败:增加 `retry_count`、写 `last_error`、按重试策略重试或标记 `failed`
示例消费者伪代码Node.js
```javascript
const fetch = require('node-fetch');
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
async function consumeOnce() {
const client = await pool.connect();
try {
await client.query('BEGIN');
const res = await client.query("SELECT * FROM push_queue WHERE status='pending' ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT 1");
if (res.rowCount === 0) { await client.query('COMMIT'); return; }
const rec = res.rows[0];
await client.query("UPDATE push_queue SET status='processing', updated_at=now() WHERE id=$1", [rec.id]);
await client.query('COMMIT');
// 调用云函数
const body = {
token: process.env.PUSH_TOKEN,
push_clientid: rec.push_clientid,
title: rec.title,
content: rec.content,
payload: rec.payload
};
const r = await fetch(process.env.CLOUD_FUNC_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
const txt = await r.text();
if (r.ok) {
await pool.query("UPDATE push_queue SET status='success', updated_at=now() WHERE id=$1", [rec.id]);
} else {
await pool.query("UPDATE push_queue SET status='pending', retry_count = retry_count + 1, last_error=$2, updated_at=now() WHERE id=$1", [rec.id, txt]);
}
} catch (e) {
await client.query('ROLLBACK');
console.error('consume error', e);
} finally { client.release(); }
}
setInterval(consumeOnce, 1000); // 或使用更复杂的 worker 池
```
云函数调用约定
- 云函数在 `index.js` 中接收 `event`,你的函数示例:
- 检查 `event.token` 是否与环境变量 `PUSH_TOKEN` 匹配(鉴权);
- 必要字段 `push_clientid` 缺失返回 400
- 成功后调用 `uniPush.sendMessage(...)` 并返回 `{ errCode:0, errMsg:'success' }`
安全与鉴权
- 在后端调用云函数时带上 `token`,不要把明文 token 写入仓库,使用环境变量或 CI secrets管理。
- `push-server` 的对外 API若开放应加鉴权Bearer token/IP白名单等
重试与幂等
- 采用 `retry_count` + 指数退避策略;超过阈值标记 `failed` 并告警。
- 消费前将任务标记为 `processing`(或使用 `FOR UPDATE SKIP LOCKED`),防止多个 worker 重复处理。
部署注意
- 云函数只需上传/发布一次;后续调用使用 invoke URL。
-`CLOUD_FUNC_URL`, `PUSH_TOKEN`, `DATABASE_URL` 等设置为运行环境的环境变量。
监控与日志
- 记录每次调用响应、失败原因、重试次数;在失败率异常时发送报警。
- 可把 `push_queue` 的任务历史导出用于审计。
示例 SQL + 流程总结见上方片段。
下一步建议
- 我可以为你:
1) 在 `push-server` 中实现上面的 PostgreSQL consumer并提交代码
2) 实现基于 Supabase Realtime 的监听示例并集成;或
3) 添加一个业务 webhook API业务方直接 POST 任务到 `push-server`,由 `push-server` 写队列并触发消费。
文件位置server/PUSH_WORKFLOW.md

View File

@@ -0,0 +1,79 @@
本文件为消息推送Push Server与云函数投递的精简说明聚焦该服务的职责、运行要点、快速启动命令与云函数调用约定。
目标读者:后端开发、联调/运维人员。更详细的实现细节请参考对应子文档与源码链接(下文有指引)。
概览
- 组件:
- webhook-receiver接收第三方物流回调并写入 `platform_express_*`(见 pages/mall/delivery/webhook-server
- notify-worker`notify_queue` 生成 `express_notifications`(消息中心记录)。
- push-server管理设备 CID、入队通知、消费者轮询并调用云函数下发推送见 server/push-server.js
运行要点(快速)
- 一键启动Windows PowerShell
```powershell
powershell -ExecutionPolicy Bypass -File .\server\scripts\start-delivery-backend.ps1
```
- 停止:
```powershell
powershell -ExecutionPolicy Bypass -File .\server\scripts\stop-delivery-backend.ps1
```
脚本说明Windows PowerShell 5.1
- PID 文件:`server/.runtime/delivery-backend.pids.json`(由 start 脚本写入UTF-8 无 BOM
- stop 脚本优先按 PID 停止异常时会兜底释放端口7201/7301
- 不停止直接重复启动,常见会报端口占用(`EADDRINUSE`)或出现多实例重复消费。
- 单独启动 push-server示例
```powershell
$env:CONFIG_FILE=(Resolve-Path .\server\config.json)
node server/push-server.js
```
- 启动 notify-worker
```powershell
node server/notify-worker.js
```
- 启动 webhook-receiver开发
```powershell
node .\pages\mall\delivery\webhook-server\webhook-receiver.js
# 测试脚本
node .\pages\mall\delivery\webhook-server\test-send.js
```
必备环境变量(最常用)
- `SUPA_URL``SUPA_KEY`(或 `SERVICE_ROLE_KEY`)— Supabase/PostgREST 地址与服务 key后端专用
- `CLOUD_FUNC_URL` — 云函数 HTTP invoke 地址push-server consumer 调用)。
- `PUSH_TOKEN` — 可选:云函数鉴权 token由 push-server 透传)。
- `ENABLE_CONSUMER` / `CONSUMER_POLL_MS` — 是否启用消费者与轮询间隔。
云函数调用约定push-server → 云函数)
- POST bodyJSON示例
```json
{
"token":"<PUSH_TOKEN>",
"push_clientid":"<CID>",
"title":"...",
"content":"...",
"payload": { }
}
```
- 云函数兼容性提示:部分云厂商将真实请求体放在 `event.body`(字符串),云函数需先尝试解析 `event.body`
- 成功判定HTTP 2xx 且(可选)返回 JSON `{ errCode:0 }`
快速验证(端到端)
- 确保有一个活跃设备(写入 `server/data/push_devices.json` 或 Supabase `push_devices`)。
- 写入通知:
```bash
curl -X POST http://localhost:7301/api/v1/notifications -H "Content-Type: application/json" -d '{"aud":"user","recipient_id":123,"notification":{"title":"测试","body":"hello"}}'
```
- 等待消费者轮询并在 push-server 日志或 Supabase `express_notifications.send_status` 查看处理结果。
移除/精简说明
- 本文件已删除冗长的逐行排障示例、重复的 PowerShell 编码说明与历史已弃用的代理字段(如 `PUSH_PROXY_URL` 注释)。详细故障排查请看 `DELIVERY_WEBHOOK_PUSH_SERVER_OVERVIEW.md` 与各子文档。
重要链接
- push-server 源码: [server/push-server.js](../push-server.js)
- notify-worker [server/notify-worker.js](../notify-worker.js)
- webhook-receiver [pages/mall/delivery/webhook-server/README.md](../../pages/mall/delivery/webhook-server/README.md)
- 部署脚本: [server/scripts/start-delivery-backend.ps1](../scripts/start-delivery-backend.ps1)
- 迁移脚本与 SQL pages/mall/delivery/doc/需求文档/
如需我把这份精简后的内容合并到 `DELIVERY_WEBHOOK_PUSH_SERVER_OVERVIEW.md` 或把常用启动命令加入到仓库根 README我可以继续提交修改。

View File

@@ -0,0 +1,116 @@
UniPush2 云函数化指南
概述
- 后台实现概览:
- `webhook-receiver`pages/mall/delivery/webhook-server接收第三方/Mock Webhook验签并写入 `platform_express_tracking_events`(原始留痕 `platform_express_event_raw`)。
- `notify_queue`(或触发器):新事件入队,作为消息生成的可靠中间件。
- `notify-worker.js`:消费 `notify_queue`,为每个收件人生成/upsert 到 `express_notifications`(含 `message_id`/`dedupe_key``event_text_safe` 等)。
- `push-server.js`:提供设备管理 API`push/register` 等)并作为推送消费者(轮询 `express_notifications`),对每个目标 `cid` POST 到配置的 `CLOUD_FUNC_URL`(云函数),或在非云函数模式下调用推送通道。
- 云函数UniPush2实际负责把推送请求转为厂商推送uni-push2 / 小米 / 华为 等),并返回调用结果;云函数应做鉴权、日志与重试上报。
云函数化的目标
- 把设备下发逻辑从后端移到可托管的云函数,降低后端对第三方 SDK 的依赖,便于横向扩展与运维隔离。
云函数实现要点(示例流程)
1) 接收:`push-server` 对每个 `cid` 发起 POST 到 `CLOUD_FUNC_URL`body 包含:`token, push_clientid, title, content, payload`
2) 云函数处理:
- 兼容各云厂商:若 `event.body` 为字符串,先 JSON.parse兼容 `event` 直接对象场景。
- 鉴权:校验 `event.token` 与环境变量 `PUSH_TOKEN`
- 校验:要求 `push_clientid` 存在,否则返回 4xx。
- 调用 uni-push2用云端可用的 SDK 或 HTTP 接口下发推送(参见示例代码)。
- 返回统一响应HTTP 2xx + JSON { errCode:0, errMsg:'success' } 表示成功。
云函数示例Node.js需按云平台调整
```javascript
// index.js
const fetch = require('node-fetch'); // 如果云平台不提供 fetch
module.exports.main = async (event, context) => {
let input = event || {};
if (typeof input.body === 'string') {
try { input = JSON.parse(input.body); } catch (e) { /* ignore */ }
}
const { token, push_clientid, title, content, payload } = input;
if (process.env.PUSH_TOKEN && token !== process.env.PUSH_TOKEN) {
return { errCode:401, errMsg:'unauthorized' };
}
if (!push_clientid) return { errCode:400, errMsg:'push_clientid required' };
// TODO: 替换为你云函数环境支持的 uni-push2 调用
// 示例:调用一个第三方/自托管的推送服务 API
const uniPushUrl = process.env.UNI_PUSH_API_URL; // 可选:如果使用自建转发层
const body = {
appid: process.env.UNI_PUSH_APPID,
clientid: push_clientid,
title, content, payload
};
try {
// 如果你在云函数中安装了 uni-push2 SDK请在此调用 SDK
// 下面为通用 fetch 示例(替换为真实厂商 API
const r = await fetch(uniPushUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const txt = await r.text();
if (!r.ok) return { errCode: 500, errMsg: 'push-failed', detail: txt };
return { errCode:0, errMsg:'success', data: txt };
} catch (e) {
return { errCode:500, errMsg:'exception', detail: e && e.message };
}
};
```
依赖与打包
- 两种方式:
1) 使用仓库已有的 `server/tools/deploy-cloudfunc.js` 自动打包并上传(推荐):
-`server` 目录安装依赖:`npm install archiver node-fetch form-data`(如脚本需要)。
- 配置环境变量 `CLOUD_UPLOAD_URL``CLOUD_UPLOAD_TOKEN`(按脚本要求),运行:
```powershell
cd server
node tools/deploy-cloudfunc.js --upload
```
- 脚本会把 `uniCloud-alipay/cloudfunctions/testUnipush2` 打包为 ZIP 并上传。
2) 手工打包上传:将云函数目录打包为 zip使用云厂商控制台或 API 上传并发布。
必备环境变量(运行时)
- `PUSH_TOKEN`:云函数鉴权 token与 `push-server` 中传出的 token 一致)。
- `UNI_PUSH_APPID` / `UNI_PUSH_SECRET`(如厂商需要)。
- `UNI_PUSH_API_URL`:可选;当云函数以 HTTP 方式调用第三方推送代理时使用。
测试方法
- 本地快速测试(若云函数开放公网可访问):
```bash
curl -X POST https://your-cloudfunc.example/handle \
-H "Content-Type: application/json" \
-d '{"token":"<PUSH_TOKEN>","push_clientid":"CID_TEST","title":"测试","content":"hello","payload":{}}'
```
- PowerShell 注意 UTF-8 编码,避免中文变成问号:
```powershell
$json = '{"token":"<PUSH_TOKEN>","push_clientid":"CID_TEST","title":"测试","content":"内容"}'
$bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
Invoke-RestMethod -Uri 'https://your-cloudfunc.example/handle' -Method POST -ContentType 'application/json; charset=utf-8' -Body $bytes
```
运维与监控建议
- 日志:云函数应把每次调用的 request/response 摘要记录到云日志(包括 `push_clientid`、status、耗时、ret code便于排查。不要在日志中输出完整敏感 payload。
- 重试:`push-server` consumer 应实现指数退避的重试;云函数若返回非 2xx被视为失败并重试。云函数内部也应对推送通道失败做重试或将错误上报。
- 鉴权与安全:`PUSH_TOKEN` 放在云函数的环境变量中并由 `push-server` 透传;上传/部署脚本的 `CLOUD_UPLOAD_TOKEN` 等密钥使用 CI Secrets。
常见坑
- Cloud 函数平台把请求体放在 `event.body`(字符串)——必须解析。示例代码已处理此场景。
- PowerShell 发送 JSON 时编码问题,导致中文变问号——请用 UTF-8 bytes 或 `curl.exe`。
- 若云函数调用 uni-push SDK需要在函数包中包含对应 SDK 并在打包时一并上传。
参考文件
- push-server 实现server/push-server.js
- notify-workerserver/notify-worker.js
- 云函数目录示例uniCloud-alipay/cloudfunctions/testUnipush2
- 自动部署脚本server/tools/deploy-cloudfunc.js
---
作者:自动生成文档(可手动微调)
日期2026-03-11