推送接入
本页说明推送相关能力的接入步骤。webhook 部分是当前可用的全部能力;token、通道、点击跳转与角标为规划中,接口与命名尚未冻结,请勿在代码中依赖。
本项目没有 APNs、FCM 或国内厂商的发送实现,没有通知点击跳转与角标。
已实现:设备 token 台账(客户端 REST 注册/查询/注销,幂等 + 每用户 20 个上限)、6 个厂商通道的配置存储与校验、投递尝试记录(no_token / not_configured,sent 恒 0)与手动重试。
另有服务端“离线消息 webhook”与 push.send:接收者无在线连接时 POST 应用配置的 URL,3 秒超时、无签名/重试。
完整状态机与新接口见 推送通道与投递;本页保留为链路背景。
步骤 0:配置应用推送 webhook(已实现)
通过 Admin API 设置应用级 webhook。示例使用本地地址,仅用于联调;生产环境请使用 https,并在接收端做鉴权与幂等。
curl -sS -X POST http://127.0.0.1:8080/api/admin/apps/demo/push \
-H 'X-Admin-Secret: dev-admin' \
-H 'content-type: application/json' \
-d '{"webhookUrl":"http://127.0.0.1:9000/hook","enabled":true}'
# 查询当前配置
curl -sS http://127.0.0.1:8080/api/admin/apps/demo/push \
-H 'X-Admin-Secret: dev-admin'| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| webhookUrl | string | 是 | http(s) 地址;服务端 POST JSON,3 秒超时 |
| enabled | boolean | 是 | false 关闭派发但保留 URL |
步骤 1:实现 webhook 接收端(已实现)
接收端只需要一个能处理 JSON POST 的 HTTP 服务。注意:服务端不签名、不重试,建议按 message.messageId 去重并落库后再做业务处理。
import { createServer } from 'node:http';
const seen = new Set();
createServer((req, res) => {
if (req.method !== 'POST' || req.url !== '/hook') {
res.writeHead(404).end();
return;
}
let raw = '';
req.on('data', (chunk) => { raw += chunk; });
req.on('end', () => {
const payload = JSON.parse(raw);
const message = payload.message;
// 幂等:同一条消息只处理一次
if (!seen.has(message.messageId)) {
seen.add(message.messageId);
console.log('[offline]', payload.userId, message.content);
// 在这里调用 APNs/FCM/厂商通道(本项目尚未实现)
}
res.writeHead(200, { 'content-type': 'application/json' });
res.end('{"ok":true}');
});
}).listen(9000, '127.0.0.1', () => console.log('webhook listening on :9000'));步骤 2:制造离线消息并验证(已实现)
- 让接收者离线
接收者断开 WS 连接或退出应用;只要该用户没有在线会话,消息就会进入离线队列并成为推送候选。
- 发送消息
用 发送单聊消息 或客户端 SDK 向离线用户发送任意已实现类型(text/image/file/voice/location/custom)。
- 观察 webhook
接收端应收到
{ appkey, userId, reason: "offline", message };reason固定为 offline。 - 核对日志
用 push.log.list 或控制台推送页核对 pushId 与投递结果。
接收者仍在线(包括 Web/PC 在线)时不会触发;聊天室消息不落离线、不触发;系统通知不经过该 webhook;撤回不新增消息、不触发。
步骤 3:发送不落地通知(已实现,但仅 webhook)
push.send 按范围投递标题/内容/extras,不写入任何会话。下面的 Node 示例使用 Open API 签名;scope=user 时 target 传 userId。
import { createHash, randomBytes } from 'node:crypto';
const BASE = 'https://im.example.com';
const APP_KEY = 'demo';
const APP_SECRET = 'YOUR_APP_SECRET';
const nonce = randomBytes(16).toString('hex');
const timestamp = String(Date.now());
const signature = createHash('sha1').update(APP_SECRET + nonce + timestamp).digest('hex');
const res = await fetch(BASE + '/im/v2/push/send', {
method: 'POST',
headers: {
'content-type': 'application/json',
'App-Key': APP_KEY,
Nonce: nonce,
Timestamp: timestamp,
Signature: signature,
},
body: JSON.stringify({
scope: 'user',
target: 'user2',
title: '系统通知',
content: '你有一条新的业务提醒',
extras: { route: '/orders/9f2a' },
}),
});
console.log(await res.json());响应中的 pushId 可用于 push.stats.by-id 查询投递统计。未配置 webhook 时 delivered=0,但推送日志仍会记录。
步骤 4:设备 token 注册(已实现)
客户端用 Bearer 用户 token 调 POST /api/apps/{appkey}/push/tokens 注册厂商 token;服务端按 (userId, platform, token) 幂等,每用户最多 20 个活跃 token,同一 token 被其他用户注册时旧记录自动停用。
| 端 | 需要获取的 token | 注册方式 | 状态 |
|---|---|---|---|
| Android | 各厂商推送 SDK 的 registrationId / deviceToken | REST 注册,platform=android 或具体厂商 | ✅ 台账可用 |
| iOS | APNs deviceToken | REST 注册,platform=ios | ✅ 台账可用 |
| Web / Electron | 浏览器 PushSubscription(如使用 Web Push) | REST 注册,platform=web | ✅ 台账可用 |
| Flutter / Native | 依赖宿主平台的厂商通道 token | 由宿主 App 注册 | ✅ 台账可用 |
Web / Electron SDK 已提供 registerPushToken/unregisterPushToken/getPushTokens;Android / Flutter / Native / iOS 尚未实现,请直接调用 REST。
注册 token 只代表“台账里有设备”,厂商发送仍未实现:投递记录只会得到 not_configured。
步骤 5:通道与证书配置(仅存储)
控制台「推送通道」/ Admin API 可以保存 APNs、FCM、华为、小米、OPPO、vivo 六个通道的必填字段与启用位;敏感字段回读脱敏,通道测试返回 implemented=false。这一步不会让通知真正发出。
| 通道 | 必填字段 | 本项目 |
|---|---|---|
| APNs | keyId / teamId / bundleId / privateKey(production 可选) | 🟡 配置可存;发送未实现 |
| FCM | projectId / serviceAccountJson | 🟡 配置可存;发送未实现 |
| 华为 / 小米 / OPPO / vivo | appId/appKey/appSecret/packageName 等(见通道页) | 🟡 配置可存;发送未实现 |
| RongPush | 融云自建通道 | ⛔ 不在范围 |
步骤 6:点击跳转(规划中)
融云在点击远程通知时发出隐式 Intent(会话 / 会话列表 / 不落地通知),也支持 PushEventListener 自定义。本项目没有通知,因此没有点击事件可处理。
- 现有 webhook 载荷已包含
message.conversationType、targetId、messageId,应用可据此自行构造跳转。 - 规划中的客户端 SDK 需要提供通知点击回调与默认会话跳转;命名未冻结,暂不在文档中给出。
步骤 7:角标(规划中)
本项目只有会话 unreadCount,没有系统角标同步、没有免打扰会话的角标抑制规则,也没有厂商角标接口。规划中。
步骤 8:测试方法
| 测试项 | 方法 | 当前能否验证 |
|---|---|---|
| webhook 配置生效 | Admin API 设置后查询返回 webhookUrl/enabled | ✅ 可以 |
| 离线消息触发 | 接收者离线 → 发消息 → 本地接收器收到 POST | ✅ 可以 |
| 在线不触发 | 接收者保持 WS 连接 → 发消息 → 接收器无请求 | ✅ 可以 |
| 关闭派发 | enabled=false 后重复离线消息 → 无请求 | ✅ 可以 |
| 不落地通知 | push.send 后接收器收到标题/内容/extras | ✅ 可以(仅 webhook) |
| 设备 token 注册/注销 | REST 注册 → 管理端台账可查;重复注册 tokenId 不变 | ✅ 可以 |
| 投递状态诚实性 | Admin 创建投递 → no_token/not_configured,sent=0 | ✅ 可以 |
| 厂商通道到达 | 模拟器/真机收到系统通知并点击进会话 | ⛔ 无法验证(通道未实现) |
| 角标/多语言 | 系统角标与推送语言切换 | ⛔ 无法验证(未实现) |
当前只能提供 webhook 的本地接收日志与推送日志;不能声称“模拟器收到推送”,因为不存在通道实现。
常见问题
| 现象 | 排查 |
|---|---|
| 接收器无请求 | 确认接收者确实全部离线;确认 enabled=true、URL 为 http(s);查看服务端日志 [push] webhook error |
| 收到重复请求 | 服务端无重试,但多端/多次触发可能重复;按 messageId 去重 |
| 3 秒超时 | 接收端同步处理过慢;先落库快速 200,再异步处理 |
| 想直接接 APNs | 当前需在 webhook 接收端自行实现;本项目不提供证书与通道管理层 |
相关页面
- 推送通道与投递:token 台账、6 个通道、投递状态机与重试
- 推送总览
- push.send
- push.log.list
- 消息计数、存储与推送策略
厂商通道发送尚未提供;在具备可验证的发送证据之前,本页“规划中”结论不得改写为已实现。