Skip to content

XHIM 业务通知、服务账号与异步任务

本文是 XHIM 业务通知控制面的 canonical 合同,适用于自建后台、企业 OA、 运营系统和 XHIM 管理台。普通客户端 SDK 只接收最终消息,不暴露服务账号创建、 全量发送或任务取消 API。

1. 安全边界

  • 业务通知固定使用 application/vnd.xhim.business-notification+jsonserver_visible=trueencrypted=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:create
json
{
  "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:update
json
{
  "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_revision409 revision_conflict 后重新读取,不能盲目覆盖。

创建与更新返回账号对象,并在 mutation 外层返回 changedidempotent_replay。响应不包含控制 actor 或控制凭据。

2.3 列表

text
POST /v1/admin/notification-accounts:list
POST /v1/server/notification-accounts:list
json
{
  "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_iduser_idmutation_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:send
json
{
  "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 只能是 usergroupdata 可省略;存在时必须是 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:submit

4.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

其中 succeededpartial_failedfailedcanceled 是终态。任务查询只 返回:

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=succeededserver_message_id 非空时才为 true。 失败行的 last_error_code 只能是:

text
recipient_unavailable | service_unavailable | attempts_exhausted | transient_failure

5.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 获取收件人租约:

  1. 每次成功 claim 时 attempt + 1,首次为 1;
  2. 每次 claim 同时增加 lease_revision,防止过期 worker 的 ABA 写回;
  3. 未取消任务的过期租约进入 retry_wait,可按有界指数退避重新领取;
  4. 最多尝试 5 次,随后进入 failed / attempts_exhausted
  5. 明确不可用的身份直接 failed,不重试;
  6. 请求取消后不再分配新租约。

终态任务只擦除 payload 与 fallback;payload hash、字节数、任务统计、收件人状态 和 server_message_id 继续保留,以便审计和故障复核。

7. 认证与稳定错误

  • Admin 路由使用现有管理员会话、CSRF 或 X-XHIM-Admin-Key
  • Business 路由统一使用 X-XHIM-Business-Key
  • Business Key 只存在客户服务端,不能写入网页、移动端或 Electron Renderer。
HTTPstable code处理
400invalid_requestJSON 格式错误
400invalid_argument字段、audience 或全量确认错误
401unauthorized重新建立管理会话或检查 Business Key
403forbiddenactor 无控制权限
404not_found账号、目标或任务不存在
409conflict任务终态或取消意图冲突
409idempotency_conflictoperation / mutation ID 被不同请求复用
409revision_conflict重新读取账号并用最新 revision 决策
500internal_error保留 trace ID 后重试或告警

管理台只显示 stable code,不显示服务端自由文本错误或通知 payload。

8. 迁移、部署与回滚

数据库迁移 0047 是加法迁移:

  • users 增加 user_typenotification_revision;既有用户默认按 human 兼容;
  • 新增通知账号 mutation、job、recipient 表及必要索引;
  • 不改写既有聊天消息,不执行破坏性 down migration。

升级顺序:

  1. 备份数据库并记录当前 XHIM Server 二进制和配置;
  2. 使用目标发行包自带迁移器执行 0047
  3. 启动新 Server/Worker,先创建测试服务账号,对测试用户发送单条通知;
  4. 验证任务、收件人分页、取消和审计后,再开放全量权限。

旧 Server 二进制可读取 users 的既有列并忽略新表,但不支持新的通知数据面。 应用回滚时停止新任务提交和 worker,保留新增表;恢复到支持版本后可继续处理。 不要删除通知表,也不要物理删除已经产生的通知聊天消息。数据库迁移是前向保留, 不是“回滚应用即删除数据”。

XHIM 客户端 SDK 与服务端文档