From 2e650b84ffa6197b6ce6d0385d9292074ab61d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Fri, 30 Oct 2026 11:33:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=A2=E5=95=86=E9=A3=8E=E9=99=A9?= =?UTF-8?q?=E6=AF=8F=E6=97=A5=E5=AE=9A=E6=97=B6=E5=88=B7=E6=96=B0=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=EF=BC=88C101-1=E9=A3=8E=E9=99=A9=E7=AD=89=E7=BA=A7?= =?UTF-8?q?=E4=B8=8EA801-5=E9=87=91=E8=9D=B6=E9=BB=91=E5=90=8D=E5=8D=95?= =?UTF-8?q?=E6=8C=89200=E5=AE=A2=E5=95=86=E5=88=86=E6=89=B9=E8=90=BD?= =?UTF-8?q?=E5=BA=93=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transport/constant/HcsSyncConstant.java | 22 ++ .../support/hcs/HcsCreditCheckSupport.java | 290 ++++++++++++++++-- .../task/CustomerRiskRefreshTask.java | 72 +++++ .../springblade/hcs/config/HcsProperties.java | 5 + doc/nacos/third-party-api.yaml | 4 + 5 files changed, 361 insertions(+), 32 deletions(-) create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/task/CustomerRiskRefreshTask.java 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,一期暂不对接)