推送通道与投递

本页是推送能力的当前事实源:设备 token 台账已经实现,厂商通道发送仍未实现。投递管线会真实记录 no_token / not_configured,sent 恒为 0,绝不伪造成功。旧的 推送总览 保留作背景阅读。

能力边界(先读这一段)

已实现:设备 token 注册/查询/注销(客户端 REST + 管理端台账)、6 个厂商通道的配置存储与校验、投递尝试记录(no_token/not_configured)与手动重试、推送统计中的 token/delivery 维度。

未实现:APNs/FCM/华为/小米/OPPO/vivo 的真实发送(不持有凭据、不发起网络请求)、通知点击跳转、角标、通知分组、多语言推送。

离线消息 webhook 与 push.send 是另一条链路(fire-and-forget,3 秒超时、无重试/签名),不要与厂商通道混淆。

端到端链路​

环节当前实现状态
1. 客户端取得厂商 token由宿主 App 集成厂商 SDK 后自行取得(服务端不提供获取能力)✅ 由业务侧完成
2. 注册设备 token客户端 REST POST /api/apps/{appkey}/push/tokens(Bearer 用户 token)✅ 已实现(台账 + 幂等 + 换绑)
3. 配置厂商通道Admin API POST /api/admin/apps/{appkey}/push/channels(6 个通道)✅ 仅存储/校验/脱敏,deliveryImplemented=false
4. 选择 token 并发送投递尝试只写入 no_token 或 not_configured,不发起网络请求⛔ 厂商发送未实现
5. 系统通知展示无(服务端不生成通知载荷)⛔ 未实现
6. 点击跳转无(投递记录不含点击事件)⛔ 规划中
7. 角标无(只有会话 unreadCount)⛔ 规划中

设备 token 注册(客户端 REST)​

token 注册属于客户端接口,使用用户登录后的 Bearer token,不是 App-Key 签名,也不是 Admin API。服务端按 (userId, platform, token) 幂等:重复注册返回既有 tokenId 并重新置 active=true。

操作方法与路径说明
注册 / 刷新POST /api/apps/{appkey}/push/tokensbody {token, platform, deviceId?, appVersion?}
查询本人GET /api/apps/{appkey}/push/tokens含 active=false 历史;按 createdAt 倒序
注销本人DELETE /api/apps/{appkey}/push/tokens/{tokenId}软删除 active=false;非本人返回 1001(不泄露他人 token)
管理端查询GET /api/admin/apps/{appkey}/push/tokens过滤 userId/platform/active,分页
管理端注销DELETE /api/admin/apps/{appkey}/push/tokens/{tokenId}写审计;不存在 404/1001

字段与规则​

字段 / 规则值失败表现
platformios / android / web / huawei / xiaomi / oppo / vivo / fcm(8 个)未知平台 1001
token8–512 字符(trim 后)长度非法 1001
每用户活跃上限20 个超限 1001(需先注销)
设备换绑同一 (platform, token) 被其他用户注册时,旧记录 active=false响应 reboundFrom 列出旧 userId
注销/封禁用户不允许注册 token1016
返回字段tokenId/userId/platform/active/createdAt/updatedAt/deviceId/appVersion/duplicate/reboundFrom—
deviceId/appVersion可选;deviceId ≤128 字符、appVersion ≤32 字符;不参与幂等键超长 1001
Shell
curl -sS -X POST "http://127.0.0.1:8080/api/apps/demo/push/tokens" \
  -H "authorization: Bearer $USER_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"token":"vendor-device-token-abc","platform":"android","deviceId":"pixel-7","appVersion":"2.0.0"}'
SDK 状态

Web / Electron SDK 已实现 registerPushToken / unregisterPushToken / getPushTokens(当前版本,走同一组 REST)。

Android / Flutter / Native / iOS 当前没有这三个方法(IDL 已声明、实现缺失);这些端请直接调用上面的 REST,不要把 IDL 声明当作已交付能力。

6 个厂商通道配置​

通道配置在 控制台 · 推送通道 或 Admin API。配置只做字段校验、敏感字段脱敏与启用位存储,不会让消息真的走厂商通道。

通道必填字段敏感字段(回读脱敏)
apnskeyId / teamId / bundleId / privateKey;production 可选privateKey
fcmprojectId / serviceAccountJsonserviceAccountJson
huaweiappId / appSecretappSecret
xiaomiappSecret / packageNameappSecret
oppoappKey / masterSecretmasterSecret
vivoappId / appKey / appSecretappSecret
  • 敏感字段读取只返回 ****末四位,并给出 secretsSet 布尔;保存时传以 **** 开头的值表示保持原值。
  • 通道测试 POST /api/admin/apps/{appkey}/push/channels/{channel}/test 返回 implemented=false、delivered=false 与原因,不伪造成功。
  • 上报字段 supported 为 6 个通道;投递适配器视图按 8 个平台展开(ios/android/web/huawei/xiaomi/oppo/vivo/fcm)。
Shell
curl -sS -X POST "http://127.0.0.1:8080/api/admin/apps/demo/push/channels" \
  -H 'X-Admin-Secret: dev-admin' \
  -H 'content-type: application/json' \
  -d '{"channel":"apns","enabled":true,"fields":{"keyId":"ABC123","teamId":"TEAM123","bundleId":"com.example.app","privateKey":"-----BEGIN PRIVATE KEY-----..."}}'

curl -sS -X POST "http://127.0.0.1:8080/api/admin/apps/demo/push/channels/apns/test" \
  -H 'X-Admin-Secret: dev-admin'

投递尝试记录(Admin API)​

投递记录用于回答“这条推送为什么没到”。创建时按用户逐条生成记录:有活跃 token → not_configured(适配器未实现);没有活跃 token → no_token。两者都不发起网络请求。

操作方法与路径关键规则
创建POST /api/admin/apps/{appkey}/push/deliveries≤100 用户;title 1–128 字符、body 1–2048、extras ≤4096 字节
列表GET /api/admin/apps/{appkey}/push/deliveries按 userId/status 过滤;返回 counts 与 retryableStatuses
单条GET .../push/deliveries/{deliveryId}含 attemptLog(每次尝试的状态/厂商/错误/时间)
重试POST .../push/deliveries/{deliveryId}/retry仅 failed / not_configured;生成新记录、attempts+1、retryOf 指向旧记录

状态机​

状态含义本次实现会写入吗
no_token该用户没有 active 设备 token✅ 会
not_configured有 token,但厂商适配器未配置/未实现;未发起网络请求✅ 会(终态)
queued已入队待发送⛔ 预留
sent厂商已接受⛔ 预留(恒为 0)
failed厂商明确失败⛔ 预留(真实适配器接入后才可能出现)

重试只接受 failed | not_configured;no_token 需先补设备 token。重试会重新选择当前活跃 token,因此可能从 not_configured 变为 no_token。

真实运行结果(本地隔离实例)​

Text
admin.push.deliveries.create (no token): {"requested":1,"sent":0,"status":"no_token","counts":{"no_token":1,"not_configured":0,"sent":0,"failed":0,"queued":0}}
client push token register: {"code":0,"tokenId":"pt_e7a59fe6892c49cbb75e100f18f01ad7"}
admin.push.deliveries.create (token present): {"requested":1,"sent":0,"status":"not_configured","counts":{"no_token":0,"not_configured":1,"sent":0,"failed":0,"queued":0}}
admin.push.deliveries.retry: {"status":"not_configured","attempts":2}
admin.push.stats: {"sent":0,"delivered":0,"deliveryCounts":{"total":3,"byStatus":{"no_token":1,"not_configured":2,"sent":0,"failed":0,"queued":0}}}
JavaScript
import { AdminClient } from '@ni-paas-im/server-sdk';

const admin = new AdminClient({ baseUrl: 'http://127.0.0.1:8080', adminSecret: process.env.IM_ADMIN_SECRET });

const created = await admin.createPushDeliveries({
  appkey: 'demo',
  userIds: ['alice'],
  title: '订单提醒',
  body: '你的订单已发货',
});
console.log(created.sent, created.deliveries[0].status); // 0 no_token | not_configured

if (created.deliveries[0].status === 'not_configured') {
  const retried = await admin.retryPushDelivery({ appkey: 'demo', deliveryId: created.deliveries[0].deliveryId });
  console.log(retried.attempts); // 2
}

保留期与统计口径​

项值说明
投递记录保留每应用最近 5000 条维护周期淘汰最旧;retentionTruncated 标记是否已截断
已注销 token 保留每用户最近 100 条维护周期清理
统计 sent/delivered/failed来自既有 push_logs(push.send/webhook)不是厂商回执
deliveryCounts来自服务端投递尝试记录sent 只可能由未来真实适配器写入,当前恒 0
channelStatsAvailablefalse没有通道级投递数据
适配器视图8 个平台,sendImplemented=false含 channelConfigStored / channelEnabled

离线 webhook 与 push.send(另一条链路)​

  • 接收者无在线 WS 会话时,服务端把消息 JSON POST 到应用配置的 webhookUrl;3 秒超时、无签名、无重试、无死信。
  • push.send 按 scope 投递“不落地通知”到同一个 webhook;未配置 webhook 时 delivered=0,但推送日志仍记录。
  • 这条链路与厂商通道无关;应用可在 webhook 接收端自行调用 APNs/FCM/厂商接口(本项目不代做)。
  • 用户推送免打扰时段(user.push.period.*)只保存配置,当前未在 webhook 派发时生效。

未实现 / 规划中​

能力状态说明
APNs/FCM/华为/小米/OPPO/vivo 真实发送⛔ 未实现适配器恒为 not_configured;不持有凭据、不发起网络请求
通知点击跳转⛔ 未实现投递记录没有 click 事件;webhook 载荷可由业务自行跳转
角标 / 通知分组⛔ 未实现只有会话 unreadCount
多语言推送模板⛔ 未实现title/body 由调用方原样传入
自动重试队列⛔ 未实现只有手动 retry;failed/not_configured 才能重试
客户端 SDK token 方法🟡 部分平台Web/Electron 已实现;Android/Flutter/Native/iOS 缺失,可用 REST

相关页面​