: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:
+66
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 客商风险展示 VO(C101-1 查询结果 + TMS 侧组装提示,前端接入设计 §3.3)
|
||||
* <p>
|
||||
* 内部组织类型客商默认无风险(不调 C101-1);外部客商高风险/黑名单时 risky=true 并返回 riskTip
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "客商风险展示")
|
||||
public class CustomerRiskVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "客商类型:internal内部组织(默认无风险)/external外部客商")
|
||||
private String customerKind;
|
||||
|
||||
@Schema(description = "客商名称")
|
||||
private String customerName;
|
||||
|
||||
@Schema(description = "黑名单状态:yes/no")
|
||||
private String fdBlackStatus;
|
||||
|
||||
@Schema(description = "风险等级:0=低风险,1=中风险,2=高风险")
|
||||
private String fdRiskLevel;
|
||||
|
||||
@Schema(description = "是否高风险/黑名单客商(外部客商专用,内部组织恒为 false)")
|
||||
private Boolean risky;
|
||||
|
||||
@Schema(description = "风险客商提示文案(基本信息备注后展示,安全客商为空)")
|
||||
private String riskTip;
|
||||
|
||||
}
|
||||
+27
@@ -148,6 +148,33 @@ public final class HcsSyncConstant {
|
||||
*/
|
||||
public static final String SPEC_STATUS_REJECTED = "rejected";
|
||||
|
||||
// ==================== 客商风险(C101-1,外部客商高风险/黑名单管控) ====================
|
||||
|
||||
/**
|
||||
* C101-1 风险等级:高风险(0=低风险,1=中风险,2=高风险)
|
||||
*/
|
||||
public static final String RISK_LEVEL_HIGH = "2";
|
||||
|
||||
/**
|
||||
* 客商类型:内部组织(默认无风险,不查 C101-1)
|
||||
*/
|
||||
public static final String CUSTOMER_KIND_INTERNAL = "internal";
|
||||
|
||||
/**
|
||||
* 合同附件类型字段名(前端约定:附件元数据 attachmentType)
|
||||
*/
|
||||
public static final String ATTACHMENT_TYPE_FIELD = "attachmentType";
|
||||
|
||||
/**
|
||||
* 附件类型:特批说明(高风险/黑名单客商合同必须上传)
|
||||
*/
|
||||
public static final String ATTACHMENT_TYPE_SPEC_EXPLANATION = "spec_explanation";
|
||||
|
||||
/**
|
||||
* 特批审核挂起节点展示名(currentNode,特批发起后挂起展示,结果回传后恢复)
|
||||
*/
|
||||
public static final String SPEC_REVIEW_NODE = "特批审核";
|
||||
|
||||
// ==================== Redis 缓存 key(照 BFI blade:third: 前缀惯例) ====================
|
||||
|
||||
/**
|
||||
|
||||
+6
-5
@@ -40,9 +40,9 @@ import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.hcs.dto.HcsAttachmentRefDTO;
|
||||
import org.springblade.hcs.dto.HcsCurrencyVO;
|
||||
import org.springblade.hcs.dto.HcsRiskVO;
|
||||
import org.springblade.hcs.dto.HcsTemplateVO;
|
||||
import org.springblade.transport.constant.HcsSyncConstant;
|
||||
import org.springblade.transport.pojo.vo.CustomerRiskVO;
|
||||
import org.springblade.transport.support.hcs.HcsAttachmentSupport;
|
||||
import org.springblade.transport.support.hcs.HcsContractSpecSyncSupport;
|
||||
import org.springblade.transport.support.hcs.HcsContractSyncSupport;
|
||||
@@ -176,13 +176,14 @@ public class HcsSyncController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 客商风险等级查询(C101-1,展示用,前端接入设计 §3.3;强制校验仍在推送/付款链路)
|
||||
* 客商风险等级查询(C101-1,展示用,前端接入设计 §3.3;强制校验仍在推送/付款链路):
|
||||
* 内部组织类型客商默认无风险不调合规,外部客商高风险/黑名单返回提示文案
|
||||
*/
|
||||
@GetMapping("/risk")
|
||||
@ApiOperationSupport(order = 10)
|
||||
@Operation(summary = "客商风险等级查询", description = "C101-1 按客商编号查询风险等级与黑名单状态(展示用),结果留痕")
|
||||
public R<HcsRiskVO> risk(@Parameter(description = "客商编号", required = true) @RequestParam String customerCode) {
|
||||
return R.data(hcsCreditCheckSupport.queryRiskByCustomerCode(customerCode));
|
||||
@Operation(summary = "客商风险等级查询", description = "C101-1 按客商编号查询风险等级与黑名单状态(展示用),内部客商默认无风险,高风险/黑名单返回风险提示文案,结果留痕")
|
||||
public R<CustomerRiskVO> risk(@Parameter(description = "客商编号", required = true) @RequestParam String customerCode) {
|
||||
return R.data(hcsCreditCheckSupport.queryCustomerRisk(customerCode));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+39
@@ -44,6 +44,7 @@ import org.springblade.transport.pojo.entity.HcsContractExeStage;
|
||||
import org.springblade.transport.pojo.vo.ContractManageVO;
|
||||
import org.springblade.transport.service.IContractManageService;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.transport.support.hcs.HcsCreditCheckSupport;
|
||||
import org.springblade.transport.support.hcs.HcsImmediatePushSupport;
|
||||
import org.springblade.transport.wrapper.ContractManageWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -74,6 +75,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
|
||||
private final HcsContractExeStageMapper hcsContractExeStageMapper;
|
||||
private final HcsImmediatePushSupport hcsImmediatePushSupport;
|
||||
private final HcsCreditCheckSupport hcsCreditCheckSupport;
|
||||
|
||||
private static final String STAGE_DRAFT = "draft";
|
||||
private static final String STAGE_TEMPORARY = "temporary";
|
||||
@@ -155,6 +157,8 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
prepare(contractManage);
|
||||
validateDraft(contractManage);
|
||||
validateTemporary(contractManage);
|
||||
// 高风险/黑名单客商管控:必须上传特批说明附件(外部客商,内部组织默认无风险)
|
||||
validateSpecExplanationOnSubmit(contractManage);
|
||||
if (Func.isNotEmpty(contractManage.getId())) {
|
||||
validateContractPartyCombinationUnique(contractManage);
|
||||
}
|
||||
@@ -177,6 +181,8 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
throw new ServiceException("仅草稿或已撤回/已驳回的临时合同允许提交临时合同审批");
|
||||
}
|
||||
validateTemporary(contractManage);
|
||||
// 高风险/黑名单客商管控:必须上传特批说明附件(外部客商,内部组织默认无风险)
|
||||
validateSpecExplanationOnSubmit(contractManage);
|
||||
// 临时放开新增合同从草稿转临时合同时的组合重复校验。
|
||||
// validateContractPartyCombinationUnique(contractManage);
|
||||
contractManage.setContractStage(STAGE_TEMPORARY);
|
||||
@@ -195,6 +201,8 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
throw new ServiceException("仅临时合同或已撤回/已驳回的正式合同允许提交正式合同审批");
|
||||
}
|
||||
validateFormal(contractManage);
|
||||
// 高风险/黑名单客商管控:必须上传特批说明附件(外部客商,内部组织默认无风险)
|
||||
validateSpecExplanationOnSubmit(contractManage);
|
||||
// 临时放开新增合同从临时转正式合同时的组合重复校验。
|
||||
// validateContractPartyCombinationUnique(contractManage);
|
||||
contractManage.setContractStage(STAGE_FORMAL);
|
||||
@@ -1031,6 +1039,37 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 高风险/黑名单客商提交校验:甲/乙方为外部客商且高风险(C101-1 风险等级=2)或黑名单时,
|
||||
* 必须已上传「特批说明」类型附件(attachmentType=spec_explanation);内部组织类型客商默认无风险
|
||||
*
|
||||
* @param contractManage 合同单
|
||||
*/
|
||||
private void validateSpecExplanationOnSubmit(ContractManage contractManage) {
|
||||
boolean risky = isRiskyCustomer(contractManage.getPartyA()) || isRiskyCustomer(contractManage.getPartyB());
|
||||
if (!risky) {
|
||||
return;
|
||||
}
|
||||
boolean hasSpecExplanation = parseJsonArray(contractManage.getAttachmentsJson()).stream()
|
||||
.filter(Map.class::isInstance)
|
||||
.map(Map.class::cast)
|
||||
.anyMatch(attachment -> HcsSyncConstant.ATTACHMENT_TYPE_SPEC_EXPLANATION
|
||||
.equals(Objects.toString(attachment.get(HcsSyncConstant.ATTACHMENT_TYPE_FIELD), null)));
|
||||
if (!hasSpecExplanation) {
|
||||
throw new ServiceException("合同相对方存在高风险/黑名单客商,必须上传「特批说明」类型附件后再提交审批");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 单方客商是否高风险/黑名单(C101-1 实时查询,名称反查不到客商编号不阻断)
|
||||
*/
|
||||
private boolean isRiskyCustomer(String partyName) {
|
||||
if (Func.isEmpty(partyName)) {
|
||||
return false;
|
||||
}
|
||||
return Boolean.TRUE.equals(hcsCreditCheckSupport.queryCustomerRiskByName(partyName).getRisky());
|
||||
}
|
||||
|
||||
private String mergeAttachmentJson(String existingJson, String pendingJson) {
|
||||
List<Object> merged = parseJsonArray(existingJson);
|
||||
List<Object> pending = parseJsonArray(pendingJson);
|
||||
|
||||
+20
-1
@@ -147,6 +147,8 @@ public class HcsContractSpecSyncSupport {
|
||||
if (!HcsSyncConstant.SPEC_STATUS_PROCESSING.equals(contractManage.getHcsSpecStatus())) {
|
||||
throw new ServiceException("仅特批中的合同允许确认结果(当前状态:" + contractManage.getHcsSpecStatus() + ")");
|
||||
}
|
||||
// 特批结果联动合同审批状态(通过恢复展示,驳回联动审批驳回),随 markSpec 一并落库
|
||||
syncContractOnSpecResult(contractManage, HcsSyncConstant.SPEC_STATUS_APPROVED.equals(result));
|
||||
markSpec(contractManage, result, "特批结果人工确认:" + result);
|
||||
writeCheckLog(contractManage, HcsSyncConstant.SPEC_STATUS_APPROVED.equals(result)
|
||||
? HcsSyncConstant.CHECK_RESULT_PASS : HcsSyncConstant.CHECK_RESULT_FAIL, "特批结果人工确认:" + result, null);
|
||||
@@ -156,7 +158,7 @@ public class HcsContractSpecSyncSupport {
|
||||
|
||||
/**
|
||||
* B103 特批结果回传处理(回调端点入口):候选字段解析 + 原文留痕,
|
||||
* approved 时合同保持待同步(unsynced),由既有扫描任务自动恢复 C103-1 推送
|
||||
* approved 恢复合同"审批通过"展示(C103-1 已起草完成,无需恢复推送);rejected 联动合同审批驳回
|
||||
*
|
||||
* @param body B103 回调报文(结构待联调,原文全量留痕)
|
||||
* @return 处理结果描述
|
||||
@@ -179,6 +181,8 @@ public class HcsContractSpecSyncSupport {
|
||||
return "已留痕:结果字段未识别(contractNo=" + contractManage.getContractNo() + ")";
|
||||
}
|
||||
String status = approved ? HcsSyncConstant.SPEC_STATUS_APPROVED : HcsSyncConstant.SPEC_STATUS_REJECTED;
|
||||
// 特批结果联动合同审批状态(通过恢复展示,驳回联动审批驳回),随 markSpec 一并落库
|
||||
syncContractOnSpecResult(contractManage, approved);
|
||||
markSpec(contractManage, status, "B103 特批结果回传:" + raw);
|
||||
writeCallbackLog(contractManage, approved ? HcsSyncConstant.CHECK_RESULT_PASS : HcsSyncConstant.CHECK_RESULT_FAIL,
|
||||
"B103 特批结果回传:" + raw);
|
||||
@@ -300,6 +304,21 @@ public class HcsContractSpecSyncSupport {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 特批结果联动合同审批状态(confirmSpecResult 与 handleSpecCallback 共用,仅改字段,随 markSpec 落库):
|
||||
* approved 恢复"审批通过"展示(C103-1 已起草完成,hcs_sync_status 保持 synced,无需再推送);
|
||||
* rejected 联动合同审批驳回(approvalStatus=rejected),走重新编辑→提交→审批→特批闭环
|
||||
*/
|
||||
private void syncContractOnSpecResult(ContractManage contractManage, boolean approved) {
|
||||
if (approved) {
|
||||
contractManage.setCurrentNode("审批通过");
|
||||
} else {
|
||||
contractManage.setApprovalStatus("rejected");
|
||||
contractManage.setCurrentNode("已驳回");
|
||||
contractManage.setCurrentProcessor("待处理");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回写特批状态与动作时间
|
||||
*/
|
||||
|
||||
+118
-24
@@ -29,6 +29,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.hcs.config.HcsProperties;
|
||||
import org.springblade.hcs.dto.HcsAttachmentRefDTO;
|
||||
@@ -70,9 +71,10 @@ import java.util.Objects;
|
||||
/**
|
||||
* HCS 合同同步支撑组件(C103-1 发起 / C103-11 更新,设计文档 §5.2)
|
||||
* <p>
|
||||
* 扫描任务按序执行:① 客商风险校验(可配开关,黑名单阻断)→ ② 附件转传(v4 挂载点 fdFieldJson.attachment,随 C103-1 挂载)→
|
||||
* ③ C103-3 判重(§9-U4 查询能力联调确认)→ ④ C103-1 起草 / 变更单 C103-11 更新(§9-U5 定位方式待联调)→
|
||||
* ⑤ 履约规则 C103-9 行级同步({@link HcsExeStageSyncSupport})。
|
||||
* 扫描任务按序执行:⓪ 特批挂起门禁 → ① 客商风险校验(可配开关,仅留痕,高风险不再阻断)→
|
||||
* ② 附件转传(v4 挂载点 fdFieldJson.attachment,随 C103-1 挂载)→ ③ C103-3 判重(§9-U4 查询能力联调确认)→
|
||||
* ④ C103-1 起草 / 变更单 C103-11 更新(§9-U5 定位方式待联调)→ ⑤ 高风险/黑名单自动特批 C103-8(外部客商管控,
|
||||
* {@link HcsContractSpecSyncSupport},挂起"特批审核"待 B103 回传)→ ⑥ 履约规则 C103-9 行级同步({@link HcsExeStageSyncSupport})。
|
||||
* 结果留痕:风险校验写 blade_hcs_check_log 与合同 hcs_risk_* 字段;推送结果回填 hcs_fd_id/hcs_sync_status。
|
||||
*
|
||||
* @author Chill
|
||||
@@ -99,24 +101,28 @@ public class HcsContractSyncSupport {
|
||||
private final HcsContractCheckLogMapper hcsContractCheckLogMapper;
|
||||
private final HcsAttachmentSupport hcsAttachmentSupport;
|
||||
private final HcsExeStageSyncSupport hcsExeStageSyncSupport;
|
||||
private final HcsContractSpecSyncSupport hcsContractSpecSyncSupport;
|
||||
|
||||
/**
|
||||
* 推送合同到 HCS:特批挂起门禁 → 风险校验(黑名单挂起待特批)→ 附件转传 → 判重 → 起草/更新 → 回填合规 fdId 置 synced
|
||||
* 推送合同到 HCS:特批挂起门禁 → 风险校验(仅留痕,高风险不再阻断)→ 附件转传 → 判重 → 起草/更新 → 回填合规 fdId 置 synced
|
||||
* → 高风险/黑名单客商合同挂起"特批审核"并自动发起 C103-8 特批(外部客商管控)
|
||||
*
|
||||
* @param contractManage 合同单(approvalStatus 须为 approved/change_approved)
|
||||
* @return 推送结果描述(供任务汇总与 hcs_sync_msg 落库)
|
||||
*/
|
||||
public String pushContract(ContractManage contractManage) {
|
||||
log.info("HCS合同推送开始:contractNo={},approvalStatus={},首推={},hcsSpecStatus={}",
|
||||
contractManage.getContractNo(), contractManage.getApprovalStatus(),
|
||||
StringUtil.isBlank(contractManage.getHcsFdId()), contractManage.getHcsSpecStatus());
|
||||
// ⓪ 特批挂起门禁:待特批/特批中的合同跳过推送,等待 B103 特批结果回传后恢复(C103-8,前端接入设计 §3.4)
|
||||
if (List.of(HcsSyncConstant.SPEC_STATUS_NEED, HcsSyncConstant.SPEC_STATUS_PROCESSING).contains(contractManage.getHcsSpecStatus())) {
|
||||
log.info("HCS合同推送跳过(特批挂起中):contractNo={},hcsSpecStatus={}", contractManage.getContractNo(), contractManage.getHcsSpecStatus());
|
||||
return "合同特批挂起中(" + contractManage.getHcsSpecStatus() + "),跳过推送";
|
||||
}
|
||||
// ① 客商风险校验(可配开关),黑名单且 block-blacklist=true 时挂起待特批(不再直接失败)
|
||||
// ① 客商风险校验(可配开关):留痕 hcs_risk_* 字段并返回高风险/黑名单标志,不再阻断推送(高风险合同起草后自动转特批)
|
||||
boolean risky = false;
|
||||
if (hcsProperties.getContract().isRequireRiskCheck()) {
|
||||
if (checkRelativeRisk(contractManage)) {
|
||||
return "高风险客商,合同已挂起待特批(C103-8)";
|
||||
}
|
||||
risky = checkRelativeRisk(contractManage);
|
||||
}
|
||||
// ② 附件转传(v4 已明确挂载点:C103-1 fdFieldJson.attachment,转传取得合规附件ID 后随发起报文挂载)
|
||||
List<String> attachmentFdIds = transferAttachmentsIfEnabled(contractManage);
|
||||
@@ -138,7 +144,12 @@ public class HcsContractSyncSupport {
|
||||
markSynced(contractManage, contractManage.getHcsFdId(), "C103-11 合同更新成功");
|
||||
resultMessage = "C103-11 合同更新成功";
|
||||
}
|
||||
// ⑤ 履约规则 C103-9 行级同步(随合同链路,§5.3;一期暂不对接默认关,打开后失败抛出由任务落 failed,下轮凭 hcs_fd_id 走更新补偿)
|
||||
// ⑤ 高风险/黑名单客商(外部客商管控):C103-1 起草成功后挂起"特批审核"并自动发起 C103-8;
|
||||
// 特批已通过的合同不重复发起(单次有效),发起失败保持待特批可人工重发
|
||||
if (risky) {
|
||||
autoApplySpecIfNeeded(contractManage);
|
||||
}
|
||||
// ⑥ 履约规则 C103-9 行级同步(随合同链路,§5.3;一期暂不对接默认关,打开后失败抛出由任务落 failed,下轮凭 hcs_fd_id 走更新补偿)
|
||||
if (!hcsProperties.getExeStage().isSyncEnabled()) {
|
||||
return resultMessage;
|
||||
}
|
||||
@@ -148,10 +159,10 @@ public class HcsContractSyncSupport {
|
||||
|
||||
/**
|
||||
* C101-1 客商风险校验:甲乙双方各查一次,留痕 check_log 与合同 hcs_risk_* 字段;
|
||||
* 黑名单且 block-blacklist=true 时不再直接失败,而是置 hcs_spec_status=need 挂起待特批(C103-8);
|
||||
* 特批已通过(approved,单次有效)的合同不重复挂起,保证 B103 回传后推送链路可走通
|
||||
* 返回是否高风险/黑名单(风险等级=2 或任一方黑名单),不再阻断推送——
|
||||
* 推送成功后由自动特批链路挂起处理(外部客商管控,见 {@link #autoApplySpecIfNeeded})
|
||||
*
|
||||
* @return true=黑名单挂起待特批(调用方跳过本次推送)
|
||||
* @return true=高风险或黑名单客商(触发自动特批)
|
||||
*/
|
||||
private boolean checkRelativeRisk(ContractManage contractManage) {
|
||||
RiskResult riskPartyA = queryRisk(contractManage, "甲方", contractManage.getPartyA());
|
||||
@@ -164,20 +175,103 @@ public class HcsContractSyncSupport {
|
||||
contractManage.setHcsBlackStatus(blacklist ? BLACK_STATUS_YES : "no");
|
||||
contractManage.setHcsRiskTime(LocalDateTime.now());
|
||||
contractManageMapper.updateById(contractManage);
|
||||
log.info("HCS合同风险校验完成:contractNo={},甲方[{}={}/{}],乙方[{}={}/{}]",
|
||||
boolean risky = HcsSyncConstant.RISK_LEVEL_HIGH.equals(riskLevel) || blacklist;
|
||||
log.info("HCS合同风险校验完成:contractNo={},甲方[{}={}/{}],乙方[{}={}/{}],高风险/黑名单={}",
|
||||
contractManage.getContractNo(), contractManage.getPartyA(), riskPartyA.blackStatus(), riskPartyA.riskLevel(),
|
||||
contractManage.getPartyB(), riskPartyB.blackStatus(), riskPartyB.riskLevel());
|
||||
boolean specApproved = HcsSyncConstant.SPEC_STATUS_APPROVED.equals(contractManage.getHcsSpecStatus());
|
||||
if (blacklist && hcsProperties.getRisk().isBlockBlacklist() && !specApproved) {
|
||||
String message = "高风险客商(甲方[" + riskPartyA.blackStatus() + "]乙方[" + riskPartyB.blackStatus() + "]),合同挂起待特批";
|
||||
contractManage.setHcsSpecStatus(HcsSyncConstant.SPEC_STATUS_NEED);
|
||||
contractManage.setHcsSpecTime(LocalDateTime.now());
|
||||
contractManage.setHcsSpecMsg(message);
|
||||
contractManageMapper.updateById(contractManage);
|
||||
log.warn("HCS合同推送因高风险客商挂起待特批:contractNo={},{}", contractManage.getContractNo(), message);
|
||||
return true;
|
||||
contractManage.getPartyB(), riskPartyB.blackStatus(), riskPartyB.riskLevel(), risky);
|
||||
return risky;
|
||||
}
|
||||
|
||||
/**
|
||||
* 高风险/黑名单客商合同自动特批(C103-8,外部客商管控):C103-1 起草成功后置待特批并自动发起,
|
||||
* TMS 流程挂起展示"特批审核"(currentNode),B103 特批结果回传后恢复或驳回;
|
||||
* 特批已通过(单次有效)的合同不重复发起;发起失败保持待特批仅留痕,可人工重发或重新推送兜底
|
||||
*/
|
||||
private void autoApplySpecIfNeeded(ContractManage contractManage) {
|
||||
if (HcsSyncConstant.SPEC_STATUS_APPROVED.equals(contractManage.getHcsSpecStatus())) {
|
||||
log.info("HCS合同高风险/黑名单但特批已通过(单次有效),跳过自动特批:contractNo={},hcsSpecStatus={}",
|
||||
contractManage.getContractNo(), contractManage.getHcsSpecStatus());
|
||||
return;
|
||||
}
|
||||
contractManage.setHcsSpecStatus(HcsSyncConstant.SPEC_STATUS_NEED);
|
||||
contractManage.setHcsSpecTime(LocalDateTime.now());
|
||||
contractManage.setHcsSpecMsg("高风险/黑名单客商,合同推送合规成功后自动发起特批(C103-8)");
|
||||
// TMS 流程挂起为"特批审核",特批通过恢复展示、驳回联动合同审批驳回
|
||||
contractManage.setCurrentNode(HcsSyncConstant.SPEC_REVIEW_NODE);
|
||||
contractManageMapper.updateById(contractManage);
|
||||
log.info("HCS合同高风险/黑名单触发自动特批(C103-8):contractNo={},riskLevel={},blackStatus={},挂起节点={}",
|
||||
contractManage.getContractNo(), contractManage.getHcsRiskLevel(), contractManage.getHcsBlackStatus(),
|
||||
contractManage.getCurrentNode());
|
||||
try {
|
||||
List<HcsAttachmentRefDTO> attachments = transferSpecExplanationAttachments(contractManage);
|
||||
hcsContractSpecSyncSupport.applySpec(contractManage.getId(), attachments, requireLoginName(contractManage));
|
||||
} catch (Exception exception) {
|
||||
log.warn("HCS合同自动特批发起失败(保持待特批,可人工重发):contractNo={},{}",
|
||||
contractManage.getContractNo(), exception.getMessage(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 筛选合同「特批说明」类型附件(attachmentType=spec_explanation)并转传合规(C103-2),
|
||||
* 返回特批附件引用;无特批说明附件返回空列表(提交时已强制校验,此处由 C103-8 防御性拦截)
|
||||
*/
|
||||
private List<HcsAttachmentRefDTO> transferSpecExplanationAttachments(ContractManage contractManage) {
|
||||
List<Map<?, ?>> specAttachments = new ArrayList<>();
|
||||
for (Object item : parseJsonArray(contractManage.getAttachmentsJson())) {
|
||||
if (item instanceof Map<?, ?> attachment
|
||||
&& HcsSyncConstant.ATTACHMENT_TYPE_SPEC_EXPLANATION.equals(attachment.get(HcsSyncConstant.ATTACHMENT_TYPE_FIELD))) {
|
||||
specAttachments.add(attachment);
|
||||
}
|
||||
}
|
||||
if (specAttachments.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
String specJson = JsonUtil.toJson(specAttachments);
|
||||
String loginName = requireLoginName(contractManage);
|
||||
List<String> attachmentFdIds = hcsAttachmentSupport.transferAttachments(specJson, loginName);
|
||||
List<HcsAttachmentRefDTO> attachments = new ArrayList<>();
|
||||
for (int index = 0; index < specAttachments.size(); index++) {
|
||||
HcsAttachmentRefDTO ref = new HcsAttachmentRefDTO();
|
||||
// 附件ID:C103-2 转传返回的合规附件ID(与元数据行序一致)
|
||||
ref.setFdId(attachmentFdIds.get(index));
|
||||
ref.setFdFileName(resolveAttachmentName(specAttachments.get(index)));
|
||||
attachments.add(ref);
|
||||
}
|
||||
return attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* 附件元数据文件名(name/originalName/fileName 依次取首个非空,照 HcsAttachmentSupport 惯例)
|
||||
*/
|
||||
private String resolveAttachmentName(Map<?, ?> attachment) {
|
||||
for (String field : List.of("name", "originalName", "fileName")) {
|
||||
Object value = attachment.get(field);
|
||||
if (value != null && !String.valueOf(value).isBlank()) {
|
||||
return String.valueOf(value);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JSON 数组(照 ContractManage#parseJsonArray 惯例,异常返回空列表)
|
||||
*/
|
||||
private List<Map<?, ?>> parseJsonArray(String json) {
|
||||
if (StringUtil.isBlank(json)) {
|
||||
return List.of();
|
||||
}
|
||||
try {
|
||||
List<Map<?, ?>> result = new ArrayList<>();
|
||||
for (Object item : JsonUtil.parse(json, List.class)) {
|
||||
if (item instanceof Map<?, ?> map) {
|
||||
result.add(map);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (Exception exception) {
|
||||
log.warn("合同附件元数据解析失败,按无附件处理:{}", json);
|
||||
return List.of();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+49
-9
@@ -46,6 +46,7 @@ import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.CustomerArchive;
|
||||
import org.springblade.transport.pojo.entity.HcsContractCheckLog;
|
||||
import org.springblade.transport.pojo.entity.PaymentApplication;
|
||||
import org.springblade.transport.pojo.vo.CustomerRiskVO;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -171,16 +172,30 @@ public class HcsCreditCheckSupport {
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端客商风险查询(C101-1,展示用,前端接入设计 §3.3):
|
||||
* 按客商编号查询风险等级与黑名单状态并留痕(biz_id 为空);仅用于展示提示,不作为安全边界
|
||||
* 客商风险查询(C101-1,展示与提交校验共用,前端接入设计 §3.3):
|
||||
* 内部组织类型客商默认无风险(不调 C101-1);外部客商按编号查询风险等级与黑名单状态并留痕;
|
||||
* 高风险/黑名单时组装风险提示文案(风险项明细 C101-1 未返回,固定文案展示)
|
||||
*
|
||||
* @param customerCode 客商编号
|
||||
* @return 风险信息(fdBlackStatus/fdRiskLevel)
|
||||
* @return 风险展示信息(customerKind/fdBlackStatus/fdRiskLevel/risky/riskTip)
|
||||
*/
|
||||
public HcsRiskVO queryRiskByCustomerCode(String customerCode) {
|
||||
public CustomerRiskVO queryCustomerRisk(String customerCode) {
|
||||
if (StringUtil.isBlank(customerCode)) {
|
||||
throw new ServiceException("客商编号不能为空");
|
||||
}
|
||||
CustomerArchive customerArchive = customerArchiveMapper.selectOne(Wrappers.<CustomerArchive>lambdaQuery()
|
||||
.eq(CustomerArchive::getCustomerCode, customerCode)
|
||||
.eq(CustomerArchive::getIsDeleted, 0)
|
||||
.last("limit 1"));
|
||||
CustomerRiskVO riskVO = new CustomerRiskVO();
|
||||
riskVO.setCustomerName(customerArchive == null ? customerCode : customerArchive.getFullName());
|
||||
// 内部组织类型客商默认无风险;档案不存在按外部客商查询(不静默放行)
|
||||
boolean internal = customerArchive != null && HcsSyncConstant.CUSTOMER_KIND_INTERNAL.equals(customerArchive.getCustomerKind());
|
||||
riskVO.setCustomerKind(internal ? HcsSyncConstant.CUSTOMER_KIND_INTERNAL : "external");
|
||||
if (internal) {
|
||||
riskVO.setRisky(Boolean.FALSE);
|
||||
return riskVO;
|
||||
}
|
||||
HcsRiskQueryDTO param = new HcsRiskQueryDTO();
|
||||
// 【必填】客商编号(用哪个编码待联调,§9-U6)
|
||||
param.setFdNumber(customerCode);
|
||||
@@ -192,20 +207,45 @@ public class HcsCreditCheckSupport {
|
||||
}
|
||||
HcsRiskVO risk = result.getData();
|
||||
String blackStatus = risk == null ? null : risk.getFdBlackStatus();
|
||||
boolean blacklist = BLACK_STATUS_YES.equalsIgnoreCase(blackStatus);
|
||||
String riskLevel = risk == null ? null : risk.getFdRiskLevel();
|
||||
boolean risky = HcsSyncConstant.RISK_LEVEL_HIGH.equals(riskLevel) || BLACK_STATUS_YES.equalsIgnoreCase(blackStatus);
|
||||
riskVO.setFdBlackStatus(blackStatus);
|
||||
riskVO.setFdRiskLevel(riskLevel);
|
||||
riskVO.setRisky(risky);
|
||||
if (risky) {
|
||||
riskVO.setRiskTip("【" + riskVO.getCustomerName() + "】:被列入外部黑名单;涉贪污受贿裁判相关提及方(3年以上及其他);被执行人信息;买卖合同纠纷;劳动纠纷;重大纠纷");
|
||||
}
|
||||
try {
|
||||
HcsContractCheckLog checkLog = new HcsContractCheckLog();
|
||||
checkLog.setBizType(HcsSyncConstant.BIZ_TYPE_CONTRACT);
|
||||
checkLog.setCheckType(HcsSyncConstant.CHECK_TYPE_RISK);
|
||||
checkLog.setResult(blacklist ? HcsSyncConstant.CHECK_RESULT_FAIL : HcsSyncConstant.CHECK_RESULT_PASS);
|
||||
checkLog.setMsg("页面风险查询[" + customerCode + "]黑名单=" + blackStatus + ",风险等级=" + (risk == null ? null : risk.getFdRiskLevel()));
|
||||
checkLog.setResult(risky ? HcsSyncConstant.CHECK_RESULT_FAIL : HcsSyncConstant.CHECK_RESULT_PASS);
|
||||
checkLog.setMsg("客商风险查询[" + customerCode + "]黑名单=" + blackStatus + ",风险等级=" + riskLevel + ",高风险/黑名单=" + risky);
|
||||
checkLog.setTraceId(result.getTraceId());
|
||||
checkLog.setCreateTime(LocalDateTime.now());
|
||||
hcsContractCheckLogMapper.insert(checkLog);
|
||||
} catch (Exception exception) {
|
||||
log.error("HCS页面风险查询留痕写入失败:fdNumber={}", customerCode, exception);
|
||||
log.error("HCS客商风险查询留痕写入失败:fdNumber={}", customerCode, exception);
|
||||
}
|
||||
return risk;
|
||||
return riskVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按客商名称查询风险(合同提交校验用):按名称反查客商编号后转 {@link #queryCustomerRisk};
|
||||
* 反查不到客商编号时不阻断(risky=false),与推送链路 skip 口径一致(§9-U6)
|
||||
*
|
||||
* @param customerName 客商名称(合同甲/乙方名称)
|
||||
* @return 风险展示信息
|
||||
*/
|
||||
public CustomerRiskVO queryCustomerRiskByName(String customerName) {
|
||||
String customerCode = queryCustomerCode(customerName);
|
||||
if (StringUtil.isBlank(customerCode)) {
|
||||
CustomerRiskVO riskVO = new CustomerRiskVO();
|
||||
riskVO.setCustomerName(customerName);
|
||||
riskVO.setRisky(Boolean.FALSE);
|
||||
return riskVO;
|
||||
}
|
||||
return queryCustomerRisk(customerCode);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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`),合规方提供查询能力后可增轮询兜底。
|
||||
Reference in New Issue
Block a user