diff --git a/pages/mall/delivery/doc/需求文档/20260224_add_push_devices_and_notifications.sql b/pages/mall/delivery/doc/需求文档/db/20260224_add_push_devices_and_notifications.sql similarity index 100% rename from pages/mall/delivery/doc/需求文档/20260224_add_push_devices_and_notifications.sql rename to pages/mall/delivery/doc/需求文档/db/20260224_add_push_devices_and_notifications.sql diff --git a/pages/mall/delivery/doc/需求文档/20260309_add_express_notifications_send_status.sql b/pages/mall/delivery/doc/需求文档/db/20260309_add_express_notifications_send_status.sql similarity index 100% rename from pages/mall/delivery/doc/需求文档/20260309_add_express_notifications_send_status.sql rename to pages/mall/delivery/doc/需求文档/db/20260309_add_express_notifications_send_status.sql diff --git a/pages/mall/delivery/doc/需求文档/20260309_add_notify_queue_and_trigger.sql b/pages/mall/delivery/doc/需求文档/db/20260309_add_notify_queue_and_trigger.sql similarity index 100% rename from pages/mall/delivery/doc/需求文档/20260309_add_notify_queue_and_trigger.sql rename to pages/mall/delivery/doc/需求文档/db/20260309_add_notify_queue_and_trigger.sql diff --git a/pages/mall/delivery/doc/需求文档/20260310_fix_express_notifications_on_conflict_message_id.sql b/pages/mall/delivery/doc/需求文档/db/20260310_fix_express_notifications_on_conflict_message_id.sql similarity index 100% rename from pages/mall/delivery/doc/需求文档/20260310_fix_express_notifications_on_conflict_message_id.sql rename to pages/mall/delivery/doc/需求文档/db/20260310_fix_express_notifications_on_conflict_message_id.sql diff --git a/pages/mall/delivery/doc/需求文档/express_tracking_platform_upgrade.sql b/pages/mall/delivery/doc/需求文档/db/express_tracking_platform_upgrade.sql similarity index 100% rename from pages/mall/delivery/doc/需求文档/express_tracking_platform_upgrade.sql rename to pages/mall/delivery/doc/需求文档/db/express_tracking_platform_upgrade.sql diff --git a/server/.runtime/delivery-backend.pids.json b/server/.runtime/delivery-backend.pids.json deleted file mode 100644 index b0880c3d..00000000 --- a/server/.runtime/delivery-backend.pids.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "push-server": 27380, - "webhook-receiver": 13140, - "notify-worker": 37804 -} diff --git a/server/README.md b/server/README.md deleted file mode 100644 index 82c98ae1..00000000 --- a/server/README.md +++ /dev/null @@ -1,349 +0,0 @@ -如需我在本地使用你的 Supabase 凭证演示一次完整的注册→查询→发送流程,或将持久化切换为仅 Supabase(移除本地 JSON 回退),请直接告诉我你的选择并提供测试凭证或确认权限范围。 - -## Cloud 函数 & 消费者(自动调用) - -- 说明:`server/push-server.js` 并非由 Supabase 在插入时直接触发云函数;当启用消费者(轮询)时,服务会定期读取 Supabase 表 `express_notifications` 的待处理记录,然后对配置的 `CLOUD_FUNC_URL` 发起 POST 请求。 -- 关键代码位置(仓库): - - 轮询待推送记录: [server/push-server.js](server/push-server.js#L271-L286) (`fetchPendingNotifications`) - - 调用云函数的实现: [server/push-server.js](server/push-server.js#L313-L323) (`invokeCloudFuncForCid`) - - 在处理记录時对每个 `cid` 调用云函数: [server/push-server.js](server/push-server.js#L360-L370) - - 启动消费者(定时轮询): [server/push-server.js](server/push-server.js#L417-L418) (`setInterval(consumerOnce, CONSUMER_POLL_MS)`) - - 写入通知的 HTTP 接口(会插入 `express_notifications`): [server/push-server.js](server/push-server.js#L594-L610) (`POST /api/v1/notifications`) - -- 所需/可选环境变量: - - 必要:`SUPA_URL`、`SUPA_KEY`(或 `SERVICE_ROLE_KEY`,用于读取/写入 Supabase) - - 启用消费者:`ENABLE_CONSUMER=true` 或 `CONSUMER_ENABLED=true` - - 云函数地址:`CLOUD_FUNC_URL`(每个目标 `cid` 会对该 URL 发 POST) - - 可选鉴权透传:`PUSH_TOKEN`(会在 POST body 的 `token` 字段中传递) - - 轮询与重试配置:`CONSUMER_POLL_MS`、`MAX_RETRIES`、`RETRY_INITIAL_MS`、`RETRY_FACTOR`、`RETRY_MAX_MS` - -- POST 请求体(发送到 `CLOUD_FUNC_URL`): - -```json -{ - "token": "(来自 env:PUSH_TOKEN 或 null)", - "push_clientid": "目标 cid", - "title": "通知标题", - "content": "通知内容", - "payload": { } -} -``` - -- 成功判定: - - HTTP 层面:云函数应返回 HTTP 2xx(服务会把非 2xx 或网络错误视为失败并按重试策略重试或标记失败)。 - - 业务层面(推荐约定):若云函数返回 JSON 且包含 `ok:false` 或 `errCode!=0`,push-server 会视为失败并写入 `last_error`。 - -- 快速启用示例(PowerShell): - -```powershell -$env:SUPA_URL="https://your-supabase.example" -$env:SUPA_KEY="your-service-role-key" -$env:CLOUD_FUNC_URL="https://your-cloudfunc.example/handle" -$env:ENABLE_CONSUMER="true" -node server/push-server.js -``` - -- 本地测试:直接调用云函数验证可达性: - -```bash -curl -X POST https://your-cloudfunc.example/handle -H "Content-Type: application/json" \ - -d '{"token":"test","push_clientid":"CID123","title":"测试","content":"hello","payload":{}}' -``` - -- 通过完整链路测试(写入通知 -> 消费者轮询 -> 云函数 POST): - -> 注意:`POST /api/v1/notifications` 在“仅云函数模式”下只负责把通知写入 `express_notifications`(排队),不会在该请求内立即下发;实际下发由消费者轮询后对 `CLOUD_FUNC_URL` 执行 POST。 - -> 前置条件:目标用户/商户必须在 `push_devices` 表(或本地 `server/data/push_devices.json`)中存在至少一个 `is_active=true` 的设备,否则该条通知会被标记为 `no-targets`。 - -```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"},"payload":{}}' -``` - -- 端到端验证(推荐步骤,PowerShell): - -```powershell -# 1) 注册一个设备(cid + user_id),写入 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) 写入一条通知到 express_notifications(排队) -$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 查询最近记录的 `send_status` / `last_error`: - -> 说明:`status_code` 是物流/业务状态(由 notify-worker 写入);consumer 的投递状态使用 `send_status`。 -> 若你的数据库还没有该字段,请先执行迁移:`pages/mall/delivery/doc/需求文档/20260309_add_express_notifications_send_status.sql`。 - -```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; -``` - -- 常见现象解释: - - push-server 日志里 `supaFetch response preview: []`:表示当前没有 pending/retrying 且到期的记录可处理(队列为空)。 - -## 常见问题:手机没通知/显示 ???? - -### 1) `/api/v1/push/send` 返回 `errCode: 400, errMsg: "push_clientid required"` - -含义:云函数没有拿到 `push_clientid`,因此没有真正调用 uni-push 下发。 - -常见原因:`CLOUD_FUNC_URL` 指向的是“HTTP 触发”云函数(例如 cloudbasefunction.cn)。这类云函数往往把 JSON 请求体放在 `event.body`(字符串)里,而不是直接放在 `event.push_clientid`。 - -云函数侧的最小兼容写法(示例): - -```js -let input = event || {} -if (input && typeof input.body === 'string') { - try { input = JSON.parse(input.body) } catch (e) { input = {} } -} else if (input && input.body && typeof input.body === 'object') { - input = input.body -} -const { token, push_clientid, title, content, payload } = input -``` - -### 2) 手机通知标题/内容变成 `????` - -含义:中文在“发出请求前”已被错误编码成问号(常见于 Windows PowerShell 5.1 发送 JSON)。 - -推荐做法:用 UTF-8 字节发送请求体(并带上 charset): - -```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`(更接近 Linux curl 行为)来避免 PowerShell 的编码坑。 - -### 3) 云函数“回显 recv”后反而收不到消息 - -如果你为了排查乱码在云函数里加了 `return { recv: ... }` 之类的提前返回,云函数会在执行 `uniPush.sendMessage()` 之前就退出,自然不会真的推送。排查完成后请移除/注释该提前 return。 - -- 注意:如果你需要“插入时立即触发云函数”的实时行为,可考虑将轮询改为 Supabase Realtime 订阅或使用 Supabase 的 Edge Function / webhook 触发器;我可以协助把轮询替换为实时订阅的示例实现。 - ---- - -## 故障排查记录(已执行) - -- 我们在仓库中运行了两个辅助脚本以排查 Supabase 凭证问题: - - `server/probe_supa.js`:对单个 key 发起 REST 请求并打印响应(用于快速验证)。 - - `server/try_supa_keys.js`:扫描仓库中形似 JWT 的字符串并逐一尝试对 Supabase REST 发起请求,打印每个 candidate 的 HTTP 状态与响应预览。 - -- 排查结果摘要: - - 在仓库中发现 6 个看起来像 JWT 的候选 key,均对 `GET /rest/v1/push_devices?user_id=eq.` 返回 HTTP 401(包括 `PGRST301` / `Invalid authentication credentials`)。 - - 说明这些仓库内的字符串不是当前 Supabase 项目可用的 `service_role` key,或 Supabase 项目/Key 已被替换/撤销,或请求的 Supabase URL 与当时不同。 - -## 推荐操作(二选一) - -### A)使用正确的 Supabase `service_role` key(推荐) - -1. 在 Supabase 控制台:Project → Settings → API → Service role key,复制该 key(仅在后端使用,切勿公开)。 -2. 在服务器启动前注入环境变量并启动: - -```powershell -$env:SUPA_URL='http://192.168.1.62:18000' -$env:SUPA_KEY='(粘贴你的 service_role key)' -node server/push-server.js -``` - -或后台运行并记录日志: - -```powershell -$env:SUPA_URL='http://192.168.1.62:18000' -$env:SUPA_KEY='(你的 key)' -Start-Process node -ArgumentList 'server/push-server.js' -RedirectStandardOutput '.\\server.log' -RedirectStandardError '.\\server.err' -PassThru -``` - -运行后可检查 `server.log`(包含 `supaFetch` 调试输出),并用以下命令验证: - -```powershell -Invoke-RestMethod -Uri 'http://localhost:7301/api/v1/push/devices?user_id=' -Method GET | ConvertTo-Json -Depth 5 -``` - -### B)绕过 Supabase(仅用于本地快速验证) - -如果你只是想立即验证“从服务端发起 -> 调用云函数”的链路,可把已知的 device(cid + user_id)写入本地 `server/data/push_devices.json`,并确保已配置 `CLOUD_FUNC_URL`,然后调用 `/api/v1/push/send`(会直接 POST 到云函数): - -```powershell -# 查看本地 devices 文件 -Get-Content .\server\data\push_devices.json -Raw | ConvertFrom-Json - -# 手动追加(示例,替换 cid 与 user_id) -$devs = Get-Content .\server\data\push_devices.json -Raw | ConvertFrom-Json -$devs += @{ cid='d9aa69ec415...'; user_id='a8e3a568-fc1f-4237-bcc5-5722e2fca0a3'; platform='android'; created_at=(Get-Date).ToString('o'); updated_at=(Get-Date).ToString('o'); active=$true } -$devs | ConvertTo-Json -Depth 5 | Set-Content .\server\data\push_devices.json -Encoding utf8 - -# 然后测试发送(会直接调用 CLOUD_FUNC_URL) -Invoke-RestMethod -Uri 'http://localhost:7301/api/v1/push/send' -Method POST -ContentType 'application/json' -Body (@{ user_id='a8e3a568-fc1f-4237-bcc5-5722e2fca0a3'; notification=@{ title='测试'; body='hello' } } | ConvertTo-Json) -``` - -## 已添加的辅助脚本 - -- `server/probe_supa.js` — 对单个 key 发起请求并打印响应(用于快速验证)。 -- `server/try_supa_keys.js` — 在仓库中查找 JWT 样式字符串并逐一尝试调用 Supabase REST,打印每个 candidate 的状态与响应预览。 - -如果你把正确的 `service_role` key 提供给我(仅用于本次会话),我可以代为注入环境、重启服务并把 `supaFetch` 的调试输出和最终结果贴回给你;或我可以现在把截图中已有的 cid/user_id 写入本地 JSON 并立即做一次 mock 下发测试。 -# Push Server (开发用) - -这是一个用于本地开发与调试的轻量 Node.js 推送后端(mock)。 - -功能: -- 注册/更新设备:`POST /api/v1/push/register` { cid, user_id, platform } -- 注销设备:`POST /api/v1/push/unregister` { cid | user_id } -- 列出设备:`GET /api/v1/push/devices?user_id=...&active=true|false` -- 发送推送(云函数模式):`POST /api/v1/push/send` { cids:[], user_id, notification, payload }(直接 POST 到 `CLOUD_FUNC_URL`) - - - -快速使用: - -```bash -cd server -npm install -npm start -``` - -默认监听端口:`7301`,可通过 `PORT` 环境变量修改。 - -设备存储在 `server/data/push_devices.json`,用于本地持久化。 - -可选:直接写入 Supabase - -服务器可以可选地将 `push_devices` 记录直接写入 Supabase/Postgres(适用于生产或集成测试)。启用方法:在启动前设置下列环境变量: - -- `SUPA_URL` — Supabase 项目 URL,例如 `https://xyzcompany.supabase.co` -- `SUPA_KEY` — Supabase `service_role` key(仅在服务器端使用,切勿暴露给客户端) - -当这两个变量存在时,服务会通过 PostgREST REST API 向 `/rest/v1/push_devices` 发起 upsert(使用 `on_conflict=cid`),并在注销时将设备的 `is_active` 标记为 `false`。同时本地 JSON 存储仍会保留为回退。 - -示例(PowerShell): - -```powershell -$env:SUPA_URL = 'https://xyz.supabase.co' -$env:SUPA_KEY = 'eyJhbGci...' -npm start -``` - -请确保你的 Supabase 项目已经包含与仓库迁移脚本匹配的 `push_devices` 表: -`pages/mall/delivery/doc/需求文档/20260224_add_push_devices_and_notifications.sql`。 - -Supabase 表结构与权限(快速参考) - -下面是 `push_devices` 表的最小字段集合,供你在 Supabase 中快速验证或手动创建: - -```sql -CREATE TABLE public.push_devices ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - user_id uuid NULL, - cid varchar(255) NOT NULL, - platform varchar(32) NOT NULL DEFAULT 'android', - appid varchar(128) NOT NULL DEFAULT 'default', - is_active boolean NOT NULL DEFAULT true, - last_seen_at timestamptz NULL, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now() -); -CREATE UNIQUE INDEX IF NOT EXISTS ux_push_devices_appid_cid ON public.push_devices(appid, cid); -``` - -权限与安全建议: -- 生产环境请务必只在后端使用 `service_role`(`SUPA_KEY`),不要将其暴露给客户端。后端应负责向 Supabase 写入/更新设备记录。 -- 如果你需要通过客户端直接调用 Supabase REST(不推荐),请配置 RLS 策略并只允许安全的最小操作。 -- 推荐做法:后端使用 `service_role` 写入;客户端只调用后端的 `/api/v1/push/register`。 - -示例:使用 `service_role` 通过 PostgREST 插入设备(等同于本服务中执行的请求): - -```bash -curl -X POST "${SUPA_URL}/rest/v1/push_devices?on_conflict=cid" \ - -H "apikey: ${SUPA_KEY}" \ - # 如需发送 Authorization: Bearer,请设置 SUPA_USE_BEARER=true(默认只发送 apikey) - -H "Content-Type: application/json" \ - -d '[{"cid":"CID_TEST_001","user_id":"","platform":"android","appid":"default","is_active":true}]' -``` - -注意事项: -- 如果你使用的是 Supabase 的 `uuid_generate_v4()` 功能,可能需要在数据库安装 `uuid-ossp` 或使用 `gen_random_uuid()`(pgcrypto)。迁移脚本中已包含兼容性处理。 -- 本服务对 Supabase 的写入为异步非阻塞:写入失败不会阻止本地响应,但会在日志中记录错误。可根据需要修改为同步错误返回策略。 - -如需我把本地 JSON 回退移除并只保留 Supabase 持久化(需你确认并提供测试凭证),我可以继续实现并更新代码与文档。 - -## 服务说明(快速参考) - -**服务简介** - -- **目的**:为开发/集成阶段提供一个轻量的推送后端,用于接收客户端上报的推送客户端 ID(CID)、管理设备状态,并在开发中触发/代理推送以验证链路。 -- **主要职责**:接收设备注册/注销、列出设备、触发推送(mock 或代理到真实推送服务)、可选将设备写入 Supabase。 - -**暴露的接口(HTTP)** -- `GET /health`:健康检查 -- `POST /api/v1/push/register`:注册或更新设备,body: `{ cid, user_id, platform, ... }` -- `POST /api/v1/push/unregister`:注销设备,body: `{ cid | user_id }` -- `GET /api/v1/push/devices`:列出设备,query: `user_id`, `active` -- `POST /api/v1/push/send`:发送推送(仅云函数:直接调用 `CLOUD_FUNC_URL`;未配置则返回错误) - -**数据与持久化** -- 开发默认将设备保存在本地文件:`server/data/push_devices.json`(回退/离线使用)。 -- 可选:当设置 `SUPA_URL` 和 `SUPA_KEY` 时,服务会通过 Supabase REST(PostgREST)向 `push_devices` 表 upsert/patch 同步设备记录(异步、非阻塞)。 - -**重要环境变量** -- `PORT`:监听端口(默认 7301) -- `SUPA_URL` / `SUPA_KEY` / `SUPA_SCHEMA`:启用 Supabase 同步(`SUPA_KEY` 应为服务端 `service_role`,仅服务器端使用) -- `SUPA_USE_BEARER`:(可选)仅当为 `true` 时才发送 `Authorization: Bearer `;默认只发送 `apikey`。 -- `CLOUD_FUNC_URL` / `PUSH_TOKEN`:云函数调用地址 / (可选)鉴权 token - -**安全与部署注意** -- 切勿将 `SUPA_KEY`(service_role)暴露给客户端;只能在后端使用。 -- 生产场景应:后端持有凭证并负责写入/验证;对客户端使用后端接口,不直接对 Supabase 写入。 -- 如直接允许客户端写 Supabase,必须开启 RLS 并最小化权限(一般不推荐)。 - -**表结构与迁移** -- 已提供迁移脚本:`pages/mall/delivery/doc/需求文档/20260224_add_push_devices_and_notifications.sql`(包含 `push_devices` 与 `express_notifications` 表及触发器、索引)。 - -**快速测试示例** -PowerShell: -```powershell -Invoke-RestMethod -Uri 'http://localhost:7301/api/v1/push/register' -Method POST -ContentType 'application/json' -Body '{"cid":"CID_TEST_001","user_id":"user-1","platform":"android"}' -``` -curl(示例 upsert 到 Supabase,需替换变量): -```bash -curl -X POST "${SUPA_URL}/rest/v1/push_devices?on_conflict=cid" \ - -H "apikey: ${SUPA_KEY}" \ - -H "Authorization: Bearer ${SUPA_KEY}" \ - -H "Content-Type: application/json" \ - -d '[{"cid":"CID_TEST_001","user_id":"","platform":"android","appid":"default","is_active":true}]' -``` - -如需我在本地使用你的 Supabase 凭证演示一次完整的注册→查询→发送流程,或将持久化切换为仅 Supabase(移除本地 JSON 回退),请直接告诉我你的选择并提供测试凭证或确认权限范围。 diff --git a/server/config.json b/server/config.json index ccd9ca7c..5f5175eb 100644 --- a/server/config.json +++ b/server/config.json @@ -1,6 +1,8 @@ { "SUPA_URL": "http://192.168.1.62:18000", "SUPA_KEY": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UtMSIsImlhdCI6MTc2OTY3NjQ5OCwiZXhwIjoxOTI3MzU2NDk4fQ.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", + "SUPA_USE_BEARER": "false", + "NOTIFY_WORKER_SUPA_USE_BEARER": "true", "CLOUD_FUNC_URL": "https://env-00jy5x5oy9zd.dev-hz.cloudbasefunction.cn/test", "ENABLE_CONSUMER": "true", "CONSUMER_POLL_MS": "2000", diff --git a/server/notify-worker.js b/server/notify-worker.js index 1425cd82..690fc55d 100644 --- a/server/notify-worker.js +++ b/server/notify-worker.js @@ -31,19 +31,26 @@ const fetchImpl = (globalThis.fetch ? globalThis.fetch.bind(globalThis) : (() => const SUPA_URL = (process.env.SUPA_URL || process.env.SUPA_URL_OVERRIDE || '').replace(/\/$/, '') // Prefer service role key when present (server-side), to avoid RLS issues. const SUPA_KEY = process.env.SERVICE_ROLE_KEY || process.env.SUPA_KEY || '' -const SUPA_USE_BEARER = (process.env.SUPA_USE_BEARER === 'true') +// notify-worker needs to read ml_orders which is often protected by RLS. +// To avoid changing behavior of other services, allow a worker-specific toggle. +const SUPA_USE_BEARER = ( + process.env.NOTIFY_WORKER_SUPA_USE_BEARER || + process.env.WORKER_SUPA_USE_BEARER || + process.env.SUPA_USE_BEARER || + 'false' +) === 'true' const POLL_MS = Number(process.env.NOTIFY_WORKER_POLL_MS || process.env.WORKER_POLL_MS || 2000) const BATCH_SIZE = Number(process.env.NOTIFY_WORKER_BATCH_SIZE || process.env.WORKER_BATCH_SIZE || 20) const RUN_ONCE = (process.env.RUN_ONCE === 'true' || process.env.RUN_ONCE === '1') -function supaFetch(restPath, opts = {}) { +function supaFetch(restPath, opts = {}, useBearer = false) { const url = `${SUPA_URL}/rest/v1/${restPath}` const headers = Object.assign({}, opts.headers || {}, { apikey: SUPA_KEY, Accept: 'application/json' }) - if (SUPA_USE_BEARER) headers.Authorization = `Bearer ${SUPA_KEY}` + if (useBearer) headers.Authorization = `Bearer ${SUPA_KEY}` return fetchImpl(url, Object.assign({}, opts, { headers })) } @@ -58,7 +65,7 @@ function buildMessageId({ aud, waybill_id, dedupe_key }) { async function fetchPendingQueue(limit) { const q = `notify_queue?processed_at=is.null&order=created_at.asc&limit=${encodeURIComponent(String(limit))}` - const resp = await supaFetch(q, { method: 'GET' }) + const resp = await supaFetch(q, { method: 'GET' }, false) if (!resp.ok) { const txt = await resp.text().catch(() => '') throw new Error(`fetch notify_queue failed: HTTP ${resp.status} ${txt}`) @@ -67,7 +74,11 @@ async function fetchPendingQueue(limit) { } async function fetchWaybill(waybillId) { - const resp = await supaFetch(`platform_express_waybills?id=eq.${encodeURIComponent(waybillId)}&select=id,order_id,order_no,carrier,tracking_no`, { method: 'GET' }) + const resp = await supaFetch( + `platform_express_waybills?id=eq.${encodeURIComponent(waybillId)}&select=id,order_id,order_no,carrier,tracking_no`, + { method: 'GET' }, + false + ) if (!resp.ok) { const txt = await resp.text().catch(() => '') throw new Error(`fetch waybill failed: HTTP ${resp.status} ${txt}`) @@ -77,7 +88,11 @@ async function fetchWaybill(waybillId) { } async function fetchOrderById(orderId) { - const resp = await supaFetch(`ml_orders?id=eq.${encodeURIComponent(orderId)}&select=id,order_no,user_id,merchant_id`, { method: 'GET' }) + const resp = await supaFetch( + `ml_orders?id=eq.${encodeURIComponent(orderId)}&select=id,order_no,user_id,merchant_id`, + { method: 'GET' }, + SUPA_USE_BEARER + ) if (!resp.ok) { const txt = await resp.text().catch(() => '') throw new Error(`fetch order (id) failed: HTTP ${resp.status} ${txt}`) @@ -87,7 +102,11 @@ async function fetchOrderById(orderId) { } async function fetchOrderByNo(orderNo) { - const resp = await supaFetch(`ml_orders?order_no=eq.${encodeURIComponent(orderNo)}&select=id,order_no,user_id,merchant_id`, { method: 'GET' }) + const resp = await supaFetch( + `ml_orders?order_no=eq.${encodeURIComponent(orderNo)}&select=id,order_no,user_id,merchant_id`, + { method: 'GET' }, + SUPA_USE_BEARER + ) if (!resp.ok) { const txt = await resp.text().catch(() => '') throw new Error(`fetch order (order_no) failed: HTTP ${resp.status} ${txt}`) diff --git a/server/scripts/start-delivery-backend.ps1 b/server/scripts/start-delivery-backend.ps1 index 7ab931fc..2321e126 100644 --- a/server/scripts/start-delivery-backend.ps1 +++ b/server/scripts/start-delivery-backend.ps1 @@ -54,6 +54,7 @@ foreach ($s in $services) { Ensure-Dir (Join-Path $root 'server\.runtime') $pidsFile = Join-Path $root 'server\.runtime\delivery-backend.pids.json' -$pids | ConvertTo-Json -Depth 5 | Set-Content -Path $pidsFile -Encoding UTF8 +$json = $pids | ConvertTo-Json -Depth 5 +[IO.File]::WriteAllText($pidsFile, $json + "`r`n", (New-Object System.Text.UTF8Encoding($false))) Write-Host "Saved pids: $pidsFile" Write-Host "Done. Use test sender: node .\pages\mall\delivery\webhook-server\test-send.js" \ No newline at end of file diff --git a/server/scripts/stop-delivery-backend.ps1 b/server/scripts/stop-delivery-backend.ps1 index a5867970..427b9d8f 100644 --- a/server/scripts/stop-delivery-backend.ps1 +++ b/server/scripts/stop-delivery-backend.ps1 @@ -1,27 +1,60 @@ -$ErrorActionPreference = 'SilentlyContinue' +$ErrorActionPreference = 'Stop' + +function Stop-ListeningPort($port) { + try { + $lines = netstat -ano | Select-String (":$port ") + foreach ($l in $lines) { + $m = [regex]::Match($l.ToString(), "\sLISTENING\s+(\d+)$") + if ($m.Success) { + $procId = [int]$m.Groups[1].Value + try { + Stop-Process -Id $procId -Force -ErrorAction Stop + Write-Host "Stopped PID $procId on port $port" + } catch { + Write-Host "Failed to stop PID $procId on port ${port}: $($_.Exception.Message)" + } + } + } + } catch { + Write-Host "Failed to inspect port ${port}: $($_.Exception.Message)" + } +} $root = Resolve-Path (Join-Path $PSScriptRoot "..\..") $pidsFile = Join-Path $root 'server\.runtime\delivery-backend.pids.json' if (Test-Path $pidsFile) { try { - $pids = Get-Content $pidsFile -Raw | ConvertFrom-Json - foreach ($name in $pids.PSObject.Properties.Name) { - $pid = [int]$pids.$name - if ($pid -gt 0) { + # Windows PowerShell 5.1 + Set-Content -Encoding UTF8 often writes BOM; trim it before JSON parse. + $raw = Get-Content -Path $pidsFile -Raw -Encoding UTF8 + if ($raw.Length -gt 0 -and $raw[0] -eq [char]0xFEFF) { $raw = $raw.Substring(1) } + $pids = $raw | ConvertFrom-Json + foreach ($prop in $pids.PSObject.Properties) { + $name = $prop.Name + $procPid = [int]$prop.Value + if ($procPid -gt 0) { try { - Stop-Process -Id $pid -Force - Write-Host "Stopped $name PID=$pid" + Stop-Process -Id $procPid -Force -ErrorAction Stop + Write-Host "Stopped $name PID=$procPid" } catch { - Write-Host "Failed to stop $name PID=$pid" + Write-Host "Failed to stop $name PID=${procPid}: $($_.Exception.Message)" } } } + + try { + Remove-Item -Path $pidsFile -Force -ErrorAction Stop + Write-Host "Removed pid file: $pidsFile" + } catch { + Write-Host "Failed to remove pid file: $($_.Exception.Message)" + } } catch { - Write-Host "Failed to parse $pidsFile" + Write-Host "Failed to parse ${pidsFile}: $($_.Exception.Message)" } } else { Write-Host "No pid file found: $pidsFile" } -Write-Host "If ports still occupied, run: netstat -ano | Select-String ':7201 ' / ':7301 '" \ No newline at end of file +# Fallback: ensure ports are freed even if pid file is missing/invalid. +Stop-ListeningPort 7201 +Stop-ListeningPort 7301 \ No newline at end of file diff --git a/docs/DELIVERY_E2E_TROUBLESHOOTING_20260310.md b/server/消息推送文档/DELIVERY_E2E_TROUBLESHOOTING_20260310.md similarity index 74% rename from docs/DELIVERY_E2E_TROUBLESHOOTING_20260310.md rename to server/消息推送文档/DELIVERY_E2E_TROUBLESHOOTING_20260310.md index ac0879b9..958fe01a 100644 --- a/docs/DELIVERY_E2E_TROUBLESHOOTING_20260310.md +++ b/server/消息推送文档/DELIVERY_E2E_TROUBLESHOOTING_20260310.md @@ -28,9 +28,15 @@ ALTER TABLE public.ml_orders DISABLE ROW LEVEL SECURITY; ``` +> 备注:你当前观察到“把安全策略关上就可以进行推送”,就是因为 notify-worker 能重新读到 `ml_orders.user_id/merchant_id` 以解析收件人。 + **恢复建议(生产化方向)** - 不建议长期关闭 RLS。 -- 建议让后端使用可被 PostgREST 正确解码/信任的 service_role JWT,或通过 RPC/安全视图完成必要查询;避免依赖 `DISABLE 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 key(JWT) +- 备选:通过 RPC/安全视图完成必要查询;避免依赖 `DISABLE RLS`。 --- @@ -98,6 +104,33 @@ Stop-Process -Id -Force --- +## 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`,但手机没弹通知 **现象** @@ -179,6 +212,15 @@ 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`),再重试发送。 diff --git a/docs/DELIVERY_WEBHOOK_PUSH_SERVER_OVERVIEW.md b/server/消息推送文档/DELIVERY_WEBHOOK_PUSH_SERVER_OVERVIEW.md similarity index 96% rename from docs/DELIVERY_WEBHOOK_PUSH_SERVER_OVERVIEW.md rename to server/消息推送文档/DELIVERY_WEBHOOK_PUSH_SERVER_OVERVIEW.md index b754dde9..7bc5e66d 100644 --- a/docs/DELIVERY_WEBHOOK_PUSH_SERVER_OVERVIEW.md +++ b/server/消息推送文档/DELIVERY_WEBHOOK_PUSH_SERVER_OVERVIEW.md @@ -411,6 +411,19 @@ powershell -ExecutionPolicy Bypass -File .\server\scripts\stop-delivery-backend. > 说明:脚本按自身路径定位仓库 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) 先启动后台(见上节)。 diff --git a/server/DEPLOY_WORKFLOW.md b/server/消息推送文档/DEPLOY_WORKFLOW.md similarity index 100% rename from server/DEPLOY_WORKFLOW.md rename to server/消息推送文档/DEPLOY_WORKFLOW.md diff --git a/server/NOTIFY_WORKER_README.md b/server/消息推送文档/NOTIFY_WORKER_README.md similarity index 91% rename from server/NOTIFY_WORKER_README.md rename to server/消息推送文档/NOTIFY_WORKER_README.md index 664c45e4..1f0a364a 100644 --- a/server/NOTIFY_WORKER_README.md +++ b/server/消息推送文档/NOTIFY_WORKER_README.md @@ -25,7 +25,7 @@ - **运行时**:Node.js(建议 18+,以确保 `fetch` 可用;旧版本会回退到 `node-fetch`) - **通信方式**:直接调用 Supabase PostgREST REST API(`$SUPA_URL/rest/v1/...`) -- **鉴权**:默认仅发送 `apikey` header;仅当 `SUPA_USE_BEARER=true` 才附加 `Authorization: Bearer ...` +- **鉴权**:默认仅发送 `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 @@ -83,7 +83,8 @@ ### 4.2 关键环境变量 - `SUPA_URL`(必需) - `SERVICE_ROLE_KEY` 或 `SUPA_KEY`(必需,推荐 service_role) -- `SUPA_USE_BEARER`(可选,默认 false) +- `NOTIFY_WORKER_SUPA_USE_BEARER`(可选,默认 false):仅对 notify-worker 生效,若为 `true` 则发送 `Authorization: Bearer ` +- `SUPA_USE_BEARER`(可选,默认 false):全局开关(会影响其它服务;一般不建议在自托管 JWT_SECRET 不一致时开启) worker 行为: - `NOTIFY_WORKER_POLL_MS`(默认 2000) diff --git a/server/PUSH_SERVER_README.md b/server/消息推送文档/PUSH_SERVER_README.md similarity index 100% rename from server/PUSH_SERVER_README.md rename to server/消息推送文档/PUSH_SERVER_README.md diff --git a/server/PUSH_WORKFLOW.md b/server/消息推送文档/PUSH_WORKFLOW.md similarity index 100% rename from server/PUSH_WORKFLOW.md rename to server/消息推送文档/PUSH_WORKFLOW.md diff --git a/server/消息推送文档/README.md b/server/消息推送文档/README.md new file mode 100644 index 00000000..716fba4e --- /dev/null +++ b/server/消息推送文档/README.md @@ -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 body(JSON)示例: +```json +{ + "token":"", + "push_clientid":"", + "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,我可以继续提交修改。 diff --git a/server/消息推送文档/UNI_PUSH2_CLOUD_FUNCTION.md b/server/消息推送文档/UNI_PUSH2_CLOUD_FUNCTION.md new file mode 100644 index 00000000..267672af --- /dev/null +++ b/server/消息推送文档/UNI_PUSH2_CLOUD_FUNCTION.md @@ -0,0 +1,116 @@ +Uni‑Push2 云函数化指南 + +概述 +- 后台实现概览: + - `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`(云函数),或在非云函数模式下调用推送通道。 + - 云函数(Uni‑Push2):实际负责把推送请求转为厂商推送(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_clientid":"CID_TEST","title":"测试","content":"hello","payload":{}}' +``` +- PowerShell 注意 UTF-8 编码,避免中文变成问号: +```powershell +$json = '{"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-worker:server/notify-worker.js +- 云函数目录(示例):uniCloud-alipay/cloudfunctions/testUnipush2 +- 自动部署脚本:server/tools/deploy-cloudfunc.js + + +--- +作者:自动生成文档(可手动微调) +日期:2026-03-11