docs: 新增HCS合同推送业务流程梳理(审批零埋点/定时扫描/五步推送链/履约规则行级同步/状态机与补偿)

This commit is contained in:
刘泉佳
2026-09-28 00:21:49 +08:00
parent 6dd1d67844
commit fc5bca2b10
@@ -0,0 +1,235 @@
# HCS 合同推送业务流程梳理(现状实现)
> **日期**:2026-09-28
> **范围**:仅合同部分(C103-1 发起 / C103-11 更新 / C103-3 判重 / C103-9 履约规则 / C101-1 风险校验 / C103-2 附件转传),项目立项、付款、开票、收票链路不在本文
> **依据**:[2026-09-27-桂物合同合规系统HCS接入技术设计.md](./2026-09-27-桂物合同合规系统HCS接入技术设计.md) §5.2/§5.3/§6、[ADR-0001](../../adr/0001-hcs推送采用定时扫描模式.md)、[ADR-0002](../../adr/0002-履约规则独立数据表.md)
> **状态清单**:[2026-09-28-HCS接入实施状态与确认清单.md](./2026-09-28-HCS接入实施状态与确认清单.md)
---
## 1. 链路总览
```mermaid
flowchart TD
A[合同起草/变更<br>履约规则页签维护行] --> B{审批入口}
B -->|页面 /approve| C[ContractManageServiceImpl.approve]
B -->|MK 审批中台回调| D[ContractHandler.approveFromProcess]
C --> E[approvalStatus = approved / change_approved<br>hcs_sync_status = unsynced<br>履约规则未同步行置 row_op=add<br>合同主文件置 approved=true]
D --> E
E --> F{HcsPushTask @Scheduled<br>sync-enabled 开关 + 分布式锁}
F -->|扫 unsynced / failed且未超限| G[HcsContractSyncSupport.pushContract]
G --> G1[① C101-1 客商风险校验]
G --> G2[② 附件转传 C103-2]
G --> G3{③ hcs_fd_id 已存在?}
G3 -->|否| G4[C103-3 判重 → C103-1 起草]
G3 -->|是| G5[C103-11 更新]
G4 --> H[回填 hcs_fd_id 置 synced]
G5 --> H
H --> I[④ 履约规则 C103-9 行级同步]
I --> J[回填 fdExeplanId 与阶段 fdId]
I -->|任一步失败| K[failed + retry_count+1 + msg<br>下轮扫描补偿,超限人工重推]
G1 -->|黑名单且阻断开启| K
G2 -->|转传失败| K
```
一句话概括:**审批只落状态(零埋点),定时任务扫描 `unsynced` 后按"风险校验 → 附件转传 → 判重/起草或更新 → 履约规则"四步推送,失败自动补偿,超限人工重推。**
---
## 2. 阶段一:数据准备与审批(不调 HCS)
### 2.1 履约规则行维护(合同表单"履约规则"页签)
- 端点:`GET/POST /contract-exe-stage/list|submit|remove`(`ContractExeStageController` → `ContractExeStageServiceImpl`)
- 数据表:`blade_contract_exe_stage`(ADR-0002),合同维度维护
- 行级差异标记(保存时自动判定):
| 操作 | 生成的标记 | 说明 |
|---|---|---|
| 新增行 | `row_op=add`,`hcs_sync_status=unsynced` | 待随合同链路首推 |
| 编辑未同步行(无 hcs_stage_fd_id) | 保持 `add` | 尚未推过 HCS,仍按新增上报 |
| 编辑已同步行(有 hcs_stage_fd_id) | 置 `update` | C103-9 行级更新必须带合规阶段ID |
| 删除已同步行 | 软删(is_deleted=1)+ 置 `delete` | C103-9 凭 hcs_stage_fd_id 上报删除;**不可物理删除** |
| 删除未同步行 | 仅软删,`row_op` 不变 | HCS 侧无此行,无需上报 |
### 2.2 审批通过(两条入口,零埋点)
| 入口 | 代码位置 | 共同动作 |
|---|---|---|
| 页面审批 | `ContractManageController /approve` → `ContractManageServiceImpl.approve` | ① `approvalStatus = approved`(变更单为 `change_approved`);② `hcs_sync_status = unsynced`(**唯一的 HCS 埋点,不调任何接口**,ADR-0001);③ 履约规则未同步行批量置 `row_op=add`(ADR-0002);④ 变更单合并变更附件、清空变更字段;⑤ 合同主文件 JSON 置 `approved=true`(`markContractFilesApproved`,供附件转传筛选) |
| MK 审批中台回调 | `MkProcessBizHandlers.ContractHandler.approveFromProcess` | 同上 ①②(幂等:已 approved/change_approved 直接返回) |
> 重审/变更场景不重置 `unsynced` 就会漏推:首次入库靠数据库默认值 `unsynced`,但已 `synced` 的单据变更后必须重新置位才能再次进入推送队列。
---
## 3. 阶段二:定时扫描(`HcsPushTask`)
| 项 | 实现 |
|---|---|
| 调度 | `@Scheduled(cron = "${thirdParty.hcs.push.cron:0 */5 * * * ?}")`,总开关 `push.sync-enabled` **默认 false**(Nacos 灰度打开) |
| 多实例防重 | Redis 分布式锁 `blade:third:hcs:push:task:lock`,`setIfAbsent` 抢锁 + 持有值比对释放,TTL 240s |
| 任务顺序 | 项目 → **合同** → 付款 → 开票 → 收票,各类独立 try/catch 互不影响 |
| 合同扫描条件 | `approvalStatus IN (approved, change_approved)` 且 `hcs_sync_status IN (unsynced, failed)` 且(`hcs_retry_count` 为空 或 `< push.max-retry`),`LIMIT push.batch-size`(默认 50),按主键序 |
| 单条处理 | 调 `HcsContractSyncSupport.pushContract`;异常统一 `markFailed`(`failed` + 重试计数+1 + msg),不中断本轮其余单据 |
---
## 4. 阶段三:单条推送链(`pushContract` 五步)
### ① 客商风险校验(开关 `contract.require-risk-check`,默认开)
1. 甲方、乙方各查一次:按名称反查 `CustomerArchive`(fullName/shortName 精确匹配)取客商编号,作为 C101-1 的 `fdNumber`(**用哪个编码待联调,§9-U6**;反查不到 → 留痕 `skip`,不阻断)。
2. 调 C101-1 `getRelativeRisk` → `fdBlackStatus` / `fdRiskLevel`。
3. 留痕:`blade_hcs_check_log`(biz_type=contract, check_type=risk, result=pass/fail/skip, trace_id)+ 合同 `hcs_risk_level`(取先查到的非空值)/ `hcs_black_status`(任一方黑名单记 yes)/ `hcs_risk_time`。
4. **阻断条件**:黑名单(fdBlackStatus=yes,取值口径待联调 §9-U3)且 `risk.block-blacklist=true` → 抛异常,合同落 `failed`,msg="客商黑名单,禁止推送"。
### ② 附件转传(开关 `attachment.transfer-enabled`,默认开)
- 范围:合同主文件 `contractFileJson` 中 `approved=true` 的文件 + 其它附件 `attachmentsJson`(去重键照 `attachmentKey` 惯例:url|link|fileUrl|downloadUrl|uid|originalName|name)。
- 流程:按元数据 URL 下载(相对路径拼 `attachment.file-download-base`)→ C103-2 multipart 上传(fdAnonymous=false, fdLoginName=承办人)→ 取合规附件 fdId。
- 映射缓存:Redis `blade:third:hcs:att:{attachmentKey}` → fdId,TTL 7 天,命中不重传;未命中重传(重复上传仅浪费存储无业务风险)。
- **任一附件失败 → 整单失败进补偿重试**(§6.5)。
- ⚠️ **挂载点未定(§9-U8)**:v4 的 C103-1 入参未定义附件字段,当前 fdId 仅转传+缓存,C103-1 报文不带附件;合规方确认后接线。
### ③ 合同起草 / 更新(先判重)
**首推(`hcs_fd_id` 为空)**:
1. 判重:C103-3 `conditions={"fdNumber": contractNo}`(查询能力待联调 §9-U4)→ 命中直接回填已有合规合同ID 置 `synced`,跳过新建。
2. 未命中 → C103-1 `draft`,顶层与字段:
- `fdTemplateId` ← `contract.template-mapping[contractCategory]`(**未配置则推送失败并提示,§9-U7**)
- `fdOuterSystem` ← `src-system`(约定值 TMS,D14);`fdLoginName` ← 承办人(handlerUserId → UserCache 登录名,解析不到失败);`saveType=submit`
- `xformFields`(值一律字符串传,时间字段带 `fdType=timestamp`):
| 字段 | TMS 来源 | 备注 |
|---|---|---|
| fdNumber / CONTRACT_NO | contractNo | 判重候选键 |
| fdSubject / CONTRACT_NAME | contractName | |
| fdAmount / AMOUNT | contractAmount | 字符串传值 |
| fdCurrencyId / CURRENCY | settlementCurrency → `currency-name-mapping` | 默认 RMB→人民币 |
| fdSettlementType | settlementTypeMapping[settlementMode] | 未配置不传并 warn(§9-U3) |
| fdSepcailContract | 固定 "false" | D15,原文拼写笔误必须沿用 |
| CONTRACT_TYPE | contractCategory | |
| PARTY_A / PARTY_B | partyA / partyB | 名称字符串 |
| SIGN_DATE / fdEstimateStartTime / fdEstimateEndTime | signDate / startDate / endDate | timestamp |
| CREATE_TIME / fdCreateTime / fd_syn_time | createTime / 推送时刻 | timestamp |
| CREATE_USER_NAME | handlerUserName | 承办人名称 |
| CONTRACT_STATUS | 不传 | 是否必传待联调(§9-U3) |
- `signer[]`:甲/乙各一条 `{fdStanding, fdName=partyA/partyB, fdNumber=反查客商编号(查不到留空)}`;`fdType`(inner/外部)不传(§9-U6)
3. 成功返回 `data.fdContractId` → **立即 markSynced 持久化**(`hcs_fd_id`、`synced`、retry=0、sync_time、msg)——履约规则同步若失败,下轮凭已持久化的 fdId 走更新补偿,不会在 HCS 重复建合同。
**更新(`hcs_fd_id` 非空,即变更单 change_approved 或补偿重试)**:C103-11 `updateDraft`,入参同发起;响应无 data,success 判断(**HCS 侧如何定位已有合同待联调,§9-U5**)。
### ④ 履约规则 C103-9 行级同步(`HcsExeStageSyncSupport.syncExeStages`)
前置:合同已推送(`hcs_fd_id` 非空)。
1. 取行:`selectSyncRows` 原生 SQL 查 `row_op <> 'none'` 的行(**含软删行**,delete 行需照常上报;无待同步行直接返回提示)。
2. 组装:顶层 `fdContractId` = **HCS 合同ID(hcs_fd_id)**、`fdOuterSystem`、`fdLoginName`(承办人)、`fdEopExeplanOperateType` = 首次 `add`(无 hcs_exeplan_id)/ 后续 `update`、`fdExeplanId` = 已回填的计划ID(有则传)。
3. 行字段:`fdPerformType`(payment/collection)、`fdFundNature`、`fdFundAmount`(元)、`fdFundDate`(毫秒)、`fdInvoiceRule`、`fdInvoiceCycle`、`fdFundType`(行值为空取 `exe-stage.fund-type` 默认 byAmount)、`fdIsFinish`、`fdExtendContent="{}"`;**update/delete 行必须带 `fdId` = `hcs_stage_fd_id`**,add 行不带。
4. 回填(合规返回的 fdId **必须持久化**,ADR-0002):
- `data.fdExeplanId` → `blade_contract_manage.hcs_exeplan_id`
- `data.fdChangeStages[].fdId` → 行 `hcs_stage_fd_id`:返回行数与请求一致时**按行序对应**;不一致时按业务属性组合键(performType|fundNature|amount|date)兜底匹配
- 成功行:`row_op=none`、`hcs_sync_status=synced`
- **add 行未匹配到阶段ID**:保持待同步(failed + msg),下轮重试——存在重复建行风险,联调时校准对应关系
5. 同步失败 → 抛异常 → 合同 `markFailed`(fdId 已保留,下轮走 C103-11 → C103-9 继续补偿)。
### ⑤ 结果
全链成功:合同 `synced`,返回汇总(如 "C103-1 合同发起成功;C103-9 履约规则同步成功(add,3 行)")。
---
## 5. 状态机与失败补偿
```
审批通过/MK回调 推送成功
(未推送)unsynced ──────────→ 扫描推送 ──────────→ synced
↑ │
│ 人工重推(重置unsynced+清零) │ 失败
│ ↓
└────────────── failed (retry_count+1)
retry < max-retry → 下轮自动重试
retry ≥ max-retry → 仅人工重推
```
- 权威状态在 TMS 侧字段(`hcs_sync_status`/`hcs_sync_msg`),HCS 不回写业务状态;**排查推送问题以此为准,不以审批日志为准**。
- 失败日志均携带合规 traceId(`msg=xx, traceId=xx`),排查时提供给合规方。
---
## 6. 手动操作与对账入口
| 端点 | 用途 |
|---|---|
| `POST /hcs/sync/contract/{id}` | 单条重推:重置 unsynced + retry=0 后立即推送(仅 approved/change_approved 允许) |
| `POST /hcs/sync/contract` | 批量推一轮(复用扫描逻辑) |
| `POST /hcs/master-data/refresh` | 刷新模板/分类/币种缓存,并校验 template-mapping 中模板ID 是否存在(仅告警) |
| `GET /hcs/recon/contract?contractNo=` | 按合同编号回查 HCS 侧合同(C103-3) |
| `GET /hcs/recon/contract/detail?fdId=|fdNumber=` | 合同详情回查(C103-4,出参结构联调后明确) |
| `GET /contract-exe-stage/list?contractId=` | 查合同履约规则行 |
---
## 7. 数据落点汇总
| 表 | 字段 | 写入时机 |
|---|---|---|
| `blade_contract_manage` | hcs_fd_id | C103-1/C103-3 判重命中后立即持久化 |
| | hcs_sync_status / hcs_sync_time / hcs_sync_msg / hcs_retry_count | 每次推送成功/失败回写 |
| | hcs_exeplan_id | C103-9 返回 fdExeplanId 后回填 |
| | hcs_risk_level / hcs_black_status / hcs_risk_time | 每次推送前风险校验后留痕 |
| `blade_contract_exe_stage` | row_op | 维护接口/审批通过置 add;同步成功置 none |
| | hcs_stage_fd_id / hcs_sync_status / hcs_sync_msg | C103-9 回填与结果回写 |
| `blade_hcs_check_log` | 整行 | 每次风险校验留痕(pass/fail/skip + trace_id) |
---
## 8. 合同链路配置依赖(Nacos `thirdParty.hcs.*`)
| 配置 | 默认 | 说明 |
|---|---|---|
| push.sync-enabled | false | 总开关,联调稳定后打开 |
| push.cron / batch-size / max-retry | 5分钟 / 50 / 5 | 扫描节奏与补偿上限 |
| contract.template-mapping | 空 | **合同推送必配**(U7,合规方提供模板清单) |
| contract.settlement-type-mapping | 空 | fdSettlementType 字典映射,未配置不传(U3) |
| contract.currency-name-mapping | {"RMB": "人民币"} | 币种名称映射 |
| contract.require-risk-check | true | 合同推送前风险校验开关 |
| risk.block-blacklist | true | 黑名单是否阻断(置 failed) |
| exe-stage.fund-type | byAmount | 履约规则行金额类型默认值 |
| attachment.transfer-enabled / file-download-base | true / 空 | 附件转传开关与文件服务基址 |
| src-system | TMS | fdOuterSystem / fd_sources(D14) |
| auth-type / app-key / app-secret / base-url | — | 认证与地址(P0 依赖项) |
---
## 9. 合同链路待联调确认点(对应 §9)
| # | 事项 | 当前处理 |
|---|---|---|
| U3 | fdSettlementType 字典值、CONTRACT_STATUS 是否必传、fdBlackStatus 取值 | 未配置不传 / 不传 / 按 "yes" 等值判定 |
| U4 | C103-3 conditions 按 fdNumber 精确查询是否支持;是否按 fd_kingdee_id 幂等 | 按文档 conditions 实现,联调验证 |
| U5 | C103-11 如何定位已有合同 | 入参同发起,补偿重试会重复调 C103-11 |
| U6 | signer 编号体系与 fdType 判定;C101-1 用哪个客商编码 | 按名称反查 CustomerArchive.customerCode,fdType 不传 |
| U7 | 模板ID清单 | template-mapping 空占位,未配置推送失败 |
| U8 | 合同附件传递方式 | 已转传+缓存 fdId,报文暂不挂载 |
| (新增) | C103-9 返回 fdChangeStages 与请求行的对应关系 | 行序优先、属性组合键兜底 |
---
## 10. 代码索引
| 类 | 位置 | 职责 |
|---|---|---|
| `ContractExeStageController` / `ContractExeStageServiceImpl` | blade-transport controller / service.impl | 履约规则行维护与行级差异标记 |
| `HcsPushTask#pushUnsyncedContracts` | blade-transport task | 合同扫描推送(含锁) |
| `HcsContractSyncSupport#pushContract` | blade-transport support.hcs | 五步链编排 |
| `HcsExeStageSyncSupport#syncExeStages` | blade-transport support.hcs | C103-9 行级同步与回填 |
| `HcsAttachmentSupport#transferContractAttachments` | blade-transport support.hcs | 附件转传与映射缓存 |
| `HcsMasterDataSupport` | blade-transport support.hcs | 模板/分类/币种缓存与映射校验 |
| `HcsReconSupport#reconContract / reconContractDetail` | blade-transport support.hcs | 对账回查 |
| `ContractManageServiceImpl#approve` / `MkProcessBizHandlers$ContractHandler` | blade-transport service.impl / mk | 审批零埋点置状态 |
| `HcsSyncController` | blade-transport controller | 手动重推/批量/主数据刷新/对账端点 |
| `IHcsClient`(contractDraft/contractUpdate/contractList/contractDetail/exeplanAdd/uploadAttachment/getRelativeRisk) | blade-third-party-api/blade-hcs-api | HCS 接口客户端 |