主题
第三方系统对接:待办汇聚与消息汇聚接口(oac-bus)
本文档面向需要把“待办”和“消息”接入统一 OA 门户/平台的第三方业务系统(以及门户服务端等中间层),描述对接的完整业务语义、鉴权方式、接口定义、字段清单与请求/响应示例。
[TOC]
1. 业务背景与总体流程
1.1 待办汇聚(Todo Hub)
目标:把各业务系统产生的“待办(HANDLE)/待阅(REVIEW)”统一汇聚到平台待办库,OA 门户通过统一接口拉取并展示“我的待办/我的待阅/我的已办/我的已阅”,实现跨系统一致的待办体验。
典型流程:
- 业务系统产生待办(例如:审批、用印、合同、报销)。
- 业务系统调用“新增/更新待办(upsert)”把待办上报到平台。
- OA 门户(或门户服务端)按用户维度分页查询待办列表,展示给用户。
- 用户在业务系统完成处理/阅读后:
- 业务系统调用“待办闭环/消除(clear)”把记录从 OPEN 置为 CLEARED(HANDLE 进入“已办”,REVIEW 进入“已阅”)。
- 或业务系统在撤回/作废时调用“待办失效(invalidate)”把待办置为 INVALID。
1.2 消息汇聚(Message Hub)
目标:把各业务系统产生的“通知/提醒/告警”等消息统一汇聚为用户收件箱;OA 门户提供消息列表、未读数、标记已读等能力,并通过 SSE 提供“有变化”的实时通知机制。
典型流程:
- 业务系统产生消息(例如:审批提醒、状态变更通知、告警)。
- 业务系统调用“推送消息(push)”把消息发送到平台(按目标用户扇出落库)。
- OA 门户(或门户服务端)通过:
- SSE 订阅变化事件(仅通知发生变化,不承载完整列表);
- HTTP 拉取收件箱列表、未读数;
- 标记已读(写入 readAt 并触发已读事件)。
2. 网络与鉴权
2.1 服务与路由
oac-bus 对外暴露两个路由前缀:
- 待办汇聚:
/todo-hub/** - 消息汇聚:
/message-hub/**
在网关场景下,通常会将上述前缀转发到 oac-bus 服务(实际以部署时网关路由为准)。
2.2 认证方式(OAuth2 Bearer Token)
所有 Hub 接口均要求客户端令牌(Client Token)并具备 SCOPE_client 权限(即 token 的 scope 包含 client)。
请求头:
Authorization: Bearer {access_token}Content-Type: application/json(POST/PUT 类接口)
获取 Token(client_credentials 示例):
http
POST /api/login/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id={客户端ID}&client_secret={客户端密钥}&scope=client响应示例(字段名以现有文档约定为准):
json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 259200,
"scope": "client"
}说明:
- oac-bus 会从 JWT 解析当前
clientId并用于覆盖“来源系统/应用”字段(避免调用方伪造来源)。例如:- 消息推送会覆盖
sourceAppId - 待办 upsert 会覆盖
sourceSystem
- 消息推送会覆盖
3. 统一约定
3.1 统一响应体
成功(非分页)
json
{
"code": "0",
"msg": "请求成功",
"data": {}
}data 为业务对象或 null。
成功(分页)
分页接口的统一响应结构如下:
json
{
"code": "0",
"msg": "请求成功",
"data": [],
"page": {
"page": 1,
"totalCount": 0,
"totalPages": 0,
"size": 20
},
"count": 0
}注意:
- 返回体中的
page.page为 1-based(从 1 开始)。 - 请求参数中的
page为 0-based(从 0 开始,page=0表示第一页)。
3.2 分页请求参数(Spring Data 默认)
所有列表查询接口(GET)均支持以下参数:
page:页码(0 开始)size:页大小sort:排序(可选,形如sort=modifyTime,desc)
如果不传 sort,接口会使用服务端默认排序:
- 待办:按
modifyTime desc - 收件箱:按
occurredAt desc
3.3 时间字段格式
- 审计字段(
createTime/modifyTime)固定序列化格式:yyyy-MM-dd HH:mm:ss。 - 业务时间字段(例如
dueAt/occurredAt/readAt/clearedAt)为java.time.Instant,请求/响应建议使用 ISO-8601 字符串(示例:2026-02-02T09:00:00Z),由 Spring/Jackson 负责转换(实际以运行环境 ObjectMapper 配置为准)。
3.4 枚举值
待办类型:
HANDLE:待办(需要处理)REVIEW:待阅(只需阅读/知会)
类型业务语义(对接方指定):
type由对接系统在上报时指定:HANDLE=待办、REVIEW=待阅。- 完成态名称随类型变化:当
status=CLEARED时:type=HANDLE:业务名称为“已办”type=REVIEW:业务名称为“已阅”
待办状态:
OPEN:未完成(展示在“我的待办/我的待阅”,由 type 决定视图)CLEARED:已闭环(展示在“我的已办/我的已阅”,由 type 决定视图)INVALID:已失效(一般不展示或按业务展示)
消息状态:
UNREAD:未读READ:已读
4. 待办汇聚接口(/todo-hub)
4.1 新增/更新待办(Upsert)
POST /todo-hub/todos
使用场景:
- 新创建待办/待阅:业务系统产生新的待办任务或待阅事项,需要出现在 OA 门户“我的待办/我的待阅”。
- 更新待办:业务标题、摘要、处理链接、到期时间等发生变化,需要同步到门户侧。
请求体:JSON
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| todoId | string | 是 | 待办全局唯一 ID(平台侧也作为主键 id 存储) |
| type | string | 是 | 对接方指定:HANDLE=待办、REVIEW=待阅 |
| sourceSystem | string | 否 | 来源系统标识;服务端会从 token 解析并覆盖 |
| sourceBizId | string | 是 | 来源业务主键(业务单据/流程实例/任务等) |
| assigneeUsername | string | 是 | 待办处理人用户名(门户侧以该字段过滤“我的待办”) |
| initiatorUsername | string | 否 | 发起人用户名 |
| title | string | 是 | 待办标题 |
| summary | string | 否 | 待办摘要/描述 |
| category | string | 是 | 待办类别(首页展示为“事项类型”) |
| actionUrl | string | 是 | 处理跳转地址(通常是业务系统页面 URL) |
| dueAt | string(Instant) | 否 | 到期时间(ISO-8601) |
服务端逻辑(关键语义):
- 如果
todoId不存在:创建新记录并默认status=OPEN。 - 如果
todoId已存在:覆盖更新请求内字段并刷新modifyTime,但不会自动把状态改回 OPEN。
请求示例:
http
POST /todo-hub/todos
Authorization: Bearer {access_token}
Content-Type: application/json
{
"todoId": "TODO-20260202-0001",
"type": "HANDLE",
"sourceBizId": "BIZ-99123",
"assigneeUsername": "zhangsan",
"initiatorUsername": "lisi",
"title": "用印审批",
"summary": "请尽快处理",
"category": "用印事项",
"actionUrl": "https://oa.example.com/workflow/99123",
"dueAt": "2026-02-05T00:00:00Z"
}响应体:统一响应体,data 为 TodoItem(返回存储后的完整对象)
TodoItem 字段(包含业务字段与审计字段):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string | 主键(等于 todoId) |
| type | string | HANDLE/REVIEW |
| sourceSystem | string | 来源系统标识(由 token 派生) |
| sourceBizId | string | 来源业务主键 |
| assigneeUsername | string | 处理人用户名 |
| initiatorUsername | string | 发起人用户名 |
| title | string | 标题 |
| summary | string | 摘要 |
| category | string | 待办类别(事项类型) |
| actionUrl | string | 跳转地址 |
| actionAppId | string | 目标应用标识(可为空,预留扩展) |
| dueAt | string(Instant) | 到期时间 |
| status | string | OPEN/CLEARED/INVALID |
| result | string | 闭环结果(clear 写入) |
| clearedAt | string(Instant) | 闭环时间(clear 写入) |
| clearedBy | string | 闭环人(clear 写入) |
| reason | string | 备注/原因(clear/invalidate 写入) |
| createTime | string | 创建时间 yyyy-MM-dd HH:mm:ss |
| modifyTime | string | 修改时间 yyyy-MM-dd HH:mm:ss |
| creator/creatorId/modifier/modifierId/delete | string/bool | 审计字段 |
响应示例:
json
{
"code": "0",
"msg": "请求成功",
"data": {
"id": "TODO-20260202-0001",
"type": "HANDLE",
"sourceSystem": "oa",
"sourceBizId": "BIZ-99123",
"assigneeUsername": "zhangsan",
"initiatorUsername": "lisi",
"title": "用印审批",
"summary": "请尽快处理",
"category": "用印事项",
"actionUrl": "https://oa.example.com/workflow/99123",
"dueAt": "2026-02-05T00:00:00Z",
"status": "OPEN",
"createTime": "2026-02-02 17:00:00",
"modifyTime": "2026-02-02 17:00:00",
"delete": false
}
}4.2 查询待办(分页)
GET /todo-hub/todos
使用场景:
- 门户拉取“我的待办”:
status=OPEN&type=HANDLE。 - 门户拉取“我的待阅”:
status=OPEN&type=REVIEW。 - 门户拉取“我的已办”:
status=CLEARED&type=HANDLE。 - 门户拉取“我的已阅”:
status=CLEARED&type=REVIEW。
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| assigneeUsername | string | 否 | - | 处理人用户名 |
| status | string | 否 | OPEN | 待办状态 |
| type | string | 否 | HANDLE | 待办类型(不传时默认 HANDLE;建议门户按视图显式传入 HANDLE/REVIEW) |
| page | number | 否 | 0 | 页码(0-based) |
| size | number | 否 | 20 | 页大小(Spring Data 默认) |
| sort | string | 否 | - | 排序字段(默认 modifyTime desc) |
请求示例:
http
GET /todo-hub/todos?assigneeUsername=zhangsan&status=OPEN&page=0&size=20
Authorization: Bearer {access_token}响应示例(分页):
json
{
"code": "0",
"msg": "请求成功",
"data": [
{
"id": "TODO-20260202-0001",
"type": "HANDLE",
"sourceSystem": "oa",
"sourceBizId": "BIZ-99123",
"assigneeUsername": "zhangsan",
"initiatorUsername": "lisi",
"title": "用印审批",
"summary": "请尽快处理",
"category": "用印事项",
"actionUrl": "https://oa.example.com/workflow/99123",
"dueAt": "2026-02-05T00:00:00Z",
"status": "OPEN",
"createTime": "2026-02-02 17:00:00",
"modifyTime": "2026-02-02 17:00:00",
"delete": false
}
],
"page": {
"page": 1,
"totalCount": 1,
"totalPages": 1,
"size": 20
},
"count": 1
}4.3 待办闭环/消除(Clear)
POST /todo-hub/todos/{todoId}/clear
使用场景:
- 用户在业务系统完成办理/阅读后,业务系统回调该接口,平台把记录从 OPEN 置为 CLEARED;门户侧进入“已办(HANDLE)/已阅(REVIEW)”。
路径参数:
todoId:待办 ID(等同于上报时的todoId)
请求体:JSON
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| result | string | 是 | 处理结果(业务自定义,例如 done/reject/cancel) |
| clearedAt | string(Instant) | 否 | 闭环时间,不传则服务端写 now |
| clearedBy | string | 否 | 闭环人(用户名/系统标识等) |
| reason | string | 否 | 备注/原因 |
请求示例:
http
POST /todo-hub/todos/TODO-20260202-0001/clear
Authorization: Bearer {access_token}
Content-Type: application/json
{
"result": "done",
"clearedBy": "zhangsan",
"reason": "已处理完成"
}响应示例(返回更新后的 TodoItem):
json
{
"code": "0",
"msg": "请求成功",
"data": {
"id": "TODO-20260202-0001",
"status": "CLEARED",
"result": "done",
"clearedAt": "2026-02-02T09:10:00Z",
"clearedBy": "zhangsan",
"reason": "已处理完成",
"modifyTime": "2026-02-02 17:10:00",
"delete": false
}
}4.4 待办失效(Invalidate)
POST /todo-hub/todos/{todoId}/invalidate
使用场景:
- 业务撤回、流程作废、任务被系统自动取消等场景,发起方主动把待办置为失效,避免门户仍展示可办理入口。
路径参数:
todoId:待办 ID
Query 参数:
reason:失效原因(可选)
请求示例:
http
POST /todo-hub/todos/TODO-20260202-0001/invalidate?reason=%E6%B5%81%E7%A8%8B%E5%B7%B2%E6%92%A4%E5%9B%9E
Authorization: Bearer {access_token}响应示例:
json
{
"code": "0",
"msg": "请求成功",
"data": {
"id": "TODO-20260202-0001",
"status": "INVALID",
"reason": "流程已撤回",
"modifyTime": "2026-02-02 17:20:00",
"delete": false
}
}5. 消息汇聚接口(/message-hub)
5.1 推送消息(Push)
POST /message-hub/messages
使用场景:
- 业务系统向指定用户发送通知(审批提醒、状态变更、待办补充提醒等)。
- 同一条业务消息需要通知多个用户(接口支持
targetUsernames扇出)。
请求体:JSON
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| sourceAppId | string | 否 | 来源应用标识;服务端会从 token 解析并覆盖 |
| sourceMessageId | string | 是 | 来源消息 ID(业务侧幂等主键) |
| targetUsernames | string[] | 是 | 目标用户名列表(至少 1 个) |
| title | string | 是 | 消息标题 |
| content | string | 是 | 消息正文 |
| category | string | 否 | 分类(业务自定义,如 workflow/alarm) |
| actions | MessageAction[] | 否 | 可操作入口列表 |
| occurredAt | string(Instant) | 否 | 业务发生时间,不传则服务端写 now |
MessageAction:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| label | string | 否 | 按钮/链接文案 |
| url | string | 否 | 跳转地址 |
幂等语义:
- 平台对每个目标用户会落一条收件箱记录,并以
idempotencyKey({sourceAppId}:{sourceMessageId}:{username})做唯一去重,重复推送会被忽略。
请求示例:
http
POST /message-hub/messages
Authorization: Bearer {access_token}
Content-Type: application/json
{
"sourceMessageId": "MSG-20260202-0001",
"targetUsernames": ["zhangsan", "lisi"],
"title": "审批提醒",
"content": "你有一条新的审批待处理",
"category": "workflow",
"actions": [
{ "label": "去处理", "url": "https://oa.example.com/todo/123" }
],
"occurredAt": "2026-02-02T09:00:00Z"
}响应示例:
json
{
"code": "0",
"msg": "请求成功",
"data": null
}5.2 查询收件箱(分页)
GET /message-hub/inbox
使用场景:
- 门户拉取用户的消息列表(按未读/已读筛选)。
- 门户服务端按来源应用/来源消息 ID 做定位与排障。
查询参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| username | string | 否 | 用户名 |
| status | string | 否 | UNREAD/READ |
| sourceAppId | string | 否 | 来源应用标识 |
| sourceMessageId | string | 否 | 来源消息 ID |
| page/size/sort | - | 否 | Spring Data 分页参数(默认 occurredAt desc) |
请求示例:
http
GET /message-hub/inbox?username=zhangsan&status=UNREAD&page=0&size=20
Authorization: Bearer {access_token}响应数据对象:InboxMessage(包含业务字段与审计字段)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string | 收件箱记录 ID(Mongo 自动生成) |
| username | string | 收件人用户名 |
| sourceAppId | string | 来源应用标识 |
| sourceMessageId | string | 来源消息 ID |
| idempotencyKey | string | 幂等键 |
| title/content/category | string | 标题/正文/分类 |
| actions | MessageAction[] | 可操作入口 |
| status | string | UNREAD/READ |
| occurredAt | string(Instant) | 发生时间 |
| readAt | string(Instant) | 已读时间(未读时为空) |
| createTime/modifyTime/... | - | 审计字段 |
响应示例(分页):
json
{
"code": "0",
"msg": "请求成功",
"data": [
{
"id": "67a0f3c4e3b1b2c3d4e5f678",
"username": "zhangsan",
"sourceAppId": "oa",
"sourceMessageId": "MSG-20260202-0001",
"idempotencyKey": "oa:MSG-20260202-0001:zhangsan",
"title": "审批提醒",
"content": "你有一条新的审批待处理",
"category": "workflow",
"actions": [
{ "label": "去处理", "url": "https://oa.example.com/todo/123" }
],
"status": "UNREAD",
"occurredAt": "2026-02-02T09:00:00Z",
"createTime": "2026-02-02 17:00:01",
"modifyTime": "2026-02-02 17:00:01",
"delete": false
}
],
"page": {
"page": 1,
"totalCount": 1,
"totalPages": 1,
"size": 20
},
"count": 1
}5.3 查询未读数
GET /message-hub/unread-count?username={username}
使用场景:
- 门户首页角标展示未读数量。
- SSE 收到变化事件后刷新未读数。
请求示例:
http
GET /message-hub/unread-count?username=zhangsan
Authorization: Bearer {access_token}响应示例:
json
{
"code": "0",
"msg": "请求成功",
"data": 5
}统计口径:按 username + status=UNREAD 计数。
5.4 标记已读
POST /message-hub/messages/{id}/read?username={username}
重要说明:
- 路径参数名是
{id},但该值实际按 sourceMessageId 来匹配收件箱记录(不是 inbox 的 Mongoid)。
使用场景:
- 门户用户打开消息详情后,把该来源消息在该用户下的收件箱记录置为 READ。
请求示例:
http
POST /message-hub/messages/MSG-20260202-0001/read?username=zhangsan
Authorization: Bearer {access_token}响应示例:
json
{
"code": "0",
"msg": "请求成功",
"data": null
}行为:
- 遍历该用户下
sourceMessageId={id}的消息记录,把未读的置为 READ 并写readAt=now; - 每次实际从 UNREAD 切换到 READ 时,会推送 SSE
read事件。
5.5 SSE 实时事件流
GET /message-hub/stream?user={username}produces: text/event-stream
使用场景:
- 门户前端(或门户服务端)订阅用户事件流:当有新消息或已读状态变化时,立即收到事件,然后再按需调用 HTTP 接口拉取列表/未读数。
连接管理:
- 同一用户名只保留 1 条连接;新连接会替换旧连接。
- 服务端 emitter 永不过期。
- 建议客户端重连时间:3 秒。
心跳:
- 服务端定时广播
heartbeat事件,默认每 15 秒一次(可通过oac.bus.sse.heartbeat-interval配置)。
事件体:
| 字段 | 类型 | 说明 |
|---|---|---|
| type | string | message/read/heartbeat |
| id | string | 事件关联 ID:message 为 inbox 的 Mongo id;read 为 sourceMessageId;heartbeat 为 null |
| ts | number | 毫秒时间戳 |
SSE 事件示例(HTTP Streaming):
text
event: message
id: 67a0f3c4e3b1b2c3d4e5f678
data: {"type":"message","id":"67a0f3c4e3b1b2c3d4e5f678","ts":1769999999000}6. 错误处理与常见问题
6.1 业务校验失败(@Valid)
Hub 接口对请求体做了 @Valid 校验。
当必填字段缺失时,会返回:
json
{
"code": "-1",
"msg": "待办ID不能为空",
"data": null
}说明:目前只返回第一个字段错误的 message。
6.2 幂等与重复提交
- 消息 push:按
idempotencyKey(sourceAppId:sourceMessageId:username)唯一去重,重复推送不会产生重复收件箱记录,也不会重复触发 SSE。 - 待办 upsert:以
todoId为主键天然幂等;重复提交会覆盖更新并刷新modifyTime。
6.3 不存在的 todoId
clear / invalidate 在找不到 todoId 时会返回系统异常(code=-1),msg 为异常信息。
建议调用方在 clear/invalidate 前确保本地存在该待办映射关系,或通过业务侧保证幂等与顺序一致。
6.4 401/403(认证失败/权限不足)
当 Authorization 缺失/无效或不具备 SCOPE_client 时,会直接返回 401/403(响应体由安全组件决定,可能不是本文约定的统一响应体结构)。