# 开放平台组织与预算同步 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 推荐流程 ```text 组织/员工/预算数据变更 ↓ 写入业务表 ↓ 写入开放事件表 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` 返回示例: ```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 ` - `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": "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 事件消息最小模型 ```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 /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` 请求示例: ```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 /open/v1/auth/refresh` ### 10.3 查询当前应用 `GET /open/v1/auth/me` 返回内容: - 应用 ID - 应用名称 - 权限范围 - 绑定集团 - 可见公司 - 可见部门 - IP 白名单状态 --- ## 11. 订阅管理接口设计 ### 11.1 创建订阅 `POST /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 /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` 返回示例: ```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 /open/v1/events/{event_id}/ack` 请求示例: ```json { "received_at": "2026-07-03T09:00:05+08:00", "receiver": "hr-sync-worker-01" } ``` ### 12.3 事件确认失败 `POST /open/v1/events/{event_id}/nack` 请求示例: ```json { "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` - 仅返回当前窗口内允许在线拉取的数据 响应示例: ```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 /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` 用途: - 订阅者在开始一轮同步前登记任务 - 平台记录该次同步和哪些事件关联 请求示例: ```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 /open/v1/sync/jobs/{job_id}` ### 14.3 回报同步结果 `POST /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 /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 /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 统一响应格式 ```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 脚本 - 后端模块目录设计 - 接口权限矩阵 - 事件发布与补数流程图