Skip to content

第三方系统对接:待办汇聚与消息汇聚接口(oac-bus)

本文档面向需要把“待办”和“消息”接入统一 OA 门户/平台的第三方业务系统(以及门户服务端等中间层),描述对接的完整业务语义、鉴权方式、接口定义、字段清单与请求/响应示例。


[TOC]

1. 业务背景与总体流程

1.1 待办汇聚(Todo Hub)

目标:把各业务系统产生的“待办(HANDLE)/待阅(REVIEW)”统一汇聚到平台待办库,OA 门户通过统一接口拉取并展示“我的待办/我的待阅/我的已办/我的已阅”,实现跨系统一致的待办体验。

典型流程

  1. 业务系统产生待办(例如:审批、用印、合同、报销)。
  2. 业务系统调用“新增/更新待办(upsert)”把待办上报到平台。
  3. OA 门户(或门户服务端)按用户维度分页查询待办列表,展示给用户。
  4. 用户在业务系统完成处理/阅读后:
    • 业务系统调用“待办闭环/消除(clear)”把记录从 OPEN 置为 CLEARED(HANDLE 进入“已办”,REVIEW 进入“已阅”)。
    • 或业务系统在撤回/作废时调用“待办失效(invalidate)”把待办置为 INVALID。

1.2 消息汇聚(Message Hub)

目标:把各业务系统产生的“通知/提醒/告警”等消息统一汇聚为用户收件箱;OA 门户提供消息列表、未读数、标记已读等能力,并通过 SSE 提供“有变化”的实时通知机制。

典型流程

  1. 业务系统产生消息(例如:审批提醒、状态变更通知、告警)。
  2. 业务系统调用“推送消息(push)”把消息发送到平台(按目标用户扇出落库)。
  3. 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.page1-based(从 1 开始)。
  • 请求参数中的 page0-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

字段类型必填说明
todoIdstring待办全局唯一 ID(平台侧也作为主键 id 存储)
typestring对接方指定:HANDLE=待办REVIEW=待阅
sourceSystemstring来源系统标识;服务端会从 token 解析并覆盖
sourceBizIdstring来源业务主键(业务单据/流程实例/任务等)
assigneeUsernamestring待办处理人用户名(门户侧以该字段过滤“我的待办”)
initiatorUsernamestring发起人用户名
titlestring待办标题
summarystring待办摘要/描述
categorystring待办类别(首页展示为“事项类型”)
actionUrlstring处理跳转地址(通常是业务系统页面 URL)
dueAtstring(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 字段(包含业务字段与审计字段):

字段类型说明
idstring主键(等于 todoId)
typestringHANDLE/REVIEW
sourceSystemstring来源系统标识(由 token 派生)
sourceBizIdstring来源业务主键
assigneeUsernamestring处理人用户名
initiatorUsernamestring发起人用户名
titlestring标题
summarystring摘要
categorystring待办类别(事项类型)
actionUrlstring跳转地址
actionAppIdstring目标应用标识(可为空,预留扩展)
dueAtstring(Instant)到期时间
statusstringOPEN/CLEARED/INVALID
resultstring闭环结果(clear 写入)
clearedAtstring(Instant)闭环时间(clear 写入)
clearedBystring闭环人(clear 写入)
reasonstring备注/原因(clear/invalidate 写入)
createTimestring创建时间 yyyy-MM-dd HH:mm:ss
modifyTimestring修改时间 yyyy-MM-dd HH:mm:ss
creator/creatorId/modifier/modifierId/deletestring/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

查询参数

参数类型必填默认值说明
assigneeUsernamestring-处理人用户名
statusstringOPEN待办状态
typestringHANDLE待办类型(不传时默认 HANDLE;建议门户按视图显式传入 HANDLE/REVIEW)
pagenumber0页码(0-based)
sizenumber20页大小(Spring Data 默认)
sortstring-排序字段(默认 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

字段类型必填说明
resultstring处理结果(业务自定义,例如 done/reject/cancel
clearedAtstring(Instant)闭环时间,不传则服务端写 now
clearedBystring闭环人(用户名/系统标识等)
reasonstring备注/原因

请求示例

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

字段类型必填说明
sourceAppIdstring来源应用标识;服务端会从 token 解析并覆盖
sourceMessageIdstring来源消息 ID(业务侧幂等主键)
targetUsernamesstring[]目标用户名列表(至少 1 个)
titlestring消息标题
contentstring消息正文
categorystring分类(业务自定义,如 workflow/alarm
actionsMessageAction[]可操作入口列表
occurredAtstring(Instant)业务发生时间,不传则服务端写 now

MessageAction:

字段类型必填说明
labelstring按钮/链接文案
urlstring跳转地址

幂等语义

  • 平台对每个目标用户会落一条收件箱记录,并以 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 做定位与排障。

查询参数

参数类型必填说明
usernamestring用户名
statusstringUNREAD/READ
sourceAppIdstring来源应用标识
sourceMessageIdstring来源消息 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(包含业务字段与审计字段)

字段类型说明
idstring收件箱记录 ID(Mongo 自动生成)
usernamestring收件人用户名
sourceAppIdstring来源应用标识
sourceMessageIdstring来源消息 ID
idempotencyKeystring幂等键
title/content/categorystring标题/正文/分类
actionsMessageAction[]可操作入口
statusstringUNREAD/READ
occurredAtstring(Instant)发生时间
readAtstring(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 的 Mongo id)。

使用场景

  • 门户用户打开消息详情后,把该来源消息在该用户下的收件箱记录置为 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 配置)。

事件体

字段类型说明
typestringmessage/read/heartbeat
idstring事件关联 ID:message 为 inbox 的 Mongo idreadsourceMessageIdheartbeat 为 null
tsnumber毫秒时间戳

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:按 idempotencyKeysourceAppId: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(响应体由安全组件决定,可能不是本文约定的统一响应体结构)。