**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` 已禁用 UNI‑PUSH/代理分支，发送只走 `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）。
  - 支持字段：`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`）。
- 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 REST（PostgREST）地址（内部建议 `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","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 并触发推送）：
```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"}}'
```

UNI‑PUSH 集成注意事项
<!--
仅云函数模式：此服务不再直接对接 UNI‑PUSH；请在云函数内部完成 UNI‑PUSH2 调用与鉴权。
-->

故障与排查要点
- 如果 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 endpoint（upload 接口）
  - `CLOUD_UPLOAD_TOKEN` : 上传接口的 Bearer token
  - `CLOUD_DEPLOY_API` : （可选）触发云函数部署/发布的 API
  - `CLOUD_DEPLOY_TOKEN` : （可选）部署 API 的 token
  - `CLOUD_FUNC_URL` : （可选）已部署云函数的外网 URL，CI 将对其执行一次 smoke 测试
  - `PUSH_TOKEN` : （可选）云函数鉴权 token，用于 smoke 测试
  - `TEST_DEVICE_CID` : （可选）用于 CI smoke 测试的设备 CID

- 我也添加了本地 helper 脚本：`server/tools/ci-deploy.ps1`，可在本地执行相同流程（打包 -> 上传 -> 触发 -> 调用）。

如果需要，我可以：
- 把 adapter 的请求体精确匹配你现有成功的 uni-push curl（请把 curl 发来）；或
- 为通知添加重试/记录字段并实现简单重试机制。

自动部署成功示例
-----------------

下面是一个你实际执行并确认成功的示例，便于复制到文档或在团队内复现：

- 启用自动部署并设置 token（PowerShell 示例）：

```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) CI（GitHub 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`（`send_status IS NULL` 或 `send_status='retrying'` 且到期）并选取记录；
  - 通过带过滤的 PATCH 抢占（将 `send_status` 设为 `processing`）以避免并发重复处理；
  - 查询目标设备（`push_devices`），对每个 `cid` 构造 event 并 POST 到 `CLOUD_FUNC_URL`；若未配置 `CLOUD_FUNC_URL` 则本次处理将失败并写入错误原因；
  - 根据调用结果回写 `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`）或引入队列系统。

文档实践
---------------------------------
- 变更策略：今后每次由自动化工具或我生成/修改后端代码时，将同步把该变更的作用、配置项与启用方法记录到本文件（`server/PUSH_SERVER_README.md`）或对应 workflow 文档中，确保运行/运维人员能直接查阅到最新说明。

