feat: 付款审批通过累加履约计划款项金额并推送合同系统,金蝶付款申请携带履约计划id

This commit is contained in:
刘泉佳
2026-10-22 16:33:00 +08:00
parent 2afc18b8de
commit dcda231c51
9 changed files with 168 additions and 23 deletions
@@ -67,7 +67,7 @@ public class HcsContractExeStage extends TenantEntity {
@Schema(description = "款项性质 prepayment/progress/acceptance/retention")
private String fundNature;
@Schema(description = "计划金额(元)")
@Schema(description = "款项金额(元,付款审批通过按申请付款金额累加更新)")
private BigDecimal fundAmount;
@Schema(description = "计划资金日期")
@@ -254,6 +254,21 @@ public final class HcsSyncConstant {
*/
public static final String FUND_NATURE_PROGRESS = "progress";
/**
* 款项性质:预付款
*/
public static final String FUND_NATURE_PREPAYMENT = "prepayment";
/**
* 付款类型→款项性质映射:正式结算付款对应进度款计划行,预付款(进度预付/项目预付)对应预付款计划行
*
* @param paymentType 付款申请付款类型
* @return 款项性质
*/
public static String fundNatureOfPayment(String paymentType) {
return TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(paymentType) ? FUND_NATURE_PROGRESS : FUND_NATURE_PREPAYMENT;
}
// ==================== Redis 缓存 key(照 BFI blade:third: 前缀惯例) ====================
/**
@@ -28,6 +28,7 @@ package org.springblade.transport.service;
import org.springblade.core.mp.base.BaseService;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.HcsContractExeStage;
import org.springblade.transport.pojo.entity.PaymentApplication;
import java.util.List;
@@ -46,6 +47,24 @@ public interface IContractExeStageService extends BaseService<HcsContractExeStag
*/
List<HcsContractExeStage> listByContractId(Long contractId);
/**
* 匹配合同的付款履约计划行(已同步,优先按款项性质精确匹配,无则取首条付款行兜底)
*
* @param contractId 合同ID
* @param fundNature 款项性质(progress/prepayment 等,空则直接取首条付款行)
* @return 付款履约计划行,无已同步付款行时返回 null
*/
HcsContractExeStage findPaymentStageRow(Long contractId, String fundNature);
/**
* 付款审批通过累加履约计划行款项金额(款项金额=申请付款金额,多次付款累加更新):
* 按款项性质匹配付款计划行,原子累加 fund_amount;已同步行置 row_op=update 凭已存合规阶段ID 随 C103-9 重推,
* 未同步行保持 add 待合同链路首推(首推即携带累加后金额)
*
* @param payment 付款申请单(审批通过)
*/
void accumulatePaidAmountOnApprove(PaymentApplication payment);
/**
* 审批通过自动创建默认履约计划行(合同下无规则行时):承运商合同=付款进度款(收票规则取合同结算模式),
* 客户合同=收款进度款;金额/资金日期留空,随合同链路推 C103-9
@@ -38,6 +38,7 @@ import org.springblade.transport.mapper.ContractManageMapper;
import org.springblade.transport.mapper.HcsContractExeStageMapper;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.HcsContractExeStage;
import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springblade.transport.service.IContractExeStageService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -67,6 +68,62 @@ public class ContractExeStageServiceImpl extends BaseServiceImpl<HcsContractExeS
.orderByAsc(HcsContractExeStage::getId));
}
@Override
public HcsContractExeStage findPaymentStageRow(Long contractId, String fundNature) {
if (contractId == null) {
return null;
}
if (StringUtil.isNotBlank(fundNature)) {
HcsContractExeStage natureRow = getOne(Wrappers.<HcsContractExeStage>lambdaQuery()
.eq(HcsContractExeStage::getContractId, contractId)
.eq(HcsContractExeStage::getPerformType, HcsSyncConstant.PERFORM_TYPE_PAYMENT)
.eq(HcsContractExeStage::getFundNature, fundNature)
.isNotNull(HcsContractExeStage::getHcsStageFdId)
.orderByAsc(HcsContractExeStage::getId)
.last("limit 1"), false);
if (natureRow != null) {
return natureRow;
}
}
// 兜底:无该款项性质已同步行时取首条已同步付款行(与 C108 履约记录历史口径一致)
return getOne(Wrappers.<HcsContractExeStage>lambdaQuery()
.eq(HcsContractExeStage::getContractId, contractId)
.eq(HcsContractExeStage::getPerformType, HcsSyncConstant.PERFORM_TYPE_PAYMENT)
.isNotNull(HcsContractExeStage::getHcsStageFdId)
.orderByAsc(HcsContractExeStage::getId)
.last("limit 1"), false);
}
@Override
public void accumulatePaidAmountOnApprove(PaymentApplication payment) {
if (payment == null || payment.getContractId() == null || payment.getAppliedAmount() == null) {
return;
}
String fundNature = HcsSyncConstant.fundNatureOfPayment(payment.getPaymentType());
HcsContractExeStage stageRow = this.findPaymentStageRow(payment.getContractId(), fundNature);
// 仅精确匹配款项性质的计划行累加,兜底行款项性质不一致时不累加(避免预付款金额计入进度款计划)
if (stageRow == null || !fundNature.equals(stageRow.getFundNature())) {
log.info("付款审批未匹配到款项性质[{}]的履约计划行,跳过款项金额累加:paymentNo={},contractId={}",
fundNature, payment.getPaymentNo(), payment.getContractId());
return;
}
// 原子累加防并发付款审批丢失更新(coalesce 兼容多数据库)
update(Wrappers.<HcsContractExeStage>lambdaUpdate()
.eq(HcsContractExeStage::getId, stageRow.getId())
.setSql("fund_amount = coalesce(fund_amount, 0) + " + payment.getAppliedAmount().toPlainString()));
boolean resyncNeeded = StringUtil.isNotBlank(stageRow.getHcsStageFdId());
if (resyncNeeded) {
// 已同步行置 update 凭已存合规阶段ID 随 C103-9 重推;未同步行保持 add 待合同链路首推
update(Wrappers.<HcsContractExeStage>lambdaUpdate()
.eq(HcsContractExeStage::getId, stageRow.getId())
.set(HcsContractExeStage::getRowOp, HcsSyncConstant.ROW_OP_UPDATE)
.set(HcsContractExeStage::getHcsSyncStatus, HcsSyncConstant.HCS_STATUS_UNSYNCED)
.set(HcsContractExeStage::getHcsSyncMsg, "付款审批款项金额累加,待重推C103-9"));
}
log.info("付款审批履约计划款项金额累加:paymentNo={},stageRowId={},累加金额={},rowOp={}",
payment.getPaymentNo(), stageRow.getId(), payment.getAppliedAmount(), resyncNeeded ? "update" : "add(待合同链路首推)");
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean submitRows(Long contractId, List<HcsContractExeStage> rows) {
@@ -86,6 +86,7 @@ import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.FormalSettlementDetail;
import org.springblade.transport.pojo.entity.FormalSettlementPayment;
import org.springblade.transport.pojo.entity.FormalSettlementSource;
import org.springblade.transport.pojo.entity.HcsContractExeStage;
import org.springblade.transport.pojo.entity.KingdeeBillLock;
import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springblade.transport.pojo.entity.PaymentApplicationInvoice;
@@ -98,6 +99,7 @@ import org.springblade.transport.pojo.entity.TemporaryCreditLimit;
import org.springblade.transport.pojo.vo.PaymentApplicationReferenceAmountVO;
import org.springblade.transport.pojo.vo.PaymentApplicationVO;
import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IContractExeStageService;
import org.springblade.transport.service.IKingdeeBillLockService;
import org.springblade.transport.service.IPaymentApplicationService;
import org.springblade.transport.service.ISettlementProjectAdvanceWriteOffService;
@@ -177,6 +179,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
private final BladeRedis bladeRedis;
private final ApplicationEventPublisher eventPublisher;
private final HcsCreditCheckSupport hcsCreditCheckSupport;
private final IContractExeStageService contractExeStageService;
@Override
public IPage<PaymentApplicationVO> selectPage(IPage<PaymentApplication> page, PaymentApplicationVO query) {
@@ -524,6 +527,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
// HCS 推送零埋点:审批通过仅置待同步状态,扫描任务据此推特批(如需)与履约记录(ADR-0001)
entity.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
updateById(entity);
// 履约计划款项金额累加(款项金额=申请付款金额,多次付款累加),已同步计划行置 update 待随付款链重推 C103-9
this.contractExeStageService.accumulatePaidAmountOnApprove(entity);
refreshFormalSettlement(entity);
// 审批事务提交后自动推送金蝶(AFTER_COMMIT 监听器消费,推送失败不影响审批结果)
eventPublisher.publishEvent(new KingdeeBillApprovedEvent(KingdeeBillApprovedEvent.BIZ_PAYMENT_APPLICATION, entity.getId()));
@@ -1305,14 +1310,38 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
entry.setProjectNumber(KingdeeBfiConstant.BFI_AP_BILL_PROJECT_NUMBER_TEST);
// 【必填】对方系统唯一行id(不能重复):付款申请号-1
entry.setAwrpEntryCode(bfiEntryCode(entity.getPaymentNo(), 1));
// 备注(非必传)
entry.setERemark(entity.getRemark());
// 备注(非必传):携带关联履约计划id(进度款计划行合规阶段ID),用于合同系统关联履约付款数据与履约计划
entry.setERemark(this.bfiExeStageRemark(entity));
param.setEntry(List.of(entry));
BfiBillSaveResultVO saveResult = bfiService.savePrePayApply(param);
return requireBfiBillIdAndFillNo(entity, saveResult, "推送金蝶付款申请");
}
/**
* 金蝶付款申请备注:携带关联履约计划id(进度款计划行合规阶段ID hcs_stage_fd_id),
* 用于合同系统关联履约付款数据与履约计划;无关联计划行时不携带(与 C108 履约记录"无关联则不传"口径一致),
* 计划行查询异常不阻断金蝶推送
*
* @param entity 付款申请实体
* @return 备注文本(履约计划id + 原备注)
*/
private String bfiExeStageRemark(PaymentApplication entity) {
String stageFdId = null;
try {
HcsContractExeStage stageRow = this.contractExeStageService.findPaymentStageRow(entity.getContractId(),
HcsSyncConstant.fundNatureOfPayment(entity.getPaymentType()));
stageFdId = stageRow == null ? null : stageRow.getHcsStageFdId();
} catch (Exception exception) {
log.warn("付款申请[{}]查询关联履约计划行异常,金蝶备注不携带履约计划id", entity.getPaymentNo(), exception);
}
String planPart = Func.isBlank(stageFdId) ? "" : "履约计划id:" + stageFdId;
if (Func.isBlank(entity.getRemark())) {
return planPart;
}
return planPart.isEmpty() ? entity.getRemark() : planPart + ";" + entity.getRemark();
}
/**
* 校验正式结算付款对应的结算单结算类型:应付(财务应付单 A102-2)直接放行,
* 应收(对应财务应收单 A302-2)推送规则待业务确认,暂拦截报错。
@@ -1389,8 +1418,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
entry.setProjectNumber(KingdeeBfiConstant.BFI_AP_BILL_PROJECT_NUMBER_TEST);
// 期望付款日(非必传,不传时根据应付单自动带出)
// entry.setEExpaydate(applyDateText);
// 备注(非必传)
// entry.setERemark(entity.getRemark());
// 备注(非必传):携带关联履约计划id(进度款计划行合规阶段ID),用于合同系统关联履约付款数据与履约计划
entry.setERemark(this.bfiExeStageRemark(entity));
// 【必填】外部单据类型 todo
entry.setAwrpSrcType(bfiProperties.getPayApply().getSrcType());
param.setEntry(List.of(entry));
@@ -133,7 +133,7 @@ public class HcsExeStageSyncSupport {
stage.setFdPerformType(row.getPerformType());
// 【必填】款项性质:prepayment/progress/acceptance/retention
stage.setFdFundNature(row.getFundNature());
// 【必填】计划金额(元)
// 【必填】款项金额(元,付款审批通过按申请付款金额累加更新)
stage.setFdFundAmount(row.getFundAmount());
// 【必填】资金日期(毫秒时间戳)
stage.setFdFundDate(toMillis(row.getFundDate()));
@@ -35,7 +35,9 @@ import org.springblade.hcs.dto.HcsLegalResultDTO;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.constant.TransportBusiConstant;
import org.springblade.transport.event.KingdeeBillApprovedEvent;
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.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
@@ -45,9 +47,9 @@ import java.util.Arrays;
import java.util.List;
/**
* HCS 付款链推送编排支撑组件(结算付款特批 C106-3/4 + 履约记录 C108-1/2 + 特批结果推送接收,设计文档 §5.0/§5.5/§5.6)
* HCS 付款链推送编排支撑组件(结算付款特批 C106-3/4 + 履约计划重推 C103-9 + 履约记录 C108-1/2 + 特批结果推送接收,设计文档 §5.0/§5.3/§5.5/§5.6)
* <p>
* 编排顺序:特批(提交时高风险/黑名单须特批附件或额度超额标记 need 时发起)→ 履约记录(审批通过新建 / 实际付款推进更新)。
* 编排顺序:特批(提交时高风险/黑名单须特批附件或额度超额标记 need 时发起)→ 履约计划重推(付款审批款项金额累加行)→ 履约记录(审批通过新建 / 实际付款推进更新)。
* 特批流程ID 与履约记录ID 在各自 Support 内即时持久化;整体同步状态(hcs_sync_status)由本组件统一回写。
* 特批发起后 TMS 挂起 currentNode="特批审核",合规 paymentLegal/result 回调联动付款审批状态
* (通过恢复"审批通过"放行 / 驳回联动审批驳回走重新提交闭环),人工确认等效兜底。
@@ -65,13 +67,15 @@ public class HcsPaymentSyncSupport {
private static final List<String> APPROVED_STATUSES = List.of("approved");
private final PaymentApplicationMapper paymentApplicationMapper;
private final ContractManageMapper contractManageMapper;
private final HcsProperties hcsProperties;
private final HcsSpecPaySyncSupport hcsSpecPaySyncSupport;
private final HcsExeStageSyncSupport hcsExeStageSyncSupport;
private final HcsPerfRecordSyncSupport hcsPerfRecordSyncSupport;
private final ApplicationEventPublisher eventPublisher;
/**
* 推送付款申请到 HCS:特批(如需)→ 履约记录(新建/更新)→ 回填整体同步状态
* 推送付款申请到 HCS:特批(如需)→ 履约计划重推(C103-9 款项金额累加行)→ 履约记录(新建/更新)→ 回填整体同步状态
*
* @param payment 付款申请单(approvalStatus=approved)
* @return 推送结果描述
@@ -82,7 +86,16 @@ public class HcsPaymentSyncSupport {
if (HcsSyncConstant.SPEC_STATUS_NEED.equals(payment.getHcsSpecStatus())) {
result.append(hcsSpecPaySyncSupport.pushSpecPay(payment));
}
// ② 履约记录:审批通过新建 C108-1 / 实际付款推进更新 C108-2(一期暂不对接默认关)
// ② 履约计划重推(C103-9 行级更新):付款审批通过累加款项金额置 row_op=update 的计划行,
// 凭已存合规阶段ID 重推;独立于履约记录开关,推送失败抛出由付款链置 failed 重试兜底
String stageResult = this.pushExeStageUpdate(payment);
if (StringUtil.isNotBlank(stageResult)) {
if (result.length() > 0) {
result.append(";");
}
result.append(stageResult);
}
// ③ 履约记录:审批通过新建 C108-1 / 实际付款推进更新 C108-2(一期暂不对接默认关)
if (!hcsProperties.getPerfRecord().isSyncEnabled()) {
markSynced(payment, result.toString());
return result.toString();
@@ -95,6 +108,25 @@ public class HcsPaymentSyncSupport {
return result.toString();
}
/**
* 履约计划重推(C103-9):聚合合同下 row_op 非 none 的计划行(含付款审批累加款项金额的 update 行)凭已存 fdId 重推;
* 合同尚未推送合规时计划行随合同链路首推(首推即携带累加后金额),此处跳过
*
* @param payment 付款申请单
* @return 推送结果描述,无待同步行返回空串
*/
private String pushExeStageUpdate(PaymentApplication payment) {
if (payment.getContractId() == null) {
return "";
}
ContractManage contractManage = contractManageMapper.selectById(payment.getContractId());
if (contractManage == null || StringUtil.isBlank(contractManage.getHcsFdId())) {
return "";
}
String stageResult = hcsExeStageSyncSupport.syncExeStages(contractManage);
return "履约规则无待同步行".equals(stageResult) ? "" : stageResult;
}
/**
* 单条手动重推:重置为待同步并清零重试计数后立即推送(页面"推送合规/重推"按钮入口)
*
@@ -25,7 +25,6 @@
*/
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.tool.utils.StringUtil;
@@ -42,11 +41,11 @@ 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.HcsContractExeStageMapper;
import org.springblade.transport.mapper.PaymentApplicationMapper;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.HcsContractExeStage;
import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springblade.transport.service.IContractExeStageService;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
@@ -76,7 +75,7 @@ public class HcsPerfRecordSyncSupport {
private final IHcsClient hcsClient;
private final HcsProperties hcsProperties;
private final ContractManageMapper contractManageMapper;
private final HcsContractExeStageMapper hcsContractExeStageMapper;
private final IContractExeStageService contractExeStageService;
private final PaymentApplicationMapper paymentApplicationMapper;
private final HcsAttachmentSupport hcsAttachmentSupport;
@@ -203,17 +202,11 @@ public class HcsPerfRecordSyncSupport {
}
/**
* 匹配合同的付款履约规则行(perform_type=payment 且已同步,取第一条)
* 匹配合同的付款履约计划行(优先按付款类型映射的款项性质精确匹配,无则取首条已同步付款行兜底)
*/
private HcsContractExeStage findPaymentStageRow(PaymentApplication payment) {
if (payment.getContractId() == null) {
return null;
}
return hcsContractExeStageMapper.selectOne(Wrappers.<HcsContractExeStage>lambdaQuery()
.eq(HcsContractExeStage::getContractId, payment.getContractId())
.eq(HcsContractExeStage::getPerformType, "payment")
.isNotNull(HcsContractExeStage::getHcsStageFdId)
.last("limit 1"));
return contractExeStageService.findPaymentStageRow(payment.getContractId(),
HcsSyncConstant.fundNatureOfPayment(payment.getPaymentType()));
}
/**
@@ -71,7 +71,7 @@ public class HcsExeplanStageDTO implements Serializable {
*/
private Integer fdInvoiceCycle;
/**
* 计划金额(元)
* 款项金额(元,付款审批通过按申请付款金额累加更新)
*/
private BigDecimal fdFundAmount;
/**