修改消息后台的启动和停止文件并整理文档
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"push-server": 27380,
|
||||
"webhook-receiver": 13140,
|
||||
"notify-worker": 37804
|
||||
}
|
||||
349
server/README.md
349
server/README.md
@@ -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.<user>` 返回 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=<user-uuid>' -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`)
|
||||
|
||||
<!--
|
||||
仅云函数模式:本仓库当前已将 push-server 改为只走 CLOUD_FUNC_URL。
|
||||
|
||||
历史能力(已注释/不再使用):
|
||||
- PUSH_PROXY_URL / PUSH_PROXY_TOKEN:将 /api/v1/push/send 代理到真实推送服务
|
||||
- UNI_PUSH_URL / UNI_PUSH_*:直接调用 dCloud uni-push HTTP 接口
|
||||
-->
|
||||
|
||||
快速使用:
|
||||
|
||||
```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":"<user-uuid>","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 <SUPA_KEY>`;默认只发送 `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":"<user-uuid>","platform":"android","appid":"default","is_active":true}]'
|
||||
```
|
||||
|
||||
如需我在本地使用你的 Supabase 凭证演示一次完整的注册→查询→发送流程,或将持久化切换为仅 Supabase(移除本地 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",
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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"
|
||||
@@ -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 '"
|
||||
# Fallback: ensure ports are freed even if pid file is missing/invalid.
|
||||
Stop-ListeningPort 7201
|
||||
Stop-ListeningPort 7301
|
||||
@@ -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 <PID> -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`),再重试发送。
|
||||
@@ -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) 先启动后台(见上节)。
|
||||
@@ -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_KEY>`
|
||||
- `SUPA_USE_BEARER`(可选,默认 false):全局开关(会影响其它服务;一般不建议在自托管 JWT_SECRET 不一致时开启)
|
||||
|
||||
worker 行为:
|
||||
- `NOTIFY_WORKER_POLL_MS`(默认 2000)
|
||||
79
server/消息推送文档/README.md
Normal file
79
server/消息推送文档/README.md
Normal 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 body(JSON)示例:
|
||||
```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,我可以继续提交修改。
|
||||
116
server/消息推送文档/UNI_PUSH2_CLOUD_FUNCTION.md
Normal file
116
server/消息推送文档/UNI_PUSH2_CLOUD_FUNCTION.md
Normal file
@@ -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_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-worker:server/notify-worker.js
|
||||
- 云函数目录(示例):uniCloud-alipay/cloudfunctions/testUnipush2
|
||||
- 自动部署脚本:server/tools/deploy-cloudfunc.js
|
||||
|
||||
|
||||
---
|
||||
作者:自动生成文档(可手动微调)
|
||||
日期:2026-03-11
|
||||
Reference in New Issue
Block a user