补充方案

This commit is contained in:
not-like-juvenile
2026-03-12 10:36:51 +08:00
parent 9cc6dcc2a6
commit 4acbb8ced5
7 changed files with 290 additions and 18 deletions

View File

@@ -36,7 +36,13 @@ ALTER TABLE public.ml_orders DISABLE ROW LEVEL SECURITY;
- 保持全局 `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`
- 备选(慎用/止血):通过 RPC`SECURITY DEFINER`完成必要查询;避免依赖 `DISABLE RLS`
- 执行 SQL`pages/mall/delivery/doc/需求文档/db/绕过RLS的方案(慎用)/20260311_add_rpc_notify_get_order_recipients.sql`
- 在数据库中设置 token用于 RPC 显式鉴权):
- 示例:`ALTER ROLE authenticator SET app.notify_worker_token = '<YOUR_LONG_RANDOM_TOKEN>';`(按你的 PostgREST 连接角色调整)
- notify-worker 配置:
- `NOTIFY_WORKER_USE_RLS_SAFE_RPC=true`
- `NOTIFY_WORKER_RPC_TOKEN=<YOUR_LONG_RANDOM_TOKEN>`
---
@@ -99,6 +105,135 @@ Stop-Process -Id <PID> -Force
- 保持 `SUPA_USE_BEARER=false`(默认)
- 使用可用的 `apikey/service_role key` 走 Kong key-auth
**如果你的目标是“用 Bearer(service_role JWT) 绕过 RLS”**(而不是仅仅避免 401
- 需要修复自托管配置一致性:确保 PostgREST(rest) 实际加载的 `PGRST_JWT_SECRET` 与签发 `SERVICE_ROLE_KEY``JWT_SECRET` 完全一致(算法/格式也要匹配),并重启 rest 组件。
- 在 JWT 未修复前,如需临时止血可参考 RPC 备选方案(慎用):执行 `pages/mall/delivery/doc/需求文档/db/绕过RLS的方案(慎用)/20260311_add_rpc_notify_get_order_recipients.sql`(仅建议用于短期联调/排障)。
### 4.1 具体怎么修(推荐按这个顺序做)
> 目标:让 `/rest/v1/*` 能接受 `Authorization: Bearer <SERVICE_ROLE_KEY>`,不再返回 401 `PGRST301`。
补充说明(结合你当前的 docker-compose
- 你贴的 compose 里 `rest` 使用 `PGRST_JWT_SECRET: ${JWT_SECRET}`,所以**如果容器已按该 env 重启生效**通常就不是“rest/auth secret 不一致”。
- 此时最常见根因变成:**你正在使用的 `SERVICE_ROLE_KEY` 并不是用当前这份 `JWT_SECRET` 签出来的**(即 key/secret 不匹配),或者只是**改了 `.env` 但容器没重启**仍在用旧 env。
1) **确认 rest(PostgREST) 当前加载的密钥**(不要直接到处打印明文;建议只比对 hash
在 Supabase 机器上Docker 场景示例,容器名因部署不同会不同):
```powershell
# 1) 找到 PostgREST/rest 容器
docker ps --format "{{.Names}}" | Select-String -Pattern "rest|postgrest"
# 2) 取 PGRST_JWT_SECRET 的 sha256避免直接输出明文
$restName = (docker ps --format "{{.Names}}" | Select-String -Pattern "rest|postgrest" | Select-Object -First 1).Line.Trim()
docker exec $restName sh -lc 'printf %s "$PGRST_JWT_SECRET" | sha256sum'
# 3) 顺手检查几个容易踩坑的配置项
docker exec $restName sh -lc 'echo PGRST_JWT_SECRET_IS_BASE64=$PGRST_JWT_SECRET_IS_BASE64'
docker exec $restName sh -lc 'echo PGRST_JWT_AUD=$PGRST_JWT_AUD'
```
2) **确认 auth(GoTrue) 使用的 JWT_SECRET**(同样用 hash 比对):
```powershell
docker ps --format "{{.Names}}" | Select-String -Pattern "auth|gotrue"
$authName = (docker ps --format "{{.Names}}" | Select-String -Pattern "auth|gotrue" | Select-Object -First 1).Line.Trim()
docker exec $authName sh -lc 'printf %s "$JWT_SECRET" | sha256sum'
```
如果这两个 hash **不一致**就会出现“auth 签发的 JWT / service_role keyrest 解不开”的典型问题。
如果这两个 hash **一致但仍然 PGRST301**,优先做下面两件事:
2.1) **验证你手上的 SERVICE_ROLE_KEY 是否匹配 JWT_SECRET最关键**
在你的开发机执行(只在本机跑,不要把 secret 提交到仓库):
```powershell
# 预期输出Signature VALID
node .\server\verify_supa_token.js <SERVICE_ROLE_KEY_JWT> <JWT_SECRET>
```
- 如果是 `Signature INVALID`:说明当前 `SERVICE_ROLE_KEY` 不是用这份 `JWT_SECRET` 签的。
- 处理方式二选一:
-`JWT_SECRET` 改回“当初生成 key 的那份”并重启(影响面大,不推荐在不确定时直接动生产)
- 重新生成一套新的 `ANON_KEY/SERVICE_ROLE_KEY`(与当前 `JWT_SECRET` 匹配并同步更新Kongkey-auth+ 你的后端配置(例如 `server/config.json`)。
可以使用本仓库脚本从 `JWT_SECRET` 生成匹配的 keyHS256
```powershell
$env:JWT_SECRET='<your jwt secret>'
$env:SUPA_JWT_ISSUER='supabase-1' # 与你现有 key 的 iss 保持一致
# $env:SUPA_JWT_AUD='authenticated' # 如需带 aud 再打开
node .\server\tools\generate-supabase-keys.js
```
如果你不想在命令行里直接暴露 `JWT_SECRET`,可以用交互式 PowerShell 脚本(输入隐藏):
```powershell
powershell -ExecutionPolicy Bypass -File .\server\tools\generate-supabase-keys.ps1 -Issuer 'supabase-1' -Years 5
```
生成后要做的同步(否则 Kong key-auth 仍然认旧 key
- 更新 Supabase 的 `.env`:把 `ANON_KEY=``SERVICE_ROLE_KEY=` 替换为新生成的值
- 重启 kong推荐同时重启 rest/auth 以减少缓存/配置疑难):
```powershell
docker compose restart kong rest auth
```
然后更新本仓库服务端配置(至少 `server/config.json``SUPA_KEY`)为新的 `SERVICE_ROLE_KEY`,再用下面的方式验证。
2.2) **确认容器确实吃到了最新 env改完一定要 restart**
```powershell
# 在 Supabase 机器上docker compose
docker compose restart rest auth kong
```
3) **确保 ANON_KEY / SERVICE_ROLE_KEY 是用同一套 JWT_SECRET 生成的**
- 你不能从一个 JWT token 反推出 secret所以通常的做法是
- 选定一套 `JWT_SECRET`rest/auth 全部用它)
- 重新生成 `ANON_KEY``SERVICE_ROLE_KEY`
- 同步更新 Kong/key-auth 里的 key否则 `apikey` 会失效)
你可以用本仓库脚本验证“某个 service_role token 是否匹配某个 secret”HS256
```powershell
# 在你的开发机执行(不要把 secret 提交到仓库)
node .\server\verify_supa_token.js <SERVICE_ROLE_KEY_JWT> <JWT_SECRET>
```
4) **修改完配置后一定要重启**
- 至少重启:`rest(PostgREST)``auth(GoTrue)`
- 如果你改了 `ANON_KEY/SERVICE_ROLE_KEY`,通常还需要重启 `kong`(让 key-auth 使用新 key
5) **验证 Bearer 是否修复**
```powershell
$cfg = Get-Content .\server\config.json -Raw | ConvertFrom-Json
$u = $cfg.SUPA_URL.TrimEnd('/')
$token = $cfg.SUPA_KEY # 确保这里是 service_role JWT
Invoke-WebRequest -UseBasicParsing -Method Get -Uri "$u/rest/v1/ml_orders?select=id&limit=1" -Headers @{
apikey = $token
Authorization = "Bearer $token"
Accept = 'application/json'
}
```
也可以用本仓库脚本快速测一次(更直观):
```powershell
$env:CONFIG_FILE=(Resolve-Path .\server\config.json)
node .\server\tools\test-postgrest-bearer.js
```
- 修好后的期望:不再是 401 `PGRST301`
相关实现:
- `server/push-server.js``supaFetch` 逻辑
@@ -159,11 +294,34 @@ Invoke-WebRequest -UseBasicParsing -Uri $u -Headers @{ apikey=$cfg.SUPA_KEY; Acc
- `last_error=no active devices`
**根因**
- 设备绑定表 `push_devices` 仅存在 `user_id` 绑定,没有 `merchant_id` 绑定。
- `push_devices` 表结构本身包含 `merchant_id` 字段,但实际设备注册时常见只写入 `user_id`,导致:
- notify-worker 能生成 `aud=merchant, recipient_id=<merchant_id>` 的通知;
- push-server consumer 查询 `push_devices?merchant_id=eq.<recipient_id>&is_active=eq.true` 时结果为空;
- 最终落为 `send_status=no-targets`
补充:如果你的 push-server 版本仍在用 `push_devices?on_conflict=cid` 做 upsert也会导致“注册接口返回 ok但 Supabase 表里没写进去”PostgREST 会提示没有匹配的唯一约束)。
**处理建议**
- 让商家端也调用注册接口写入 `merchant_id` 维度的设备(或插入对应记录)。
- push-server consumer 对 merchant 的设备查询路径是:
- 让商家端也调用注册接口,并在请求体里带上 `merchant_id`(建议同时传 `appid`,默认为 `default`
```bash
curl -X POST http://localhost:7301/api/v1/push/register \
-H 'Content-Type: application/json' \
-d '{"cid":"<CID>","merchant_id":"<MERCHANT_UUID>","platform":"android","appid":"default"}'
```
- 验证是否写入成功:
- `GET /api/v1/push/devices?merchant_id=<MERCHANT_UUID>&active=true`(优先读 Supabase
- 或直接在数据库查:
```sql
select id, appid, cid, user_id, merchant_id, is_active, last_seen_at
from public.push_devices
where merchant_id = '<MERCHANT_UUID>'
order by last_seen_at desc nulls last;
```
- push-server consumer 对 merchant 的设备查询路径REST
- `push_devices?merchant_id=eq.<recipient_id>&is_active=eq.true`
---

View File

@@ -126,13 +126,14 @@ flowchart LR
- `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`
> 当前实现的 `dedupe_key` 使用稳定哈希(基于 tracking_no / carrier / status_code / event_time / event_code / event_text 的截断组合),用于保证 webhook 重复回调不会重复写入事件表,从而避免重复入队/重复通知
### 3.4 验签(可选)
- 配置 `WEBHOOK_SECRET` 后会校验:
- `X-Timestamp``X-Signature`
- 签名算法:`HMAC-SHA256(secret, bodyText + timestamp)`,输出 hex
- 目前验签失败不会直接拒绝入库(会在 raw 表记录 `signature_valid=false`,可按需要升级为“验签失败直接 4xx”
- 签名算法:`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`,优先级是:

View File

@@ -86,6 +86,13 @@
- `NOTIFY_WORKER_SUPA_USE_BEARER`(可选,默认 false仅对 notify-worker 生效,若为 `true` 则发送 `Authorization: Bearer <SUPA_KEY>`
- `SUPA_USE_BEARER`(可选,默认 false全局开关会影响其它服务一般不建议在自托管 JWT_SECRET 不一致时开启)
RLS 开启且 Bearer/JWT 不可用时的替代方案(慎用/止血):
- `NOTIFY_WORKER_USE_RLS_SAFE_RPC`(可选,默认 false若为 `true`,则不再直查 `ml_orders`,改为调用 `rpc/notify_get_order_recipients`SECURITY DEFINER获取 `user_id/merchant_id`
- `NOTIFY_WORKER_RPC_TOKEN`(当上面为 true 时必需notify-worker 调用 RPC 时通过请求头 `x-notify-worker-token` 传递;数据库函数会校验该 token。
配套 SQL
- `pages/mall/delivery/doc/需求文档/db/绕过RLS的方案(慎用)/20260311_add_rpc_notify_get_order_recipients.sql`
worker 行为:
- `NOTIFY_WORKER_POLL_MS`(默认 2000
- `NOTIFY_WORKER_BATCH_SIZE`(默认 20

View File

@@ -21,6 +21,8 @@
新增/修改的接口(简要)
- GET `/health` — 健康检查。
- POST `/api/v1/push/register` — 注册/更新设备;会写本地 `server/data/push_devices.json`,并尝试 upsert 到 Supabase `push_devices` 表(如果配置了 SUPA_URL + SERVICE_ROLE_KEY
- 支持字段:`cid`(必填)、`user_id`(可选)、`merchant_id`(可选)、`platform`(可选)、`appid`(可选,默认 `default`)。
- 注意:数据库侧 `push_devices` 的唯一约束是 `(appid, cid)`;服务端 upsert 使用 `on_conflict=appid,cid`
- 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`)。
@@ -76,7 +78,14 @@ curl http://localhost:7301/health
```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"}'
-d '{"cid":"test-cid-1","user_id":"<USER_UUID>","platform":"android","appid":"default"}'
```
商家端注册(写 merchant_id 维度,避免 `send_status=no-targets`
```bash
curl -X POST http://localhost:7301/api/v1/push/register \
-H 'Content-Type: application/json' \
-d '{"cid":"test-cid-2","merchant_id":"<MERCHANT_UUID>","platform":"android","appid":"default"}'
```
4) 按 user 发通知(写入 express_notifications 并触发推送):
@@ -396,11 +405,11 @@ Auto-deploy process exited with code=0 signal=null
```
- 关键环境变量:`SUPA_URL`、`SUPA_KEY`Supabase REST、`CLOUD_FUNC_URL`(云函数 invoke URL、`PUSH_TOKEN`(云函数鉴权)。
- 行为摘要:
- 轮询 `express_notifications`status_code IS NULL并选取记录
- 通过带过滤的 PATCH 抢占(将 `status_code` 设为 `processing`)以避免并发重复处理;
- 轮询 `express_notifications``send_status IS NULL` 或 `send_status='retrying'` 且到期)并选取记录;
- 通过带过滤的 PATCH 抢占(将 `send_status` 设为 `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 优化并发策略
- 根据调用结果回写 `express_notifications.send_status` 为 `success` / `failed` / `no-targets`(失败时会写入 `retry_count/last_error/next_attempt_at` 用于退避重试)
- 限制与扩展点:当前 consumer 依赖 Supabase REST并发控制基于“先查后 claim”的方式实现适合 MVP。如需更强一致性/高并发,可进一步改造为 DB 端锁(`FOR UPDATE SKIP LOCKED`)或引入队列系统
文档实践
---------------------------------