:feat: 新增外部客商高风险黑名单管控与合同自动特批

1. GET /hcs/risk 返回 CustomerRiskVO:内部组织客商默认无风险不调合规,外部客商高风险/黑名单返回固定风险提示文案(riskTip),供基本信息备注后展示
2. 合同三个提交入口(submit/toTemporary/submitFormal)校验:甲乙方任一方高风险/黑名单时必须上传特批说明附件(attachmentType=spec_explanation)
3. pushContract 流程重排:C101-1 风险校验仅留痕不再阻断,C103-1 起草成功后高风险/黑名单合同自动挂起特批审核(currentNode)并发起 C103-8,解开原 need 挂起无 hcs_fd_id 的特批死锁
4. 特批结果联动:B103 回传/人工确认 approved 恢复审批通过展示,rejected 联动合同审批驳回走重新审批闭环
5. 补充推送入口/自动特批触发/跳过原因日志,流程梳理文档同步更新(§二/§七/§八/新增§九)
This commit is contained in:
刘泉佳
2026-10-11 17:47:53 +08:00
parent e6d60eecc1
commit 8e6e263921
8 changed files with 536 additions and 39 deletions
@@ -0,0 +1,211 @@
# 新建合同推送 HCS(合同合规系统)流程梳理
> 依据代码现状整理,核心类:`HcsContractSyncSupport`(blade-service/blade-transport/.../support/hcs)。
> 对接接口:C101-1 风险校验、C103-2 附件上传、C103-3 合同判重、C103-1 合同发起、C103-11 合同更新、C103-8 特批、C103-9 履约规则。
> "必填" 标注来源:代码注释中标注的 v4 接口文档要求(HCS 侧),以及 TMS 推送前强校验(缺失直接失败)。
---
## 一、触发入口(共 3 个)
| 入口 | 触发时机 | 代码位置 | 说明 |
| --- | --- | --- | --- |
| 审批通过即时推送 | 合同审批通过(含变更通过)事务提交后 | `ContractManageServiceImpl#approve` → 置 `hcsSyncStatus=unsynced` → `HcsImmediatePushSupport#pushContractAfterCommit` | 受开关 `thirdParty.hcs.push.immediate-enabled` 控制(默认关闭,走扫描兜底);afterCommit 异步执行(2 线程 daemon 池),Redis 单条锁防与扫描任务并发双推 |
| 定时扫描兜底 | 扫描周期到 | `HcsPushTask#pushUnsyncedContracts` | 扫描条件:`approvalStatus in (approved, change_approved)` 且 `hcsSyncStatus in (unsynced, failed)` 且 `hcsRetryCount < maxRetry`,按 `batchSize` 限量 |
| 页面手动重推 | 用户点"推送合规/重推"按钮 | `HcsSyncController` → `HcsContractSyncSupport#repushContract(id)` | 前置校验:仅审批通过可推、特批挂起中不可推;重置为待同步并清零重试计数后立即推送 |
三条入口最终都收敛到 `HcsContractSyncSupport#pushContract(ContractManage)`。
---
## 二、推送主流程(pushContract 内部步骤)
```
⓪ 特批挂起门禁
hcsSpecStatus ∈ {need, processing} → 跳过推送,等待 C103-8 特批结果回传
① 客商风险校验(C101-1,开关 require-risk-check)
甲方、乙方各查一次;按名称反查 CustomerArchive 客商编码,反查不到记 skip 留痕不阻断
仅留痕 hcs_risk_* 并返回高风险/黑名单标志(风险等级=2 或任一方黑名单),不再阻断推送
(2026-10-11 变更:原"黑名单挂起终止推送"逻辑取消,改为起草成功后自动转特批,见 §九)
② 附件转传(C103-2,开关 attachment.transfer-enabled)
合同主文件(approved=true)+ 其它附件上传合规,取得合规附件ID列表
③ 判重(C103-3,仅首推 hcsFdId 为空时)
conditions = {"fdNumber": 合同编号};命中 → 回填已有合规合同ID,置 synced,结束
④ 合同发起/更新
首推且未判重命中 → C103-1 发起(返回 fdContractId,必须持久化)
已有 hcsFdId(变更单)→ C103-11 更新(顶层 fdId 定位)
⑤ 自动特批(C103-8,高风险/黑名单 且 hcsSpecStatus ≠ approved 时)
置 hcsSpecStatus=need + currentNode="特批审核"(TMS 流程挂起展示)
→ 筛选「特批说明」附件(attachmentsJson 中 attachmentType=spec_explanation)转传 C103-2
→ 自动发起 C103-8 → hcsSpecStatus=processing
发起失败保持 need 仅留痕,可人工经 /hcs/contract/spec-apply 重发
⑥ 履约规则同步(C103-9,开关 exe-stage.sync-enabled,一期默认关闭)
随合同链路行级同步,失败抛出走下轮凭 hcs_fd_id 更新补偿
```
结果回填:
- **成功** `markSynced`:`hcs_fd_id` 回填、`hcs_sync_status=synced`、重试清零、记录 `hcs_sync_time/hcs_sync_msg`
- **失败** `markFailed`:`hcs_sync_status=failed`、重试次数 +1、记录失败原因,交扫描任务下轮补偿
---
## 三、C103-1 / C103-11 报文顶层字段(`HcsContractDraftDTO`)
| HCS 字段 | 说明 | TMS 取值 | HCS 必填 |
| --- | --- | --- | --- |
| `fdId` | 合同ID | `contractManage.hcsFdId`(C103-1 返回值) | **必填**(C103-11 更新时);C103-1 新建不传 |
| `fdTemplateId` | 合同模板ID | `template-mapping[contractCategory]` 配置映射 | **必填**;TMS 强校验:未配置映射直接抛异常失败(§9-U7) |
| `fdOuterSystem` | 外部系统标识 | 配置 `thirdParty.hcs.src-system`(约定值 WLTMS) | **必填** |
| `fdLoginName` | 操作人登录名 | 合同承办人:`handlerUserId` → `UserCache.getUser().getAccount()` | **必填**;TMS 强校验:承办人未维护或登录名解析不到直接失败 |
| `saveType` | 保存类型 | 固定 `submit`(提交) | **必填** |
| `fdFieldJson` | 合同字段 | xformFields + signer + attachment(见下) | **必填** |
---
## 四、fdFieldJson.xformFields 字段映射(`buildXformFields`)
值一律字符串传值;时间字段格式 `yyyy-MM-dd` 且带 `fdType=timestamp`。
| HCS 字段(name) | 中文名 | TMS 来源字段 | HCS 必填 | 备注 |
| --- | --- | --- | --- | --- |
| `fdId` | 合同ID | `hcsFdId` | **必填**(更新时) | 新建不传 |
| `fdNumber` | 合同编号 | `contractNo` | **必填** | fd 模板字段;同时用作 C103-3 判重条件 |
| `CONTRACT_NO` | 合同编号 | `contractNo` | — | CONTRACT 模板字段(与 fdNumber 同值双传) |
| `fdSubject` | 合同标题 | `contractName` | **必填** | fd 模板字段 |
| `CONTRACT_NAME` | 合同名称 | `contractName` | — | CONTRACT 模板字段 |
| `fdAmount` | 合同金额 | `contractAmount` | **必填** | 字符串传值(`toPlainString`) |
| `AMOUNT` | 合同金额 | `contractAmount` | — | CONTRACT 模板字段 |
| `fdCurrencyId` | 币种 | `settlementCurrency` 经 `currency-name-mapping` | — | 默认 RMB→人民币;映射未命中传原值并告警 |
| `CURRENCY` | 币种 | 同上 | — | CONTRACT 模板字段 |
| `fdSettlementType` | 合同结算类型 | `settlement-type-mapping[settlementMode]` | **必填** | 枚举:payment=付款 / payment_collection=收支双向 / collection=收款 / none=不结算;未配置映射则不传并告警(§9-U3 待联调) |
| `fdSepcailContract` | 特批流程 | 固定 `false` | — | 文档原文拼写笔误必须沿用(D15);开关 `contract.special-contract-enabled` 预留 |
| `CONTRACT_TYPE` | 合同类型 | `contractCategory` | — | |
| `PARTY_A` | 甲方名称 | `partyA` | — | TMS 存名称字符串 |
| `PARTY_B` | 乙方名称 | `partyB` | — | |
| `SIGN_DATE` | 签订日期 | `signDate` | **必填** | yyyy-MM-dd,fdType=timestamp |
| `fdEstimateStartTime` | 预计开始时间 | `startDate` | 期限为指定日期时必填 | fdType=timestamp |
| `fdEstimateEndTime` | 预计结束时间 | `endDate` | 期限为指定日期时必填 | fdType=timestamp |
| `fdStartTerm` | 合同开始期限 | 固定 `specifiedDate`(指定日期) | **必填** | 日期取 `startDate`;startDate 为空则传空 |
| `fdEndTerm` | 合同结束期限 | 固定 `specifiedDate`(指定日期) | **必填** | 日期取 `endDate`;endDate 为空则传空 |
| `CREATE_TIME` | 创建时间 | `createTime` | **必填** | yyyy-MM-dd,fdType=timestamp;CONTRACT 模板字段 |
| `fdCreateTime` | 创建时间 | `createTime` | **必填** | fd 模板字段(与 CREATE_TIME 同值双传) |
| `fd_syn_time` | 同步时间 | 推送时刻(`LocalDateTime.now()`) | **必填** | yyyy-MM-dd,fdType=timestamp |
| `CREATE_USER_NAME` | 承办人名称 | `handlerUserName` | **必填** | |
| `CONTRACT_STATUS` | 合同状态 | `status-mapping[approvalStatus]` | **必填**(v4) | draft/approving/approved 等;未配置映射则不传并告警(§9-U3) |
| `fdServings` | 合同份数 | `copyCount` | **必填** | 字符串传值 |
| `fdDesc` | 合同说明 | `remark` | 选填 | |
**v4 选填字段暂不传(待联调)**:`fdCoTakers` 协办人、`fdUnderTaker`/`fdUnderTakerDept` 承办人及部门(组织架构对象传值待确认)、`fdRelations` 关联合同、`fdProject` 关联项目(可经立项 hcs_fd_id 关联,对象传值待确认)、`fdContractGwNature` 结算模式、`fdContractAttribute` 合同属性、`fdUrgencyLevel` 紧急程度。
---
## 五、fdFieldJson.signer 签署方(相对方)
固定传两条:甲方(`fdStanding=甲`)、乙方(`fdStanding=乙`)。
| HCS 字段 | 说明 | TMS 取值 | HCS 必填 | 备注 |
| --- | --- | --- | --- | --- |
| `fdStanding` | 签署地位 | 固定 甲 / 乙 | — | |
| `fdName` | 相对方名称 | `partyA` / `partyB` | — | |
| `fdNumber` | 相对方编号 | 按名称反查 `CustomerArchive.customerCode`(全称或简称精确匹配) | — | 反查不到留空不阻断(§9-U6) |
| `fdType` | 单位类型(inner=内部单位) | 暂不传 | — | inner/外部判定规则待联调(§9-U6) |
---
## 六、fdFieldJson.attachment 附件(C103-2 转传后挂载)
| HCS 字段 | 说明 | TMS 取值 | HCS 必填 | 备注 |
| --- | --- | --- | --- | --- |
| 列表本体 | 附件数据 | C103-2 转传返回的合规附件ID列表 | **必填**(C103-1 时,无附件传空列表) | C103-11 更新传 null 不重复挂载 |
| `fdId` | 附件ID | C103-2 返回 | **必填** | |
| `fdBindType` | 操作类型 | 固定 `ADD`(新增) | **必填** | |
附件来源:合同主文件 `contractFileJson`(仅 approved=true 的)+ 其它附件 `attachmentsJson`;操作人登录名同 `fdLoginName`;受开关 `attachment.transfer-enabled` 控制。
---
## 七、依赖配置项(`thirdParty.hcs.*`)
| 配置 | 用途 | 缺失影响 |
| --- | --- | --- |
| `src-system` | 顶层 fdOuterSystem | 报文缺系统标识 |
| `push.immediate-enabled` | 审批通过即时推送开关(默认 false) | 关闭时仅走扫描兜底 |
| `push.max-retry` / `push.batch-size` | 扫描重试上限 / 批量大小 | — |
| `contract.template-mapping` | contractCategory → HCS 模板ID | **缺失直接推送失败** |
| `contract.settlement-type-mapping` | settlementMode → 结算类型枚举 | 未配置不传 fdSettlementType 并告警 |
| `contract.status-mapping` | approvalStatus → 合同状态 | 未配置不传 CONTRACT_STATUS 并告警 |
| `contract.currency-name-mapping` | 币种名称映射(默认 RMB→人民币) | 未命中传原值 |
| `contract.require-risk-check` | 推送前客商风险校验开关 | — |
| `risk.block-blacklist` | 黑名单阻断付款提交(付款链路);合同链路不再阻断推送,高风险/黑名单转自动特批(§九) | — |
| `attachment.transfer-enabled` | 附件转传开关 | 关闭时不挂附件 |
| `exe-stage.sync-enabled` | C103-9 履约规则同步开关(一期默认关) | — |
---
## 八、TMS 侧留痕字段(`blade_contract_manage`)
| 字段 | 含义 |
| --- | --- |
| `hcs_fd_id` | 合规合同ID(C103-1 返回的 fdContractId,判重命中也回填) |
| `hcs_sync_status` | unsynced / synced / failed |
| `hcs_retry_count` | 推送重试次数 |
| `hcs_sync_time` / `hcs_sync_msg` | 最近推送时间 / 结果描述 |
| `hcs_risk_level` / `hcs_black_status` / `hcs_risk_time` | 风险校验结果(甲乙双方取先查到的非空风险等级;任一方黑名单即为是) |
| `hcs_spec_status` / `hcs_spec_time` / `hcs_spec_msg` | 特批状态(none/need/processing/approved/rejected)及留痕 |
| `hcs_spec_fd_id` | 特批流程ID(C103-8 返回) |
| `hcs_spec_files_json` | 特批申请材料JSON(含 C103-2 合规附件ID) |
| `current_node` | 挂起展示节点:自动特批后置"特批审核",B103 通过恢复"审批通过",驳回置"已驳回" |
风险校验明细另写 `blade_hcs_check_log`(甲方/乙方各一条,含 traceId)。
---
## 九、外部客商高风险/黑名单管控(2026-10-11 增补)
**管控口径**:内部组织类型客商(`CustomerArchive.customerKind=internal`)默认无风险,不调 C101-1;外部客商(external,含档案不存在兜底按外部处理)命中 **高风险(fdRiskLevel=2)或黑名单(fdBlackStatus=yes)** 时进入管控。甲/乙任一方命中即整单管控。
### 9.1 签约前风险查询(展示用)
`GET /hcs/risk?customerCode=` → `HcsCreditCheckSupport#queryCustomerRisk` → `CustomerRiskVO`:
| 字段 | 说明 |
| --- | --- |
| `customerKind` | internal / external |
| `fdBlackStatus` / `fdRiskLevel` | C101-1 原值透传 |
| `risky` | 高风险/黑名单判定结果 |
| `riskTip` | 风险提示文案(前端展示在基本信息备注后) |
提示文案固定模板:`【公司名称】:被列入外部黑名单;涉贪污受贿裁判相关提及方(3年以上及其他);被执行人信息;买卖合同纠纷;劳动纠纷;重大纠纷`(C101-1 未返回风险项明细,联调后如有明细字段再替换)。结果留痕 `blade_hcs_check_log`。
### 9.2 提交校验(特批说明附件必传)
`ContractManageServiceImpl` 三个提交入口(`submit` / `toTemporary` / `submitFormal`)统一调 `validateSpecExplanationOnSubmit`:
- 甲/乙方按名称反查客商编号(反查不到不阻断,与推送链路 skip 口径一致)→ 调 C101-1 实时查风险;
- 任一方高风险/黑名单 → `attachmentsJson` 必须存在 `attachmentType=spec_explanation` 的附件,否则抛"合同相对方存在高风险/黑名单客商,必须上传「特批说明」类型附件后再提交审批"。
- **前端配合**:特批说明附件上传时元数据须带 `attachmentType: "spec_explanation"` 字段。
### 9.3 审批通过后自动特批链路
```
TMS 审批通过(approved / change_approved)
→ 即时推送 / 扫描兜底 → pushContract
→ ① 风险校验留痕(risky 标志,不阻断)
→ ④ C103-1 起草成功(hcs_sync_status=synced,回填 hcs_fd_id)
→ ⑤ 置 hcs_spec_status=need + currentNode="特批审核"(TMS 流程挂起)
→ 筛「特批说明」附件转传 C103-2 → 自动发起 C103-8 → hcs_spec_status=processing
```
- 原设计"黑名单挂起不推送、人工发起特批"存在死锁(挂起时无 `hcs_fd_id`,C103-8 报文必填合规合同ID 永远失败),本次变更为 **先 C103-1 起草、后 C103-8 特批**,特批通过即合规侧放行签约,无需恢复推送;
- 特批已通过(approved,单次有效)的合同推送/变更不重复发起特批;
- C103-8 发起失败保持 need 仅留痕,人工经 `/hcs/contract/spec-apply`(详情页"发起特批"按钮)重发。
### 9.4 特批结果联动(B103 回传 / 人工确认等效)
| 特批结果 | 合同联动 |
| --- | --- |
| approved | `currentNode` 恢复"审批通过"(C103-1 已起草,hcs_sync_status 保持 synced,无需再推送) |
| rejected | `approvalStatus=rejected` + `currentNode="已驳回"`,走重新编辑 → 提交 → 审批 → 重新特批闭环(重新推送时 C103-11 更新后再次自动特批) |
> 缺口:v4 无特批结果主动查询接口(§9-U1),结果依赖 B103 回调(`POST /hcs/callback/spec-result`)或页面人工确认(`POST /hcs/contract/spec/confirm`),合规方提供查询能力后可增轮询兜底。