feat: 客商风险每日定时刷新任务(C101-1风险等级与A801-5金蝶黑名单按200客商分批落库)
This commit is contained in:
+22
@@ -25,6 +25,8 @@
|
||||
*/
|
||||
package org.springblade.transport.constant;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* HCS(合同合规系统)同步常量(TMS 业务侧)
|
||||
* <p>
|
||||
@@ -175,6 +177,26 @@ public final class HcsSyncConstant {
|
||||
*/
|
||||
public static final String CUSTOMER_KIND_INTERNAL = "internal";
|
||||
|
||||
/**
|
||||
* 合同审核通过状态集合(首推/变更通过,客商风险定时刷新扫描口径与 HcsPushTask 一致)
|
||||
*/
|
||||
public static final List<String> 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)
|
||||
*/
|
||||
|
||||
+258
-32
@@ -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<String> 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<ContractManage> contractList = contractManageMapper.selectList(Wrappers.<ContractManage>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<String> partyNames = contractList.stream()
|
||||
.flatMap(contract -> Stream.of(contract.getPartyA(), contract.getPartyB()))
|
||||
.filter(Func::isNotEmpty)
|
||||
.map(String::trim)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (partyNames.isEmpty()) {
|
||||
return "合同期限内无生效合同,未刷新客商风险";
|
||||
}
|
||||
List<CustomerArchive> archiveList = customerArchiveMapper.selectList(Wrappers.<CustomerArchive>lambdaQuery()
|
||||
.in(CustomerArchive::getFullName, partyNames)
|
||||
.eq(CustomerArchive::getIsDeleted, 0));
|
||||
List<CustomerArchive> 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<CustomerArchive> batch = externalArchives.subList(start, Math.min(start + HcsSyncConstant.CUSTOMER_RISK_REFRESH_BATCH, externalArchives.size()));
|
||||
Map<String, BfiBlacklistVO> blacklistMap = this.queryBlacklistByCodes(batch.stream()
|
||||
.map(CustomerArchive::getUnifiedCreditCode)
|
||||
.filter(Func::isNotEmpty)
|
||||
.map(code -> code.trim().toUpperCase())
|
||||
.distinct()
|
||||
.toList());
|
||||
List<CustomerArchive> batchRefreshList = new ArrayList<>();
|
||||
List<HcsContractCheckLog> 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<String, BfiBlacklistVO> queryBlacklistByCodes(List<String> creditCodes) {
|
||||
if (creditCodes.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
try {
|
||||
Map<String, BfiBlacklistVO> 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<HcsContractCheckLog> checkLogs) {
|
||||
if (checkLogs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Db.saveBatch(checkLogs);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时刷新客商风险留痕批量写入失败:size={}", checkLogs.size(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按名称反查客商档案(内部/外部类型判断与客商编码获取共用;查不到返回 null)
|
||||
*/
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 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.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)
|
||||
* <p>
|
||||
* 合同期限范围内的生效合同,每日定时查询其甲乙方关联客商的风险等级/黑名单状态并落库
|
||||
* (blade_customer_archive.hcs_risk_level/hcs_black_status/hcs_risk_time),
|
||||
* 作为客商档案列表「客商风险」列展示与筛选的数据源;查询逐次留痕 blade_hcs_check_log,
|
||||
* 单个失败记录失败原因日志备查。
|
||||
* 业务逻辑收敛在 {@link HcsCreditCheckSupport#refreshCustomerRiskForActiveContracts()},便于手动触发联调。
|
||||
* <p>
|
||||
* 调度配置(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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+5
@@ -252,6 +252,11 @@ public class HcsProperties {
|
||||
* 付款提交前风险校验开关
|
||||
*/
|
||||
private boolean checkOnPayment = true;
|
||||
/**
|
||||
* 客商风险每日定时刷新开关(thirdParty.hcs.risk.refresh-enabled,默认关闭):
|
||||
* 刷新合同期限内生效合同关联客商的风险等级/黑名单状态并落库,支撑列表展示与筛选
|
||||
*/
|
||||
private boolean refreshEnabled = false;
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
@@ -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,一期暂不对接)
|
||||
|
||||
Reference in New Issue
Block a user