From ee9fabd806e88e564458b51686e747c320da5980 Mon Sep 17 00:00:00 2001 From: not-like-juvenile <16056107+not-like-juvenile@user.noreply.gitee.com> Date: Mon, 9 Mar 2026 17:27:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A8=A1=E6=8B=9F=E7=AC=AC=E4=B8=89=E6=96=B9?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E5=AD=98=E5=85=A5=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E5=B9=B6=E8=BF=9B=E8=A1=8C=E6=B6=88=E6=81=AF=E6=8E=A8=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/DELIVERY_WEBHOOK_PUSH_SERVER_OVERVIEW.md | 278 ++++++++++++++++++ server/PUSH_SERVER_README.md | 29 ++ server/README.md | 51 +++- server/push-server.js | 50 +++- .../cloudfunctions/testUnipush2/index.js | 34 +-- 5 files changed, 413 insertions(+), 29 deletions(-) create mode 100644 docs/DELIVERY_WEBHOOK_PUSH_SERVER_OVERVIEW.md diff --git a/docs/DELIVERY_WEBHOOK_PUSH_SERVER_OVERVIEW.md b/docs/DELIVERY_WEBHOOK_PUSH_SERVER_OVERVIEW.md new file mode 100644 index 00000000..e55df740 --- /dev/null +++ b/docs/DELIVERY_WEBHOOK_PUSH_SERVER_OVERVIEW.md @@ -0,0 +1,278 @@ +# 物流 Webhook 接收器 & Push Server(推送后台)总览 + +面向读者:需要理解“物流事件如何入库、如何触发消息、如何推送到 App”的后端/运维/联调同学。 + +本文只做**总结性说明**;细节请跳转到对应子文档与源码。 + +--- + +## 1. 这两套后台分别解决什么问题? + +### Webhook 接收器(webhook-receiver) +**一句话**:把第三方/承运方推过来的“物流轨迹事件”接进来,完成验签(可选)、留痕,并把事件写入 Supabase 的 `platform_express_*` 表。 + +它关注的是: +- Webhook 请求能否稳定接收(可公网暴露、可控验签) +- 原始 payload 是否可追溯(用于审计/排障) +- 轨迹事件是否能归一化写入(供业务查询、消息生成) +- 运单摘要状态是否能更新(用于页面“当前状态”展示) + +### Push Server(push-server) +**一句话**:维护“账号 ↔ 设备 CID”的映射,并提供“消息入队 + 消费下发”能力,把通知可靠地投递到云函数(由云函数完成 uni-push2 真实下发)。 + +它关注的是: +- 设备 CID 的注册/解绑与存储(本地 JSON 回退 + Supabase 表) +- 生成推送任务(写入 `express_notifications`) +- 消费推送任务并调用 `CLOUD_FUNC_URL`(重试/回写状态) + +--- + +## 1.1 用什么做的(技术栈/依赖) + +这两套服务都是“轻量 Node.js HTTP 服务”,共同特点: +- **运行时**:Node.js(建议 18+;你当前环境是 Node.js 22 也可以) +- **HTTP 框架**:Express +- **JSON 解析**:body-parser +- **与 Supabase 通信**:直接调用 PostgREST REST API(`$SUPA_URL/rest/v1/...`),通过 `apikey`(可选 Bearer)鉴权 +- **配置加载**:复用 `server/load-config.js`,支持 `.env` / `.json` / `CONFIG_FILE` 指定 + +差异点: +- webhook-receiver:额外用 `crypto` 做可选 HMAC 验签;主要写 `platform_express_*` 三表 +- push-server:额外包含“消费者轮询 + 重试退避 + 调用云函数”的逻辑;本地 JSON 作为设备表回退(`server/data/push_devices.json`) + +--- + +## 2. 整体链路(从物流事件到手机通知) + +建议把链路理解为 3 段: + +1) **事件入库段**(Webhook 接收器负责) +- 第三方回调 → webhook-receiver → 写入/更新: + - `platform_express_event_raw`(原始留痕) + - `platform_express_tracking_events`(归一化事件事实表) + - `platform_express_waybills`(运单摘要 current_status_*) + +2) **消息生成段**(通常是“事件处理器/任务/触发器”,不在这两个服务里) +- 监听/轮询 `platform_express_tracking_events` 的新增事件 +- 按推送策略(关键状态、去噪、幂等)生成“消息中心记录/推送任务” +- 写入 `express_notifications`(或调用 push-server 的 HTTP 接口让其写入) + +3) **推送下发段**(Push Server + 云函数负责) +- push-server consumer 读取 `express_notifications` 的待处理记录 +- 找到目标设备(`push_devices` 中的活跃 CID) +- 对每个 CID POST 到 `CLOUD_FUNC_URL` +- 云函数内部调用 uni-push2 下发 → App 收到通知 + +一个简化示意图: + +```mermaid +flowchart LR + Third[第三方/承运方] -->|POST webhook| WH[webhook-receiver :7201] + WH --> RAW[(platform_express_event_raw)] + WH --> EV[(platform_express_tracking_events)] + WH --> WB[(platform_express_waybills)] + + EV --> EP[事件处理器/任务/触发器] + EP --> N[(express_notifications)] + + App[App 客户端] -->|注册CID| PS[push-server :7301] + PS --> D[(push_devices)] + + PS -->|轮询待发通知| N + PS -->|POST| CF[CLOUD_FUNC_URL 云函数] + CF --> UP[uni-push2] + UP --> App +``` + +参考(更完整的业务口径与隐私约束): +- `pages/mall/delivery/doc/需求文档/物流消息推送方案_用户端与商家端.md` +- `pages/mall/delivery/doc/需求文档/推送与设备需求文档.md` + +--- + +## 3. Webhook 接收器(webhook-receiver) + +### 3.1 位置与入口 +- 代码:`pages/mall/delivery/webhook-server/webhook-receiver.js` +- 说明文档:`pages/mall/delivery/webhook-server/README.md` + +### 3.2 对外接口 +- `POST /webhook/express/status` + - 作用:接收轨迹变更回调 + - 返回: + - 成功:`{ ok: true }` + - 运单不存在(不算系统错误):`{ ok: false, message: 'waybill not found' }`(HTTP 200) + - Supabase 鉴权失败等:HTTP 502 + `{ ok:false, message:'...' }` +- `GET /health`:健康检查 + +### 3.3 写库行为(核心表) +接收器在一次 webhook 调用中做三件事(顺序上:先留痕,再归一化,再更新摘要): + +1) **原始留痕**:插入 `platform_express_event_raw` +- 保存:carrier、tracking_no、body(原始 payload)、received_at、signature_valid 等 + +2) **定位运单**:在 `platform_express_waybills` 中查找对应记录 +- 优先按 `tracking_no`,其次按 `order_no` +- 找不到时返回 `{ ok:false, message:'waybill not found' }` + +3) **写入事件 & 更新运单摘要** +- 将第三方的状态字段映射到平台统一的 `status_code`(如 `OUT_FOR_DELIVERY/DELIVERED/EXCEPTION/...`) +- `PATCH platform_express_waybills`:更新 `current_status_code/current_status_text/last_synced_at` +- `INSERT platform_express_tracking_events`:写入归一化事件事实数据(包含 raw_payload、dedupe_key 等) + +> 注意:当前实现的 `dedupe_key` 使用 `WEBHOOK_ + Date.now()`,更偏向“留痕与追溯”;如需严格幂等(重复回调不重复入库),建议按文档口径构造稳定 dedupe_key(例如 `tracking_no|event_code|event_time`)。 + +### 3.4 验签(可选) +- 配置 `WEBHOOK_SECRET` 后会校验: + - `X-Timestamp`、`X-Signature` + - 签名算法:`HMAC-SHA256(secret, bodyText + timestamp)`,输出 hex +- 目前验签失败不会直接拒绝入库(会在 raw 表记录 `signature_valid=false`),可按需要升级为“验签失败直接 4xx”。 + +### 3.5 配置与启动方式 +接收器会先通过 `server/load-config.js` 把配置注入 `process.env`,优先级是: +1) 系统环境变量 +2) `CONFIG_FILE/CONFIG_PATH` 指定的文件 +3) 接收器同目录的 `webhook.config.json` +4) `server/.env` → `server/config.json` → `server/config.json.example` + +常用环境变量: +- `SUPA_URL`(必需) +- `SUPA_KEY`(必需,建议使用 service_role,仅后端使用) +- `PORT`(默认 7201) +- `SUPA_USE_BEARER`(默认 false) +- `WEBHOOK_SECRET`(可选) + +--- + +## 4. Push Server(push-server) + +### 4.1 位置与入口 +- 代码:`server/push-server.js` +- 说明文档(偏“可运行/可运维”): + - `server/PUSH_SERVER_README.md` + - `server/README.md` + +### 4.2 核心能力 +1) **设备 CID 管理** +- `POST /api/v1/push/register`:注册/更新设备(写本地 JSON,并尝试 upsert 到 `push_devices`) +- `POST /api/v1/push/unregister`:解绑/置 inactive +- `GET /api/v1/push/devices`:列出设备(优先 Supabase) + +2) **推送下发(云函数模式)** +- `POST /api/v1/push/send`: + - 直接按 `cids` 或 `user_id` 发送(对每个 CID 调用 `CLOUD_FUNC_URL`) + +3) **通知入队 + 消费者轮询下发** +- `POST /api/v1/notifications`:写入 `express_notifications`(排队) +- consumer(可选启用): + - 定时轮询 `express_notifications` 的待处理记录 + - 取到记录后查询目标 CID 列表 + - 逐个调用 `CLOUD_FUNC_URL`,并回写状态/错误/重试次数 + +> 备注:本仓库当前为“仅云函数模式”,push-server 自身不直接对接 uni-push;真实推送逻辑在云函数里。 + +### 4.3 数据依赖(Supabase 表) +- `public.push_devices` + - 存储 `cid ↔ user_id/merchant_id`、`is_active` 等 +- `public.express_notifications` + - 存储待发通知、状态、重试信息等(用于消息中心/推送队列) + +迁移脚本参考:`pages/mall/delivery/doc/需求文档/20260224_add_push_devices_and_notifications.sql` + +### 4.4 关键配置(env) +- `PORT`:默认 7301 +- `SUPA_URL`、`SUPA_KEY`/`SERVICE_ROLE_KEY` +- `SUPA_USE_BEARER`:同 webhook-receiver,默认只发 `apikey` + +消费者相关: +- `ENABLE_CONSUMER=true`(或 `CONSUMER_ENABLED=true`) +- `CONSUMER_POLL_MS`:轮询间隔(默认 2000) +- `CLOUD_FUNC_URL`:云函数 HTTP invoke 地址(必需) +- `PUSH_TOKEN`:可选鉴权透传给云函数 +- 重试配置:`MAX_RETRIES / RETRY_INITIAL_MS / RETRY_FACTOR / RETRY_MAX_MS` + +### 4.5 常见问题(定位方向) +- Supabase 报 `PGRST301` / 401:通常是 Bearer/JWT_SECRET 不匹配导致,优先只用 `apikey`(`SUPA_USE_BEARER=false`)。 +- `/api/v1/push/send` 返回 `push_clientid required`:云函数侧没有正确解析请求体(常见于 HTTP 触发器把 body 放在 `event.body`)。 +- 中文变 `????`:Windows PowerShell 5.1 发送 JSON 编码问题,按 `server/PUSH_SERVER_README.md` 的 UTF-8 字节方式发送。 +- 消费者不工作:检查 `ENABLE_CONSUMER`、`CLOUD_FUNC_URL` 是否配置;再查 `express_notifications` 是否有待处理记录。 + +--- + +## 5. 给联调/运维的“最小心智模型” + +- Webhook-receiver 只负责:**接收** & **入库**(`platform_express_*`)。 +- Push-server 只负责:**设备表** & **通知队列** & **调用云函数**(`push_devices/express_notifications`)。 +- 中间那段“什么时候该推、推给谁、推什么文案/脱敏后字段”通常在**事件处理器/业务服务**里实现(或数据库触发器 + 消费者)。 + +把这三段拆开,排障会非常快: +1) webhook 是否收到?(看 webhook-receiver 日志 / raw 表) +2) tracking_events 是否写入?(看事件表) +3) notifications 是否生成?(看 express_notifications) +4) push-server 是否消费?(看 push-server 日志 / 重试字段) +5) 云函数是否下发成功?(看云函数日志 / uni-push 返回) + +--- + +## 5.1 一页摘要 + +### 业务价值(为什么要做) +- **提升履约感知**:物流关键节点及时触达(用户端/商家端),减少客服咨询与退款/纠纷风险。 +- **可追溯**:Webhook 原始留痕 + 事件事实表 + 消息队列表,出了问题能定位“卡在哪一段”。 +- **解耦与可演进**:Webhook 入库、消息生成、推送下发三段解耦,后续可替换推送通道/触发方式而不重写全链路。 + +### 范围边界(做了什么 / 没做什么) +- **已覆盖**:第三方物流事件接收入库;设备 CID 注册与管理;通知入队与消费;调用云函数(由云函数实际对接 uni-push2)。 +- **不在本两服务内**:消息生成策略(哪些事件要推、文案/脱敏、收件人映射、幂等 dedupe_key 规则)通常由“事件处理器/业务服务/任务”负责。 + +### 外部依赖(上线前必须明确) +- **Supabase/Postgres**:两服务都依赖 REST 读写(需要稳定网络、正确的 key 权限、相关表已迁移)。 +- **云函数**:push-server 依赖 `CLOUD_FUNC_URL` 可用;云函数内部需能成功调用 uni-push2。 +- **第三方/承运方**:Webhook 接入参数、验签口径、回调重试策略需要对齐。 + +### 风险与控制点 +- **密钥风险**:`service_role key` 属高权限,必须只在服务器环境变量中使用;严禁进入前端/日志/截图。 +- **隐私合规**:raw 表与 payload 可能含敏感字段,必须限制读取权限;推送内容只允许“脱敏/清洗后的摘要”。 +- **可靠性**:Webhook 可重复/乱序;需要稳定的幂等策略(事件 dedupe_key、消息 dedupe_key)避免重复消息与状态回退。 +- **可用性**:push-server consumer 失败要可重试、可观测、可告警;云函数故障不应影响主业务写库。 + +### 运维成本(需要投入多少人/怎么管) +- **部署形态**:两个常驻 Node 进程(端口默认 webhook 7201、push 7301),建议用进程守护(Windows 服务 / pm2 / docker / supervisor 任选其一)。 +- **日志与排障**:至少保留 7~30 天日志;能按 request_id/message_id/tracking_no 追踪。 + +### 建议监控指标(可用来验收与周报) +- Webhook:每分钟请求量、2xx 比例、验签失败率、写库失败率、waybill not found 比例。 +- 推送:待处理队列长度(pending/retrying 数)、每分钟消费量、成功率、平均重试次数、失败 Top 原因(HTTP 非 2xx / 超时 / 云函数业务 errCode)。 +- 端到端:从事件入库到推送送达的 P50/P95 延迟(分钟级即可)。 + +### 责任分工(出现问题找谁) +- webhook-receiver:对“第三方回调接入、验签、入库”负责。 +- 事件处理器/业务服务:对“消息生成规则、脱敏文案、收件人映射、幂等策略”负责。 +- push-server + 云函数:对“队列消费、重试、推送通道调用成功率”负责。 + +### 上线检查清单(最小版) +1) 表结构已迁移(`platform_express_*`、`push_devices`、`express_notifications`)。 +2) 密钥仅在服务器环境变量,日志不打印敏感值。 +3) webhook `/health` 正常;能写 raw 与 events;运单状态能更新。 +4) push-server `/health` 正常;能注册设备;能入队;consumer 能调用云函数并回写状态。 +5) 监控与告警已配置(至少:连续失败、队列堆积、云函数不可达)。 + +--- + +## 6. 进一步阅读(从“总览”到“可落地”) + +- webhook-receiver:`pages/mall/delivery/webhook-server/README.md` +- push-server(运行与变更记录):`server/PUSH_SERVER_README.md` +- push-server(消费者与云函数约定):`server/README.md` +- 配置加载器:`server/load-config.js` +- 业务方案与隐私口径: + - `pages/mall/delivery/doc/需求文档/物流消息推送方案_用户端与商家端.md` + - `pages/mall/delivery/doc/需求文档/推送与设备需求文档.md` + +--- + +## 7. 安全提醒(强烈建议) + +- `SUPA_KEY/service_role key` 只允许在后端/服务器环境使用,**严禁下发到前端**。 +- 如需把 push-server 的发送/入队接口对外开放,建议至少加一层鉴权(Bearer token、IP 白名单或内网访问)。 +- `platform_express_event_raw.body/raw_payload` 可能包含敏感信息,生产上建议严格控制读取权限并记录审计。 diff --git a/server/PUSH_SERVER_README.md b/server/PUSH_SERVER_README.md index 3d1d286d..20d5ffb8 100644 --- a/server/PUSH_SERVER_README.md +++ b/server/PUSH_SERVER_README.md @@ -124,6 +124,35 @@ UNI‑PUSH 集成注意事项 - 如果 `/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`)以限制谁能发送通知。 diff --git a/server/README.md b/server/README.md index c60d8a26..ed9a3366 100644 --- a/server/README.md +++ b/server/README.md @@ -29,7 +29,9 @@ } ``` -- 成功判定:云函数应返回 HTTP 2xx(服务会把非 2xx 或网络错误视为失败并按重试策略重试或标记失败)。 +- 成功判定: + - HTTP 层面:云函数应返回 HTTP 2xx(服务会把非 2xx 或网络错误视为失败并按重试策略重试或标记失败)。 + - 业务层面(推荐约定):若云函数返回 JSON 且包含 `ok:false` 或 `errCode!=0`,push-server 会视为失败并写入 `last_error`。 - 快速启用示例(PowerShell): @@ -94,6 +96,53 @@ 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 触发器;我可以协助把轮询替换为实时订阅的示例实现。 --- diff --git a/server/push-server.js b/server/push-server.js index 3678df09..a40ad07d 100644 --- a/server/push-server.js +++ b/server/push-server.js @@ -161,15 +161,29 @@ async function start() { async function claimNotification(id) { try { const body = { status_code: 'processing', updated_at: new Date().toISOString() } - // 仅当当前仍为 pending 或 retrying 且到期时,才抢占 + // 仅当当前仍为 pending 或 retrying 且到期时,才抢占。 + // 为了避免在 PATCH 中使用复杂 or= 逻辑树导致匹配失败,这里拆成两次尝试。 const now = new Date().toISOString() - const path = `express_notifications?id=eq.${encodeURIComponent(id)}&or=(status_code.is.null,and(status_code.eq.retrying,or(next_attempt_at.is.null,next_attempt_at.lte.${encodeURIComponent(now)})))` - const resp = await supaFetch(path, { method: 'PATCH', headers: { 'Content-Type': 'application/json', Prefer: 'return=representation' }, body: JSON.stringify(body) }) - if (!resp.ok) return null - const j = await resp.json().catch(() => null) - if (Array.isArray(j)) return j.length > 0 ? j[0] : null - if (!j || !j.id) return null - return j + const attempts = [ + // pending: status_code IS NULL + `express_notifications?id=eq.${encodeURIComponent(id)}&status_code=is.null`, + // retrying: status_code='retrying' and next_attempt_at is null or <= now + `express_notifications?id=eq.${encodeURIComponent(id)}&status_code=eq.retrying&or=(next_attempt_at.is.null,next_attempt_at.lte.${encodeURIComponent(now)})` + ] + + for (const path of attempts) { + const resp = await supaFetch(path, { method: 'PATCH', headers: { 'Content-Type': 'application/json', Prefer: 'return=representation' }, body: JSON.stringify(body) }) + if (!resp.ok) continue + const j = await resp.json().catch(() => null) + if (Array.isArray(j)) { + if (j.length > 0) return j[0] + // debug hint: matched 0 rows + try { console.log('claimNotification: updated 0 rows for', path) } catch (e) {} + continue + } + if (j && j.id) return j + } + return null } catch (e) { console.warn('claimNotification error', e) return null @@ -192,7 +206,17 @@ async function start() { const txt = await resp.text().catch(() => '') let json try { json = JSON.parse(txt) } catch (e) { json = { statusText: txt } } - return { ok: resp.ok, status: resp.status, body: json } + // Treat explicit business-level failures as errors even when HTTP is 2xx. + // Many cloud functions return `{ ok: false, ... }` with HTTP 200. + const businessOk = (() => { + if (!json || typeof json !== 'object') return true + if (json.ok === false) return false + // uniCloud/uni-push demo often returns { errCode: 0|..., errMsg: '...' } + if (typeof json.errCode === 'number') return json.errCode === 0 + if (typeof json.errCode === 'string' && json.errCode.trim() !== '') return json.errCode === '0' + return true + })() + return { ok: resp.ok && businessOk, httpOk: resp.ok, status: resp.status, body: json } } catch (e) { return { ok: false, error: String(e) } } @@ -239,7 +263,13 @@ async function start() { if (CLOUD_FUNC_URL) { const calls = await Promise.all(targets.map(cid => invokeCloudFuncForCid(CLOUD_FUNC_URL, PUSH_TOKEN, cid, notification && notification.title, notification && notification.body, payload))) for (const c of calls) { - if (!c.ok) { allOk = false; lastNote = (c.error || JSON.stringify(c.body)).toString().substring(0, 1000); break } + if (!c.ok) { + allOk = false + const err = c.error ? String(c.error) : '' + const bodyStr = c.body ? JSON.stringify(c.body) : '' + lastNote = `cloudfunc failed: status=${c.status || ''} httpOk=${c.httpOk === true} ${err} ${bodyStr}`.trim().substring(0, 1000) + break + } } } else { allOk = false diff --git a/uniCloud-alipay/cloudfunctions/testUnipush2/index.js b/uniCloud-alipay/cloudfunctions/testUnipush2/index.js index 6324b57e..005cca8f 100644 --- a/uniCloud-alipay/cloudfunctions/testUnipush2/index.js +++ b/uniCloud-alipay/cloudfunctions/testUnipush2/index.js @@ -1,25 +1,25 @@ -// 简单的使用示例 'use strict'; -// 可配置的云函数 uni-push 示例:从 event 中读取参数并校验简单 token -const uniPush = uniCloud.getPushManager({ appId: process.env.UNI_PUSH_APPID || "__UNI__9462CA7" }); + +const uniPush = uniCloud.getPushManager({ + appId: process.env.UNI_PUSH_APPID || "__UNI__9462CA7" +}); exports.main = async (event, context) => { - // event: { token, push_clientid, title, content, payload } - const { token, push_clientid, title = '', content = '', payload = {} } = event || {}; + // 兼容 HTTP 触发:参数在 event.body(string/json)里 + 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; - // 简单鉴权,建议把真实 secret 存到云函数环境变量 PUSH_TOKEN const SECRET = process.env.PUSH_TOKEN; - if (SECRET && token !== SECRET) { - console.warn('unauthorized token', { hasToken: !!token }); - return { errCode: 401, errMsg: 'unauthorized' }; - } - - if (!push_clientid) { - return { errCode: 400, errMsg: 'push_clientid required' }; - } + if (SECRET && token !== SECRET) return { errCode: 401, errMsg: 'unauthorized' }; + if (!push_clientid) return { errCode: 400, errMsg: 'push_clientid required' }; try { - console.log('sending uni-push', { push_clientid, title, content, payload }); const res = await uniPush.sendMessage({ push_clientid, force_notification: true, @@ -28,10 +28,8 @@ exports.main = async (event, context) => { settings: { ttl: 86400000 }, payload }); - console.log('uni-push response', res); return { errCode: 0, errMsg: 'success', data: res }; } catch (e) { - console.error('uni-push send error', e && (e.stack || e.message || e)); - return { errCode: 500, errMsg: e && e.message ? e.message : 'push error', detail: e }; + return { errCode: 500, errMsg: e?.message || 'push error' }; } }; \ No newline at end of file