:feat: 付款申请收款方高风险黑名单统一走特批(特批附件校验/特批审核挂起/结果联动审批状态/金蝶推送门禁)

This commit is contained in:
刘泉佳
2026-10-14 10:03:10 +08:00
parent d99488b999
commit 30028b9e27
9 changed files with 361 additions and 72 deletions
@@ -124,7 +124,7 @@ public final class HcsSyncConstant {
public static final String SPEC_STATUS_NONE = "none";
/**
* 特批状态:待特批(额度超额标记,扫描任务据此发起 C106-3)
* 特批状态:待特批(高风险/黑名单或额度超额标记,扫描任务据此发起 C106-3)
*/
public static final String SPEC_STATUS_NEED = "need";
@@ -134,17 +134,17 @@ public final class HcsSyncConstant {
public static final String SPEC_STATUS_PUSHING = "pushing";
/**
* 特批状态:特批中(C106-3 已发起,结果人工确认,§9-U1)
* 特批状态:特批中(C106-3 已发起,TMS 挂起「特批审核」,等待合规 paymentLegal/result 回调)
*/
public static final String SPEC_STATUS_PROCESSING = "processing";
/**
* 特批状态:特批通过(人工确认)
* 特批状态:特批通过(合规回调/人工确认,联动恢复"审批通过"放行)
*/
public static final String SPEC_STATUS_APPROVED = "approved";
/**
* 特批状态:特批驳回(人工确认)
* 特批状态:特批驳回(合规回调/人工确认,联动付款审批驳回走重新提交闭环)
*/
public static final String SPEC_STATUS_REJECTED = "rejected";
@@ -180,6 +180,11 @@ public final class HcsSyncConstant {
*/
public static final String ATTACHMENT_TYPE_SPEC_EXPLANATION = "特批附件";
/**
* 付款申请附件类型值:特批附件(前端付款表单附件类型枚举 attachmentType,付款附件结构与合同 fileType 结构不同)
*/
public static final String PAYMENT_ATTACH_TYPE_SPECIAL = "special_approval";
/**
* 附件类型:用印附件(双章归档文件,取值沿用 ContractManageServiceImpl#FILE_TYPE_SEAL_ARCHIVE)
*/
@@ -136,11 +136,11 @@ public class HcsSyncController {
}
/**
* 特批结果人工确认(§9-U1 过渡方案:v4 无结果查询/回调接口)
* 特批结果人工确认(§9-U1 过渡兜底:与合规 paymentLegal/result 回调等效,均联动付款审批状态)
*/
@PostMapping("/spec-pay/confirm")
@ApiOperationSupport(order = 3)
@Operation(summary = "特批结果人工确认", description = "传入付款申请ID与结果(approved/rejected),仅特批中(processing)允许确认")
@Operation(summary = "特批结果人工确认", description = "传入付款申请ID与结果(approved/rejected),仅特批中(processing)允许确认;approved 恢复审批通过放行,rejected 联动付款审批驳回")
public R<String> confirmSpecResult(@Parameter(description = "付款申请ID", required = true) @RequestParam Long id,
@Parameter(description = "确认结果 approved/rejected", required = true) @RequestParam String result) {
return R.data(hcsPaymentSyncSupport.confirmSpecResult(id, result));
@@ -248,11 +248,12 @@ public class HcsSyncController {
* <p>
* 免登免授权同合同接口(blade.secure 放行);
* iPaaS 网关注册映射:/gwzh/HTHG/paymentLegal/result → /hcs/paymentLegal/result;
* 报文同合同接口(requestId=TMS付款申请ID),付款特批结果不联动付款审批状态(§4.4)。
* 报文 {sourceSystem, requestId=TMS付款申请ID, status=APPROVED/REJECTED, reason},
* approved 恢复"审批通过"放行,rejected 联动付款审批驳回走重新提交闭环。
*/
@PostMapping("/paymentLegal/result")
@ApiOperationSupport(order = 16)
@Operation(summary = "付款特批结果回传接收", description = "合规系统推送付款特批结果,按付款申请ID定位并回写特批状态")
@Operation(summary = "付款特批结果回传接收", description = "合规系统推送付款特批结果,按付款申请ID定位并联动特批状态与付款审批状态,原文留痕")
public R<String> paymentLegalResult(@RequestBody(required = false) HcsLegalResultDTO param) {
return R.data(hcsPaymentSyncSupport.handleLegalResult(param));
}
@@ -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.HcsCreditCheckDTO;
@@ -51,13 +52,17 @@ import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* HCS 付款提交校验支撑组件(C101-1 客商风险 + C106-1 额度校验,设计文档 §5.4,D15/R2-Q3)
* <p>
* 付款申请提交(submit)事务内同步调用(查询类同步):
* ① C101-1 按收款方客商编号查风险,黑名单且 block-blacklist=true 时阻断提交;
* ① C101-1 收款方为外部客商时查风险,高风险(风险等级=2)/黑名单提示且必须上传「特批附件」,
* 并标记 hcs_spec_status=need 转付款特批链路(审批通过后扫描任务发起 C106-3,合规 paymentLegal/result 回调联动);
* 内部组织客商默认无风险不查询;
* ② C106-1 校验申请金额是否超出合同额度,超额不拦截提交,置 hcs_credit_status=over 并标记
* hcs_spec_status=need 转特批链路(D15/R2-Q2)。
* 校验依赖合同已推送(hcs_fd_id 存在),未推送时按 credit.require-contract-synced 配置阻断或跳过。
@@ -87,7 +92,7 @@ public class HcsCreditCheckSupport {
* @param payment 付款申请单(须含合同与收款方信息)
*/
public void checkOnSubmit(PaymentApplication payment) {
// ① 客商风险校验(可配开关),黑名单阻断提交
// ① 客商风险校验(可配开关):外部客商高风险/黑名单须特批附件并标记待特批
if (hcsProperties.getRisk().isCheckOnPayment()) {
checkRelativeRisk(payment);
}
@@ -99,35 +104,49 @@ public class HcsCreditCheckSupport {
}
/**
* C101-1 收款方客商风险校验:黑名单且 block-blacklist=true 时阻断提交
* C101-1 收款方客商风险校验:仅外部客商查询,高风险/黑名单必须上传特批附件,
* 并标记 hcs_spec_status=need 转付款特批链路(不再直接阻断)
*/
private void checkRelativeRisk(PaymentApplication payment) {
String customerCode = queryCustomerCode(payment.getPayeeName());
if (StringUtil.isBlank(customerCode)) {
CustomerArchive customerArchive = queryCustomerArchive(payment.getPayeeName());
if (customerArchive == null) {
writeCheckLog(payment, HcsSyncConstant.CHECK_TYPE_RISK, HcsSyncConstant.CHECK_RESULT_SKIP,
"收款方[" + payment.getPayeeName() + "]反查不到客商编号,跳过风险校验(§9-U6)", null);
log.warn("HCS付款提交风险校验跳过:paymentNo={},收款方反查不到客商编号", payment.getPaymentNo());
return;
}
// 内部组织类型客商默认无风险,不查 C101-1(与 queryCustomerRisk 展示口径一致)
if (HcsSyncConstant.CUSTOMER_KIND_INTERNAL.equals(customerArchive.getCustomerKind())) {
writeCheckLog(payment, HcsSyncConstant.CHECK_TYPE_RISK, HcsSyncConstant.CHECK_RESULT_SKIP,
"收款方[" + payment.getPayeeName() + "]为内部组织客商,默认无风险跳过校验", null);
return;
}
HcsRiskQueryDTO param = new HcsRiskQueryDTO();
// 【必填】客商编号:按收款方名称反查 CustomerArchive 客商编码(用哪个编码待联调,§9-U6)
param.setFdNumber(customerCode);
param.setFdNumber(customerArchive.getCustomerCode());
HcsResult<HcsRiskVO> result = hcsClient.getRelativeRisk(param);
if (result == null || !result.isSuccess()) {
log.error("HCS客商风险查询失败响应:{},paymentNo={},fdNumber={}",
result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), payment.getPaymentNo(), customerCode);
result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), payment.getPaymentNo(), customerArchive.getCustomerCode());
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()),
String riskLevel = risk == null ? null : risk.getFdRiskLevel();
boolean risky = HcsSyncConstant.RISK_LEVEL_HIGH.equals(riskLevel) || BLACK_STATUS_YES.equalsIgnoreCase(blackStatus);
writeCheckLog(payment, HcsSyncConstant.CHECK_TYPE_RISK, risky ? HcsSyncConstant.CHECK_RESULT_FAIL : HcsSyncConstant.CHECK_RESULT_PASS,
"收款方[" + payment.getPayeeName() + "]黑名单=" + blackStatus + ",风险等级=" + riskLevel + ",高风险/黑名单=" + risky,
result.getTraceId());
if (blacklist && hcsProperties.getRisk().isBlockBlacklist()) {
log.error("HCS付款提交被风险校验阻断:paymentNo={},收款方[{}]黑名单", payment.getPaymentNo(), payment.getPayeeName());
throw new ServiceException("收款方客商黑名单,禁止提交付款申请");
if (!risky) {
return;
}
// 高风险/黑名单:必须上传「特批附件」类型附件(提示阻断),并标记待特批(审批通过后推送 C106-3);
// 清空旧特批流程ID:特批单次有效,重新提交须重新发起 C106-3(否则会 C106-4 更新旧驳回单)
if (!hasSpecAttachment(payment.getAttachmentsJson())) {
throw new ServiceException("收款方[" + payment.getPayeeName() + "]为高风险/黑名单客商,必须上传「特批附件」类型附件后再提交审批");
}
payment.setHcsSpecFdId(null);
payment.setHcsSpecStatus(HcsSyncConstant.SPEC_STATUS_NEED);
}
/**
@@ -249,19 +268,50 @@ public class HcsCreditCheckSupport {
}
/**
* 按名称反查客商编码(照 KingdeeApBillSyncSupport#queryCustomerCode 惯例,查不到返回 null)
* 按名称反查客商档案(内部/外部类型判断与客商编码获取共用;查不到返回 null)
*/
private String queryCustomerCode(String customerName) {
private CustomerArchive queryCustomerArchive(String customerName) {
if (StringUtil.isBlank(customerName)) {
return null;
}
CustomerArchive customerArchive = customerArchiveMapper.selectOne(Wrappers.<CustomerArchive>lambdaQuery()
return customerArchiveMapper.selectOne(Wrappers.<CustomerArchive>lambdaQuery()
.and(wrapper -> wrapper.eq(CustomerArchive::getFullName, customerName).or().eq(CustomerArchive::getShortName, customerName))
.eq(CustomerArchive::getIsDeleted, 0)
.last("limit 1"));
}
/**
* 按名称反查客商编码(照 KingdeeApBillSyncSupport#queryCustomerCode 惯例,查不到返回 null)
*/
private String queryCustomerCode(String customerName) {
CustomerArchive customerArchive = queryCustomerArchive(customerName);
return customerArchive == null ? null : customerArchive.getCustomerCode();
}
/**
* 附件中是否已上传特批附件(付款附件结构 attachmentType=special_approval,即前端付款表单「特批附件」类型;
* 兼容 fileType=特批附件 结构;口径同合同提交校验 validateSpecExplanationOnSubmit)
*/
private boolean hasSpecAttachment(String attachmentsJson) {
if (StringUtil.isBlank(attachmentsJson)) {
return false;
}
try {
Object parsed = JsonUtil.parse(attachmentsJson, List.class);
if (!(parsed instanceof List<?> attachments)) {
return false;
}
return attachments.stream().filter(Map.class::isInstance).map(Map.class::cast)
.anyMatch(attachment -> HcsSyncConstant.PAYMENT_ATTACH_TYPE_SPECIAL
.equals(Objects.toString(attachment.get("attachmentType"), null))
|| HcsSyncConstant.ATTACHMENT_TYPE_SPEC_EXPLANATION
.equals(Objects.toString(attachment.get(HcsSyncConstant.ATTACHMENT_TYPE_FIELD), null)));
} catch (Exception exception) {
log.error("HCS付款提交附件JSON解析失败:按未上传特批附件处理", exception);
return false;
}
}
/**
* 校验留痕(blade_hcs_check_log,异常不影响主流程)
*/
@@ -33,6 +33,7 @@ import org.springblade.core.tool.utils.StringUtil;
import org.springblade.hcs.config.HcsProperties;
import org.springblade.hcs.dto.HcsLegalResultDTO;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.constant.TransportBusiConstant;
import org.springblade.transport.mapper.PaymentApplicationMapper;
import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springframework.stereotype.Component;
@@ -43,8 +44,10 @@ import java.util.List;
/**
* HCS 付款链推送编排支撑组件(结算付款特批 C106-3/4 + 履约记录 C108-1/2 + 特批结果推送接收,设计文档 §5.0/§5.5/§5.6)
* <p>
* 编排顺序:特批(额度超额标记 need 时发起)→ 履约记录(审批通过新建 / 实际付款推进更新)。
* 编排顺序:特批(提交时高风险/黑名单须特批附件或额度超额标记 need 时发起)→ 履约记录(审批通过新建 / 实际付款推进更新)。
* 特批流程ID 与履约记录ID 在各自 Support 内即时持久化;整体同步状态(hcs_sync_status)由本组件统一回写。
* 特批发起后 TMS 挂起 currentNode="特批审核",合规 paymentLegal/result 回调联动付款审批状态
* (通过恢复"审批通过"放行 / 驳回联动审批驳回走重新提交闭环),人工确认等效兜底。
*
* @author Chill
*/
@@ -71,7 +74,7 @@ public class HcsPaymentSyncSupport {
*/
public String pushPayment(PaymentApplication payment) {
StringBuilder result = new StringBuilder();
// ① 结算付款特批:提交时额度超额(hcs_spec_status=need)且未发起时发起 C106-3(C106-1 暂不对接后无自动置 need,仅历史数据/人工场景可达)
// ① 结算付款特批:提交时高风险/黑名单(须特批附件)或额度超额(hcs_spec_status=need)且未发起时发起 C106-3
if (HcsSyncConstant.SPEC_STATUS_NEED.equals(payment.getHcsSpecStatus())) {
result.append(hcsSpecPaySyncSupport.pushSpecPay(payment));
}
@@ -115,7 +118,7 @@ public class HcsPaymentSyncSupport {
}
/**
* 特批结果人工确认(§9-U1 过渡方案:v4 无结果查询/回调接口)
* 特批结果人工确认(页面确认与合规 paymentLegal/result 回调等效,§9-U1 过渡兜底)
*
* @param id 付款申请单ID
* @param result confirmed approved / rejected
@@ -132,6 +135,8 @@ public class HcsPaymentSyncSupport {
if (!HcsSyncConstant.SPEC_STATUS_PROCESSING.equals(payment.getHcsSpecStatus())) {
throw new ServiceException("仅特批中的付款申请允许确认结果(当前状态:" + payment.getHcsSpecStatus() + ")");
}
// 特批结果联动付款审批状态(通过恢复放行 / 驳回联动审批驳回),随 updateById 一并落库
syncPaymentOnSpecResult(payment, HcsSyncConstant.SPEC_STATUS_APPROVED.equals(result));
payment.setHcsSpecStatus(result);
paymentApplicationMapper.updateById(payment);
log.info("HCS特批结果人工确认:paymentNo={},specFdId={},结果={}", payment.getPaymentNo(), payment.getHcsSpecFdId(), result);
@@ -139,10 +144,11 @@ public class HcsPaymentSyncSupport {
}
/**
* 付款特批结果接收处理(合规 paymentLegal/result 推送,替代未建设的 B103,D15 回传机制):
* 付款特批结果接收处理(合规 paymentLegal/result 回调通知特批审批结果):
* sourceSystem 须与 third-party.hcs.src-system 配置一致(不符抛出"来源系统标识错误"异常),
* requestId=TMS 付款申请ID、status=APPROVED/REJECTED;
* 付款特批结果不联动付款审批状态(§4.4),仅回写 hcs_spec_status
* 特批结果联动付款审批状态:approved 恢复"审批通过"放行(后续金蝶推送放行),
* rejected 联动付款审批驳回(returned),走"重新编辑→提交→审批→重新特批"闭环
*
* @param param 合规推送报文
* @return 处理结果描述
@@ -169,12 +175,29 @@ public class HcsPaymentSyncSupport {
log.warn("付款特批结果状态未识别:paymentNo={},status={},reason={}", payment.getPaymentNo(), param.getStatus(), param.getReason());
return "状态字段未识别(paymentNo=" + payment.getPaymentNo() + ")";
}
syncPaymentOnSpecResult(payment, HcsSyncConstant.SPEC_STATUS_APPROVED.equals(status));
payment.setHcsSpecStatus(status);
paymentApplicationMapper.updateById(payment);
log.info("付款特批结果推送处理完成:paymentNo={},结果={},reason={}", payment.getPaymentNo(), status, param.getReason());
return "特批结果已处理:" + status;
}
/**
* 特批结果联动付款审批状态(handleLegalResult 与 confirmSpecResult 共用,仅改字段,随 updateById 落库):
* approved 恢复"审批通过"展示并放行(审批状态保持 approved,金蝶推送门禁解除);
* rejected 联动付款审批驳回(returned + "已驳回"),走重新编辑→提交→审批→重新特批闭环
* (提交时重新校验收款方风险与特批附件)
*/
private void syncPaymentOnSpecResult(PaymentApplication payment, boolean approved) {
if (approved) {
payment.setCurrentNode("审批通过");
} else {
payment.setApprovalStatus(TransportBusiConstant.RETURNED);
payment.setCurrentNode("已驳回");
payment.setCurrentProcessor("待处理");
}
}
/**
* 按 TMS 付款申请ID 定位付款申请(合规推送 requestId)
*/
@@ -30,32 +30,33 @@ import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.hcs.config.HcsProperties;
import org.springblade.hcs.dto.HcsAttachmentRefDTO;
import org.springblade.hcs.dto.HcsFormSaveVO;
import org.springblade.hcs.dto.HcsMechanismsDTO;
import org.springblade.hcs.dto.HcsResult;
import org.springblade.hcs.dto.HcsFormSaveVO;
import org.springblade.hcs.dto.HcsSpecPayFormDataDTO;
import org.springblade.hcs.dto.HcsSpecPayPropsDTO;
import org.springblade.hcs.dto.HcsSpecPaySaveDTO;
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.PaymentApplicationMapper;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springblade.transport.mapper.ContractManageMapper;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* HCS 结算付款特批同步支撑组件(C106-3 发起 / C106-4 更新,设计文档 §5.5)
* <p>
* 触发:付款申请审批通过且 hcs_spec_status=need(提交时额度超额标记)→ 扫描任务发起 C106-3,
* 返回 data.data.fdId(双层嵌套)→ hcs_spec_fd_id,状态置 processing;
* 触发:付款申请提交时收款方外部客商高风险/黑名单(须特批附件)或额度超额标记 hcs_spec_status=need
* → 审批通过后扫描任务发起 C106-3,返回 data.data.fdId(双层嵌套)→ hcs_spec_fd_id,状态置 processing
* 并挂起 currentNode="特批审核";
* 单据变更且特批流程存在时走 C106-4(顶层 fdId=hcs_spec_fd_id,附件不带 fdBindType)。
* 特批结果:v4 无结果查询/回调接口,approved/rejected 一期由页面人工确认(§9-U1)。
* 特批结果:合规 paymentLegal/result 回调联动付款审批状态(通过放行/驳回联动审批驳回),
* 页面人工确认等效兜底(§9-U1)。
*
* @author Chill
*/
@@ -91,8 +92,10 @@ public class HcsSpecPaySyncSupport {
String specFdId = extractNestedFdId(payment, result);
payment.setHcsSpecFdId(specFdId);
payment.setHcsSpecStatus(HcsSyncConstant.SPEC_STATUS_PROCESSING);
// TMS 流程挂起展示:等待合规 paymentLegal/result 回调联动付款审批状态(与合同链路 hangSpecReview 口径一致)
payment.setCurrentNode(HcsSyncConstant.SPEC_REVIEW_NODE);
paymentApplicationMapper.updateById(payment);
log.info("HCS结算付款特批发起成功:paymentNo={},specFdId={}", payment.getPaymentNo(), specFdId);
log.info("HCS结算付款特批发起成功:paymentNo={},specFdId={},挂起「{}」", payment.getPaymentNo(), specFdId, HcsSyncConstant.SPEC_REVIEW_NODE);
return "C106-3 特批发起成功";
}
// C106-4 更新:顶层 fdId 必填,内层无业务数据,以内层 success 判断
@@ -216,10 +216,6 @@ public class HcsProperties {
* 付款提交前风险校验开关
*/
private boolean checkOnPayment = true;
/**
* 黑名单是否阻断(合同置 failed / 付款提交拦截)
*/
private boolean blockBlacklist = true;
}
@Data
@@ -4,9 +4,11 @@
> 涉及模块:`blade-service/blade-transport`(业务编排/状态留痕)、`blade-third-party-api/blade-hcs-api`(Feign 客户端 + DTO + 配置)
> 关联设计文档:`doc/docs/hcs合同/2026-10-11-hcs合同推送流程梳理.md`(§九)、`2026-09-27-桂物合同合规系统HCS接入技术设计.md`(§5.4/§5.5)
> 关联提交:`8e6e263`(外部客商高风险黑名单管控与合同自动特批)
> 付款申请全链路(前端提交 → 风险/特批 → 同步金蝶 → 结果回写)专项梳理:`doc/docs/业务流程/2026-10-13-付款申请全链路业务流程(前端提交-客商风险特批-同步金蝶-结果回写).md`
>
> **2026-10-12 变更**:原计划的 B103 推送合规方未建设,特批结果改为合规审批完成后经 iPaaS 主动推送 TMS:合同 `POST /hcs/contractLegal/result`、付款 `POST /hcs/paymentLegal/result`(报文 `{sourceSystem, requestId, status=APPROVED/REJECTED, reason}`);原推测性回调端点 `/hcs/callback/spec-result` 及 `HcsFieldConstant.SPEC_CB_*` 候选键解析已删除。见 §2.5/§4.4/§五。
> **2026-10-13 变更**:C103-8 独立特批发起接口合规未提供(联调 404),合同特批改为随 C103-1/C103-11 报文携带 `fdSepcailContract=true` 发起(高风险/黑名单判定后置入报文);TMS 挂起"特批审核"(processing)后新增**定时 C103-4 结果查询兜底**(随合同推送任务执行,`fdApprovalStatus=approved/approvalinvalid` 联动恢复/驳回);特批附件改随 C103-1 附件面板 `eopCdraftSpecial` 挂载;附件类型判定值与业务字典对齐为「特批附件」。**C103-8 全链路代码已删除**(`IHcsClient#addSpecContract`/`HcsSpecContractSaveDTO`/`applySpec`/`POST /hcs/contract/spec-apply`)。**contractLegal/result 报文已按合规方最新字段表修正**:`{contractType, contractCode, requestId, success, reason, kingdeeId, sourceSystem=WCCK}`(金蝶同步结果结构,非原假设的 status=APPROVED/REJECTED),按 `contractCode`=合同编号定位,`success=true` 联动放行(kingdeeId 留痕)/`false` 联动驳回走重新提交闭环;付款 `paymentLegal/result` 仍为旧结构,待合规方字段表后同步修正。
> **2026-10-13 付款链路变更**:按最新需求,付款链路黑名单**不再直接阻断**,与高风险统一走特批:提交时收款方为**外部客商**(`CustomerArchive.customerKind≠internal`)才查 C101-1,高风险(`fdRiskLevel=2`)/黑名单(`fdBlackStatus=yes`)→ 提示且**必须上传「特批附件」**(付款附件结构 `attachmentType=special_approval`,缺失抛异常)+ 标记 `hcs_spec_status=need`(不再依赖 C106-1 额度超额这一唯一触发源);审批通过后扫描任务发起 C106-3,TMS 挂起 `currentNode="特批审核"`;合规审批完成后**回调 `paymentLegal/result`** 通知结果,`APPROVED` 恢复"审批通过"放行、`REJECTED` 联动付款审批驳回(returned)走重新提交闭环,页面人工确认等效兜底;特批审核中(need/pushing/processing)**禁止推送金蝶**(`doSyncKingdee` 门禁);前端付款表单同步增加收款方 C101-1 风险查询提示与特批附件提交校验。`risk.block-blacklist` 配置及 Nacos 项已删除。见 §三/§4.4。
---
@@ -19,27 +21,29 @@
| 触发线 | HCS 接口 | 判定条件 | 触发的特批 | 作用对象 |
| --- | --- | --- | --- | --- |
| 外部客商高风险/黑名单 | C101-1 风险查询 | `fdRiskLevel=2`(高风险)或 `fdBlackStatus=yes`(黑名单) | 合同特批(C103-1/C103-11 报文 `fdSepcailContract=true` 发起 / contractLegal/result 结果回调 + C103-4 定时查询兜底) | `ContractManage`(合同) |
| 合同额度超额 | C106-1 额度校验 | `fdIsCredit=false`(超额) | 结算付款特批(C106-3 发起 / C106-4 更新) | `PaymentApplication`(付款申请) |
| 收款方外部客商高风险/黑名单 | C101-1 风险查询 | 同上判定(仅收款方,`customerKind=internal` 内部客商不查) | 结算付款特批(提交校验特批附件 + 标记 need → C106-3 发起 / C106-4 更新 / paymentLegal/result 回调联动付款审批状态) | `PaymentApplication`(付款申请) |
| 合同额度超额 | C106-1 额度校验 | `fdIsCredit=false`(超额) | 结算付款特批(同上链路) | `PaymentApplication`(付款申请) |
补充规则:
- **内部组织客商不查**:`CustomerArchive.customerKind=internal` 默认无风险(`HcsSyncConstant.CUSTOMER_KIND_INTERNAL`)。
- **内部组织客商不查**:`CustomerArchive.customerKind=internal` 默认无风险(`HcsSyncConstant.CUSTOMER_KIND_INTERNAL`),合同链路(甲/乙方)与付款链路(收款方)口径一致。
- **甲/乙任一方命中即整单管控**(`HcsSyncConstant.java:151-176`)。
- **付款链路中黑名单不是特批而是直接阻断**:`risk.block-blacklist=true`(默认)时提交付款申请直接抛异常,不走特批。
- **付款链路高风险/黑名单不再阻断**(2026-10-13 起):提示且必须上传「特批附件」,标记 need 走 C106-3 特批;原 `risk.block-blacklist` 阻断配置已删除。
### 1.2 与普通审批的关系
特批与合同起草**同报文发起**(高风险/黑名单客商推送时 C103-1/C103-11 报文带 `fdSepcailContract=true`,合规侧随起草自动走特批流程;C103-8 独立发起接口合规未提供,相关代码已删除)。特批期间 TMS 用 `hcs_spec_status=processing` + `currentNode="特批审核"` 挂起展示,推送门禁与手动重推均拦截;特批通过即放行,驳回则联动本地审批驳回(仅合同链路)。
特批与合同起草**同报文发起**(高风险/黑名单客商推送时 C103-1/C103-11 报文带 `fdSepcailContract=true`,合规侧随起草自动走特批流程;C103-8 独立发起接口合规未提供,相关代码已删除)。特批期间 TMS 用 `hcs_spec_status=processing` + `currentNode="特批审核"` 挂起展示,推送门禁与手动重推均拦截;特批通过即放行,驳回则联动本地审批驳回(合同与付款链路均已联动)。付款链路另在特批审核中(need/pushing/processing)**禁止推送金蝶**,特批通过后放行。
### 1.3 特批状态机(两条链路共用 `HcsSyncConstant.SPEC_STATUS_*`)
```
none(默认) ──高风险/黑名单随 C103-1/C103-11 带 fdSepcailContract=true──▶ processing(挂起"特批审核")──结果回调/查询──▶ approved / rejected
none(默认) ──付款提交高风险/黑名单须特批附件 / 额度超额──▶ need ──审批通过后扫描 C106-3──▶ processing(挂起"特批审核")──paymentLegal/result 回调──▶ approved / rejected
need(存量遗留,2026-10-13 起不再产生;重推时自愈:报文带特批标记 → 挂起转 processing)
```
- 合同特批 `approved` **单次有效**:再次推送时若已 approved 不重复发起。
- 付款特批 `approved/rejected` **不联动付款审批状态**(无驳回联动),仅人工确认。
- 付款特批结果**联动付款审批状态**(2026-10-13 起):`approved` 恢复"审批通过"放行(金蝶推送门禁解除);`rejected` 联动付款审批驳回(`approvalStatus=returned` + `currentNode="已驳回"`),走"重新编辑→提交→审批→重新特批"闭环。
---
@@ -132,26 +136,33 @@ need(存量遗留,2026-10-13 起不再产生;重推时自愈:报文带
## 三、付款特批(付款申请提交触发侧)
### 3.1 入口与触发
### 3.1 入口与触发(2026-10-13 起两类触发源)
`PaymentApplicationController`(`/payment-application`)`POST /submit` → `PaymentApplicationServiceImpl#submit`(`PaymentApplicationServiceImpl.java:316-335`),事务内 :329 调 `HcsCreditCheckSupport#checkOnSubmit(entity)`(`HcsCreditCheckSupport.java:89-99`),依次执行两项校验:
`PaymentApplicationController`(`/payment-application`)`POST /submit` → `PaymentApplicationServiceImpl#submit`,事务内调 `HcsCreditCheckSupport#checkOnSubmit(entity)`(`HcsCreditCheckSupport.java:94-108`),依次执行两项校验:
**① C101-1 收款方风险校验**(开关 `risk.check-on-payment` 默认 true,:91)
**① C101-1 收款方风险校验**(开关 `risk.check-on-payment` 默认 true,:95-98)
- `checkRelativeRisk`(:104-131):按收款方 `payeeName` 反查 `CustomerArchive` 客商编号 → C101-1 实时查询;
- **黑名单且 `risk.block-blacklist=true`(默认 true)→ 直接阻断**:抛"收款方客商黑名单,禁止提交付款申请"(:127-129)——付款链路黑名单不走特批;
- 反查不到客商编号记 skip 留痕、不阻断(:106-110)。
- `checkRelativeRisk`(:110-148):按收款方 `payeeName` 反查 `CustomerArchive`:
- 反查不到客商档案 → skip 留痕、不阻断(:111-117);
- **内部组织客商(`customerKind=internal`)默认无风险,不调 C101-1**(:119-124);
- 外部客商按客商编号实时调 C101-1,高风险(`fdRiskLevel=2`)或黑名单(`fdBlackStatus=yes`)判定 risky(:136);
- **risky 时两步管控(不再直接阻断)**:
1. **特批附件必传**:`attachmentsJson` 中无 `attachmentType=special_approval`(前端付款表单「特批附件」类型,兼容 `fileType=特批附件` 旧结构)附件 → 抛"收款方[xxx]为高风险/黑名单客商,必须上传「特批附件」类型附件后再提交审批"(`hasSpecAttachment`,:295-313);
2. **标记待特批**:`hcs_spec_status=need` 并清空旧 `hcs_spec_fd_id`(特批单次有效,重新提交须重新发起 C106-3,:149-151)——审批通过后扫描任务发起 C106-3。
**② C106-1 合同额度校验**(开关 `credit.check-enabled` 默认 false,**一期未对接**,:95-97)
**② C106-1 合同额度校验**(开关 `credit.check-enabled` 默认 false,**一期未对接**,:100-103)
- `checkCredit`(:136-172):要求合同已推送(有 `hcs_fd_id`,:138-145)→ `hcsClient.contractCreditCheck`(:151);
- `fdIsCredit=true`(额度内)→ `hcs_credit_status=pass`(:160);
- **超额不拦截提交** → `hcs_credit_status=over` + `hcs_credit_msg` + **`hcs_spec_status=need`**(:167-169)——这是付款特批的唯一自动触发点。
- `checkCredit`:要求合同已推送(有 `hcs_fd_id`)→ `hcsClient.contractCreditCheck`;
- `fdIsCredit=true`(额度内)→ `hcs_credit_status=pass`;
- **超额不拦截提交** → `hcs_credit_status=over` + `hcs_credit_msg` + **`hcs_spec_status=need`**。
### 3.2 后续流转
### 3.2 后续流转(审批通过 → 推送特批 → 挂起 → 回调联动)
- 审批通过:`#approve`(:353-362)置 `hcs_sync_status=unsynced`(:359,零埋点 ADR-0001);
- **付款链没有即时推送**(`HcsImmediatePushSupport` 仅覆盖 project/contract),完全依赖定时扫描/手动端点(见 §四)。
- 审批通过:`#approve` 置 `hcs_sync_status=unsynced`(零埋点 ADR-0001);
- **付款链没有即时推送**(`HcsImmediatePushSupport` 仅覆盖 project/contract),完全依赖定时扫描/手动端点(见 §四);
- 扫描任务对 `hcs_spec_status=need` 的已审批单发起 C106-3 → 成功后 `processing` + **挂起 `currentNode="特批审核"`**(`HcsSpecPaySyncSupport#pushSpecPay`);
- **特批审核中(need/pushing/processing)禁止推送金蝶**:`PaymentApplicationServiceImpl#doSyncKingdee` 门禁抛"收款方客商为高风险/黑名单,付款特批审核中…特批完成后方可推送金蝶";
- 合规审批完成后**回调 `paymentLegal/result`**(见 §4.4):`APPROVED` → 恢复"审批通过"放行(金蝶门禁解除);`REJECTED` → 联动付款审批驳回(`approvalStatus=returned` + `currentNode="已驳回"`),走"重新编辑→提交→审批→重新特批"闭环(提交时重新校验风险与特批附件)。
### 3.3 TMS 侧留痕字段(`blade_payment_application`,`PaymentApplication.java:41`)
@@ -161,6 +172,7 @@ need(存量遗留,2026-10-13 起不再产生;重推时自愈:报文带
| `hcs_credit_status` | :135 | `pass`(额度内)/ `over`(超额) |
| `hcs_credit_msg` / `hcs_credit_time` | :137/:139 | 额度校验信息 |
| `hcs_spec_fd_id` / `hcs_spec_status` | :141/:142 | 特批流程 ID / `none/need/pushing/processing/approved/rejected` |
| `current_node` | :111 | 特批挂起展示:`特批审核`(C106-3 发起后)/ `审批通过`(特批通过恢复)/ `已驳回`(特批驳回联动) |
| `hcs_perf_fd_id` | :145 | 履约记录(C108,默认关,不在本文范围) |
### 3.4 与合同特批的关联
@@ -188,13 +200,13 @@ need(存量遗留,2026-10-13 起不再产生;重推时自愈:报文带
| 定时扫描 | `HcsPushTask#pushHcsPayment`(:163,cron 默认 `0 2/5 * * * ?`,开关 `push.payment.sync-enabled` 默认 false) | `pushUnsyncedPayments`(:285-309):`approvalStatus=approved` 且 `hcs_sync_status ∈ {unsynced, failed}` 且重试未超限 → `HcsPaymentSyncSupport#pushPayment` |
| 手动重推 | `POST /hcs/sync/payment/{id}`(`HcsSyncController.java:111`)→ `repushPayment`(:95-113);批量 `POST /hcs/sync/payment`(:129) | — |
`HcsPaymentSyncSupport#pushPayment`(`HcsPaymentSyncSupport.java:70-87`):
`HcsPaymentSyncSupport#pushPayment`(`HcsPaymentSyncSupport.java:75-91`):
1. **① 结算付款特批**:`hcs_spec_status=need` 且未发起时(:72-74)调 `HcsSpecPaySyncSupport#pushSpecPay`;
1. **① 结算付款特批**:`hcs_spec_status=need` 且未发起时(:77-80)调 `HcsSpecPaySyncSupport#pushSpecPay`(C106-3 成功后 `processing` + 挂起 `currentNode="特批审核"`);
2. ② 履约记录 C108(默认关);
3. 最后统一 `markSynced`(:142)/ `markFailed`(:154)回写整体同步状态。
3. 最后统一 `markSynced` / `markFailed` 回写整体同步状态。
> 当前 `credit.check-enabled=false`(C106-1 未对接),`need` 无自动触发源,"仅历史数据/人工场景可达"(`HcsPaymentSyncSupport.java:72` 注释)。
> `need` 触发源(2026-10-13 起):提交时收款方外部客商高风险/黑名单(须特批附件)或 C106-1 额度超额(`credit.check-enabled=false` 未对接时仅前者可达)。
### 4.3 C106-3/4 实现(`HcsSpecPaySyncSupport`)
@@ -225,11 +237,11 @@ need(存量遗留,2026-10-13 起不再产生;重推时自愈:报文带
### 4.4 结果确认
特批结果由合规系统审批完成后主动推送(2026-10-12 起,替代原计划的 B103,§9-U1 已闭环):
特批结果由合规系统审批完成后**回调 `paymentLegal/result` 通知 TMS**,并联动付款审批状态(2026-10-13 起):
- `POST /hcs/paymentLegal/result`(`HcsSyncController`,blade.secure 放行,网关注册 `/gwzh/HTHG/paymentLegal/result`)→ `HcsPaymentSyncSupport#handleLegalResult`:先校验 `sourceSystem`(不符抛"来源系统标识错误"异常),`requestId`=付款申请ID 定位,`status=APPROVED/REJECTED` 回写 `hcs_spec_status`,不联动付款审批状态;
- 等效兜底:页面人工确认 `POST /hcs/spec-pay/confirm`(仅 `processing` 可确认,置 `approved/rejected`);
- **付款特批结果不联动付款审批状态**(与合同特批的 `syncContractOnSpecResult` 不同,无驳回联动)。
- `POST /hcs/paymentLegal/result`(`HcsSyncController`,blade.secure 放行,网关注册 `/gwzh/HTHG/paymentLegal/result`)→ `HcsPaymentSyncSupport#handleLegalResult`(:156-189):先校验 `sourceSystem`(不符抛"来源系统标识错误"异常),`requestId`=付款申请ID 定位,`status=APPROVED` → 特批通过 + 恢复 `currentNode="审批通过"` 放行(金蝶推送门禁解除);`status=REJECTED` → 特批驳回 + **联动付款审批驳回**(`approvalStatus=returned` + `currentNode="已驳回"`,`syncPaymentOnSpecResult` :191-202),走"重新编辑→提交→审批→重新特批"闭环;
- 等效兜底:页面人工确认 `POST /hcs/spec-pay/confirm`(仅 `processing` 可确认,与回调同样联动付款审批状态);
- 与合同特批(contractLegal/result + C103-4 定时查询兜底)不同,付款特批**仅回调通知**(合规未提供 C106 结果查询接口),无定时查询兜底,回调未达时靠人工确认。
---
@@ -239,7 +251,7 @@ need(存量遗留,2026-10-13 起不再产生;重推时自愈:报文带
| --- | --- | --- |
| 定时扫描 | `HcsPushTask#pushHcsContract`(默认 `0 1/5 * * * ?`,同轮附带特批结果查询兜底 `syncProcessingSpecResults`) | `HcsPushTask#pushHcsPayment`(默认 `0 2/5 * * * ?`);Redis 任务锁 TTL 240s、分钟位错峰 |
| 即时推送 | `HcsImmediatePushSupport#pushContractAfterCommit`(afterCommit 异步,开关默认 false,单条锁 TTL 120s) | 无 |
| 结果推送接收 | `POST /hcs/contractLegal/result`(blade.secure 放行,sourceSystem=WCCK,contractCode=合同编号定位,success 联动审批状态,原文留痕) | `POST /hcs/paymentLegal/result`(blade.secure 放行,requestId=付款申请ID,回写特批状态;报文仍为旧结构待合规方字段表确认) |
| 结果推送接收 | `POST /hcs/contractLegal/result`(blade.secure 放行,sourceSystem=WCCK,contractCode=合同编号定位,success 联动审批状态,原文留痕) | `POST /hcs/paymentLegal/result`(blade.secure 放行,requestId=付款申请ID,回写特批状态并**联动付款审批状态**:approved 恢复放行 / rejected 联动审批驳回;报文仍为旧结构待合规方字段表确认) |
| 人工确认 | `POST /hcs/contract/spec/confirm` | `POST /hcs/spec-pay/confirm` |
| 手动发起/重推 | `POST /hcs/sync/contract/{id}`(spec-apply 已随 C103-8 删除,2026-10-13) | `POST /hcs/sync/payment/{id}`、`POST /hcs/sync/payment` |
@@ -251,9 +263,8 @@ need(存量遗留,2026-10-13 起不再产生;重推时自愈:报文带
| --- | --- | --- |
| `contract.require-risk-check` | true | 提交时特批附件强校验 |
| `risk.check-on-contract` | — | 合同链路 C101-1 查询开关 |
| `risk.check-on-payment` | true | 付款提交 C101-1 校验开关 |
| `risk.block-blacklist` | true | 付款链路黑名单阻断(合同链路已不用于阻断) |
| `credit.check-enabled` | **false** | C106-1 额度校验,一期未对接(付款特批自动触发源关闭) |
| `risk.check-on-payment` | true | 付款提交 C101-1 校验开关(收款方外部客商高风险/黑名单须特批附件并标记待特批) |
| `credit.check-enabled` | **false** | C106-1 额度校验,一期未对接(付款特批第二触发源关闭,高风险/黑名单触发不受影响) |
| `contract.special-contract-enabled` | false | C103-1/C103-11 报文 `fdSepcailContract` 强制 true(默认关;高风险/黑名单判定自动带 true,本开关可强制全量走特批,联调验证用) |
| `push.immediate-enabled` | false | 审批通过后即时推送 |
| `push.contract.sync-enabled` / `push.contract.cron` | — / `0 1/5 * * * ?` | 合同定时扫描 |
@@ -0,0 +1,200 @@
# 付款申请全链路业务流程(前端提交 → 客商风险与特批 → 同步金蝶 → 结果回写)
> 梳理日期:2026-10-13
> 涉及模块:`tms-erp-web/src/views/payment/payment-application-form.vue`(前端表单)、`blade-service/blade-transport`(业务编排)、`blade-third-party-api/blade-hcs-api`(合规 Feign)、`blade-third-party-api/blade-bfi-api`(金蝶 BFI Feign)
> 关联文档:`doc/docs/hcs合同/2026-10-12-合同特批与结算付款特批流程梳理.md`(特批机制专项)、`doc/docs/TMS业财融合-集成流程图文字版与业务流程梳理.md`(图03)、`业务流程/2026-09-16-付款管理正式付款同步金蝶….md` 与 `业务流程/2026-09-17-付款管理金蝶付款结果定时回写….md`(金蝶链路专项)
> 2026-10-13 变更:付款链路黑名单不再直接阻断,与高风险统一走特批(提示 + 特批附件必传 + C106-3 特批 + 回调联动审批状态 + 金蝶推送门禁);前端付款表单新增风险客商提示条与特批附件提交校验。
---
## 一、全链路总览
```mermaid
flowchart TD
subgraph FE["前端 · 付款申请表单"]
f1["新建付款申请<br/>选付款类型(正式结算付款/预付款/项目预付)"]
f1 --> f2["选合同/结算单 → 联动收款账户<br/>客商档案反查收款方"]
f2 --> f3{"C101-1 客商风险查询<br/>(收款方外部客商才查)"}
f3 -->|"高风险/黑名单"| f4["风险客商提示条(常驻)<br/>要求上传「特批附件」"]
f3 -->|"低风险/中风险/内部客商"| f5["正常填写"]
f4 --> f6
f5 --> f6["保存草稿 / 提交"]
end
subgraph TMS["TMS 后端 · 提交与审批"]
f6 --> t1{"提交校验<br/>(submit 事务内)"}
t1 -->|"特批附件缺失"| t2["拦截:必须上传「特批附件」"]
t1 -->|"通过"| t3["reviewing 财务审核<br/>(高风险/黑名单置 hcs_spec_status=need)"]
t3 --> t4{"本地审批"}
t4 -->|"驳回"| t5["returned 已驳回 → 重新编辑"]
t4 -->|"通过"| t6["approved 审批通过<br/>hcs_sync_status=unsynced"]
end
subgraph HCS["合同合规系统(HCS)"]
t6 -->|"定时扫描发起 C106-3"| h1["结算付款特批单生成"]
h1 --> h2["TMS 挂起 currentNode=特批审核<br/>(need/pushing/processing 禁止推送金蝶)"]
h2 --> h3{"集团特批审批"}
h3 -->|"paymentLegal/result:APPROVED"| h4["恢复 审批通过 → 放行金蝶推送"]
h3 -->|"paymentLegal/result:REJECTED"| h5["联动付款审批驳回 returned<br/>重新编辑 → 提交 → 重新特批"]
end
subgraph JD["金蝶星瀚(经 iPaaS /gwzh/BFI)"]
h4 -->|"手动/批量同步"| j1{"付款类型路由"}
j1 -->|"正式结算付款"| j2["A102-4 预检财务应付单已生成<br/>→ A103-2 幂等预检 → A103-1 应付下推"]
j1 -->|"预付款"| j3["A103-3 预付款申请保存"]
j2 --> j4["kingdeeStatus=synced(锁单)"]
j3 --> j4
j4 --> j5["财务共享审批 → 付款处理 → 付款完成"]
j5 -->|"A103-2/A103-4 定时回写"| j6["更新付款结果/履约数据"]
end
```
三条主线:**合规管控线**(C101-1 风险 → 特批附件 → C106-3 特批 → paymentLegal/result 联动)、**本地审批线**(draft → reviewing → approved/returned)、**金蝶付款线**(A103-1/A103-3 → 锁单 → A103-2/A103-4 回写)。硬约束:**高风险/黑名单收款方特批不通过,付款申请不得推送金蝶**。
---
## 二、阶段一:前端表单与提交(`payment-application-form.vue`)
| 步骤 | 动作 | 实现要点 |
|---|---|---|
| 1 | 新建/编辑付款申请 | 付款类型路由引用单据:正式结算付款→正式结算单(可多张)、预付款→预结算单、项目预付→项目+合同 |
| 2 | 选合同联动 | `fillBillTypeFromContract`:带出票据类型/付款方(合同甲方)/收款账户候选(合同乙方客商) |
| 3 | **收款方风险查询** | `loadReceiptAccountOptions`:按合同乙方名称反查客商档案 → `GET /hcs/risk?customerCode=`(C101-1 实时查,后端对内部组织客商默认返回无风险)→ `payeeRisky/payeeRiskLevel/payeeBlackStatus/payeeRiskTip` 留档 |
| 4 | **风险客商提示条** | 高风险/黑名单时基本信息"备注"下方常驻展示(橙黄底红字,同合同新增页):`风险等级:高风险|黑名单:是。【名称】:…风险项…,按集团规定不可直接付款,提交前须上传「特批附件」类型附件,审批通过后将自动发起付款特批`;附件清单同步显示"未上传:特批附件" |
| 5 | 附件材料 | 材料清单:磅单/结算单(必传)、合同签章文件(首次合同付款/项目预付)、**特批附件**(收款方高风险/黑名单或客商等级 D/E/F 时必传,`attachmentType=special_approval`)、其他 |
| 6 | 保存草稿 | `POST /payment-application`(save)→ draft,可反复编辑 |
| 7 | 提交 | `submitForm`:静默保存 → **特批附件缺失校验**(高风险/黑名单阻断,提示与后端同口径)→ `POST /payment-application/submit` → 发起 MK 审批流 |
> 风险查询失败不阻断前端填写,提交时后端强校验兜底。
---
## 三、阶段二:后端提交校验(`PaymentApplicationServiceImpl#submit`,事务内)
```
前置校验:状态(draft/returned)→ 申请金额>0 → 尾款/项目预付合同/选票/额度校验
↓
HcsCreditCheckSupport#checkOnSubmit(risk.check-on-payment 默认 true)
↓
① C101-1 收款方风险校验(checkRelativeRisk):
- 反查不到客商档案 → skip 留痕不阻断
- 内部组织客商(customerKind=internal)→ 默认无风险,不调 C101-1
- 外部客商 → C101-1 实时查询,risky = 风险等级2(高风险)或 fdBlackStatus=yes(黑名单)
- risky → 「特批附件必传」校验(attachmentsJson 含 attachmentType=special_approval,缺失抛异常)
→ hcs_spec_status=need + 清空旧 hcs_spec_fd_id(特批单次有效,重新提交重新发起)
② C106-1 额度校验(credit.check-enabled 默认 false,一期未对接):
- 合同已推送(有 hcs_fd_id)→ 额度内 pass / 超额不拦截 → hcs_credit_status=over + need
↓
全部通过:approvalStatus=reviewing,currentNode="财务审核",留痕 blade_hcs_check_log(biz_type=payment,check_type=risk/credit)
```
---
## 四、阶段三:本地审批
| 动作 | 状态流转 | 附加动作 |
|---|---|---|
| 审批通过 `approve` | reviewing → **approved**,currentNode="审批通过" | `hcs_sync_status=unsynced`(零埋点 ADR-0001,扫描任务据此接管后续推送) |
| 审批驳回 `returnBill` | reviewing → **returned**,currentNode="已驳回" | — |
| 作废 `voidBill` | approved → **voided**,currentNode="已作废" | 票据付款释放票据占用 |
---
## 五、阶段四:HCS 付款特批推送(`HcsPushTask#pushHcsPayment`)
- **定时扫描**:cron 默认 `0 2/5 * * * ?`,开关 `push.payment.sync-enabled` **默认 false**(灰度开启);条件 `approvalStatus=approved` 且 `hcs_sync_status ∈ {unsynced, failed}` 且重试 < `push.max-retry`(默认 5);Redis 任务锁 TTL 240s 防多实例重扫。
- **推送编排**(`HcsPaymentSyncSupport#pushPayment`):
1. **① 结算付款特批**(`hcs_spec_status=need` 时,`HcsSpecPaySyncSupport#pushSpecPay`):
- `hcs_spec_fd_id` 为空 → **C106-3 发起**:报文主题 `结算付款特批-{paymentNo}`,合同传 `blade_contract_manage.hcs_fd_id`,金额/收付款方/账户随单,附件经 C103-2 转传挂 `fd_spc_approval_attach`(发起带 `fdBindType=ADD`)→ 返回双层 `data.data.fdId` 回填 `hcs_spec_fd_id` → 状态 **processing** + **挂起 `currentNode="特批审核"`**;
- 非空 → **C106-4 更新**(单据变更场景,附件不带 fdBindType)。
2. ② C108 履约记录(默认关)。
3. `markSynced`/`markFailed` 回写整体同步状态。
- **手动重推**:`POST /hcs/sync/payment/{id}`(重置 unsynced 立即推)、`POST /hcs/sync/payment`(批量扫一轮)。
- **金蝶推送门禁**(`doSyncKingdee`):`hcs_spec_status ∈ {need, pushing, processing}` 抛"收款方客商为高风险/黑名单,付款特批审核中(特批审核),特批完成后方可推送金蝶"。
---
## 六、阶段五:特批结果联动(`HcsPaymentSyncSupport`)
| 路径 | 机制 | 结果联动 |
|---|---|---|
| **合规回调(主)** | 合规审批完成后回调 `POST /hcs/paymentLegal/result`(网关 `/gwzh/HTHG/paymentLegal/result`,blade.secure 放行)→ `handleLegalResult`:`sourceSystem` 须为 `third-party.hcs.src-system`(默认 WLTMS,不符抛"来源系统标识错误"),`requestId`=付款申请ID 定位,`status=APPROVED/REJECTED` | 见下 |
| **人工确认(兜底)** | `POST /hcs/spec-pay/confirm`(仅 processing 可确认,与回调等效) | 见下 |
| 特批结果 | `hcs_spec_status` | 付款审批联动 | 后续 |
|---|---|---|---|
| APPROVED | approved | currentNode="审批通过"(审批状态保持 approved) | 金蝶推送门禁解除,可同步金蝶 |
| REJECTED | rejected | **approvalStatus=returned + currentNode="已驳回"** | 走"重新编辑 → 提交(重新校验风险与特批附件、重新置 need)→ 审批 → 重新发起 C106-3"闭环 |
> 付款特批无 C106 结果查询接口,不做定时查询兜底(合同链路才有 C103-4 兜底);回调未达时靠页面人工确认。
---
## 七、阶段六:同步金蝶(`syncKingdee` → `doSyncKingdee`,单据级事务经自代理)
**前置**:`approvalStatus=approved` + 特批门禁通过 + `kingdeeStatus` 非终态(synced/paying/paid/closed 直接幂等返回)。
| 付款类型 | 链路 |
|---|---|
| **正式结算付款**(settlement_payment,应付类) | ① `requirePayableSettlement` 校验结算类型 → ② 逐张正式结算单 **A102-4** 预检财务应付单已在金蝶生成(缺则拦截"请先在正式付款单页面同步金蝶")→ ③ **A103-2** 幂等预检(上次下推成功但本地回滚的场景直接回填)→ ④ **A103-1 付款申请保存(应付下推)** |
| **预付款 / 项目预付** | 上次失败先 **A103-2** 勾稽恢复 → **A103-3 付款申请保存(预付款)**,不绑应付单 |
- **成功**:`kingdeeBillId` 回填 + `kingdeeStatus=synced`(业务单据锁定,ERP 不可改);付款申请单编号与付款状态由定时回写任务经 A103-2/A103-4 回填。
- **失败**:事务回滚后独立事务 `markKingdeeSyncFailed` → `kingdeeStatus=failed`,可重试同步。
- 批量:`syncKingdeeBatch`(全批共享档案缓存 + A102-4 批量预检,逐单单据级事务)。
---
## 八、阶段七:付款结果回写与履约联动
- **定时回写**(`PaymentKingdeeResultTask`,专项文档见 `业务流程/2026-09-17-….md`):**A103-2** 付款申请单查询 + **A103-4** 付款处理查询 → 回写 `kingdeeBillNo`/付款状态(paying/paid/closed)→ 付款完成联动正式结算付款记录(`blade_formal_settlement_payment`)与履约数据。
- **HCS 履约/附件**(默认关/独立周期):C108 履约记录推送(`perf-record.sync-enabled`)、C106-2 付款附件每日定时推送——不在本文展开,见特批专项文档 §四。
---
## 九、状态机速查
**付款审批状态 `approvalStatus`**:`draft` →(提交)`reviewing` →(通过)`approved` /(驳回)`returned` /(作废,仅 approved)`voided`;特批驳回联动 `approved → returned`。
**HCS 特批状态 `hcs_spec_status`**:`none` →(提交时高风险/黑名单或额度超额)`need` →(C106-3 发起成功)`processing`(挂起"特批审核")→(paymentLegal/result 回调/人工确认)`approved` / `rejected`。
**金蝶状态 `kingdeeStatus`**:空 →(同步成功)`synced` →(回写)`paying` → `paid` / `closed`;同步失败 `failed`(可重推)。
**当前节点 `currentNode`**:财务审核 → 审批通过 →(特批中)**特批审核** → 审批通过(放行)/ 已驳回(returned 闭环)。
---
## 十、配置项与接口速查
| 配置(`thirdParty.hcs.*`) | 默认 | 说明 |
|---|---|---|
| `risk.check-on-payment` | true | 提交时收款方 C101-1 校验开关 |
| `credit.check-enabled` | false | C106-1 额度校验(一期未对接,超额为特批第二触发源) |
| `push.payment.sync-enabled` / `push.payment.cron` | **false** / `0 2/5 * * * ?` | 付款特批+履约定时扫描(**未开启时高风险单审批通过后停在 need,且金蝶推送被门禁拦截**) |
| `push.max-retry` / `push.batch-size` | 5 / 50 | 重试上限 / 单轮批量 |
| `spec-pay.payment-purpose/nature/type`、`doc-status` | — | C106 报文字典值 |
| `src-system` | WLTMS | paymentLegal/result 回调来源校验值 |
| 接口 | 提供方 | 用途 |
|---|---|---|
| `GET /hcs/risk` | TMS 代理 C101-1 | 前端收款方风险查询(提示条) |
| C106-3 / C106-4 | 合同合规 | 结算付款特批发起 / 更新 |
| `POST /hcs/paymentLegal/result` | 合同合规 → TMS | 特批结果回调(APPROVED 放行 / REJECTED 联动审批驳回) |
| `POST /hcs/spec-pay/confirm` | TMS | 特批结果人工确认(兜底) |
| A102-4 / A103-2 | 金蝶 | 财务应付单预检 / 付款申请单幂等预检与结果回写 |
| A103-1 / A103-3 | 金蝶 | 应付下推付款申请 / 预付款申请保存 |
| B901 | ERP → 金蝶 | 锁单/解锁(已推金蝶单据 ERP 锁定) |
---
## 十一、异常与逆向
| 场景 | 处理 |
|---|---|
| C101-1 查询失败 | 提交抛"C101-1 客商风险查询失败"(不留风险缺口);前端查询失败仅跳过提示 |
| 收款方反查不到客商档案 | skip 留痕不阻断(§9-U6 口径) |
| 特批附件缺失 | 前端提交阻断 + 后端提交抛异常(双重校验) |
| C106-3 发起失败 | `hcs_sync_status=failed` + 重试计数,下轮扫描自动补偿,超限人工重推 |
| 特批驳回 | 付款审批联动驳回(returned),重新编辑提交后重新置 need、清空旧 `hcs_spec_fd_id`,重新发起特批 |
| 金蝶推送失败 | `kingdeeStatus=failed`,A103-2 幂等预检防重复推送;批量同步单条失败不影响整批 |
| 金蝶侧驳回/需修改 | 金蝶反审核+删除 → 解锁 → TMS 重推(已推单据 ERP 不可改,财务共享审批对 TMS 黑盒) |
+2 -2
View File
@@ -66,6 +66,8 @@ thirdParty:
cron: ${BFI_PAY_RESULT_CRON:0 */10 * * * ?}
# 合同合规系统
hcs:
# Mock 开关(true 时 IHcsClient 走本地 JSON 桩 F:/home/tms/mock/hcs/{方法名}.json,联调前自测用,生产必须 false;改配置需重启,改桩文件即时生效)
mock-enabled: ${HCS_MOCK_ENABLED:false}
# 合同合规系统(HCS)开放接口地址(v4 接口说明:带网关前缀,直连可配,代码不感知)
baseUrl: ${HCS_BASE_URL:http://172.16.203.134:8000/gwzh/HTHG}
# Authorization 请求头整值(含 Basic 前缀,敏感值通过环境变量注入)
@@ -128,8 +130,6 @@ thirdParty:
risk:
check-on-contract: ${HCS_RISK_CHECK_ON_CONTRACT:true}
check-on-payment: ${HCS_RISK_CHECK_ON_PAYMENT:true}
# 黑名单是否阻断(合同置 failed / 付款提交拦截)
block-blacklist: ${HCS_RISK_BLOCK_BLACKLIST:true}
# 额度校验(C106-1,一期暂不对接)
credit:
# 额度校验开关(默认关,关闭后特批无自动触发,对接后打开)