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 a99ec2cd..e056a78a 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 @@ -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) */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java index 32acee07..46956b8f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java @@ -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 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 { *

* 免登免授权同合同接口(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 paymentLegalResult(@RequestBody(required = false) HcsLegalResultDTO param) { return R.data(hcsPaymentSyncSupport.handleLegalResult(param)); } 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 c056cbce..1331708b 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 @@ -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) *

* 付款申请提交(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 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.lambdaQuery() + return customerArchiveMapper.selectOne(Wrappers.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,异常不影响主流程) */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java index 516e90cc..87e33e70 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java @@ -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) *

- * 编排顺序:特批(额度超额标记 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) */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java index 09e8d863..a9fdcfa9 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java @@ -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) *

- * 触发:付款申请审批通过且 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 判断 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 8230051c..301a44a5 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 @@ -216,10 +216,6 @@ public class HcsProperties { * 付款提交前风险校验开关 */ private boolean checkOnPayment = true; - /** - * 黑名单是否阻断(合同置 failed / 付款提交拦截) - */ - private boolean blockBlacklist = true; } @Data diff --git a/doc/docs/hcs合同/2026-10-12-合同特批与结算付款特批流程梳理.md b/doc/docs/hcs合同/2026-10-12-合同特批与结算付款特批流程梳理.md index 28c40d81..226669cb 100644 --- a/doc/docs/hcs合同/2026-10-12-合同特批与结算付款特批流程梳理.md +++ b/doc/docs/hcs合同/2026-10-12-合同特批与结算付款特批流程梳理.md @@ -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 * * * ?` | 合同定时扫描 | diff --git a/doc/docs/业务流程/2026-10-13-付款申请全链路业务流程(前端提交-客商风险特批-同步金蝶-结果回写).md b/doc/docs/业务流程/2026-10-13-付款申请全链路业务流程(前端提交-客商风险特批-同步金蝶-结果回写).md new file mode 100644 index 00000000..64022897 --- /dev/null +++ b/doc/docs/业务流程/2026-10-13-付款申请全链路业务流程(前端提交-客商风险特批-同步金蝶-结果回写).md @@ -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["新建付款申请
选付款类型(正式结算付款/预付款/项目预付)"] + f1 --> f2["选合同/结算单 → 联动收款账户
客商档案反查收款方"] + f2 --> f3{"C101-1 客商风险查询
(收款方外部客商才查)"} + f3 -->|"高风险/黑名单"| f4["风险客商提示条(常驻)
要求上传「特批附件」"] + f3 -->|"低风险/中风险/内部客商"| f5["正常填写"] + f4 --> f6 + f5 --> f6["保存草稿 / 提交"] + end + + subgraph TMS["TMS 后端 · 提交与审批"] + f6 --> t1{"提交校验
(submit 事务内)"} + t1 -->|"特批附件缺失"| t2["拦截:必须上传「特批附件」"] + t1 -->|"通过"| t3["reviewing 财务审核
(高风险/黑名单置 hcs_spec_status=need)"] + t3 --> t4{"本地审批"} + t4 -->|"驳回"| t5["returned 已驳回 → 重新编辑"] + t4 -->|"通过"| t6["approved 审批通过
hcs_sync_status=unsynced"] + end + + subgraph HCS["合同合规系统(HCS)"] + t6 -->|"定时扫描发起 C106-3"| h1["结算付款特批单生成"] + h1 --> h2["TMS 挂起 currentNode=特批审核
(need/pushing/processing 禁止推送金蝶)"] + h2 --> h3{"集团特批审批"} + h3 -->|"paymentLegal/result:APPROVED"| h4["恢复 审批通过 → 放行金蝶推送"] + h3 -->|"paymentLegal/result:REJECTED"| h5["联动付款审批驳回 returned
重新编辑 → 提交 → 重新特批"] + end + + subgraph JD["金蝶星瀚(经 iPaaS /gwzh/BFI)"] + h4 -->|"手动/批量同步"| j1{"付款类型路由"} + j1 -->|"正式结算付款"| j2["A102-4 预检财务应付单已生成
→ 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 黑盒) | diff --git a/doc/nacos/third-party-api.yaml b/doc/nacos/third-party-api.yaml index f907c119..fa11f060 100644 --- a/doc/nacos/third-party-api.yaml +++ b/doc/nacos/third-party-api.yaml @@ -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: # 额度校验开关(默认关,关闭后特批无自动触发,对接后打开)