From fc5bca2b108a5a6f714893a016737435eec17fef Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Mon, 28 Sep 2026 00:21:49 +0800
Subject: [PATCH] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9EHCS=E5=90=88=E5=90=8C?=
=?UTF-8?q?=E6=8E=A8=E9=80=81=E4=B8=9A=E5=8A=A1=E6=B5=81=E7=A8=8B=E6=A2=B3?=
=?UTF-8?q?=E7=90=86=EF=BC=88=E5=AE=A1=E6=89=B9=E9=9B=B6=E5=9F=8B=E7=82=B9?=
=?UTF-8?q?/=E5=AE=9A=E6=97=B6=E6=89=AB=E6=8F=8F/=E4=BA=94=E6=AD=A5?=
=?UTF-8?q?=E6=8E=A8=E9=80=81=E9=93=BE/=E5=B1=A5=E7=BA=A6=E8=A7=84?=
=?UTF-8?q?=E5=88=99=E8=A1=8C=E7=BA=A7=E5=90=8C=E6=AD=A5/=E7=8A=B6?=
=?UTF-8?q?=E6=80=81=E6=9C=BA=E4=B8=8E=E8=A1=A5=E5=81=BF=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../2026-09-28-HCS合同推送业务流程梳理.md | 235 ++++++++++++++++++
1 file changed, 235 insertions(+)
create mode 100644 doc/docs/hcs合同/2026-09-28-HCS合同推送业务流程梳理.md
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 接口客户端 |