feat: HCS付款提交同步校验(C101-1客商风险黑名单阻断/C106-1额度校验超额转特批/校验留痕check_log)

This commit is contained in:
刘泉佳
2026-09-27 23:41:56 +08:00
parent 594773d7c7
commit cb54b569cd
2 changed files with 207 additions and 0 deletions
@@ -95,6 +95,7 @@ import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IPaymentApplicationService;
import org.springblade.transport.service.KingdeeSyncContext;
import org.springblade.transport.support.KingdeeApBillSyncSupport;
import org.springblade.transport.support.hcs.HcsCreditCheckSupport;
import org.springblade.transport.wrapper.PaymentApplicationWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -145,6 +146,7 @@ private final CustomerReceiptAccountMapper receiptAccountMapper;
private final IBfiService bfiService;
private final KingdeeApBillSyncSupport kingdeeApBillSyncSupport;
private final BfiProperties bfiProperties;
private final HcsCreditCheckSupport hcsCreditCheckSupport;
@Override
public IPage<PaymentApplicationVO> selectPage(IPage<PaymentApplication> page, PaymentApplicationVO query) {
@@ -323,6 +325,8 @@ private final CustomerReceiptAccountMapper receiptAccountMapper;
validateProjectAdvanceContract(entity);
validateSelectedBill(entity, false);
validateQuota(entity);
// HCS 提交同步校验(事务内查询类同步,D15/R2-Q3):C101-1 风险(黑名单阻断)+ C106-1 额度(超额转特批)
hcsCreditCheckSupport.checkOnSubmit(entity);
entity.setApprovalStatus(TransportBusiConstant.REVIEWING);
entity.setCurrentNode("财务审核");
entity.setCurrentProcessor(AuthUtil.getUserName());
@@ -0,0 +1,203 @@
/**
* 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.support.hcs;
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.utils.StringUtil;
import org.springblade.hcs.config.HcsProperties;
import org.springblade.hcs.dto.HcsCreditCheckDTO;
import org.springblade.hcs.dto.HcsCreditCheckVO;
import org.springblade.hcs.dto.HcsResult;
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.transport.constant.HcsSyncConstant;
import org.springblade.transport.mapper.ContractManageMapper;
import org.springblade.transport.mapper.CustomerArchiveMapper;
import org.springblade.transport.mapper.HcsContractCheckLogMapper;
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.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Objects;
/**
* HCS 付款提交校验支撑组件(C101-1 客商风险 + C106-1 额度校验,设计文档 §5.4,D15/R2-Q3)
* <p>
* 付款申请提交(submit)事务内同步调用(查询类同步):
* ① C101-1 按收款方客商编号查风险,黑名单且 block-blacklist=true 时阻断提交;
* ② C106-1 校验申请金额是否超出合同额度,超额不拦截提交,置 hcs_credit_status=over 并标记
* hcs_spec_status=need 转特批链路(D15/R2-Q2)。
* 校验依赖合同已推送(hcs_fd_id 存在),未推送时按 credit.require-contract-synced 配置阻断或跳过。
* 结果留痕 blade_hcs_check_log(biz_type=payment)。
*
* @author Chill
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class HcsCreditCheckSupport {
/**
* C101-1 黑名单状态取值(示例 no 表示非黑名单,§9-U3 字典值联调确认)
*/
private static final String BLACK_STATUS_YES = "yes";
private final IHcsClient hcsClient;
private final HcsProperties hcsProperties;
private final ContractManageMapper contractManageMapper;
private final CustomerArchiveMapper customerArchiveMapper;
private final HcsContractCheckLogMapper hcsContractCheckLogMapper;
/**
* 付款提交同步校验入口(submit 事务内调用,校验结果字段随提交一并落库)
*
* @param payment 付款申请单(须含合同与收款方信息)
*/
public void checkOnSubmit(PaymentApplication payment) {
// ① 客商风险校验(可配开关),黑名单阻断提交
if (hcsProperties.getRisk().isCheckOnPayment()) {
checkRelativeRisk(payment);
}
// ② 额度校验(超额不拦截,转特批)
checkCredit(payment);
}
/**
* C101-1 收款方客商风险校验:黑名单且 block-blacklist=true 时阻断提交
*/
private void checkRelativeRisk(PaymentApplication payment) {
String customerCode = queryCustomerCode(payment.getPayeeName());
if (StringUtil.isBlank(customerCode)) {
writeCheckLog(payment, HcsSyncConstant.CHECK_TYPE_RISK, HcsSyncConstant.CHECK_RESULT_SKIP,
"收款方[" + payment.getPayeeName() + "]反查不到客商编号,跳过风险校验(§9-U6)", null);
log.warn("HCS付款提交风险校验跳过:paymentNo={},收款方反查不到客商编号", payment.getPaymentNo());
return;
}
HcsRiskQueryDTO param = new HcsRiskQueryDTO();
// 【必填】客商编号:按收款方名称反查 CustomerArchive 客商编码(用哪个编码待联调,§9-U6)
param.setFdNumber(customerCode);
HcsResult<HcsRiskVO> result = hcsClient.getRelativeRisk(param);
if (result == null || !result.isSuccess()) {
log.error("HCS客商风险查询失败响应:{},paymentNo={},fdNumber={}",
result == null ? "无响应" : result.getMsg(), payment.getPaymentNo(), customerCode);
throw new HcsException("C101-1 客商风险查询失败:" + (result == null ? "响应为空" : result.getMsg()));
}
HcsRiskVO risk = result.getData();
String blackStatus = risk == null ? null : risk.getFdBlackStatus();
boolean blacklist = BLACK_STATUS_YES.equalsIgnoreCase(blackStatus);
writeCheckLog(payment, HcsSyncConstant.CHECK_TYPE_RISK, blacklist ? HcsSyncConstant.CHECK_RESULT_FAIL : HcsSyncConstant.CHECK_RESULT_PASS,
"收款方[" + payment.getPayeeName() + "]黑名单=" + blackStatus + ",风险等级=" + (risk == null ? null : risk.getFdRiskLevel()),
result.getTraceId());
if (blacklist && hcsProperties.getRisk().isBlockBlacklist()) {
log.error("HCS付款提交被风险校验阻断:paymentNo={},收款方[{}]黑名单", payment.getPaymentNo(), payment.getPayeeName());
throw new ServiceException("收款方客商黑名单,禁止提交付款申请");
}
}
/**
* C106-1 额度校验:通过置 pass;超额置 over 并标记待特批(不拦截提交,D15/R2-Q2)
*/
private void checkCredit(PaymentApplication payment) {
ContractManage contractManage = payment.getContractId() == null ? null : contractManageMapper.selectById(payment.getContractId());
if (contractManage == null || StringUtil.isBlank(contractManage.getHcsFdId())) {
if (hcsProperties.getCredit().isRequireContractSynced()) {
throw new ServiceException("关联合同尚未推送合规,无法校验额度(paymentNo=" + payment.getPaymentNo() + ")");
}
log.warn("HCS付款提交额度校验跳过:paymentNo={},合同未推送合规(require-contract-synced=false)", payment.getPaymentNo());
writeCheckLog(payment, HcsSyncConstant.CHECK_TYPE_CREDIT, HcsSyncConstant.CHECK_RESULT_SKIP, "合同未推送合规,跳过额度校验", null);
return;
}
HcsCreditCheckDTO param = new HcsCreditCheckDTO();
// 【必填】合同ID:HCS 合同ID(blade_contract_manage.hcs_fd_id)
param.setFdContractId(contractManage.getHcsFdId());
// 【必填】申请金额:PaymentApplication.appliedAmount(元)
param.setFdApplyAmount(payment.getAppliedAmount() == null ? BigDecimal.ZERO : payment.getAppliedAmount());
HcsResult<HcsCreditCheckVO> result = hcsClient.contractCreditCheck(param);
if (result == null || !result.isSuccess()) {
log.error("HCS额度校验失败响应:{},paymentNo={}", result == null ? "无响应" : result.getMsg(), payment.getPaymentNo());
throw new HcsException("C106-1 额度校验失败:" + (result == null ? "响应为空" : result.getMsg()));
}
HcsCreditCheckVO credit = result.getData();
payment.setHcsCreditTime(LocalDateTime.now());
if (credit != null && Boolean.TRUE.equals(credit.getFdIsCredit())) {
// 额度通过:正常提交
payment.setHcsCreditStatus(HcsSyncConstant.CREDIT_STATUS_PASS);
payment.setHcsCreditMsg(null);
writeCheckLog(payment, HcsSyncConstant.CHECK_TYPE_CREDIT, HcsSyncConstant.CHECK_RESULT_PASS, "额度校验通过", result.getTraceId());
return;
}
// 超额:success 仍为 true,必须以 fdIsCredit 为准;不拦截提交,转入特批链路
String message = credit == null ? "响应数据为空" : credit.getFdMessage();
payment.setHcsCreditStatus(HcsSyncConstant.CREDIT_STATUS_OVER);
payment.setHcsCreditMsg(message);
payment.setHcsSpecStatus(HcsSyncConstant.SPEC_STATUS_NEED);
writeCheckLog(payment, HcsSyncConstant.CHECK_TYPE_CREDIT, HcsSyncConstant.CHECK_RESULT_FAIL, "额度超额:" + message, result.getTraceId());
log.warn("HCS付款提交额度超额(转特批):paymentNo={},{}", payment.getPaymentNo(), message);
}
/**
* 按名称反查客商编码(照 KingdeeApBillSyncSupport#queryCustomerCode 惯例,查不到返回 null)
*/
private String queryCustomerCode(String customerName) {
if (StringUtil.isBlank(customerName)) {
return null;
}
CustomerArchive customerArchive = customerArchiveMapper.selectOne(Wrappers.<CustomerArchive>lambdaQuery()
.and(wrapper -> wrapper.eq(CustomerArchive::getFullName, customerName).or().eq(CustomerArchive::getShortName, customerName))
.eq(CustomerArchive::getIsDeleted, 0)
.last("limit 1"));
return customerArchive == null ? null : customerArchive.getCustomerCode();
}
/**
* 校验留痕(blade_hcs_check_log,异常不影响主流程)
*/
private void writeCheckLog(PaymentApplication payment, String checkType, String result, String msg, String traceId) {
try {
HcsContractCheckLog checkLog = new HcsContractCheckLog();
checkLog.setTenantId(payment.getTenantId());
checkLog.setBizType(HcsSyncConstant.BIZ_TYPE_PAYMENT);
checkLog.setBizId(payment.getId());
checkLog.setCheckType(checkType);
checkLog.setResult(result);
checkLog.setMsg(msg);
checkLog.setTraceId(traceId);
checkLog.setCreateTime(LocalDateTime.now());
hcsContractCheckLogMapper.insert(checkLog);
} catch (Exception exception) {
log.error("HCS付款校验留痕写入失败:paymentNo={}", payment.getPaymentNo(), exception);
}
}
}