: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
@@ -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: 前缀惯例) ====================
/**
@@ -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));
}
/**
@@ -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);
@@ -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("待处理");
}
}
/**
* 回写特批状态与动作时间
*/
@@ -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;
}
/**
@@ -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);
}
/**