diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
index 13069ebd..7f870d9a 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
@@ -25,6 +25,8 @@
*/
package org.springblade.transport.constant;
+import java.util.List;
+
/**
* HCS(合同合规系统)同步常量(TMS 业务侧)
*
@@ -175,6 +177,26 @@ public final class HcsSyncConstant {
*/
public static final String CUSTOMER_KIND_INTERNAL = "internal";
+ /**
+ * 合同审核通过状态集合(首推/变更通过,客商风险定时刷新扫描口径与 HcsPushTask 一致)
+ */
+ public static final List CONTRACT_APPROVED_STATUSES = List.of("approved", "change_approved");
+
+ /**
+ * 合同已终止当前节点展示名(客商风险定时刷新排除已终止合同)
+ */
+ public static final String CONTRACT_TERMINATED_NODE = "已终止";
+
+ /**
+ * 客商风险等级业务字典编码(dictKey=C101-1 风险等级 0/1/2,dictValue=低风险/中风险/高风险)
+ */
+ public static final String RISK_LEVEL_DICT_CODE = "risk_level";
+
+ /**
+ * 客商风险定时刷新批量(200 客商/批:批内黑名单一次查询 + C101-1 逐个查询,查完立即提交数据库,批间互不影响)
+ */
+ public static final int CUSTOMER_RISK_REFRESH_BATCH = 200;
+
/**
* 合同附件类型字段名(与前端附件元数据 fileType 字段对齐,值取业务字典 contract_attachment_type)
*/
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
index 2b158e4e..4c74ac7c 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
@@ -26,6 +26,7 @@
package org.springblade.transport.support.hcs;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.toolkit.Db;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
@@ -40,6 +41,10 @@ import org.springblade.hcs.dto.HcsRiskQueryDTO;
import org.springblade.hcs.dto.HcsRiskVO;
import org.springblade.hcs.exception.HcsException;
import org.springblade.hcs.feign.IHcsClient;
+import org.springblade.system.cache.DictBizCache;
+import org.springblade.system.cache.DictCache;
+import org.springblade.thirdparty.bfi.pojo.vo.BfiBlacklistVO;
+import org.springblade.thirdparty.bfi.service.IBfiService;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.mapper.ContractManageMapper;
import org.springblade.transport.mapper.CustomerArchiveMapper;
@@ -52,11 +57,14 @@ import org.springblade.transport.pojo.vo.CustomerRiskVO;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
+import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.stream.Stream;
/**
* HCS 付款提交校验支撑组件(C101-1 客商风险 + C106-1 额度校验,设计文档 §5.4,D15/R2-Q3)
@@ -87,6 +95,7 @@ public class HcsCreditCheckSupport {
private final ContractManageMapper contractManageMapper;
private final CustomerArchiveMapper customerArchiveMapper;
private final HcsContractCheckLogMapper hcsContractCheckLogMapper;
+ private final IBfiService bfiService;
/**
* 付款提交同步校验入口(submit 事务内调用,校验结果字段随提交一并落库)
@@ -195,7 +204,7 @@ public class HcsCreditCheckSupport {
/**
* 客商风险查询(C101-1,展示与提交校验共用,前端接入设计 §3.3):
* 内部组织类型客商默认无风险(不调 C101-1);外部客商按编号查询风险等级与黑名单状态并留痕;
- * 高风险/黑名单时组装风险提示文案(风险项明细 C101-1 未返回,固定文案展示)
+ * riskTip 经 {@link #buildRiskDetail} 统一组装(等级 + 金蝶黑名单失信类别/列入事由),与列表悬浮明细同口径
*
* @param customerCode 客商编号
* @return 风险展示信息(customerKind/fdBlackStatus/fdRiskLevel/risky/riskTip)
@@ -208,16 +217,43 @@ public class HcsCreditCheckSupport {
.eq(CustomerArchive::getCustomerCode, customerCode)
.eq(CustomerArchive::getIsDeleted, 0)
.last("limit 1"));
- CustomerRiskVO riskVO = new CustomerRiskVO();
- riskVO.setCustomerCode(customerCode);
- riskVO.setCustomerName(customerArchive == null ? customerCode : customerArchive.getFullName());
+ String customerName = customerArchive == null ? customerCode : customerArchive.getFullName();
// 内部组织类型客商默认无风险;档案不存在按外部客商查询(不静默放行)
- boolean internal = isInternalCustomer(customerArchive);
- riskVO.setCustomerKind(internal ? HcsSyncConstant.CUSTOMER_KIND_INTERNAL : "external");
- if (internal) {
+ if (isInternalCustomer(customerArchive)) {
+ CustomerRiskVO riskVO = new CustomerRiskVO();
+ riskVO.setCustomerCode(customerCode);
+ riskVO.setCustomerName(customerName);
+ riskVO.setCustomerKind(HcsSyncConstant.CUSTOMER_KIND_INTERNAL);
riskVO.setRisky(Boolean.FALSE);
return riskVO;
}
+ RelativeRiskResult fetch = this.fetchRelativeRisk(customerCode, customerName);
+ CustomerRiskVO riskVO = fetch.risk();
+ // 表单实时提示与列表明细统一组装口径:追加金蝶黑名单段(A801-5 按档案信用代码查询,失败静默缺失)
+ String creditCode = customerArchive == null ? null : customerArchive.getUnifiedCreditCode();
+ BfiBlacklistVO hit = StringUtil.isBlank(creditCode) ? null
+ : this.queryBlacklistByCodes(List.of(creditCode.trim().toUpperCase())).get(creditCode.trim().toUpperCase());
+ String blackCategory = hit == null ? null
+ : (Func.isEmpty(hit.getAwrpCombofieldTitle()) ? hit.getAwrpCombofield() : hit.getAwrpCombofieldTitle());
+ riskVO.setRiskTip(buildRiskDetail(customerName, BLACK_STATUS_YES.equalsIgnoreCase(riskVO.getFdBlackStatus()),
+ riskVO.getFdRiskLevel(), blackCategory, hit == null ? null : hit.getAwrpTextareafield()));
+ try {
+ hcsContractCheckLogMapper.insert(this.buildRiskCheckLog(customerCode, riskVO, fetch.traceId()));
+ } catch (Exception exception) {
+ log.error("HCS客商风险查询留痕写入失败:fdNumber={}", customerCode, exception);
+ }
+ return riskVO;
+ }
+
+ /**
+ * C101-1 单客商远程查询并组装展示信息(不查档案、不写留痕,供单查与定时批量刷新复用);
+ * riskTip 不在本方法组装,由调用方经 {@link #buildRiskDetail} 统一组装
+ *
+ * @param customerCode 客商编号
+ * @param customerName 客商名称(展示信息用)
+ * @return 风险展示信息 + 合规 traceId
+ */
+ private RelativeRiskResult fetchRelativeRisk(String customerCode, String customerName) {
HcsRiskQueryDTO param = new HcsRiskQueryDTO();
// 【必填】客商编号(用哪个编码待联调,§9-U6)
param.setFdNumber(customerCode);
@@ -231,28 +267,36 @@ public class HcsCreditCheckSupport {
String blackStatus = risk == null ? null : risk.getFdBlackStatus();
String riskLevel = risk == null ? null : risk.getFdRiskLevel();
boolean risky = HcsSyncConstant.RISK_LEVEL_HIGH.equals(riskLevel) || BLACK_STATUS_YES.equalsIgnoreCase(blackStatus);
+ CustomerRiskVO riskVO = new CustomerRiskVO();
+ riskVO.setCustomerCode(customerCode);
+ riskVO.setCustomerName(customerName);
+ riskVO.setCustomerKind("external");
riskVO.setFdBlackStatus(blackStatus);
riskVO.setFdRiskLevel(riskLevel);
riskVO.setRiskLevelName(riskLevelName(blackStatus, riskLevel));
riskVO.setRisky(risky);
- if (risky) {
- // 风险提示固定短文案(C101-1 未返回风险项明细,不拼接枚举清单):黑名单与高风险区分表述
- riskVO.setRiskTip("【" + riskVO.getCustomerName() + "】:"
- + (BLACK_STATUS_YES.equalsIgnoreCase(blackStatus) ? "被列入外部黑名单" : "为高风险客商"));
- }
- try {
- HcsContractCheckLog checkLog = new HcsContractCheckLog();
- checkLog.setBizType(HcsSyncConstant.BIZ_TYPE_CONTRACT);
- checkLog.setCheckType(HcsSyncConstant.CHECK_TYPE_RISK);
- 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);
- }
- return riskVO;
+ return new RelativeRiskResult(riskVO, result.getTraceId());
+ }
+
+ /**
+ * C101-1 单次远程查询结果(风险展示信息 + 合规 traceId,留痕与批量刷新共用)
+ */
+ private record RelativeRiskResult(CustomerRiskVO risk, String traceId) {
+ }
+
+ /**
+ * 组装客商风险查询留痕(单查与定时批量刷新共用,msg 口径一致)
+ */
+ private HcsContractCheckLog buildRiskCheckLog(String customerCode, CustomerRiskVO risk, String traceId) {
+ HcsContractCheckLog checkLog = new HcsContractCheckLog();
+ checkLog.setBizType(HcsSyncConstant.BIZ_TYPE_CONTRACT);
+ checkLog.setCheckType(HcsSyncConstant.CHECK_TYPE_RISK);
+ checkLog.setResult(Boolean.TRUE.equals(risk.getRisky()) ? HcsSyncConstant.CHECK_RESULT_FAIL : HcsSyncConstant.CHECK_RESULT_PASS);
+ checkLog.setMsg("客商风险查询[" + customerCode + "]黑名单=" + risk.getFdBlackStatus()
+ + ",风险等级=" + risk.getFdRiskLevel() + ",高风险/黑名单=" + risk.getRisky());
+ checkLog.setTraceId(traceId);
+ checkLog.setCreateTime(LocalDateTime.now());
+ return checkLog;
}
/**
@@ -287,18 +331,67 @@ public class HcsCreditCheckSupport {
}
/**
- * 风险等级转义(C101-1 枚举 0=低风险/1=中风险/2=高风险):黑名单状态优先展示,无等级返回空
+ * 风险等级转义:黑名单状态优先展示;等级走业务字典 risk_level 翻译({@link #resolveRiskLevelName}),无等级返回空
*/
private String riskLevelName(String blackStatus, String riskLevel) {
if (BLACK_STATUS_YES.equalsIgnoreCase(blackStatus)) {
return "黑名单";
}
- return switch (Objects.toString(riskLevel, "")) {
- case "2" -> "高风险";
- case "1" -> "中风险";
- case "0" -> "低风险";
- default -> null;
- };
+ return resolveRiskLevelName(riskLevel);
+ }
+
+ /**
+ * 客商风险等级名称翻译(业务字典 risk_level,dictKey=C101-1 风险等级 0=低风险/1=中风险/2=高风险);
+ * 业务字典未命中兜底系统字典,仍未命中返回 null(明细不拼等级段);列表组装与展示链路统一复用本方法
+ *
+ * @param riskLevel C101-1 风险等级
+ * @return 等级名称,未命中返回 null
+ */
+ public static String resolveRiskLevelName(String riskLevel) {
+ if (Func.isEmpty(riskLevel)) {
+ return null;
+ }
+ String levelName = DictBizCache.getValue(HcsSyncConstant.RISK_LEVEL_DICT_CODE, riskLevel);
+ if (StringUtil.isBlank(levelName)) {
+ levelName = DictCache.getValue(HcsSyncConstant.RISK_LEVEL_DICT_CODE, riskLevel);
+ }
+ return StringUtil.isBlank(levelName) ? null : levelName;
+ }
+
+ /**
+ * 组装客商风险明细文案(后台统一组装、前端只展示;列表悬浮明细与表单实时提示共用本方法,口径一致),
+ * 格式:【客商名称】:被列入外部黑名单(失信类别:xx);{列入事由};风险等级:xx,分段说明:
+ * ① 黑名单段:合同系统黑名单(blackStatusYes)或金蝶黑名单命中(类别/事由任一非空)→「被列入外部黑名单」,
+ * 失信类别(A801-5 awrp_combofield_title,缺失兜底 awrp_combofield)有值时括号展示区分禁止交易/风险警示类;
+ * ② 列入事由:金蝶 A801-5 awrp_textareafield 真实明细,非空时追加;
+ * ③ 等级段:C101-1 风险等级(risk_level 字典翻译,黑名单不转义、按实际等级展示),非空时追加
+ *
+ * @param customerName 客商名称
+ * @param blackStatusYes 合同系统黑名单状态(C101-1 fdBlackStatus=yes)
+ * @param riskLevel C101-1 风险等级(0/1/2)
+ * @param blackCategory 金蝶黑名单失信类别
+ * @param blackReason 金蝶黑名单列入事由
+ * @return 明细文案,无风险信息返回空字符串
+ */
+ public static String buildRiskDetail(String customerName, boolean blackStatusYes, String riskLevel, String blackCategory, String blackReason) {
+ List parts = new ArrayList<>();
+ boolean blacklist = blackStatusYes || Func.isNotEmpty(blackCategory) || Func.isNotEmpty(blackReason);
+ if (blacklist) {
+ parts.add(Func.isNotEmpty(blackCategory)
+ ? "被列入外部黑名单(失信类别:" + blackCategory + ")"
+ : "被列入外部黑名单");
+ }
+ if (Func.isNotEmpty(blackReason)) {
+ parts.add(blackReason);
+ }
+ String levelName = resolveRiskLevelName(riskLevel);
+ if (StringUtil.isNotBlank(levelName)) {
+ parts.add("风险等级:" + levelName);
+ }
+ if (parts.isEmpty()) {
+ return "";
+ }
+ return "【" + customerName + "】:" + String.join(";", parts);
}
/**
@@ -324,6 +417,139 @@ public class HcsCreditCheckSupport {
return riskList;
}
+ /**
+ * 客商风险每日定时刷新(C101-1,客商档案列表展示与筛选数据源):
+ * ① 取合同期限内(正式/临时期限任一覆盖当天)审核通过且未终止合同的甲乙方名称;
+ * ② 按名称反查客商档案,内部组织客商默认无风险跳过;
+ * ③ 逐个查询 C101-1(接口仅支持单编号,属远程调用非数据库 N+1;复用 {@link #fetchRelativeRisk},不重复查档案),
+ * 单个失败记 error 日志不中断(失败原因备查);
+ * ④ 外部客商按 200/批切分,每批黑名单(A801-5)一次批量查询 + C101-1 逐个远程查询,失信类别/列入事由落原始字段
+ * (明细文案由列表查询统一组装);
+ * ⑤ 每批成功结果立即批量落库(Db.updateBatchById,仅提交 id + 风险列,不整行回写)+ 留痕批量写入(Db.saveBatch),
+ * 风险/黑名单批次互不影响,单批失败不中断其余批次,避免逐条操作数据库
+ *
+ * @return 结果汇总描述
+ */
+ public String refreshCustomerRiskForActiveContracts() {
+ LocalDate today = LocalDate.now();
+ List contractList = contractManageMapper.selectList(Wrappers.lambdaQuery()
+ .select(ContractManage::getPartyA, ContractManage::getPartyB)
+ .in(ContractManage::getApprovalStatus, HcsSyncConstant.CONTRACT_APPROVED_STATUSES)
+ .ne(ContractManage::getCurrentNode, HcsSyncConstant.CONTRACT_TERMINATED_NODE)
+ .and(wrapper -> wrapper
+ .and(item -> item.le(ContractManage::getStartDate, today).ge(ContractManage::getEndDate, today))
+ .or(item -> item.le(ContractManage::getTemporaryStartDate, today).ge(ContractManage::getTemporaryEndDate, today))));
+ List partyNames = contractList.stream()
+ .flatMap(contract -> Stream.of(contract.getPartyA(), contract.getPartyB()))
+ .filter(Func::isNotEmpty)
+ .map(String::trim)
+ .distinct()
+ .toList();
+ if (partyNames.isEmpty()) {
+ return "合同期限内无生效合同,未刷新客商风险";
+ }
+ List archiveList = customerArchiveMapper.selectList(Wrappers.lambdaQuery()
+ .in(CustomerArchive::getFullName, partyNames)
+ .eq(CustomerArchive::getIsDeleted, 0));
+ List externalArchives = archiveList.stream()
+ .filter(customerArchive -> !isInternalCustomer(customerArchive))
+ .toList();
+ int internalCount = archiveList.size() - externalArchives.size();
+ int refreshCount = 0;
+ int failCount = 0;
+ // 外部客商按 200/批处理:每批黑名单一次批量查询 + C101-1 逐个远程查询(接口仅支持单编号,非数据库 N+1),
+ // 批内成功结果立即批量落库与留痕,风险/黑名单批次互不影响,单批失败不中断其余批次
+ for (int start = 0; start < externalArchives.size(); start += HcsSyncConstant.CUSTOMER_RISK_REFRESH_BATCH) {
+ List batch = externalArchives.subList(start, Math.min(start + HcsSyncConstant.CUSTOMER_RISK_REFRESH_BATCH, externalArchives.size()));
+ Map blacklistMap = this.queryBlacklistByCodes(batch.stream()
+ .map(CustomerArchive::getUnifiedCreditCode)
+ .filter(Func::isNotEmpty)
+ .map(code -> code.trim().toUpperCase())
+ .distinct()
+ .toList());
+ List batchRefreshList = new ArrayList<>();
+ List batchCheckLogs = new ArrayList<>();
+ for (CustomerArchive customerArchive : batch) {
+ try {
+ RelativeRiskResult fetch = this.fetchRelativeRisk(customerArchive.getCustomerCode(), customerArchive.getFullName());
+ CustomerRiskVO risk = fetch.risk();
+ // 仅提交风险列,避免整行回写覆盖刷新期间其它并发修改(审批状态/额度/金蝶推送等)
+ CustomerArchive riskPatch = new CustomerArchive();
+ riskPatch.setId(customerArchive.getId());
+ riskPatch.setHcsRiskLevel(risk.getFdRiskLevel());
+ riskPatch.setHcsBlackStatus(risk.getFdBlackStatus());
+ riskPatch.setHcsRiskTime(LocalDateTime.now());
+ // 金蝶黑名单命中落原始字段(失信类别/列入事由),明细文案由列表查询统一组装
+ BfiBlacklistVO hit = Func.isEmpty(customerArchive.getUnifiedCreditCode())
+ ? null : blacklistMap.get(customerArchive.getUnifiedCreditCode().trim().toUpperCase());
+ if (hit != null) {
+ String category = Func.isEmpty(hit.getAwrpCombofieldTitle()) ? hit.getAwrpCombofield() : hit.getAwrpCombofieldTitle();
+ riskPatch.setKingdeeBlackCategory(category);
+ riskPatch.setKingdeeBlackReason(hit.getAwrpTextareafield());
+ }
+ batchCheckLogs.add(this.buildRiskCheckLog(customerArchive.getCustomerCode(), risk, fetch.traceId()));
+ batchRefreshList.add(riskPatch);
+ } catch (Exception exception) {
+ failCount++;
+ log.error("定时刷新客商风险失败:customerCode={},fullName={}",
+ customerArchive.getCustomerCode(), customerArchive.getFullName(), exception);
+ }
+ }
+ // 批内成功结果立即提交数据库,批间互不影响
+ if (!batchRefreshList.isEmpty()) {
+ Db.updateBatchById(batchRefreshList);
+ refreshCount += batchRefreshList.size();
+ }
+ this.saveRiskCheckLogs(batchCheckLogs);
+ }
+ String summary = String.format("生效合同 %d 份,覆盖客商 %d 个,刷新 %d 个,内部客商跳过 %d 个,失败 %d 个",
+ contractList.size(), archiveList.size(), refreshCount, internalCount, failCount);
+ if (failCount > 0) {
+ log.warn("定时刷新客商风险存在失败:{}", summary);
+ }
+ return summary;
+ }
+
+ /**
+ * 金蝶黑名单批量查询(A801-5,定时刷新专用,调用方按 200 客商/批切分保证入参 ≤200):
+ * 键为大写税号;失信类别取 awrp_combofield_title、缺失兜底 awrp_combofield(口径同提审校验 validateBlacklist);
+ * 查询异常不阻断刷新,记 error 日志备查后返回空 map(该批黑名单段缺失)
+ *
+ * @param creditCodes 统一社会信用代码集合(大写,≤200)
+ * @return 大写税号 -> 黑名单记录
+ */
+ private Map queryBlacklistByCodes(List creditCodes) {
+ if (creditCodes.isEmpty()) {
+ return Map.of();
+ }
+ try {
+ Map blacklistMap = new HashMap<>();
+ for (BfiBlacklistVO item : this.bfiService.queryBlacklist("1", creditCodes)) {
+ if (Func.isNotEmpty(item.getNumber())) {
+ blacklistMap.put(item.getNumber().trim().toUpperCase(), item);
+ }
+ }
+ return blacklistMap;
+ } catch (Exception exception) {
+ log.error("定时刷新客商风险批量查询金蝶黑名单失败:size={}", creditCodes.size(), exception);
+ return Map.of();
+ }
+ }
+
+ /**
+ * 客商风险留痕批量写入(Db.saveBatch,避免逐条 insert);失败记 error 日志备查,不阻断刷新结果
+ */
+ private void saveRiskCheckLogs(List checkLogs) {
+ if (checkLogs.isEmpty()) {
+ return;
+ }
+ try {
+ Db.saveBatch(checkLogs);
+ } catch (Exception exception) {
+ log.error("定时刷新客商风险留痕批量写入失败:size={}", checkLogs.size(), exception);
+ }
+ }
+
/**
* 按名称反查客商档案(内部/外部类型判断与客商编码获取共用;查不到返回 null)
*/
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/CustomerRiskRefreshTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/CustomerRiskRefreshTask.java
new file mode 100644
index 00000000..862f6cde
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/CustomerRiskRefreshTask.java
@@ -0,0 +1,72 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.task;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.hcs.config.HcsProperties;
+import org.springblade.transport.support.hcs.HcsCreditCheckSupport;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.stereotype.Component;
+
+/**
+ * 客商风险每日定时刷新任务(C101-1)
+ *
+ * 合同期限范围内的生效合同,每日定时查询其甲乙方关联客商的风险等级/黑名单状态并落库
+ * (blade_customer_archive.hcs_risk_level/hcs_black_status/hcs_risk_time),
+ * 作为客商档案列表「客商风险」列展示与筛选的数据源;查询逐次留痕 blade_hcs_check_log,
+ * 单个失败记录失败原因日志备查。
+ * 业务逻辑收敛在 {@link HcsCreditCheckSupport#refreshCustomerRiskForActiveContracts()},便于手动触发联调。
+ *
+ * 调度配置(Nacos thirdParty.hcs.risk.*):refreshEnabled 开关、refreshCron 表达式。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RefreshScope
+@RequiredArgsConstructor
+public class CustomerRiskRefreshTask {
+
+ private final HcsCreditCheckSupport hcsCreditCheckSupport;
+ private final HcsProperties hcsProperties;
+
+ /** 客商风险每日刷新调度,默认每天 02:30 执行(错开金蝶客商同步 02:00) */
+ @Scheduled(cron = "${thirdParty.hcs.risk.refreshCron:0 30 2 * * ?}")
+ public void refreshCustomerRisk() {
+ if (!hcsProperties.getRisk().isRefreshEnabled()) {
+ return;
+ }
+ try {
+ String summary = hcsCreditCheckSupport.refreshCustomerRiskForActiveContracts();
+ log.info("定时刷新客商风险:{}", summary);
+ } catch (Exception exception) {
+ log.error("定时刷新客商风险异常", exception);
+ }
+ }
+
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java
index dc2b4b2a..56580bb0 100644
--- a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java
@@ -252,6 +252,11 @@ public class HcsProperties {
* 付款提交前风险校验开关
*/
private boolean checkOnPayment = true;
+ /**
+ * 客商风险每日定时刷新开关(thirdParty.hcs.risk.refresh-enabled,默认关闭):
+ * 刷新合同期限内生效合同关联客商的风险等级/黑名单状态并落库,支撑列表展示与筛选
+ */
+ private boolean refreshEnabled = false;
}
@Data
diff --git a/doc/nacos/third-party-api.yaml b/doc/nacos/third-party-api.yaml
index efbaaef0..d09370cd 100644
--- a/doc/nacos/third-party-api.yaml
+++ b/doc/nacos/third-party-api.yaml
@@ -245,6 +245,10 @@ thirdParty:
risk:
check-on-contract: ${HCS_RISK_CHECK_ON_CONTRACT:true}
check-on-payment: ${HCS_RISK_CHECK_ON_PAYMENT:true}
+ # 客商风险每日定时刷新开关(刷新合同期限内生效合同关联客商并落库,支撑列表展示与筛选,默认关)
+ refresh-enabled: ${HCS_RISK_REFRESH_ENABLED:false}
+ # 客商风险刷新调度 cron,默认每天 02:30(错开金蝶客商同步 02:00)
+ refresh-cron: ${HCS_RISK_REFRESH_CRON:0 30 2 * * ?}
# 黑名单是否阻断(合同置 failed / 付款提交拦截)
# block-blacklist: ${HCS_RISK_BLOCK_BLACKLIST:true}
# 额度校验(C106-1,一期暂不对接)