主题
XHIM 业务通知、服务账号与异步任务
本文是 XHIM 业务通知控制面的 canonical 合同,适用于自建后台、企业 OA、 运营系统和 XHIM 管理台。普通客户端 SDK 只接收最终消息,不暴露服务账号创建、 全量发送或任务取消 API。
1. 安全边界
- 业务通知固定使用
application/vnd.xhim.business-notification+json,server_visible=true、encrypted=false,不能伪装成 E2EE 私聊消息。 - 仅
user_type=notification_service的服务账号可以作为发送者。普通真人账号、 已停用或已注销服务账号不能发送。 - 请求中的
service_user_id是可见 sender,不是控制面 actor。Admin actor 来自 管理会话,Business actor 来自X-XHIM-Business-Key;请求不能指定 actor。 - title、body、data 可能进入消息内容,不应包含密码、验证码、密钥或支付凭据。
- 查询任务和收件人时,服务端不返回 title、body、data、fallback 或 request hash。 只返回 payload SHA-256、字节数、统计、稳定错误码和
server_message_id。 - 管理台不把通知正文或 data 写入
localStorage/sessionStorage,提交后立即 清空对应表单。浏览器不会接触 Business Key。 - 管理台在解析
data前会拒绝超出 JavaScript 安全整数范围的数字, 避免浏览器静默改值。需要传送任意合法 JSON 数字词法时,改用 Business API 从客户服务端直接发送。
2. 服务账号
服务账号是普通用户表上的加法类型:
json
{
"app_id": "com.example.im",
"user_id": "notification_operations",
"public_user_id": "100086",
"display_name": "运营通知",
"avatar_url": "https://cdn.example.com/notify.png",
"bio": "订单与系统维护通知",
"type": "notification_service",
"lifecycle_state": "active",
"lifecycle_revision": 1,
"notification_revision": 1,
"created_at_ms": 1786210000000,
"updated_at_ms": 1786210000000
}notification_revision 只保护头像、名称和简介的 CAS 更新;停用、恢复和注销使用 lifecycle_revision。两种 revision 不能混用。
2.1 创建
Admin 与 Business 路由:
text
POST /v1/admin/notification-accounts:create
POST /v1/server/notification-accounts:createjson
{
"app_id": "com.example.im",
"user_id": "notification_operations",
"display_name": "运营通知",
"avatar_url": "https://cdn.example.com/notify.png",
"bio": "订单与系统维护通知",
"mutation_id": "notification-account-create-019f",
"trace_id": "trace-019f"
}user_id 可省略或传空,由服务端生成 notification_* ID;display_name trim 后 必须非空。同一 mutation_id 只有请求完全相同时才会幂等重放,重放继续返回 首次生成的 ID。
2.2 更新
text
POST /v1/admin/notification-accounts:update
POST /v1/server/notification-accounts:updatejson
{
"app_id": "com.example.im",
"user_id": "notification_operations",
"display_name": "新的运营通知",
"avatar_url": "",
"bio": "",
"expected_revision": 3,
"mutation_id": "notification-account-update-019f",
"trace_id": "trace-019f"
}expected_revision 必须取最近一次读取的正整数 notification_revision。 409 revision_conflict 后重新读取,不能盲目覆盖。
创建与更新返回账号对象,并在 mutation 外层返回 changed 与 idempotent_replay。响应不包含控制 actor 或控制凭据。
2.3 列表
text
POST /v1/admin/notification-accounts:list
POST /v1/server/notification-accounts:listjson
{
"app_id": "com.example.im",
"after_user_id": "notification_operations",
"limit": 100
}limit 默认 100,最大 500。响应:
json
{
"accounts": [],
"next_after_user_id": "notification_security"
}2.4 生命周期
服务账号复用企业账号治理路由:
text
POST /v1/admin/users:suspend|unsuspend|unregister
POST /v1/server/users:suspend|unsuspend|unregister请求必须包含 app_id、user_id、mutation_id、最近的 lifecycle_revision 作为 expected_revision,以及 trim 后 1—256 UTF-8 字节的 reason。注销还必须传 confirm:true。
停用或注销服务账号会原子请求取消该账号的所有非终态通知任务;恢复账号不会 恢复旧 Token,也不会恢复已经取消的任务。
3. 单用户与群通知
text
POST /v1/admin/notifications:send
POST /v1/server/notifications:sendjson
{
"app_id": "com.example.im",
"service_user_id": "notification_operations",
"target_kind": "user",
"target_id": "user_alice",
"title": "订单状态更新",
"body": "您的订单已经发货",
"data": {"order_id": "public-order-1"},
"client_message_id": "notification-order-1",
"operation_id": "notification-send-019f",
"trace_id": "trace-019f"
}target_kind 只能是 user 或 group。data 可省略;存在时必须是 JSON 对象。operation_id 必填;client_message_id 省略时使用 operation_id。
成功响应只返回消息元数据,不回显通知正文:
json
{
"app_id": "com.example.im",
"conversation_id": "direct_user_alice_notification_operations",
"server_message_id": "message-019f",
"client_message_id": "notification-order-1",
"server_sequence": 81,
"sent_at_ms": 1786210000000,
"idempotent_replay": false,
"content_type": "application/vnd.xhim.business-notification+json",
"server_visible": true,
"encrypted": false
}既有 /v1/server/messages:send 不接受这一专用 content type,也不能用于冒充 通知服务账号。
3.1 可靠回调通知(不进入聊天)
text
POST /v1/admin/notifications:notify
POST /v1/server/notifications:notify请求字段与 notifications:send 相同,但不接受 client_message_id。成功响应为:
json
{
"app_id": "com.example.im",
"notification_event_id": "notification-event-019f",
"operation_id": "notification-notify-019f",
"recipient_count": 1,
"occurred_at_ms": 1786210000000,
"idempotent_replay": false
}这一路由把规范业务通知写入每个活跃真人收件人的账号 Sync 流。它不创建消息或 会话,不增加未读,不发送 Push,也不触发消息 webhook。SDK 只有在本地投影事务 和 Sync 游标提交之后才发出 reliableBusinessNotificationReceived;同一个已提交 事件重放不会再次回调。旧 SDK 可按 skippable 跳过并继续推进游标。
通知正文和 data 只存在于强类型通知对象中;所有平台的默认 toString、日志和 Electron renderer IPC 都只输出策略元数据。应用需要在主进程/SDK 回调内显式读取 正文,不能把回调当作聊天历史或离线 Push。
4. 批量任务
text
POST /v1/admin/notification-jobs:submit
POST /v1/server/notification-jobs:submit4.1 指定用户
json
{
"app_id": "com.example.im",
"service_user_id": "notification_operations",
"audience": {
"kind": "users",
"user_ids": ["user_alice", "user_bob"]
},
"title": "维护提醒",
"body": "今晚 23:00 进行系统维护",
"data": {"maintenance_id": "public-maintenance-1"},
"operation_id": "notification-job-019f",
"trace_id": "trace-019f"
}user_ids 必须为 1—1000 个原始唯一 ID。任一目标不存在、不是真人、不是 active,或误把服务账号自身作为目标时,整单失败且不创建任务。指定用户请求 不能携带 confirm_all_active_humans。
4.2 全部活跃真人
json
{
"app_id": "com.example.im",
"service_user_id": "notification_operations",
"audience": {"kind": "all_active_humans"},
"confirm_all_active_humans": true,
"title": "平台公告",
"body": "服务条款已更新",
"operation_id": "notification-job-all-019f",
"trace_id": "trace-019f"
}confirm_all_active_humans:true 只允许且必须用于全量请求。提交事务冻结当时的 活跃真人收件人快照;之后新增的用户不会加入,之后停用的用户会在投递时按状态 失败。快照可以为 0 人,此时任务直接成功。任务是异步的,提交成功不代表立即 送达。
submit 成功响应是扁平的任务模型:job_id 与下节的其他任务字段均位于 响应顶层,并额外包含 idempotent_replay;不使用 { "job": { ... } } 包装。消费方应校验非空 job_id,缺失时按不完整响应失败关闭。
4.3 任务模型
任务 state 精确枚举:
text
queued | running | canceling | succeeded | partial_failed | failed | canceled其中 succeeded、partial_failed、failed、canceled 是终态。任务查询只 返回:
json
{
"app_id": "com.example.im",
"job_id": "notification-job-019f",
"service_user_id": "notification_operations",
"audience_kind": "all_active_humans",
"operation_id": "notification-job-all-019f",
"trace_id": "trace-019f",
"payload_sha256": "0123456789abcdef",
"payload_bytes": 342,
"state": "running",
"recipient_count": 900,
"pending_count": 300,
"succeeded_count": 590,
"failed_count": 10,
"canceled_count": 0,
"cancel_requested": false,
"created_at_ms": 1786210000000,
"updated_at_ms": 1786210030000
}5. 任务与收件人 API
Admin 与 Business 路由分别使用 /v1/admin 与 /v1/server 前缀:
text
POST /notification-jobs:get
POST /notification-jobs:list
POST /notification-jobs/recipients:list
POST /notification-jobs:cancel完整路径示例为 /v1/server/notification-jobs:list。
5.1 读取与分页
json
{"app_id":"com.example.im","job_id":"notification-job-019f"}json
{"app_id":"com.example.im","after_job_id":"notification-job-019e","limit":100}列表响应为 {"jobs":[],"next_after_job_id":"..."}。limit 默认 100, 最大 500。
收件人分页请求:
json
{
"app_id": "com.example.im",
"job_id": "notification-job-019f",
"state": "failed",
"after_user_id": "user_alice",
"limit": 100
}state 可省略;存在时只能是:
text
pending | leased | retry_wait | succeeded | failed | canceled收件人只返回稳定投递元数据:
json
{
"recipients": [
{
"user_id": "user_alice",
"state": "succeeded",
"attempt": 1,
"server_message_id": "message-019f",
"delivered": true
}
],
"next_after_user_id": "user_bob"
}delivered 只有在 state=succeeded 且 server_message_id 非空时才为 true。 失败行的 last_error_code 只能是:
text
recipient_unavailable | service_unavailable | attempts_exhausted | transient_failure5.2 取消
json
{
"app_id": "com.example.im",
"service_user_id": "notification_operations",
"job_id": "notification-job-019f",
"operation_id": "notification-job-cancel-019f",
"trace_id": "trace-019f"
}- pending / retry_wait 收件人立即变为 canceled;
- leased 收件人允许当前租约提交一次:成功则保持 succeeded,需要重试则转 canceled;租约到期直接 canceled,不再领取;
- 已 succeeded / failed 的收件人不回滚;
- 所有 leased 收口后任务进入 canceled,并擦除正文;
- 已终态任务的新取消意图返回
409 conflict;原 operation ID 的完全一致重放 返回原结果,其他 operation ID 返回冲突。
6. Worker、租约与重试
Worker 通过数据库原子 claim 获取收件人租约:
- 每次成功 claim 时
attempt + 1,首次为 1; - 每次 claim 同时增加
lease_revision,防止过期 worker 的 ABA 写回; - 未取消任务的过期租约进入 retry_wait,可按有界指数退避重新领取;
- 最多尝试 5 次,随后进入 failed /
attempts_exhausted; - 明确不可用的身份直接 failed,不重试;
- 请求取消后不再分配新租约。
终态任务只擦除 payload 与 fallback;payload hash、字节数、任务统计、收件人状态 和 server_message_id 继续保留,以便审计和故障复核。
7. 认证与稳定错误
- Admin 路由使用现有管理员会话、CSRF 或
X-XHIM-Admin-Key; - Business 路由统一使用
X-XHIM-Business-Key; - Business Key 只存在客户服务端,不能写入网页、移动端或 Electron Renderer。
| HTTP | stable code | 处理 |
|---|---|---|
| 400 | invalid_request | JSON 格式错误 |
| 400 | invalid_argument | 字段、audience 或全量确认错误 |
| 401 | unauthorized | 重新建立管理会话或检查 Business Key |
| 403 | forbidden | actor 无控制权限 |
| 404 | not_found | 账号、目标或任务不存在 |
| 409 | conflict | 任务终态或取消意图冲突 |
| 409 | idempotency_conflict | operation / mutation ID 被不同请求复用 |
| 409 | revision_conflict | 重新读取账号并用最新 revision 决策 |
| 500 | internal_error | 保留 trace ID 后重试或告警 |
管理台只显示 stable code,不显示服务端自由文本错误或通知 payload。
8. 迁移、部署与回滚
数据库迁移 0047 是加法迁移:
- users 增加
user_type与notification_revision;既有用户默认按 human 兼容; - 新增通知账号 mutation、job、recipient 表及必要索引;
- 不改写既有聊天消息,不执行破坏性 down migration。
升级顺序:
- 备份数据库并记录当前 XHIM Server 二进制和配置;
- 使用目标发行包自带迁移器执行
0047; - 启动新 Server/Worker,先创建测试服务账号,对测试用户发送单条通知;
- 验证任务、收件人分页、取消和审计后,再开放全量权限。
旧 Server 二进制可读取 users 的既有列并忽略新表,但不支持新的通知数据面。 应用回滚时停止新任务提交和 worker,保留新增表;恢复到支持版本后可继续处理。 不要删除通知表,也不要物理删除已经产生的通知聊天消息。数据库迁移是前向保留, 不是“回滚应用即删除数据”。