Files
prop-ai-hr/docs/open-org-sync-api-design-v1.md
T
admin d06ea63214 docs: sync knowledge base after 帮道 rename and summary-card fix
- rename brand docs to 帮道 prefix; reframe 20260721 plan as historical snapshot
- compress AGENTS.md rule detail into API_INTEGRATION.md, keep boundaries only
- scrub test phone numbers from design-qa, DEV_SETUP, org-sync and rebuild docs
- document summary-card evidence binding contract and 20260725 config migrations
- pin relative dates in README and audit baseline
2026-07-25 02:55:42 +08:00

1703 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 开放平台组织与预算同步 API 设计方案 v1
> 线上前缀修正(2026-07-15):当前域名 `https://wuye.meihe.cc` 只有 `/api/*` 会代理到后端开放平台;实际接入必须使用 `https://wuye.meihe.cc/api/open/v1`。不要使用 `https://wuye.meihe.cc/open/v1`,否则可能命中前端 SPA 或 nginx,返回 HTML 而不是开放接口 JSON。
## 1. 文档目标
本文档用于定义本系统对外开放的组织与预算同步方案,覆盖以下对象:
- 集团
- 公司
- 部门
- 员工
- 预算
本文档重点解决以下问题:
- 如何对外提供统一、可控、可审计的开放接口
- 如何通过“变更通知 + 主动拉取”的方式降低系统开销
- 如何通过 7 天拉取窗口、过期处理、补数申请提高安全性
- 如何在跨数据中心、跨公网传递时通过传输加密与内容加密保障数据安全
- 如何记录通知、拉取、同步结果,便于事后追踪和排障
- 如何在现有系统表结构基础上最小代价落地
---
## 2. 设计结论
本方案采用以下核心模式:
1. 内部业务数据发生变化后,系统生成变更事件
2. 系统将变更事件通知给订阅者
3. 订阅者收到通知后,在 7 天有效窗口内主动拉取最新数据
4. 超过 7 天后,不再允许直接拉取对应增量数据
5. 如订阅者错过窗口,必须发起补数申请
6. 全链路记录通知状态、拉取状态、同步结果和失败原因
这是一种兼顾性能、安全和可运维性的企业级设计。
---
## 3. 总体架构
### 3.1 角色
- 主系统:当前 `backend` 服务,对外统一提供开放接口
- 订阅者系统:外部 ERP、HR、财务、BI、协同系统等
- 开放平台应用:每个外部系统在本系统中注册的独立接入身份
- 通知中心:负责事件生成、投递、重试、过期、补数审批
### 3.2 架构原则
- 对外统一入口:全部走 `backend` 的 `/api/open/v1/*`
- 内外账号隔离:外部系统不用内部员工登录账号
- 事件通知最小化:通知消息只传事件元信息,不传完整业务数据
- 数据主动拉取:业务明细通过开放接口主动拉取
- 时间窗口受控:增量数据默认仅保留 7 天在线可拉取能力
- 审计可追踪:每次通知、每次拉取、每次补数都有日志
- 数据范围受控:必须按 `group_id / company_id / department_id` 控制可见范围
### 3.3 推荐流程
```text
组织/员工/预算数据变更
↓
写入业务表
↓
写入开放事件表 open_event_outbox
↓
按订阅关系分发通知
↓
订阅者收到消息
↓
7天内调用 /api/open/v1/sync/changes 或对象接口主动拉取
↓
记录同步任务、同步明细、游标、审计日志
↓
超过7天未拉取 → 事件窗口过期
↓
订阅者发起补数申请
↓
平台审批后执行事件重放或快照补数
```
---
## 4. 时间窗口与过期策略
### 4.1 7 天拉取窗口
每条变更事件从 `occurred_at` 开始计算,有一个固定的在线增量拉取窗口:
- 默认窗口:7 天
- 字段体现:`available_until = occurred_at + 7天`
- 在 `available_until` 之前:
- 允许按 `event_id`
- 允许按 `cursor`
- 允许按 `since_time`
- 允许按资源对象主动拉取最新数据
### 4.2 为什么采用 7 天窗口
- 足以覆盖大多数下游系统短时故障、周末值守空档、版本发布波动
- 可以显著降低系统长期保留高频增量拉取资源的压力
- 可以降低历史数据被无限期遍历和恶意批量拉取的风险
### 4.3 过期处理
当当前时间大于 `available_until` 时:
- 增量接口不再返回该事件对应的明细数据
- 事件状态从 `available` 进入 `expired`
- 同步接口返回过期错误
建议错误返回:
- HTTP:`410 Gone`
- 业务错误码:`EVENT_WINDOW_EXPIRED`
返回示例:
```json
{
"success": false,
"code": "EVENT_WINDOW_EXPIRED",
"message": "该事件的在线拉取窗口已过期,请发起补数申请",
"data": {
"event_id": "evt_202607030001",
"occurred_at": "2026-07-03T09:00:00+08:00",
"available_until": "2026-07-10T09:00:00+08:00",
"replay_supported": true
}
}
```
### 4.4 过期后的补救方式
过期后不允许直接下载原增量,但允许走受控补数机制:
1. 事件重放
2. 时间段补数
3. 指定对象补数
4. 全量快照重建
### 4.5 推荐保留策略
- 在线可拉取增量明细:7 天
- 事件元数据与同步日志:至少 180 天
- 审计日志:至少 180 天,建议 1 年
- 补数申请记录:长期保留
---
## 5. 安全设计
### 5.1 接入身份
每个订阅者系统必须创建独立开放平台应用,不复用内部用户账号。
推荐使用:
- `client_id`
- `client_secret`
- 可选 `app_key`
- 可选 IP 白名单
- 可选 mTLS
### 5.2 鉴权模式
开放平台建议采用两层鉴权:
1. `client_id + client_secret` 换取访问令牌
2. 每次请求携带签名头做防篡改、防重放校验
### 5.3 请求头规范
- `Authorization: Bearer <access_token>`
- `X-Client-Id`
- `X-Timestamp`
- `X-Nonce`
- `X-Signature`
- `X-Trace-Id`
- `X-Content-Encrypted`
- `X-Encryption-Key-Id`
- `X-Encryption-Alg`
### 5.4 签名规则
建议签名原文:
```text
HTTP_METHOD + "\n" +
PATH + "\n" +
SORTED_QUERY_STRING + "\n" +
BODY_SHA256 + "\n" +
X-Timestamp + "\n" +
X-Nonce
```
签名算法:
- `HMAC-SHA256`
### 5.5 安全控制项
- 时间戳有效期:5 分钟
- `nonce` 防重放:同一个 `client_id + nonce` 在有效期内不可重复使用
- IP 白名单:按开放平台应用控制
- 限流:按应用、IP、接口三级限流
- 敏感字段脱敏:手机号、身份证、银行卡等默认脱敏
- 预算权限分离:预算查询、预算调整、预算审批分开授权
- 审计全留痕:所有请求、响应、失败原因、耗时必须可追踪
### 5.6 跨数据中心安全分层
如果接口调用需要跨数据中心并通过互联网传递,安全控制必须分为四层:
- 网络层:专线、VPN、云企业网、IPSec 隧道优先;公网直连必须使用固定出口 IP 和白名单。
- 传输层:强制 HTTPS TLS 1.2+,推荐 TLS 1.3;重要订阅者启用 mTLS 双向证书。
- 消息层:请求签名、防重放、时间戳、nonce、trace_id 全量校验。
- 内容层:涉及敏感字段或预算金额时,对响应内容进行字段级或报文级加密。
结论:
- HTTPS 解决链路加密。
- HMAC 签名解决身份校验和防篡改。
- 内容加密解决数据离开系统边界后的二次保护。
- mTLS 解决服务身份强校验。
### 5.7 内容级加密策略
内容级加密用于保护业务数据本身,即使 HTTPS 终止在网关、代理、负载均衡或跨中心链路中间节点,也不会暴露明文业务内容。
建议按数据敏感度启用:
- 普通组织字段:可只使用 HTTPS + 签名。
- 员工敏感字段:必须字段级加密或脱敏。
- 身份证、银行卡、薪资、紧急联系人:默认不开放;获批后必须字段级加密。
- 预算金额、预算计划、预算执行:跨公网传递时建议报文级加密。
- 补数快照:必须报文级加密,并限制临时窗口有效期。
### 5.8 推荐加密方式
建议采用信封加密:
1. 服务端生成一次性数据密钥 `data_key`
2. 使用 `AES-256-GCM` 加密业务响应内容
3. 使用订阅者公钥或平台托管密钥加密 `data_key`
4. 响应中返回加密后的 `encrypted_key`、`iv`、`tag`、`ciphertext`
5. 订阅者使用自己的私钥或密钥管理服务解开 `data_key`
6. 再使用 `data_key` 解密业务内容
推荐算法:
- 对称加密:`AES-256-GCM`
- 非对称加密:`RSA-OAEP-256` 或 `SM2`
- 摘要算法:`SHA-256`
- 签名算法:`HMAC-SHA256`
如果国产化要求较强,可采用:
- `SM2` 用于密钥交换或数字信封
- `SM4-GCM` 用于内容加密
- `SM3` 用于摘要
### 5.9 加密响应格式
当 `X-Content-Encrypted: true` 时,响应体建议统一为:
```json
{
"success": true,
"code": "OK",
"message": "操作成功",
"data": {
"encrypted": true,
"key_id": "key_20260703_001",
"alg": "AES-256-GCM",
"encrypted_key": "base64-rsa-oaep-encrypted-data-key",
"iv": "base64-iv",
"tag": "base64-gcm-tag",
"ciphertext": "base64-encrypted-json"
}
}
```
加密前明文示例:
```json
{
"event_id": "evt_202607030001",
"resource_type": "employee",
"resource_id": 135,
"payload": {
"name": "张三",
"phone": "<手机号>",
"department_id": 108
}
}
```
### 5.10 密钥管理要求
密钥管理必须独立于业务数据管理。
建议要求:
- 不在数据库中明文保存任何 `client_secret`、私钥或数据密钥。
- `client_secret` 只保存哈希。
- 内容加密主密钥由 KMS、HSM 或独立密钥服务托管。
- 每个订阅者至少独立配置一个 `key_id`。
- 支持密钥轮换,旧密钥只保留解密能力,不再用于新数据加密。
- 支持密钥吊销,吊销后订阅者不能继续拉取加密数据。
- 审计日志只记录 `key_id` 和加密状态,不记录明文密钥或明文敏感数据。
### 5.11 公网传输安全基线
跨公网调用必须满足以下基线:
- 强制 HTTPS,禁止 HTTP。
- 启用服务端证书校验,禁止忽略证书错误。
- 生产环境推荐 mTLS。
- 配置固定出口 IP 和订阅方 IP 白名单。
- 所有请求必须签名并校验 `nonce`。
- 敏感响应必须启用内容加密。
- 审计日志禁止落明文敏感字段。
- 补数快照必须走临时窗口和内容加密。
- 下载类接口必须设置短有效期 URL,且绑定订阅者身份与 IP。
---
## 6. 数据范围模型
### 6.1 基本原则
开放平台数据访问必须按租户和组织范围控制,不允许“接口全开放”。
### 6.2 推荐范围控制维度
- `group_id`
- `company_id`
- `department_id`
- 资源类型
- 权限范围 `scope`
### 6.3 建议权限范围
- `group.read`
- `company.read`
- `department.read`
- `employee.read`
- `budget.read`
- `sync.read`
- `sync.replay`
- `subscription.manage`
- `audit.read`
### 6.4 订阅范围限制
每个订阅关系必须明确指定允许访问的范围:
- 指定集团
- 指定公司列表
- 指定部门列表
- 指定资源类型
- 指定事件类型
---
## 7. 数据同步模型
### 7.1 事件类型
#### 组织事件
- `group.created`
- `group.updated`
- `group.deleted`
- `company.created`
- `company.updated`
- `company.deleted`
- `department.created`
- `department.updated`
- `department.deleted`
- `org.structure.changed`
#### 员工事件
- `employee.created`
- `employee.updated`
- `employee.departed`
- `employee.reinstated`
- `employee.transferred`
#### 预算事件
- `budget.version.created`
- `budget.version.updated`
- `budget.subject.updated`
- `budget.item.updated`
- `budget.plan.created`
- `budget.plan.updated`
- `budget.plan.locked`
### 7.2 事件消息最小模型
```json
{
"event_id": "evt_202607030001",
"trace_id": "trace_20260703_001",
"event_type": "employee.updated",
"resource_type": "employee",
"resource_id": 135,
"group_id": 209,
"company_id": 33,
"department_id": 108,
"version": 12,
"occurred_at": "2026-07-03T09:00:00+08:00",
"available_until": "2026-07-10T09:00:00+08:00",
"changed_fields": ["department_id", "job_title", "status"],
"replay_supported": true
}
```
### 7.3 为什么不在消息里直接放完整数据
- 降低通知体积
- 降低敏感数据在消息通道暴露的风险
- 便于版本控制
- 便于重放和幂等处理
- 便于订阅者自主控制拉取节奏
---
## 8. 状态机设计
### 8.1 事件状态
- `pending`:事件已生成,待投递
- `sent`:已向订阅者下发通知
- `received`:订阅者已收到通知
- `acked`:订阅者已确认收到
- `available`:处于 7 天在线拉取窗口
- `expired`:已过期,不能再在线拉取
- `replayed`:已触发重放
- `closed`:生命周期已关闭
### 8.2 同步状态
- `pending`:待同步
- `pulling`:订阅者正在拉取
- `success`:同步成功
- `partial_success`:部分成功
- `failed`:同步失败
- `retrying`:系统正在重试
- `stale`:因版本过旧被丢弃
- `expired`:窗口过期未同步
### 8.3 补数申请状态
- `pending_review`:待审批
- `approved`:已批准
- `rejected`:已拒绝
- `processing`:补数处理中
- `completed`:补数完成
- `failed`:补数失败
- `cancelled`:已取消
---
## 9. API 总览
开放平台统一前缀:
```text
/api/open/v1
```
### 9.1 认证接口
- `POST /api/open/v1/auth/token`
- `POST /api/open/v1/auth/refresh`
- `GET /api/open/v1/auth/me`
### 9.2 订阅管理接口
- `POST /api/open/v1/subscriptions`
- `GET /api/open/v1/subscriptions`
- `GET /api/open/v1/subscriptions/{subscription_id}`
- `PATCH /api/open/v1/subscriptions/{subscription_id}`
- `POST /api/open/v1/subscriptions/{subscription_id}/enable`
- `POST /api/open/v1/subscriptions/{subscription_id}/disable`
### 9.3 事件通知接口
- `GET /api/open/v1/events`
- `GET /api/open/v1/events/{event_id}`
- `POST /api/open/v1/events/{event_id}/ack`
- `POST /api/open/v1/events/{event_id}/nack`
### 9.4 数据拉取接口
- `GET /api/open/v1/sync/changes`
- `GET /api/open/v1/sync/snapshot`
- `GET /api/open/v1/groups/{id}`
- `GET /api/open/v1/companies/{id}`
- `GET /api/open/v1/departments/{id}`
- `GET /api/open/v1/employees/{id}`
- `GET /api/open/v1/budget-versions/{id}`
- `GET /api/open/v1/budget-subjects/{id}`
- `GET /api/open/v1/budget-items/{id}`
- `GET /api/open/v1/budget-plans/{id}`
### 9.5 同步任务接口
- `POST /api/open/v1/sync/jobs`
- `GET /api/open/v1/sync/jobs/{job_id}`
- `POST /api/open/v1/sync/jobs/{job_id}/complete`
### 9.6 补数申请接口
- `POST /api/open/v1/replay-requests`
- `GET /api/open/v1/replay-requests`
- `GET /api/open/v1/replay-requests/{request_id}`
- `POST /api/open/v1/replay-requests/{request_id}/cancel`
- `POST /api/open/v1/replay-requests/{request_id}/approve`
- `POST /api/open/v1/replay-requests/{request_id}/reject`
- `POST /api/open/v1/replay-requests/{request_id}/execute`
### 9.7 追踪与审计接口
- `GET /api/open/v1/traces/{trace_id}`
- `GET /api/open/v1/audits`
- `GET /api/open/v1/subscriptions/{subscription_id}/deliveries`
- `GET /api/open/v1/subscriptions/{subscription_id}/sync-records`
---
## 10. 认证接口设计
### 10.1 获取令牌
`POST /api/open/v1/auth/token`
请求示例:
```json
{
"grant_type": "client_credentials",
"client_id": "ext_hr_001",
"client_secret": "******"
}
```
响应示例:
```json
{
"success": true,
"data": {
"access_token": "token_xxx",
"token_type": "Bearer",
"expires_in": 7200,
"scope": "company.read department.read employee.read sync.read"
}
}
```
### 10.2 刷新令牌
`POST /api/open/v1/auth/refresh`
### 10.3 查询当前应用
`GET /api/open/v1/auth/me`
返回内容:
- 应用 ID
- 应用名称
- 权限范围
- 绑定集团
- 可见公司
- 可见部门
- IP 白名单状态
---
## 11. 订阅管理接口设计
### 11.1 创建订阅
`POST /api/open/v1/subscriptions`
请求示例:
```json
{
"subscriber_name": "外部HR系统",
"callback_url": "https://hr.example.com/open/events",
"pull_base_url": "https://hr.example.com/open/pull",
"event_types": [
"department.created",
"department.updated",
"employee.created",
"employee.updated",
"employee.departed"
],
"scope": {
"group_id": 209,
"company_ids": [33, 34],
"department_ids": []
},
"security": {
"signing_method": "hmac-sha256",
"ip_whitelist": ["10.10.10.10"],
"allow_replay": true,
"content_encryption_required": true,
"encryption_alg": "AES-256-GCM",
"encryption_key_id": "key_20260703_001"
}
}
```
响应示例:
```json
{
"success": true,
"data": {
"subscription_id": 10001,
"status": "enabled"
}
}
```
### 11.2 查询订阅列表
`GET /api/open/v1/subscriptions`
支持筛选:
- `status`
- `subscriber_name`
- `group_id`
### 11.3 启停订阅
- `POST /api/open/v1/subscriptions/{subscription_id}/enable`
- `POST /api/open/v1/subscriptions/{subscription_id}/disable`
---
## 12. 事件通知接口设计
### 12.1 拉取事件列表
`GET /api/open/v1/events`
查询参数:
- `status`
- `event_type`
- `since_time`
- `limit`
返回示例:
```json
{
"success": true,
"data": {
"items": [
{
"event_id": "evt_202607030001",
"trace_id": "trace_20260703_001",
"event_type": "employee.updated",
"resource_type": "employee",
"resource_id": 135,
"occurred_at": "2026-07-03T09:00:00+08:00",
"available_until": "2026-07-10T09:00:00+08:00",
"status": "available"
}
]
}
}
```
### 12.2 事件确认收到
`POST /api/open/v1/events/{event_id}/ack`
请求示例:
```json
{
"received_at": "2026-07-03T09:00:05+08:00",
"receiver": "hr-sync-worker-01"
}
```
### 12.3 事件确认失败
`POST /api/open/v1/events/{event_id}/nack`
请求示例:
```json
{
"error_code": "SIGNATURE_INVALID",
"error_message": "签名校验失败"
}
```
---
## 13. 数据拉取接口设计
### 13.1 增量拉取
`GET /api/open/v1/sync/changes`
查询参数:
- `since_time`:开始时间
- `cursor`:上次同步游标
- `resource_type`:资源类型
- `event_type`:事件类型
- `limit`:单次拉取条数,建议不超过 500
说明:
- 优先使用 `cursor`
- 没有 `cursor` 时可降级使用 `since_time`
- 仅返回当前窗口内允许在线拉取的数据
响应示例:
```json
{
"success": true,
"data": {
"cursor": "cur_20260703090000123",
"has_more": true,
"items": [
{
"event_id": "evt_202607030001",
"trace_id": "trace_20260703_001",
"event_type": "employee.updated",
"resource_type": "employee",
"resource_id": 135,
"version": 12,
"changed_fields": ["department_id", "job_title"],
"occurred_at": "2026-07-03T09:00:00+08:00",
"available_until": "2026-07-10T09:00:00+08:00"
}
]
}
}
```
### 13.2 全量快照拉取
`GET /api/open/v1/sync/snapshot`
查询参数:
- `resource_type`
- `group_id`
- `company_id`
- `department_id`
- `page`
- `page_size`
用途:
- 首次接入
- 下游系统重建
- 补数审批后全量重拉
### 13.3 单对象拉取
#### 集团
`GET /api/open/v1/groups/{id}`
#### 公司
`GET /api/open/v1/companies/{id}`
#### 部门
`GET /api/open/v1/departments/{id}`
#### 员工
`GET /api/open/v1/employees/{id}`
#### 预算版本
`GET /api/open/v1/budget-versions/{id}`
#### 预算科目
`GET /api/open/v1/budget-subjects/{id}`
#### 预算项
`GET /api/open/v1/budget-items/{id}`
#### 预算计划
`GET /api/open/v1/budget-plans/{id}`
### 13.4 单对象接口通用参数
- `fields`:指定返回字段
- `version`:期望版本号
- `include_deleted`:是否包含已删除记录
---
## 14. 同步任务接口设计
### 14.1 创建同步任务
`POST /api/open/v1/sync/jobs`
用途:
- 订阅者在开始一轮同步前登记任务
- 平台记录该次同步和哪些事件关联
请求示例:
```json
{
"sync_type": "incremental",
"subscription_id": 10001,
"start_cursor": "cur_20260703090000123",
"resource_types": ["department", "employee"]
}
```
响应示例:
```json
{
"success": true,
"data": {
"job_id": "job_20260703_0001",
"job_status": "pending"
}
}
```
### 14.2 查询同步任务
`GET /api/open/v1/sync/jobs/{job_id}`
### 14.3 回报同步结果
`POST /api/open/v1/sync/jobs/{job_id}/complete`
请求示例:
```json
{
"result": "partial_success",
"pull_started_at": "2026-07-03T09:00:10+08:00",
"pull_finished_at": "2026-07-03T09:00:22+08:00",
"synced_count": 120,
"failed_count": 2,
"error_items": [
{
"resource_type": "employee",
"resource_id": 138,
"error_code": "VERSION_CONFLICT",
"error_message": "下游版本较新,拒绝覆盖"
}
]
}
```
---
## 15. 7 天过期处理接口行为
### 15.1 增量接口过期行为
当请求的数据窗口已超过 7 天:
- 不返回业务明细
- 返回过期错误
- 返回推荐补数路径
### 15.2 对象接口过期行为
说明:
- 单对象接口始终返回对象当前最新状态
- 但不保证返回“某个过期事件时刻”的历史镜像
- 如果要恢复当时增量链路,必须走补数申请
### 15.3 埋点建议
在同步链路中必须记录:
- 事件通知时间
- 订阅者确认收到时间
- 开始拉取时间
- 拉取完成时间
- 同步成功/失败状态
- 是否因过期进入补数流程
---
## 16. 补数申请接口设计
### 16.1 设计目标
补数申请用于处理以下场景:
- 下游系统故障错过 7 天窗口
- 某类资源同步失败需要补拉
- 指定部门或员工数据需要重新对齐
- 全量快照需要重建
### 16.2 补数模式
- `event_replay`:按事件重放
- `time_range_replay`:按时间段补数
- `resource_replay`:按对象补数
- `snapshot_rebuild`:全量快照重建
### 16.3 发起补数申请
`POST /api/open/v1/replay-requests`
请求示例:
```json
{
"replay_type": "time_range_replay",
"subscription_id": 10001,
"reason": "下游系统数据库锁表,7天内未完成同步",
"resource_types": ["employee", "department"],
"start_time": "2026-06-20T00:00:00+08:00",
"end_time": "2026-06-28T23:59:59+08:00",
"group_id": 209,
"company_ids": [33]
}
```
响应示例:
```json
{
"success": true,
"data": {
"request_id": "rr_202607030001",
"status": "pending_review"
}
}
```
### 16.4 查询补数申请
`GET /api/open/v1/replay-requests`
### 16.5 查询补数申请详情
`GET /api/open/v1/replay-requests/{request_id}`
### 16.6 取消补数申请
`POST /api/open/v1/replay-requests/{request_id}/cancel`
### 16.7 审批补数申请
#### 批准
`POST /api/open/v1/replay-requests/{request_id}/approve`
#### 驳回
`POST /api/open/v1/replay-requests/{request_id}/reject`
### 16.8 执行补数
`POST /api/open/v1/replay-requests/{request_id}/execute`
执行方式:
- 重新开放临时拉取窗口
- 生成重放事件批次
- 生成受控快照任务
---
## 17. 追踪与审计接口设计
### 17.1 查询链路追踪
`GET /api/open/v1/traces/{trace_id}`
返回应包含:
- 事件信息
- 投递记录
- ACK 记录
- 同步任务
- 同步结果
- 失败原因
- 是否过期
- 是否补数
### 17.2 查询审计日志
`GET /api/open/v1/audits`
可按以下条件筛选:
- `client_id`
- `subscription_id`
- `trace_id`
- `api_path`
- `start_time`
- `end_time`
- `response_code`
---
## 18. 错误码设计
### 18.1 通用错误码
- `INVALID_PARAM`:参数错误
- `UNAUTHORIZED`:未授权
- `FORBIDDEN_SCOPE`:超出授权范围
- `SIGNATURE_INVALID`:签名错误
- `TIMESTAMP_EXPIRED`:时间戳过期
- `NONCE_REUSED`:随机串重复使用
- `RATE_LIMITED`:接口限流
- `RESOURCE_NOT_FOUND`:资源不存在
- `VERSION_CONFLICT`:版本冲突
- `INTERNAL_ERROR`:系统内部错误
### 18.2 同步相关错误码
- `EVENT_WINDOW_EXPIRED`:事件在线拉取窗口已过期
- `REPLAY_NOT_ALLOWED`:当前应用无补数权限
- `REPLAY_REVIEW_REQUIRED`:需要审批后方可补数
- `SNAPSHOT_TOO_LARGE`:快照范围过大
- `CURSOR_INVALID`:游标非法
- `SYNC_JOB_NOT_FOUND`:同步任务不存在
- `CONTENT_ENCRYPTION_REQUIRED`:当前资源必须启用内容加密
- `ENCRYPTION_KEY_NOT_FOUND`:未找到可用加密密钥
- `ENCRYPTION_KEY_REVOKED`:加密密钥已吊销
- `DECRYPTION_FAILED`:订阅方解密失败或密文格式不正确
---
## 19. 分页与返回格式
### 19.1 统一响应格式
```json
{
"success": true,
"code": "OK",
"message": "操作成功",
"data": {}
}
```
### 19.2 分页响应格式
```json
{
"success": true,
"data": {
"items": [],
"page": 1,
"page_size": 50,
"total": 1200
}
}
```
### 19.3 游标响应格式
```json
{
"success": true,
"data": {
"items": [],
"cursor": "cur_xxx",
"has_more": true
}
}
```
---
## 20. 业务对象字段建议
### 20.1 集团对象
建议字段:
- `id`
- `name`
- `code`
- `status`
- `created_at`
- `updated_at`
### 20.2 公司对象
建议字段尽量复用现有 `companies`:
- `id`
- `group_id`
- `name`
- `code`
- `status`
- `address`
- `contact_person`
- `contact_phone`
- `description`
- `updated_at`
### 20.3 部门对象
建议字段尽量复用现有 `departments`:
- `id`
- `group_id`
- `company_id`
- `name`
- `code`
- `parent_id`
- `level`
- `sort_order`
- `manager_id`
- `status`
- `updated_at`
### 20.4 员工对象
建议字段尽量复用现有 `employees`,但对外输出做裁剪与脱敏:
- `id`
- `group_id`
- `company_id`
- `department_id`
- `user_id`
- `employee_number`
- `name`
- `phone`
- `email`
- `status`
- `job_title`
- `hire_date`
- `leave_date`
- `updated_at`
说明:
- `id_card`
- `bank_account`
- `salary_*`
- `emergency_*`
默认不对外开放,必须经过单独授权。
### 20.5 预算对象
为减少改造量,建议对外预算同步分 4 类:
- `budget_versions`
- `budget_subjects`
- `budget_items`
- `budget_plans`
---
## 21. 表结构设计
本节分为两类:
1. 复用现有业务主表
2. 新增开放平台支撑表
### 21.1 复用现有业务主表
#### 21.1.1 集团表 `groups`
说明:
- 当前 `companies.group_id` 已关联 `groups.id`
- 对外集团数据建议直接基于现有 `groups` 表输出
建议对外字段:
- `id`
- `name`
- `code`
- `status`
- `created_at`
- `updated_at`
#### 21.1.2 公司表 `companies`
说明:
- 已存在,建议直接复用
- 当前关键字段已满足对外同步主诉求
关键字段:
- `id`
- `group_id`
- `name`
- `code`
- `address`
- `logo_url`
- `description`
- `introduction`
- `legal_representative`
- `business_license`
- `status`
- `company_nature`
- `contact_person`
- `contact_phone`
- `created_at`
- `updated_at`
#### 21.1.3 部门表 `departments`
说明:
- 已存在,建议直接复用
关键字段:
- `id`
- `name`
- `code`
- `description`
- `parent_id`
- `level`
- `sort_order`
- `status`
- `company_id`
- `group_id`
- `manager_id`
- `created_at`
- `updated_at`
#### 21.1.4 员工表 `employees`
说明:
- 已存在,建议直接复用
- 只是在开放接口层做字段裁剪
关键字段:
- `id`
- `employee_id`
- `name`
- `phone`
- `email`
- `hire_date`
- `department_id`
- `position_id`
- `status`
- `user_id`
- `company_id`
- `group_id`
- `employee_number`
- `leave_date`
- `updated_at`
#### 21.1.5 用户表 `users`
说明:
- 认证身份仍然只使用 `users`
- 员工与账号通过 `employees.user_id -> users.id` 关联
- 对外不建议直接暴露完整 `users` 表,只在员工对象中按需映射
#### 21.1.6 预算版本表 `budget_versions`
关键字段:
- `id`
- `version_code`
- `version_name`
- `version_type`
- `budget_year`
- `start_month`
- `end_month`
- `status`
- `description`
- `group_id`
- `company_id`
- `created_by`
- `updated_by`
- `created_at`
- `updated_at`
#### 21.1.7 预算科目表 `budget_subjects`
说明:
- 已存在,建议直接复用
- 对外用于同步预算科目树
#### 21.1.8 预算项表 `budget_items`
说明:
- 已存在,建议直接复用
- 对外用于同步预算项
#### 21.1.9 预算计划表 `budget_plans`
说明:
- 已存在,建议直接复用
- 对外用于同步预算计划主数据
### 21.2 新增开放平台表
以下表建议新增,统一使用 `utf8mb4`,并补齐中文注释。
#### 21.2.1 开放平台应用表 `open_client_apps`
用途:
- 管理每个外部接入系统的应用身份
建议字段:
- `id` bigint 主键
- `client_id` varchar(64) 唯一,应用标识
- `client_name` varchar(128) 应用名称
- `client_secret_hash` varchar(255) 密钥哈希
- `status` varchar(32) 状态:enabled/disabled
- `group_id` int 允许访问的默认集团
- `allow_scopes` json 允许的权限范围
- `ip_whitelist` json 白名单 IP 列表
- `token_expire_seconds` int 访问令牌有效期秒数
- `allow_replay` tinyint 是否允许补数申请
- `content_encryption_required` tinyint 是否强制内容加密
- `encryption_mode` varchar(32) 加密模式:none/field/payload
- `encryption_alg` varchar(64) 内容加密算法
- `active_key_id` varchar(64) 当前启用密钥 ID
- `mtls_required` tinyint 是否强制 mTLS
- `public_key_fingerprint` varchar(128) 订阅者公钥指纹
- `remark` varchar(500) 备注
- `created_by` int 创建人用户 ID
- `updated_by` int 更新人用户 ID
- `created_at` datetime 创建时间
- `updated_at` datetime 更新时间
#### 21.2.2 开放平台订阅表 `open_subscriptions`
用途:
- 管理订阅者接收哪些事件、哪些范围的数据
建议字段:
- `id` bigint 主键
- `client_app_id` bigint 应用 ID
- `subscriber_name` varchar(128) 订阅者名称
- `callback_url` varchar(255) 回调地址
- `pull_base_url` varchar(255) 拉取基础地址
- `event_types` json 订阅事件列表
- `scope_group_id` int 订阅集团范围
- `scope_company_ids` json 订阅公司范围
- `scope_department_ids` json 订阅部门范围
- `resource_types` json 订阅资源类型
- `status` varchar(32) enabled/disabled
- `content_encryption_required` tinyint 是否强制内容加密
- `encryption_key_id` varchar(64) 当前订阅使用的密钥 ID
- `last_sync_at` datetime 最近同步时间
- `created_at` datetime 创建时间
- `updated_at` datetime 更新时间
#### 21.2.3 开放事件主表 `open_event_outbox`
用途:
- 记录业务变更产生的标准事件
建议字段:
- `id` bigint 主键
- `event_id` varchar(64) 唯一事件号
- `trace_id` varchar(64) 链路追踪号
- `event_type` varchar(64) 事件类型
- `resource_type` varchar(32) 资源类型
- `resource_id` bigint 资源主键
- `group_id` int 集团 ID
- `company_id` int 公司 ID
- `department_id` int 部门 ID
- `version` bigint 资源版本号
- `payload` json 事件最小载荷
- `payload_encrypted` tinyint 事件载荷是否已加密
- `payload_key_id` varchar(64) 事件载荷加密密钥 ID
- `payload_alg` varchar(64) 事件载荷加密算法
- `occurred_at` datetime 事件发生时间
- `available_until` datetime 在线拉取截止时间
- `status` varchar(32) pending/sent/available/expired/closed
- `replay_supported` tinyint 是否允许补数
- `created_at` datetime 创建时间
#### 21.2.4 事件投递表 `open_event_deliveries`
用途:
- 记录每条事件针对每个订阅者的投递情况
建议字段:
- `id` bigint 主键
- `event_id` varchar(64) 事件号
- `subscription_id` bigint 订阅 ID
- `trace_id` varchar(64) 链路追踪号
- `delivery_status` varchar(32) sent/received/acked/failed/expired
- `sent_at` datetime 发送时间
- `received_at` datetime 接收时间
- `acked_at` datetime 确认时间
- `retry_count` int 重试次数
- `last_retry_at` datetime 最近重试时间
- `http_status` int 回调响应码
- `error_code` varchar(64) 错误码
- `error_message` varchar(500) 错误信息
- `created_at` datetime 创建时间
- `updated_at` datetime 更新时间
#### 21.2.5 同步任务表 `open_sync_jobs`
用途:
- 记录每一轮增量或全量同步任务
建议字段:
- `id` bigint 主键
- `job_id` varchar(64) 唯一任务号
- `subscription_id` bigint 订阅 ID
- `trace_id` varchar(64) 链路追踪号
- `sync_type` varchar(32) incremental/full/replay/snapshot
- `start_cursor` varchar(255) 开始游标
- `end_cursor` varchar(255) 结束游标
- `since_time` datetime 增量开始时间
- `until_time` datetime 增量结束时间
- `job_status` varchar(32) pending/pulling/success/partial_success/failed
- `synced_count` int 成功条数
- `failed_count` int 失败条数
- `expired_count` int 过期条数
- `pull_started_at` datetime 开始拉取时间
- `pull_finished_at` datetime 拉取结束时间
- `error_message` varchar(500) 错误信息
- `created_at` datetime 创建时间
- `updated_at` datetime 更新时间
#### 21.2.6 同步明细表 `open_sync_job_items`
用途:
- 记录同步任务内每个对象的执行结果
建议字段:
- `id` bigint 主键
- `job_id` varchar(64) 任务号
- `event_id` varchar(64) 事件号
- `resource_type` varchar(32) 资源类型
- `resource_id` bigint 资源 ID
- `version` bigint 资源版本号
- `item_status` varchar(32) success/failed/skipped/stale/expired
- `error_code` varchar(64) 错误码
- `error_message` varchar(500) 错误信息
- `created_at` datetime 创建时间
#### 21.2.7 订阅游标表 `open_sync_cursors`
用途:
- 保存每个订阅者的最近成功游标
建议字段:
- `id` bigint 主键
- `subscription_id` bigint 订阅 ID
- `resource_type` varchar(32) 资源类型
- `current_cursor` varchar(255) 当前游标
- `last_event_id` varchar(64) 最近事件号
- `last_synced_at` datetime 最近同步时间
- `version` bigint 游标版本
- `updated_at` datetime 更新时间
#### 21.2.8 补数申请表 `open_replay_requests`
用途:
- 记录订阅者的补数申请和审批结果
建议字段:
- `id` bigint 主键
- `request_id` varchar(64) 唯一申请号
- `subscription_id` bigint 订阅 ID
- `client_app_id` bigint 应用 ID
- `replay_type` varchar(32) event_replay/time_range_replay/resource_replay/snapshot_rebuild
- `reason` varchar(500) 申请原因
- `resource_types` json 资源类型列表
- `event_ids` json 事件号列表
- `group_id` int 集团 ID
- `company_ids` json 公司列表
- `department_ids` json 部门列表
- `start_time` datetime 开始时间
- `end_time` datetime 结束时间
- `status` varchar(32) pending_review/approved/rejected/processing/completed/failed/cancelled
- `review_comment` varchar(500) 审批意见
- `approved_by` int 审批人用户 ID
- `approved_at` datetime 审批时间
- `executed_at` datetime 执行时间
- `created_at` datetime 创建时间
- `updated_at` datetime 更新时间
#### 21.2.9 临时补数窗口表 `open_replay_windows`
用途:
- 为已审批的补数申请生成临时可拉取窗口
建议字段:
- `id` bigint 主键
- `request_id` varchar(64) 关联申请号
- `window_token` varchar(128) 补数窗口令牌
- `start_time` datetime 允许拉取开始时间
- `end_time` datetime 允许拉取结束时间
- `expires_at` datetime 临时窗口过期时间
- `status` varchar(32) active/used/expired/revoked
- `created_at` datetime 创建时间
- `updated_at` datetime 更新时间
#### 21.2.10 审计日志表 `open_api_audit_logs`
用途:
- 记录开放接口全部访问痕迹
建议字段:
- `id` bigint 主键
- `trace_id` varchar(64) 链路追踪号
- `client_id` varchar(64) 应用标识
- `subscription_id` bigint 订阅 ID
- `api_path` varchar(255) 接口路径
- `http_method` varchar(16) 请求方法
- `request_ip` varchar(64) 请求 IP
- `request_headers` json 请求头摘要
- `request_body` json 请求体摘要
- `response_code` int 响应码
- `response_body` json 响应体摘要
- `content_encrypted` tinyint 响应内容是否加密
- `encryption_key_id` varchar(64) 本次响应使用的密钥 ID
- `cost_ms` int 耗时毫秒
- `created_at` datetime 创建时间
注意:
- `request_body` 和 `response_body` 只能保存摘要、字段名、脱敏值或密文摘要。
- 禁止在审计表中保存身份证、银行卡、薪资、手机号完整明文。
- 如需排障,应通过 `trace_id`、`event_id`、`job_id` 关联业务链路,不通过审计日志还原明文数据。
---
## 22. 关键索引建议
### 22.1 `open_event_outbox`
建议索引:
- `uk_event_id (event_id)`
- `idx_event_type_occurred_at (event_type, occurred_at)`
- `idx_group_company_occurred_at (group_id, company_id, occurred_at)`
- `idx_status_available_until (status, available_until)`
- `idx_resource (resource_type, resource_id, version)`
### 22.2 `open_event_deliveries`
建议索引:
- `idx_subscription_status (subscription_id, delivery_status)`
- `idx_event_subscription (event_id, subscription_id)`
- `idx_trace_id (trace_id)`
### 22.3 `open_sync_jobs`
建议索引:
- `uk_job_id (job_id)`
- `idx_subscription_created_at (subscription_id, created_at)`
- `idx_job_status (job_status)`
- `idx_trace_id (trace_id)`
### 22.4 `open_replay_requests`
建议索引:
- `uk_request_id (request_id)`
- `idx_subscription_status (subscription_id, status)`
- `idx_created_at (created_at)`
---
## 23. 版本与幂等要求
### 23.1 版本号要求
建议所有开放资源同步时都带 `version` 字段。
规则:
- 同一对象每次有效变更都递增版本号
- 下游只允许更高版本覆盖更低版本
- 避免旧消息覆盖新状态
### 23.2 幂等要求
以下动作必须幂等:
- 事件 ACK
- 同步任务创建
- 同步结果回报
- 补数申请创建
- 补数执行
幂等键建议:
- `event_id`
- `job_id`
- `request_id`
- `trace_id`
---
## 24. 落地实施建议
### 24.1 第一阶段
- 新增开放平台支撑表
- 接入 `client_id + secret`
- 先开放组织和员工查询接口
- 先做通知下发和增量拉取
### 24.2 第二阶段
- 接入 7 天窗口控制
- 接入补数申请流程
- 接入链路追踪和审计报表
### 24.3 第三阶段
- 接入预算开放接口
- 接入审批流和预算高敏字段控制
- 接入控制台页面
---
## 25. 最终建议
本方案最重要的 5 个落地原则如下:
1. 对外通知只发事件,不直接发完整业务数据
2. 增量数据只保留 7 天在线拉取窗口
3. 过期后必须走受控补数,不允许无限制历史遍历
4. 同步链路必须全埋点,保证“谁在什么时候同步了什么”可反查
5. 跨公网传输必须采用 HTTPS + 签名 + 防重放,敏感内容必须做内容级加密
6. 尽量复用现有 `companies / departments / employees / users / budget_*` 表,降低改造成本
---
## 26. 后续落地输出建议
基于本文档,下一步建议继续输出以下内容:
- OpenAPI 3.0 Swagger 草稿
- MySQL 建表 SQL 脚本
- 后端模块目录设计
- 接口权限矩阵
- 事件发布与补数流程图