diff --git a/doc/docs/hcs合同/2026-09-28-HCS合同推送业务流程梳理.md b/doc/docs/hcs合同/2026-09-28-HCS合同推送业务流程梳理.md new file mode 100644 index 00000000..269176cb --- /dev/null +++ b/doc/docs/hcs合同/2026-09-28-HCS合同推送业务流程梳理.md @@ -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[合同起草/变更
履约规则页签维护行] --> B{审批入口} + B -->|页面 /approve| C[ContractManageServiceImpl.approve] + B -->|MK 审批中台回调| D[ContractHandler.approveFromProcess] + C --> E[approvalStatus = approved / change_approved
hcs_sync_status = unsynced
履约规则未同步行置 row_op=add
合同主文件置 approved=true] + D --> E + E --> F{HcsPushTask @Scheduled
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
下轮扫描补偿,超限人工重推] + 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 接口客户端 |