Files
prop-ai-hr/docs/open-org-sync-api-design-v1.md
T
admin f5137a2b05 feat(aihr): add open org snapshot sync endpoint
新增 POST /api/aihr/org/sync,从开放组织系统 /open/v1/sync/snapshot
拉取公司/部门/员工快照刷新 aihr_org_snapshot;支持 dryRun 预检、
Bearer/client-credentials 鉴权与可选 HMAC 签名;外部未配置时保留
SQL seed。附接口契约设计 v1 与 API/DEV_SETUP 配置说明。
2026-07-07 11:05:41 +08:00

37 KiB
Raw Blame History

开放平台组织与预算同步 API 设计方案 v1

1. 文档目标

本文档用于定义本系统对外开放的组织与预算同步方案,覆盖以下对象:

  • 集团
  • 公司
  • 部门
  • 员工
  • 预算

本文档重点解决以下问题:

  • 如何对外提供统一、可控、可审计的开放接口
  • 如何通过“变更通知 + 主动拉取”的方式降低系统开销
  • 如何通过 7 天拉取窗口、过期处理、补数申请提高安全性
  • 如何在跨数据中心、跨公网传递时通过传输加密与内容加密保障数据安全
  • 如何记录通知、拉取、同步结果,便于事后追踪和排障
  • 如何在现有系统表结构基础上最小代价落地

2. 设计结论

本方案采用以下核心模式:

  1. 内部业务数据发生变化后,系统生成变更事件
  2. 系统将变更事件通知给订阅者
  3. 订阅者收到通知后,在 7 天有效窗口内主动拉取最新数据
  4. 超过 7 天后,不再允许直接拉取对应增量数据
  5. 如订阅者错过窗口,必须发起补数申请
  6. 全链路记录通知状态、拉取状态、同步结果和失败原因

这是一种兼顾性能、安全和可运维性的企业级设计。


3. 总体架构

3.1 角色

  • 主系统:当前 backend 服务,对外统一提供开放接口
  • 订阅者系统:外部 ERP、HR、财务、BI、协同系统等
  • 开放平台应用:每个外部系统在本系统中注册的独立接入身份
  • 通知中心:负责事件生成、投递、重试、过期、补数审批

3.2 架构原则

  • 对外统一入口:全部走 backend 的 /open/v1/*
  • 内外账号隔离:外部系统不用内部员工登录账号
  • 事件通知最小化:通知消息只传事件元信息,不传完整业务数据
  • 数据主动拉取:业务明细通过开放接口主动拉取
  • 时间窗口受控:增量数据默认仅保留 7 天在线可拉取能力
  • 审计可追踪:每次通知、每次拉取、每次补数都有日志
  • 数据范围受控:必须按 group_id / company_id / department_id 控制可见范围

3.3 推荐流程

组织/员工/预算数据变更
    ↓
写入业务表
    ↓
写入开放事件表 open_event_outbox
    ↓
按订阅关系分发通知
    ↓
订阅者收到消息
    ↓
7天内调用 /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

返回示例:

{
  "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 签名规则

建议签名原文:

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 时,响应体建议统一为:

{
  "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"
  }
}

加密前明文示例:

{
  "event_id": "evt_202607030001",
  "resource_type": "employee",
  "resource_id": 135,
  "payload": {
    "name": "张三",
    "phone": "13800000000",
    "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 事件消息最小模型

{
  "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 总览

开放平台统一前缀:

/open/v1

9.1 认证接口

  • POST /open/v1/auth/token
  • POST /open/v1/auth/refresh
  • GET /open/v1/auth/me

9.2 订阅管理接口

  • POST /open/v1/subscriptions
  • GET /open/v1/subscriptions
  • GET /open/v1/subscriptions/{subscription_id}
  • PATCH /open/v1/subscriptions/{subscription_id}
  • POST /open/v1/subscriptions/{subscription_id}/enable
  • POST /open/v1/subscriptions/{subscription_id}/disable

9.3 事件通知接口

  • GET /open/v1/events
  • GET /open/v1/events/{event_id}
  • POST /open/v1/events/{event_id}/ack
  • POST /open/v1/events/{event_id}/nack

9.4 数据拉取接口

  • GET /open/v1/sync/changes
  • GET /open/v1/sync/snapshot
  • GET /open/v1/groups/{id}
  • GET /open/v1/companies/{id}
  • GET /open/v1/departments/{id}
  • GET /open/v1/employees/{id}
  • GET /open/v1/budget-versions/{id}
  • GET /open/v1/budget-subjects/{id}
  • GET /open/v1/budget-items/{id}
  • GET /open/v1/budget-plans/{id}

9.5 同步任务接口

  • POST /open/v1/sync/jobs
  • GET /open/v1/sync/jobs/{job_id}
  • POST /open/v1/sync/jobs/{job_id}/complete

9.6 补数申请接口

  • POST /open/v1/replay-requests
  • GET /open/v1/replay-requests
  • GET /open/v1/replay-requests/{request_id}
  • POST /open/v1/replay-requests/{request_id}/cancel
  • POST /open/v1/replay-requests/{request_id}/approve
  • POST /open/v1/replay-requests/{request_id}/reject
  • POST /open/v1/replay-requests/{request_id}/execute

9.7 追踪与审计接口

  • GET /open/v1/traces/{trace_id}
  • GET /open/v1/audits
  • GET /open/v1/subscriptions/{subscription_id}/deliveries
  • GET /open/v1/subscriptions/{subscription_id}/sync-records

10. 认证接口设计

10.1 获取令牌

POST /open/v1/auth/token

请求示例:

{
  "grant_type": "client_credentials",
  "client_id": "ext_hr_001",
  "client_secret": "******"
}

响应示例:

{
  "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 /open/v1/auth/refresh

10.3 查询当前应用

GET /open/v1/auth/me

返回内容:

  • 应用 ID
  • 应用名称
  • 权限范围
  • 绑定集团
  • 可见公司
  • 可见部门
  • IP 白名单状态

11. 订阅管理接口设计

11.1 创建订阅

POST /open/v1/subscriptions

请求示例:

{
  "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"
  }
}

响应示例:

{
  "success": true,
  "data": {
    "subscription_id": 10001,
    "status": "enabled"
  }
}

11.2 查询订阅列表

GET /open/v1/subscriptions

支持筛选:

  • status
  • subscriber_name
  • group_id

11.3 启停订阅

  • POST /open/v1/subscriptions/{subscription_id}/enable
  • POST /open/v1/subscriptions/{subscription_id}/disable

12. 事件通知接口设计

12.1 拉取事件列表

GET /open/v1/events

查询参数:

  • status
  • event_type
  • since_time
  • limit

返回示例:

{
  "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 /open/v1/events/{event_id}/ack

请求示例:

{
  "received_at": "2026-07-03T09:00:05+08:00",
  "receiver": "hr-sync-worker-01"
}

12.3 事件确认失败

POST /open/v1/events/{event_id}/nack

请求示例:

{
  "error_code": "SIGNATURE_INVALID",
  "error_message": "签名校验失败"
}

13. 数据拉取接口设计

13.1 增量拉取

GET /open/v1/sync/changes

查询参数:

  • since_time:开始时间
  • cursor:上次同步游标
  • resource_type:资源类型
  • event_type:事件类型
  • limit:单次拉取条数,建议不超过 500

说明:

  • 优先使用 cursor
  • 没有 cursor 时可降级使用 since_time
  • 仅返回当前窗口内允许在线拉取的数据

响应示例:

{
  "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 /open/v1/sync/snapshot

查询参数:

  • resource_type
  • group_id
  • company_id
  • department_id
  • page
  • page_size

用途:

  • 首次接入
  • 下游系统重建
  • 补数审批后全量重拉

13.3 单对象拉取

集团

GET /open/v1/groups/{id}

公司

GET /open/v1/companies/{id}

部门

GET /open/v1/departments/{id}

员工

GET /open/v1/employees/{id}

预算版本

GET /open/v1/budget-versions/{id}

预算科目

GET /open/v1/budget-subjects/{id}

预算项

GET /open/v1/budget-items/{id}

预算计划

GET /open/v1/budget-plans/{id}

13.4 单对象接口通用参数

  • fields:指定返回字段
  • version:期望版本号
  • include_deleted:是否包含已删除记录

14. 同步任务接口设计

14.1 创建同步任务

POST /open/v1/sync/jobs

用途:

  • 订阅者在开始一轮同步前登记任务
  • 平台记录该次同步和哪些事件关联

请求示例:

{
  "sync_type": "incremental",
  "subscription_id": 10001,
  "start_cursor": "cur_20260703090000123",
  "resource_types": ["department", "employee"]
}

响应示例:

{
  "success": true,
  "data": {
    "job_id": "job_20260703_0001",
    "job_status": "pending"
  }
}

14.2 查询同步任务

GET /open/v1/sync/jobs/{job_id}

14.3 回报同步结果

POST /open/v1/sync/jobs/{job_id}/complete

请求示例:

{
  "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 /open/v1/replay-requests

请求示例:

{
  "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]
}

响应示例:

{
  "success": true,
  "data": {
    "request_id": "rr_202607030001",
    "status": "pending_review"
  }
}

16.4 查询补数申请

GET /open/v1/replay-requests

16.5 查询补数申请详情

GET /open/v1/replay-requests/{request_id}

16.6 取消补数申请

POST /open/v1/replay-requests/{request_id}/cancel

16.7 审批补数申请

批准

POST /open/v1/replay-requests/{request_id}/approve

驳回

POST /open/v1/replay-requests/{request_id}/reject

16.8 执行补数

POST /open/v1/replay-requests/{request_id}/execute

执行方式:

  • 重新开放临时拉取窗口
  • 生成重放事件批次
  • 生成受控快照任务

17. 追踪与审计接口设计

17.1 查询链路追踪

GET /open/v1/traces/{trace_id}

返回应包含:

  • 事件信息
  • 投递记录
  • ACK 记录
  • 同步任务
  • 同步结果
  • 失败原因
  • 是否过期
  • 是否补数

17.2 查询审计日志

GET /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 统一响应格式

{
  "success": true,
  "code": "OK",
  "message": "操作成功",
  "data": {}
}

19.2 分页响应格式

{
  "success": true,
  "data": {
    "items": [],
    "page": 1,
    "page_size": 50,
    "total": 1200
  }
}

19.3 游标响应格式

{
  "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 脚本
  • 后端模块目录设计
  • 接口权限矩阵
  • 事件发布与补数流程图