Merge branch 'feature/payment-apply-bfi-autoSync' into feature/payment-apply-bfi-sync
# Conflicts: # blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java # blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
This commit is contained in:
+14
-1
@@ -22,6 +22,7 @@ import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
import org.springblade.transport.listener.KingdeeApprovalSyncListener;
|
||||
import org.springblade.transport.pojo.dto.FormalSettlementSaveRequest;
|
||||
import org.springblade.transport.pojo.dto.FormalSettlementPaymentRequest;
|
||||
import org.springblade.transport.pojo.dto.FormalSettlementBatchPaymentRequest;
|
||||
@@ -196,7 +197,19 @@ public class FormalSettlementController extends BladeController {
|
||||
@PostMapping("/approve")
|
||||
@ApiOperationSupport(order = 11)
|
||||
@Operation(summary = "审批通过")
|
||||
public R approve(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.approve(request); return R.success("审批通过"); }
|
||||
public R approve(@RequestBody FormalSettlementStatusRequest request) {
|
||||
formalSettlementService.approve(request);
|
||||
return approveResponse();
|
||||
}
|
||||
|
||||
/** 审批接口统一响应:审批已生效,自动推送失败原因以警告文案返回(data=false 供前端区分提示样式) */
|
||||
private R approveResponse() {
|
||||
String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure();
|
||||
if (syncFailure != null) {
|
||||
return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure);
|
||||
}
|
||||
return R.success("审批通过");
|
||||
}
|
||||
|
||||
@PostMapping("/return")
|
||||
@ApiOperationSupport(order = 12)
|
||||
|
||||
+10
@@ -35,6 +35,7 @@ import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.listener.KingdeeApprovalSyncListener;
|
||||
import org.springblade.transport.pojo.dto.InvoiceApplicationSaveRequest;
|
||||
import org.springblade.transport.pojo.dto.InvoiceApplicationStatusRequest;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementDetail;
|
||||
@@ -131,6 +132,15 @@ public class InvoiceApplicationController extends BladeController {
|
||||
@Operation(summary = "审批通过开票申请")
|
||||
public R approve(@RequestBody InvoiceApplicationStatusRequest request) {
|
||||
invoiceApplicationService.approve(request);
|
||||
return approveResponse();
|
||||
}
|
||||
|
||||
/** 审批接口统一响应:审批已生效,自动推送失败原因以警告文案返回(data=false 供前端区分提示样式) */
|
||||
private R approveResponse() {
|
||||
String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure();
|
||||
if (syncFailure != null) {
|
||||
return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure);
|
||||
}
|
||||
return R.success("审批通过");
|
||||
}
|
||||
|
||||
|
||||
+10
@@ -36,6 +36,7 @@ import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.listener.KingdeeApprovalSyncListener;
|
||||
import org.springblade.transport.pojo.dto.InvoiceReceiptSaveRequest;
|
||||
import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest;
|
||||
import org.springblade.transport.pojo.dto.KingdeeInvoicePoolQueryDTO;
|
||||
@@ -146,6 +147,15 @@ public class InvoiceReceiptController extends BladeController {
|
||||
@Operation(summary = "审批通过收票登记")
|
||||
public R approve(@RequestBody InvoiceReceiptStatusRequest request) {
|
||||
invoiceReceiptService.approve(request);
|
||||
return approveResponse();
|
||||
}
|
||||
|
||||
/** 审批接口统一响应:审批已生效,自动推送失败原因以警告文案返回(data=false 供前端区分提示样式) */
|
||||
private R approveResponse() {
|
||||
String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure();
|
||||
if (syncFailure != null) {
|
||||
return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure);
|
||||
}
|
||||
return R.success("审批通过");
|
||||
}
|
||||
|
||||
|
||||
+14
-1
@@ -29,6 +29,7 @@ import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.listener.KingdeeApprovalSyncListener;
|
||||
import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest;
|
||||
import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest;
|
||||
import org.springblade.transport.pojo.vo.PaymentApplicationReferenceAmountVO;
|
||||
@@ -81,7 +82,19 @@ public class PaymentApplicationController extends BladeController {
|
||||
@PostMapping("/approve")
|
||||
@ApiOperationSupport(order = 7)
|
||||
@Operation(summary = "审批通过付款申请")
|
||||
public R approve(@RequestBody PaymentApplicationStatusRequest request) { paymentApplicationService.approve(request); return R.success("审批通过"); }
|
||||
public R approve(@RequestBody PaymentApplicationStatusRequest request) {
|
||||
paymentApplicationService.approve(request);
|
||||
return approveResponse();
|
||||
}
|
||||
|
||||
/** 审批接口统一响应:审批已生效,自动推送失败原因以警告文案返回(data=false 供前端区分提示样式) */
|
||||
private R approveResponse() {
|
||||
String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure();
|
||||
if (syncFailure != null) {
|
||||
return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure);
|
||||
}
|
||||
return R.success("审批通过");
|
||||
}
|
||||
@PostMapping("/return")
|
||||
@ApiOperationSupport(order = 8)
|
||||
@Operation(summary = "驳回付款申请")
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
|
||||
* Copyright of this software remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.event;
|
||||
|
||||
/**
|
||||
* 单据审批通过事件:审批事务提交后由 {@code KingdeeApprovalSyncListener} 消费,
|
||||
* 自动推送对应单据至金蝶(星瀚),省掉页面手动点「同步金蝶」。
|
||||
* <p>
|
||||
* bizType 复用 MkProcessBizHandlers 既有取值;只在审批状态真正变为 approved 的那次发布,
|
||||
* 重复回调被 approve 状态校验拒绝,天然不重复推送。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public record KingdeeBillApprovedEvent(String bizType, Long id) {
|
||||
|
||||
/** 正式结算单(应付 A102-2 / 应收 A302-2) */
|
||||
public static final String BIZ_FORMAL_SETTLEMENT = "formal-settlement";
|
||||
|
||||
/** 付款申请(A103-1 正式 / A103-3 预付) */
|
||||
public static final String BIZ_PAYMENT_APPLICATION = "payment-application";
|
||||
|
||||
/** 开票申请(A303-1) */
|
||||
public static final String BIZ_INVOICE_APPLICATION = "invoice-application";
|
||||
|
||||
/** 收票登记(A303-2 + A302-6) */
|
||||
public static final String BIZ_INVOICE_RECEIPT = "invoice-receipt";
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
|
||||
* Copyright of this software remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.listener;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.transport.event.KingdeeBillApprovedEvent;
|
||||
import org.springblade.transport.service.IFormalSettlementService;
|
||||
import org.springblade.transport.service.IInvoiceApplicationService;
|
||||
import org.springblade.transport.service.IInvoiceReceiptService;
|
||||
import org.springblade.transport.service.IPaymentApplicationService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.event.TransactionPhase;
|
||||
import org.springframework.transaction.event.TransactionalEventListener;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 单据审批通过自动同步金蝶监听器:审批事务提交(AFTER_COMMIT)后复用手动按钮同一 Service 方法推送,
|
||||
* 与客商档案审批后自动推送同线程模式。
|
||||
* <p>
|
||||
* 不变式:审批结果不受推送影响——监听器整体 try/catch 吞异常,推送异常只落 failed + 日志;
|
||||
* 失败单据由 {@code KingdeeAutoSyncRetryTask} 定时重推、手动按钮兜底。
|
||||
* <p>
|
||||
* 推送失败原因通过 {@link #pollSyncFailure()} 传递给审批接口响应:AFTER_COMMIT 监听与 approve
|
||||
* 同线程同请求,Controller 在 approve 返回后取走原因拼进提示文案;OA 回调路径无人取走,
|
||||
* 由监听入口清空兜底,不会串单。
|
||||
* <p>
|
||||
* ponytail: 同步推送占用审批请求线程,若 BFI 网关大面积变慢导致审批接口可感知变慢,
|
||||
* 再把路由调用提交到守护线程池(MkProcessMessageService 已有同款裸 ScheduledExecutorService 模式)。
|
||||
* <p>
|
||||
* 域开关(Nacos thirdParty.bfi.autoSync.*)默认全部关闭,与既有定时任务开关风格一致;
|
||||
* 关闭某域 = 该域回到现状(审批不自动推送、手动按钮照用)。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class KingdeeApprovalSyncListener {
|
||||
|
||||
private final IFormalSettlementService formalSettlementService;
|
||||
private final IPaymentApplicationService paymentApplicationService;
|
||||
private final IInvoiceApplicationService invoiceApplicationService;
|
||||
private final IInvoiceReceiptService invoiceReceiptService;
|
||||
|
||||
/** 结算单审批后自动推 A102-2/A302-2 开关,默认关闭 */
|
||||
@Value("${thirdParty.bfi.autoSync.settlement:false}")
|
||||
private boolean settlementEnabled;
|
||||
|
||||
/** 付款申请审批后自动推 A103-1/A103-3 开关,默认关闭 */
|
||||
@Value("${thirdParty.bfi.autoSync.payment:false}")
|
||||
private boolean paymentEnabled;
|
||||
|
||||
/** 开票申请审批后自动推 A303-1 开关,默认关闭 */
|
||||
@Value("${thirdParty.bfi.autoSync.invoiceApply:false}")
|
||||
private boolean invoiceApplyEnabled;
|
||||
|
||||
/** 收票登记审批后自动推 A303-2+A302-6 开关,默认关闭 */
|
||||
@Value("${thirdParty.bfi.autoSync.receipt:false}")
|
||||
private boolean receiptEnabled;
|
||||
|
||||
/** 当前审批请求线程的自动推送失败原因,供 Controller 取走提示前端(见类注释线程说明) */
|
||||
private static final ThreadLocal<String> SYNC_FAILURE = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 取走并清除当前审批请求的自动推送失败原因;null 表示推送成功或未触发推送。
|
||||
* 须在 service.approve 返回后立即调用(监听器在审批事务提交时同步执行完毕)。
|
||||
*/
|
||||
public static String pollSyncFailure() {
|
||||
String failure = SYNC_FAILURE.get();
|
||||
SYNC_FAILURE.remove();
|
||||
return failure;
|
||||
}
|
||||
|
||||
/**
|
||||
* 事务提交后同步推送。fallbackExecution=true 兜底:approve 未开启事务时事件不再被默认丢弃。
|
||||
*
|
||||
* @param event 审批通过事件(bizType + 单据id)
|
||||
*/
|
||||
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
|
||||
public void onBillApproved(KingdeeBillApprovedEvent event) {
|
||||
SYNC_FAILURE.remove();
|
||||
try {
|
||||
switch (event.bizType()) {
|
||||
case KingdeeBillApprovedEvent.BIZ_FORMAL_SETTLEMENT -> pushSettlement(event.id());
|
||||
case KingdeeBillApprovedEvent.BIZ_PAYMENT_APPLICATION -> pushPayment(event.id());
|
||||
case KingdeeBillApprovedEvent.BIZ_INVOICE_APPLICATION -> pushInvoiceApply(event.id());
|
||||
case KingdeeBillApprovedEvent.BIZ_INVOICE_RECEIPT -> pushReceipt(event.id());
|
||||
default -> log.warn("审批通过自动同步金蝶跳过:未知业务类型 bizType={} id={}", event.bizType(), event.id());
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
// 推送失败只记日志(失败状态已在推送方法内独立事务落库),不影响审批结果,交给重推任务与手动按钮兜底;
|
||||
// 原因暂存线程变量,由审批 Controller 取走提示前端
|
||||
log.error("审批通过自动同步金蝶失败 bizType={} id={}", event.bizType(), event.id(), exception);
|
||||
String reason = exception.getMessage() == null ? "金蝶接口调用异常" : exception.getMessage();
|
||||
// 手动按钮同文案已带"推送金蝶失败:"前缀,拼提示时去掉避免重复
|
||||
SYNC_FAILURE.set(reason.startsWith("推送金蝶失败:") ? reason.substring("推送金蝶失败:".length()) : reason);
|
||||
}
|
||||
}
|
||||
|
||||
private void pushSettlement(Long id) {
|
||||
if (!settlementEnabled) {
|
||||
return;
|
||||
}
|
||||
log.info("正式结算单[{}]审批通过自动同步金蝶开始", id);
|
||||
String billNo = formalSettlementService.syncKingdee(id);
|
||||
log.info("正式结算单[{}]审批通过自动同步金蝶完成:billNo={}", id, billNo);
|
||||
}
|
||||
|
||||
private void pushPayment(Long id) {
|
||||
if (!paymentEnabled) {
|
||||
return;
|
||||
}
|
||||
log.info("付款申请[{}]审批通过自动同步金蝶开始", id);
|
||||
String kingdeeBillId = paymentApplicationService.syncKingdee(id);
|
||||
log.info("付款申请[{}]审批通过自动同步金蝶完成:kingdeeBillId={}", id, kingdeeBillId);
|
||||
}
|
||||
|
||||
private void pushInvoiceApply(Long id) {
|
||||
if (!invoiceApplyEnabled) {
|
||||
return;
|
||||
}
|
||||
log.info("开票申请[{}]审批通过自动同步金蝶开始", id);
|
||||
String billNo = invoiceApplicationService.syncKingdee(id);
|
||||
log.info("开票申请[{}]审批通过自动同步金蝶完成:billNo={}", id, billNo);
|
||||
}
|
||||
|
||||
private void pushReceipt(Long id) {
|
||||
if (!receiptEnabled) {
|
||||
return;
|
||||
}
|
||||
log.info("收票登记[{}]审批通过自动推送金蝶收票单开始", id);
|
||||
String summary = invoiceReceiptService.pushKingdeeReceivedInvoice(List.of(id), true);
|
||||
log.info("收票登记[{}]审批通过自动推送金蝶收票单完成:{}", id, summary);
|
||||
}
|
||||
}
|
||||
+26
-9
@@ -4,8 +4,10 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.transport.pojo.dto.BillPaymentStatusRequest;
|
||||
import org.springblade.transport.pojo.dto.FormalSettlementStatusRequest;
|
||||
import org.springblade.transport.pojo.dto.InvoiceApplicationStatusRequest;
|
||||
import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest;
|
||||
import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest;
|
||||
import org.springblade.transport.pojo.entity.BillPayment;
|
||||
import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlement;
|
||||
@@ -311,11 +313,18 @@ public final class MkProcessBizHandlers {
|
||||
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
|
||||
return entity != null;
|
||||
}
|
||||
entity.setApprovalStatus("approved");
|
||||
entity.setCurrentNode("审核通过");
|
||||
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
entity.setApprovedTime(LocalDateTime.now());
|
||||
return service.updateById(entity);
|
||||
// 统一走 service.approve:与手动审批同一入口(补变更记录),审批事务提交后自动触发金蝶推送事件;
|
||||
// 仅审批中单据允许通过,重复回调被状态校验拒绝,不重复推送
|
||||
FormalSettlementStatusRequest request = new FormalSettlementStatusRequest();
|
||||
request.setId(id);
|
||||
service.approve(request);
|
||||
FormalSettlement updated = service.getById(id);
|
||||
if (updated != null) {
|
||||
updated.setCurrentNode("审核通过");
|
||||
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
service.updateById(updated);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -374,10 +383,18 @@ public final class MkProcessBizHandlers {
|
||||
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
|
||||
return entity != null;
|
||||
}
|
||||
entity.setApprovalStatus("approved");
|
||||
entity.setCurrentNode("审核通过");
|
||||
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
return service.updateById(entity);
|
||||
// 统一走 service.approve:与手动审批同一入口(补票据额度联动 useBillBalance),审批事务提交后自动触发金蝶推送事件;
|
||||
// 仅审批中单据允许通过,重复回调被状态校验拒绝,不重复推送
|
||||
PaymentApplicationStatusRequest request = new PaymentApplicationStatusRequest();
|
||||
request.setId(id);
|
||||
service.approve(request);
|
||||
PaymentApplication updated = service.getById(id);
|
||||
if (updated != null) {
|
||||
updated.setCurrentNode("审核通过");
|
||||
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
service.updateById(updated);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+17
@@ -60,6 +60,23 @@ public interface IFormalSettlementService extends BaseService<FormalSettlement>
|
||||
*/
|
||||
String doSyncKingdee(Long id);
|
||||
|
||||
/**
|
||||
* 独立事务落"生成失败"状态并记失败原因(在推送事务回滚、行锁释放后执行,与付款/收票口径统一);
|
||||
* 仅更新审批通过且未同步的单据,未审批/已同步/已删除单据自动跳过,保持原状态
|
||||
*
|
||||
* @param id 正式结算单id
|
||||
* @param reason 失败原因(写变更记录留档)
|
||||
*/
|
||||
void markKingdeeSyncFailed(Long id, String reason);
|
||||
|
||||
/**
|
||||
* 重推金蝶生成失败(kingdeeSyncStatus=failed 且审批通过)的正式结算单:逐单独立成败,失败不中断。
|
||||
* 只扫 failed 不扫 unsynced——存量"审批通过但从未推送"的历史单不自动补推,仍走手动按钮
|
||||
*
|
||||
* @return 执行摘要
|
||||
*/
|
||||
String retryKingdeeFailed();
|
||||
|
||||
String syncKingdeeArResult();
|
||||
List<FormalSettlementDetailFee> detailFees(Long detailId);
|
||||
void adjustDetail(PreSettlementDetailAdjustRequest request);
|
||||
|
||||
+17
@@ -74,5 +74,22 @@ public interface IInvoiceApplicationService extends BaseService<InvoiceApplicati
|
||||
*/
|
||||
String doSyncKingdee(Long id);
|
||||
|
||||
/**
|
||||
* 独立事务落"生成失败"状态并记失败原因(在推送事务回滚、行锁释放后执行,与付款/收票口径统一);
|
||||
* 仅更新审批通过且未同步/未开票的单据,其余自动跳过,保持原状态
|
||||
*
|
||||
* @param id 开票申请id
|
||||
* @param reason 失败原因(写操作流水留档)
|
||||
*/
|
||||
void markKingdeeSyncFailed(Long id, String reason);
|
||||
|
||||
/**
|
||||
* 重推金蝶生成失败(kingdeeStatus=failed 且审批通过)的开票申请:逐单独立成败,失败不中断。
|
||||
* 只扫 failed 不扫 unsynced——存量"审批通过但从未推送"的历史单不自动补推,仍走手动按钮
|
||||
*
|
||||
* @return 执行摘要
|
||||
*/
|
||||
String retryKingdeeFailed();
|
||||
|
||||
String syncKingdeeInvoiceResult();
|
||||
}
|
||||
|
||||
+18
@@ -115,6 +115,24 @@ public interface IInvoiceReceiptService extends BaseService<InvoiceReceipt> {
|
||||
*/
|
||||
String pushKingdeeReceivedInvoice(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 自动推送路径批量推送金蝶收票单(审批通过事件 / 定时重推专用):
|
||||
* 前置拦截(未绑定镜像/查验未通过/前置应付单未生成等)一并置 failed + 原因,纳入定时重推循环
|
||||
*
|
||||
* @param ids 收票登记id列表
|
||||
* @param autoSync 自动推送标记(前置拦截置 failed)
|
||||
* @return 执行摘要(逐单失败原因)
|
||||
*/
|
||||
String pushKingdeeReceivedInvoice(List<Long> ids, boolean autoSync);
|
||||
|
||||
/**
|
||||
* 重推金蝶推送失败(kingdeePushStatus=failed 且审批通过)的收票登记:逐单独立成败,失败不中断。
|
||||
* 只扫 failed 不扫 unsynced——存量"审批通过但从未推送"的历史单不自动补推,仍走手动按钮
|
||||
*
|
||||
* @return 执行摘要
|
||||
*/
|
||||
String retryKingdeeFailed();
|
||||
|
||||
/**
|
||||
* 定时回写金蝶收票单结果(A801-3):金蝶侧删除/红冲/作废 → 回收为 failed 提示人工(不自动作废登记);
|
||||
* pushed(保存成功后中断)→ 指定补偿;assigned 仅补登缺失的收票单号
|
||||
|
||||
+8
@@ -73,6 +73,14 @@ public interface IPaymentApplicationService extends BaseService<PaymentApplicati
|
||||
*/
|
||||
void markKingdeeSyncFailed(Long id);
|
||||
|
||||
/**
|
||||
* 重推金蝶生成失败(kingdeeStatus=failed 且审批通过)的付款申请:逐单独立成败,失败不中断。
|
||||
* 只扫 failed 不扫 unsynced——存量"审批通过但从未推送"的历史单不自动补推,仍走手动按钮
|
||||
*
|
||||
* @return 执行摘要
|
||||
*/
|
||||
String retryKingdeeFailed();
|
||||
|
||||
/**
|
||||
* 金蝶单据解锁(ADR-0005):金蝶侧单据已被反审核删除时解除本地锁定——
|
||||
* 金蝶状态置生成失败、清空金蝶单据内码与编号(原值日志留档),不自动重推,等待人工核实后重新推送。
|
||||
|
||||
+77
-2
@@ -95,11 +95,13 @@ import org.springblade.transport.support.KingdeeSyncLockSupport;
|
||||
import org.springblade.transport.support.KingdeeAttachmentSyncSupport;
|
||||
import org.springblade.transport.wrapper.PreSettlementWrapper;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.transport.event.KingdeeBillApprovedEvent;
|
||||
import org.springblade.transport.wrapper.FormalSettlementWrapper;
|
||||
import org.springblade.transport.wrapper.PaymentApplicationWrapper;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springblade.core.tool.utils.SpringUtil;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -173,6 +175,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
private final KingdeeArBillSyncSupport kingdeeArBillSyncSupport;
|
||||
private final KingdeeAttachmentSyncSupport kingdeeAttachmentSyncSupport;
|
||||
private final KingdeeSyncLockSupport kingdeeSyncLockSupport;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
/**
|
||||
* 金蝶应收单勾稽 single-flight 锁租约秒数,须大于最坏一轮耗时(批量远程查询 + 分批纠偏回写),
|
||||
@@ -572,6 +575,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
@Override public void returnBill(FormalSettlementStatusRequest request) { changeStatus(request.getId(), TransportBusiConstant.REVIEWING, TransportBusiConstant.RETURNED, "已驳回", request.getReason()); }
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void approve(FormalSettlementStatusRequest request) {
|
||||
FormalSettlement settlement = existing(request.getId());
|
||||
if (!TransportBusiConstant.REVIEWING.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批中的正式结算单允许审核");
|
||||
@@ -581,6 +585,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
settlement.setApprovedTime(LocalDateTime.now());
|
||||
updateById(settlement);
|
||||
saveChange(settlement.getId(), "结算单基本信息", null, "调整", "正式结算审批通过", request.getReason());
|
||||
// 审批事务提交后自动推送金蝶(AFTER_COMMIT 监听器消费,推送失败不影响审批结果)
|
||||
eventPublisher.publishEvent(new KingdeeBillApprovedEvent(KingdeeBillApprovedEvent.BIZ_FORMAL_SETTLEMENT, settlement.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -625,8 +631,16 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
|
||||
@Override
|
||||
public String syncKingdee(Long id) {
|
||||
// 单据级事务经自代理调用:单条失败只回滚自身
|
||||
String billNo = SpringUtil.getBean(IFormalSettlementService.class).doSyncKingdee(id);
|
||||
String billNo;
|
||||
try {
|
||||
// 单据级事务经自代理调用:单条失败只回滚自身
|
||||
billNo = SpringUtil.getBean(IFormalSettlementService.class).doSyncKingdee(id);
|
||||
} catch (Exception exception) {
|
||||
log.error("正式结算单[{}]推送金蝶异常", id, exception);
|
||||
// 推送事务已回滚、行锁已释放,此处独立事务落"生成失败"状态(与付款/收票口径统一,失败可被重推任务续推)
|
||||
self().markKingdeeSyncFailed(id, exception.getMessage());
|
||||
throw exception;
|
||||
}
|
||||
// 主单事务提交后锁定金蝶单据(lockBusiData):失败只留 failed 锁记录由补偿任务重锁,不污染主单同步状态;
|
||||
// 锁定失败摘要拼入返回值,页面直接可见
|
||||
String lockFailSummary = lockKingdeeBillAfterPush(id, billNo);
|
||||
@@ -663,6 +677,67 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
}
|
||||
}
|
||||
|
||||
/** 自代理:跨方法调用确保事务注解生效(失败状态独立事务) */
|
||||
private IFormalSettlementService self() {
|
||||
return SpringUtil.getBean(IFormalSettlementService.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void markKingdeeSyncFailed(Long id, String reason) {
|
||||
if (id == null) return;
|
||||
// 条件更新:仅审批通过且未同步的单据落"生成失败"状态,未审批/已同步/已删除单据自动跳过保持原状态
|
||||
int updated = baseMapper.update(null, Wrappers.<FormalSettlement>lambdaUpdate()
|
||||
.eq(FormalSettlement::getId, id)
|
||||
.eq(FormalSettlement::getApprovalStatus, TransportBusiConstant.APPROVED)
|
||||
.ne(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_SYNCED)
|
||||
.set(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED));
|
||||
if (updated > 0) {
|
||||
// 失败原因写变更记录留档(推送事务已回滚,此处独立事务);原因过长截断留档,不抛错
|
||||
String failReason = Func.toStrWithEmpty(reason, "未知异常");
|
||||
saveChange(id, "结算单基本信息", null, "调整",
|
||||
"同步金蝶失败:" + (failReason.length() > 200 ? failReason.substring(0, 200) : failReason), null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String retryKingdeeFailed() {
|
||||
List<Long> failedIds = list(Wrappers.<FormalSettlement>lambdaQuery()
|
||||
.eq(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED)
|
||||
.eq(FormalSettlement::getApprovalStatus, TransportBusiConstant.APPROVED)
|
||||
.orderByAsc(FormalSettlement::getCreateTime))
|
||||
.stream().map(FormalSettlement::getId).toList();
|
||||
return executeKingdeeRetry(failedIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 逐单重推失败单据:单条失败独立 try/catch 不中断整批,返回执行摘要。
|
||||
* 推送前预检(A102-4/A302-4 幂等回填)保证重推不重复生成金蝶单据。
|
||||
*
|
||||
* @param failedIds 失败单据id集合
|
||||
* @return 执行摘要
|
||||
*/
|
||||
private String executeKingdeeRetry(List<Long> failedIds) {
|
||||
if (Func.isEmpty(failedIds)) {
|
||||
return "无生成失败单据";
|
||||
}
|
||||
int success = 0;
|
||||
List<String> failures = new ArrayList<>();
|
||||
for (Long id : failedIds) {
|
||||
try {
|
||||
syncKingdee(id);
|
||||
success++;
|
||||
} catch (Exception exception) {
|
||||
failures.add(id + ":" + exception.getMessage());
|
||||
}
|
||||
}
|
||||
String summary = "重推生成失败单据:本批 " + failedIds.size() + " 条,成功 " + success + " 条";
|
||||
if (!failures.isEmpty()) {
|
||||
summary = summary + ",失败 " + failures.size() + " 条:" + String.join(";", failures);
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* 附件联带上传(A902):主单推送成功(含幂等预检命中)后调用,以本地 kingdeeBillId 定位金蝶单据;
|
||||
* 本地缺失(存量数据/幂等跳过路径未带回内码)时经 A102-4/A302-4 按防重键反查回填;
|
||||
|
||||
+75
-4
@@ -58,6 +58,7 @@ import org.springblade.transport.constant.KingdeeBfiConstant;
|
||||
import org.springblade.transport.constant.KingdeeLockConstant;
|
||||
import org.springblade.transport.pojo.entity.KingdeeBillLock;
|
||||
import org.springblade.transport.constant.TransportBusiConstant;
|
||||
import org.springblade.transport.event.KingdeeBillApprovedEvent;
|
||||
import org.springblade.transport.mapper.ContractManageMapper;
|
||||
import org.springblade.transport.mapper.CustomerArchiveMapper;
|
||||
import org.springblade.transport.mapper.CustomerContactMapper;
|
||||
@@ -104,6 +105,7 @@ import org.springblade.transport.wrapper.InvoiceApplicationWrapper;
|
||||
import org.springblade.transport.wrapper.WaybillWrapper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springblade.core.tool.utils.SpringUtil;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -159,6 +161,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
private final KingdeeArBillSyncSupport kingdeeArBillSyncSupport;
|
||||
private final KingdeeAttachmentSyncSupport kingdeeAttachmentSyncSupport;
|
||||
private final KingdeeSyncLockSupport kingdeeSyncLockSupport;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
/** 金蝶单据锁定记录服务(lockBusiData 锁定/补偿/解锁守卫) */
|
||||
private final IKingdeeBillLockService kingdeeBillLockService;
|
||||
@@ -516,6 +519,8 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void approve(InvoiceApplicationStatusRequest request) {
|
||||
changeStatus(request.getId(), TransportBusiConstant.REVIEWING, TransportBusiConstant.APPROVED, "approve", "审批通过", null);
|
||||
// 审批事务提交后自动推送金蝶(AFTER_COMMIT 监听器消费,推送失败不影响审批结果)
|
||||
eventPublisher.publishEvent(new KingdeeBillApprovedEvent(KingdeeBillApprovedEvent.BIZ_INVOICE_APPLICATION, request.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -571,8 +576,16 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
|
||||
@Override
|
||||
public String syncKingdee(Long id) {
|
||||
// 单据级事务经自代理调用:单条失败只回滚自身
|
||||
String billNo = SpringUtil.getBean(IInvoiceApplicationService.class).doSyncKingdee(id);
|
||||
String billNo;
|
||||
try {
|
||||
// 单据级事务经自代理调用:单条失败只回滚自身
|
||||
billNo = SpringUtil.getBean(IInvoiceApplicationService.class).doSyncKingdee(id);
|
||||
} catch (Exception exception) {
|
||||
log.error("开票申请[{}]推送金蝶异常", id, exception);
|
||||
// 推送事务已回滚、行锁已释放,此处独立事务落"生成失败"状态(与付款/收票口径统一,失败可被重推任务续推)
|
||||
self().markKingdeeSyncFailed(id, exception.getMessage());
|
||||
throw exception;
|
||||
}
|
||||
// 主单事务提交后锁定金蝶开票单(lockBusiData):每张发票张次一张金蝶单,接口1对1逐张锁定;
|
||||
// 失败只留 failed 锁记录由补偿任务重锁,不污染主单同步状态;锁定失败摘要拼入返回值,页面直接可见
|
||||
String lockFailSummary = lockKingdeeBillAfterPush(id, billNo);
|
||||
@@ -607,6 +620,62 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
}
|
||||
}
|
||||
|
||||
/** 自代理:跨方法调用确保事务注解生效(失败状态独立事务) */
|
||||
private IInvoiceApplicationService self() {
|
||||
return SpringUtil.getBean(IInvoiceApplicationService.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void markKingdeeSyncFailed(Long id, String reason) {
|
||||
if (id == null) return;
|
||||
// 条件更新:仅审批通过且未同步/未开票的单据落"生成失败"状态,其余自动跳过保持原状态
|
||||
int updated = baseMapper.update(null, Wrappers.<InvoiceApplication>lambdaUpdate()
|
||||
.eq(InvoiceApplication::getId, id)
|
||||
.eq(InvoiceApplication::getApprovalStatus, TransportBusiConstant.APPROVED)
|
||||
.notIn(InvoiceApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_SYNCED,
|
||||
KingdeeBfiConstant.KINGDEE_STATUS_INVOICED)
|
||||
.set(InvoiceApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED));
|
||||
if (updated > 0) {
|
||||
// 失败原因写操作流水留档(推送事务已回滚,此处独立事务)
|
||||
record(id, "sync", "同步金蝶失败", TransportBusiConstant.APPROVED, TransportBusiConstant.APPROVED,
|
||||
truncateReason(reason), null);
|
||||
}
|
||||
}
|
||||
|
||||
/** 失败原因截断留档(流水 reason 字段兜底 500 长度) */
|
||||
private String truncateReason(String reason) {
|
||||
String value = Func.toStrWithEmpty(reason, "未知异常");
|
||||
return value.length() > 500 ? value.substring(0, 500) : value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String retryKingdeeFailed() {
|
||||
List<Long> failedIds = list(Wrappers.<InvoiceApplication>lambdaQuery()
|
||||
.eq(InvoiceApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED)
|
||||
.eq(InvoiceApplication::getApprovalStatus, TransportBusiConstant.APPROVED)
|
||||
.orderByAsc(InvoiceApplication::getCreateTime))
|
||||
.stream().map(InvoiceApplication::getId).toList();
|
||||
if (Func.isEmpty(failedIds)) {
|
||||
return "无生成失败单据";
|
||||
}
|
||||
int success = 0;
|
||||
List<String> failures = new ArrayList<>();
|
||||
for (Long id : failedIds) {
|
||||
try {
|
||||
syncKingdee(id);
|
||||
success++;
|
||||
} catch (Exception exception) {
|
||||
failures.add(id + ":" + exception.getMessage());
|
||||
}
|
||||
}
|
||||
String summary = "重推生成失败单据:本批 " + failedIds.size() + " 条,成功 " + success + " 条";
|
||||
if (!failures.isEmpty()) {
|
||||
summary = summary + ",失败 " + failures.size() + " 条:" + String.join(";", failures);
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* 附件联带上传(A902):主单推送成功(含 A303-4 勾稽回填)后调用,以本地 kingdeeBillId 定位金蝶单据;
|
||||
* 本地缺失(存量数据)跳过待重推补传;附件上传异常独立捕获,绝不影响主单同步结果
|
||||
@@ -896,10 +965,12 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
// param.setTotaltax(totaltax);
|
||||
// 数据来源:固定 3
|
||||
// param.setBillsource("3");
|
||||
|
||||
// 来源单据:ar_finarbill 财务应收单(金蝶联调确认开票申请须挂上游应收单)
|
||||
// param.setAwrpSrcBt(KingdeeBfiConstant.BFI_AR_BILL_SRC_BT);
|
||||
param.setAwrpSrcBt(KingdeeBfiConstant.BFI_AR_BILL_SRC_BT);
|
||||
// 上游单据号:首张关联结算单的金蝶应收单防重键 awrp_src_code(联调确认金蝶按防重键匹配源单,A302-4 预检已确认存在)
|
||||
// param.setAwrpSrcConNo(firstArSrcCode);
|
||||
param.setAwrpSrcConNo(firstArSrcCode);
|
||||
|
||||
// 数据来源系统
|
||||
param.setSystemsource(bfiProperties.getSrcSys());
|
||||
|
||||
|
||||
+76
-3
@@ -53,6 +53,7 @@ import org.springblade.thirdparty.bfi.service.IBfiService;
|
||||
import org.springblade.transport.constant.KingdeeAttachmentConstant;
|
||||
import org.springblade.transport.constant.KingdeeBfiConstant;
|
||||
import org.springblade.transport.constant.TransportBusiConstant;
|
||||
import org.springblade.transport.event.KingdeeBillApprovedEvent;
|
||||
import org.springblade.transport.mapper.CustomerArchiveMapper;
|
||||
import org.springblade.transport.mapper.CustomerContactMapper;
|
||||
import org.springblade.transport.mapper.FormalSettlementDetailMapper;
|
||||
@@ -84,6 +85,7 @@ import org.springblade.transport.support.KingdeeReceivedInvoiceSyncSupport;
|
||||
import org.springblade.transport.support.KingdeeSyncLockSupport;
|
||||
import org.springblade.transport.wrapper.InvoiceReceiptWrapper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -149,6 +151,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
private final KingdeeReceivedInvoiceSyncSupport kingdeeReceivedInvoiceSyncSupport;
|
||||
private final KingdeeAttachmentSyncSupport kingdeeAttachmentSyncSupport;
|
||||
private final KingdeeSyncLockSupport kingdeeSyncLockSupport;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
/** 金蝶收票单结果回写 single-flight 锁键前缀(多副本定时任务与手动触发共用) */
|
||||
private static final String RECEIVED_RESULT_SYNC_LOCK_PREFIX = "transport:invoice-receipt:kingdee-received-result-sync:";
|
||||
@@ -491,6 +494,8 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
}
|
||||
validateStoredAllocation(entity);
|
||||
changeStatus(entity, TransportBusiConstant.APPROVED, "approve", "审批通过", null);
|
||||
// 审批事务提交后自动推送金蝶收票单(AFTER_COMMIT 监听器消费,推送失败不影响审批结果)
|
||||
eventPublisher.publishEvent(new KingdeeBillApprovedEvent(KingdeeBillApprovedEvent.BIZ_INVOICE_RECEIPT, entity.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -536,6 +541,24 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
*/
|
||||
@Override
|
||||
public String pushKingdeeReceivedInvoice(List<Long> ids) {
|
||||
// 手动批量推送:前置拦截保持现状(blockedReason 不动推送状态,逐单报因)
|
||||
return pushKingdeeReceivedInvoice(ids, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量推送收票登记至金蝶(图05 环节②③:A303-2 收票单批量保存 + A302-6 指定应付)。
|
||||
* <p>
|
||||
* 编排流程:参数去重校验 → 批量组装推送计划(分摊/镜像/结算单/结算明细一次取回防 N+1)→
|
||||
* A102-4 应付单已生成预检 → A801-3 幂等预检(金蝶已有收票单则跳过保存续推指定)→
|
||||
* 逐单推送(单笔失败不中断整批)→ 汇总成功/失败笔数返回
|
||||
*
|
||||
* @param ids 收票登记主键集合
|
||||
* @param autoSync 自动推送路径(审批通过事件/定时重推):前置拦截置 failed + 原因纳入重推循环;
|
||||
* 手动批量路径保持现状(拦截不改状态,逐单报因)
|
||||
* @return 推送结果汇总(本批笔数、成功/失败笔数及失败原因明细)
|
||||
*/
|
||||
@Override
|
||||
public String pushKingdeeReceivedInvoice(List<Long> ids, boolean autoSync) {
|
||||
if (Func.isEmpty(ids)) {
|
||||
throw new ServiceException("请选择需要推送金蝶的收票登记");
|
||||
}
|
||||
@@ -603,7 +626,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
List<String> failures = new ArrayList<>();
|
||||
for (InvoiceReceipt receipt : receipts) {
|
||||
try {
|
||||
pushOneReceivedInvoice(planByReceipt.get(receipt.getId()), kingdeeRowByInvoiceNo, apBillNoBySrcCode, context);
|
||||
pushOneReceivedInvoice(planByReceipt.get(receipt.getId()), kingdeeRowByInvoiceNo, apBillNoBySrcCode, context, autoSync);
|
||||
success++;
|
||||
} catch (RuntimeException pushException) {
|
||||
failures.add("发票" + Func.toStrWithEmpty(receipt.getInvoiceNo(), String.valueOf(receipt.getId()))
|
||||
@@ -619,29 +642,79 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
return summary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String retryKingdeeFailed() {
|
||||
List<Long> failedIds = list(Wrappers.<InvoiceReceipt>lambdaQuery()
|
||||
.eq(InvoiceReceipt::getKingdeePushStatus, KingdeeBfiConstant.RECEIVED_PUSH_FAILED)
|
||||
.eq(InvoiceReceipt::getApprovalStatus, TransportBusiConstant.APPROVED)
|
||||
.orderByAsc(InvoiceReceipt::getCreateTime))
|
||||
.stream().map(InvoiceReceipt::getId).toList();
|
||||
if (Func.isEmpty(failedIds)) {
|
||||
return "无推送失败单据";
|
||||
}
|
||||
int success = 0;
|
||||
List<String> failures = new ArrayList<>();
|
||||
for (Long id : failedIds) {
|
||||
try {
|
||||
// 批量推送方法逐单吞异常返回摘要,成败以推送落库状态为准(pushed/assigned 视为成功)
|
||||
pushKingdeeReceivedInvoice(List.of(id), true);
|
||||
InvoiceReceipt pushed = getById(id);
|
||||
String pushStatus = pushed == null ? null : pushed.getKingdeePushStatus();
|
||||
if (KingdeeBfiConstant.RECEIVED_PUSH_PUSHED.equals(pushStatus)
|
||||
|| KingdeeBfiConstant.RECEIVED_PUSH_ASSIGNED.equals(pushStatus)) {
|
||||
success++;
|
||||
} else {
|
||||
failures.add(id + ":" + (pushed == null ? "单据不存在" : Func.toStrWithEmpty(pushed.getKingdeePushMsg(), "推送未完成")));
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
failures.add(id + ":" + exception.getMessage());
|
||||
}
|
||||
}
|
||||
String summary = "重推推送失败单据:本批 " + failedIds.size() + " 条,成功 " + success + " 条";
|
||||
if (!failures.isEmpty()) {
|
||||
summary = summary + ",失败 " + failures.size() + " 条:" + String.join(";", failures);
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单张收票登记推送编排:前置校验 → A102-4 预检 → A801-3 幂等预检 → A303-2 保存 → pushed 落库 →
|
||||
* A302-6 指定应付 → assigned;保存/指定阶段失败置 failed(预检拦截不改状态),断点可凭防重键续推
|
||||
* A302-6 指定应付 → assigned;保存/指定阶段失败置 failed,断点可凭防重键续推。
|
||||
* 自动推送路径(autoSync=true)前置拦截一并置 failed + 原因,纳入定时重推循环;
|
||||
* 手动批量路径前置拦截保持不动状态(逐单报因),避免人工误触把可自助修复的单据打成失败
|
||||
*
|
||||
* @param plan 预检组装的推送计划
|
||||
* @param kingdeeRows A801-3 幂等预检命中的金蝶收票单(发票号码 -> 行)
|
||||
* @param apBillNoBySrcCode A102-4 已生成的财务应付单(防重键 -> 单据编号)
|
||||
* @param context 金蝶同步上下文(合同/客商编码缓存)
|
||||
* @param autoSync 自动推送路径标记(前置拦截置 failed)
|
||||
* @return 推送结果描述
|
||||
*/
|
||||
private String pushOneReceivedInvoice(ReceivedInvoicePushPlan plan, Map<String, BfiReceivedInvoiceVO> kingdeeRows,
|
||||
Map<String, String> apBillNoBySrcCode, KingdeeSyncContext context) {
|
||||
Map<String, String> apBillNoBySrcCode, KingdeeSyncContext context, boolean autoSync) {
|
||||
InvoiceReceipt receipt = plan.receipt();
|
||||
log.info("收票登记[{}]推送金蝶收票单开始:id={},推送状态={},应付单防重键={}", receipt.getInvoiceNo(), receipt.getId(),
|
||||
receipt.getKingdeePushStatus(), plan.apBillSrcCode());
|
||||
if (Func.isNotBlank(plan.blockedReason())) {
|
||||
log.warn("收票登记[{}]推送金蝶收票单拦截:{}", receipt.getInvoiceNo(), plan.blockedReason());
|
||||
// 自动推送路径:前置不满足置 failed + 原因纳入重推循环("已指定应付"的良性跳过不改状态)
|
||||
if (autoSync && !KingdeeBfiConstant.RECEIVED_PUSH_ASSIGNED.equals(
|
||||
Func.toStrWithEmpty(receipt.getKingdeePushStatus(), KingdeeBfiConstant.RECEIVED_PUSH_UNSYNCED))) {
|
||||
applyReceivedInvoicePushResult(receipt, receipt.getKingdeeReceivedInvoiceNo(), receipt.getKingdeeReceivedInvoiceId(),
|
||||
KingdeeBfiConstant.RECEIVED_PUSH_FAILED, truncatePushMsg(plan.blockedReason()), true);
|
||||
}
|
||||
throw new ServiceException(plan.blockedReason());
|
||||
}
|
||||
String apBillSrcCode = plan.apBillSrcCode();
|
||||
if (!apBillNoBySrcCode.containsKey(apBillSrcCode)) {
|
||||
log.warn("收票登记[{}]推送金蝶收票单拦截:结算单[{}]未生成财务应付单(防重键={})", receipt.getInvoiceNo(),
|
||||
plan.settlement().getFormalSettlementNo(), apBillSrcCode);
|
||||
// 自动推送路径:前置应付单未生成置 failed + 原因,上游结算单就绪后由重推任务自动续推
|
||||
if (autoSync) {
|
||||
applyReceivedInvoicePushResult(receipt, receipt.getKingdeeReceivedInvoiceNo(), receipt.getKingdeeReceivedInvoiceId(),
|
||||
KingdeeBfiConstant.RECEIVED_PUSH_FAILED,
|
||||
truncatePushMsg("请先同步金蝶应付单:结算单" + plan.settlement().getFormalSettlementNo() + "未生成财务应付单"), true);
|
||||
}
|
||||
throw new ServiceException("请先同步金蝶应付单:结算单" + plan.settlement().getFormalSettlementNo()
|
||||
+ "未生成财务应付单");
|
||||
}
|
||||
|
||||
+32
@@ -53,6 +53,7 @@ import org.springblade.transport.constant.KingdeeAttachmentConstant;
|
||||
import org.springblade.transport.constant.KingdeeBfiConstant;
|
||||
import org.springblade.transport.constant.KingdeeLockConstant;
|
||||
import org.springblade.transport.constant.TransportBusiConstant;
|
||||
import org.springblade.transport.event.KingdeeBillApprovedEvent;
|
||||
import org.springblade.transport.mapper.BillLedgerMapper;
|
||||
import org.springblade.transport.mapper.BillLedgerUsageMapper;
|
||||
import org.springblade.transport.mapper.ContractManageMapper;
|
||||
@@ -103,6 +104,7 @@ import org.springblade.transport.support.KingdeeAttachmentSyncSupport;
|
||||
import org.springblade.transport.support.KingdeeSyncLockSupport;
|
||||
import org.springblade.transport.wrapper.PaymentApplicationWrapper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -165,6 +167,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
private final BfiProperties bfiProperties;
|
||||
private final KingdeeSyncLockSupport kingdeeSyncLockSupport;
|
||||
private final BladeRedis bladeRedis;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
/**
|
||||
* 金蝶付款结果回写 single-flight 锁租约秒数,须大于最坏一轮耗时(分批远程查询 + 逐单回写),
|
||||
@@ -436,6 +439,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
entity.setApprovalStatus(TransportBusiConstant.APPROVED); entity.setCurrentNode("审批通过"); entity.setCurrentProcessor(AuthUtil.getUserName());
|
||||
updateById(entity);
|
||||
refreshFormalSettlement(entity);
|
||||
// 审批事务提交后自动推送金蝶(AFTER_COMMIT 监听器消费,推送失败不影响审批结果)
|
||||
eventPublisher.publishEvent(new KingdeeBillApprovedEvent(KingdeeBillApprovedEvent.BIZ_PAYMENT_APPLICATION, entity.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -596,6 +601,33 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
.set(PaymentApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String retryKingdeeFailed() {
|
||||
List<Long> failedIds = list(Wrappers.<PaymentApplication>lambdaQuery()
|
||||
.eq(PaymentApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED)
|
||||
.eq(PaymentApplication::getApprovalStatus, TransportBusiConstant.APPROVED)
|
||||
.orderByAsc(PaymentApplication::getCreateTime))
|
||||
.stream().map(PaymentApplication::getId).toList();
|
||||
if (Func.isEmpty(failedIds)) {
|
||||
return "无生成失败单据";
|
||||
}
|
||||
int success = 0;
|
||||
List<String> failures = new ArrayList<>();
|
||||
for (Long id : failedIds) {
|
||||
try {
|
||||
syncKingdee(id);
|
||||
success++;
|
||||
} catch (Exception exception) {
|
||||
failures.add(id + ":" + exception.getMessage());
|
||||
}
|
||||
}
|
||||
String summary = "重推生成失败单据:本批 " + failedIds.size() + " 条,成功 " + success + " 条";
|
||||
if (!failures.isEmpty()) {
|
||||
summary = summary + ",失败 " + failures.size() + " 条:" + String.join(";", failures);
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void unlockBill(Long id, String source) {
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
|
||||
* Copyright of this software remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.task;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.transport.service.IFormalSettlementService;
|
||||
import org.springblade.transport.service.IInvoiceApplicationService;
|
||||
import org.springblade.transport.service.IInvoiceReceiptService;
|
||||
import org.springblade.transport.service.IPaymentApplicationService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 金蝶自动同步失败定时重推任务(兜底):仅重推各域 failed 单据,不扫描 unsynced——
|
||||
* 存量"审批通过但从未推送"的历史单不自动补推(避免上线即批量推历史数据),仍走手动按钮。
|
||||
* <p>
|
||||
* 前置未就绪(如上游应付单未生成)→ 预检拦截 → 再次 failed → 下轮继续;
|
||||
* 前置就绪 → 自动续推成功(节点硬顺序由既有 A102-4/A302-4/A303-4/A801-3 预检保证,不做级联)。
|
||||
* <p>
|
||||
* ponytail: 重推无次数上限,永久性坏数据(如联行号未维护)会每轮失败一次;
|
||||
* 噪音可接受后,再加 retry_count 上限列。
|
||||
* <p>
|
||||
* 调度配置(Nacos thirdParty.bfi.autoSync.*):retryEnabled 开关、retryCron 表达式。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class KingdeeAutoSyncRetryTask {
|
||||
|
||||
private final IPaymentApplicationService paymentApplicationService;
|
||||
private final IFormalSettlementService formalSettlementService;
|
||||
private final IInvoiceApplicationService invoiceApplicationService;
|
||||
private final IInvoiceReceiptService invoiceReceiptService;
|
||||
|
||||
/** failed 重推开关,代码默认关闭(安全兜底),由 Nacos thirdParty.bfi.autoSync.retryEnabled 按环境显式开启 */
|
||||
@Value("${thirdParty.bfi.autoSync.retryEnabled:false}")
|
||||
private boolean retryEnabled;
|
||||
|
||||
/** failed 定时重推调度,默认每10分钟执行一次 */
|
||||
@Scheduled(cron = "${thirdParty.bfi.autoSync.retryCron:0 */10 * * * ?}")
|
||||
public void retryKingdeeFailed() {
|
||||
if (!retryEnabled) {
|
||||
return;
|
||||
}
|
||||
// ① 结算单(A102-2/A302-2)
|
||||
try {
|
||||
String summary = formalSettlementService.retryKingdeeFailed();
|
||||
log.info("定时重推金蝶生成失败结算单:{}", summary);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时重推金蝶生成失败结算单异常", exception);
|
||||
}
|
||||
// ② 付款申请(A103-1/A103-3)
|
||||
try {
|
||||
String summary = paymentApplicationService.retryKingdeeFailed();
|
||||
log.info("定时重推金蝶生成失败付款申请:{}", summary);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时重推金蝶生成失败付款申请异常", exception);
|
||||
}
|
||||
// ③ 开票申请(A303-1)
|
||||
try {
|
||||
String summary = invoiceApplicationService.retryKingdeeFailed();
|
||||
log.info("定时重推金蝶生成失败开票申请:{}", summary);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时重推金蝶生成失败开票申请异常", exception);
|
||||
}
|
||||
// ④ 收票登记(A303-2+A302-6)
|
||||
try {
|
||||
String summary = invoiceReceiptService.retryKingdeeFailed();
|
||||
log.info("定时重推金蝶推送失败收票登记:{}", summary);
|
||||
} catch (Exception exception) {
|
||||
log.error("定时重推金蝶推送失败收票登记异常", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
# 审批通过自动同步金蝶设计(事件触发 + 定时重推 + 手动兜底)
|
||||
|
||||
> 整理日期:2026-10-05
|
||||
> 目标:推送到金蝶的各单据,**审批通过后自动推送,省掉页面手动点「同步金蝶」按钮这一步**;节点间的先后顺序由既有硬预检 + 失败重推消化,全程无需人工点击。
|
||||
> 依据:`2026-09-27-金蝶(星瀚)BFI接口调用全链路梳理(按业务域汇总·更新版).md`、`TMS业财融合-集成流程图文字版与业务流程梳理.md`、当前代码(探索核实)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 范围
|
||||
|
||||
| 业务域 | 推送链路 | 现状触发 | 本设计 |
|
||||
|---|---|---|---|
|
||||
| 正式结算单(应付/应收) | A102-4 预检 → A102-2 / A302-2 | 手动按钮 | ✅ 审批通过自动推 |
|
||||
| 付款申请(正式/预付) | A102-4 硬校验 → A103-2 预检 → A103-1 / A103-3 | 手动按钮(单/批) | ✅ 审批通过自动推 |
|
||||
| 开票申请 | A302-4 硬校验 → A303-4 预检 → A303-1 逐张次 | 手动按钮 | ✅ 审批通过自动推 |
|
||||
| 收票登记 | A102-4 预检 + A801-3 预检 → A303-2 → A302-6 | 手动批量按钮 | ✅ 审批通过自动推 |
|
||||
| 客商档案 | A902-2 | 已是审批通过自动推 | ➖ 不动(先例) |
|
||||
| 收款认领 | A301-3 | 认领页面动作事务内推送 + ReceiptClaimPushTask 定时 | ➖ 不动(无审批节点) |
|
||||
| 查询回写类 | A103-2 / A303-5 / A801-3 / A301-1 | 定时/手动 | ➖ 不动(不是推送) |
|
||||
|
||||
## 2. 现状与差距(代码核实结论)
|
||||
|
||||
1. **无工作流引擎**:blade-transport 全模块零 flowable 引用,审批 = 直接置 `approvalStatus=approved`(`TransportBusiConstant.java:44`)。
|
||||
2. **审批有两条入口**,自动推送必须全覆盖:
|
||||
- 手动:`POST /xxx/approve` → `service.approve(request)`;
|
||||
- OA 回调:`MkProcessPublicController` → `MkProcessMessageService` → `MkProcessBizHandlers.approveFromProcess`。
|
||||
- ⚠️ 其中 **formal-settlement 与 payment-application 的 OA handler 直接置 approved、绕过 `service.approve`**(付款侧因此漏掉票据额度联动 `useBillBalance`,结算侧不写变更记录——既有不一致,本设计顺手统一)。
|
||||
3. **推送失败落库口径不一致**:付款/收票失败落 `failed`;结算单/开票失败整体回滚保持 `unsynced`("没推过"与"推失败"无法区分,总纲 11.2-#3 缺陷)。
|
||||
4. **事件设施现成**:voucher 模块已有 `ApplicationEventPublisher` + `@TransactionalEventListener(AFTER_COMMIT)` 模式。
|
||||
5. 审批端点全部单条,无批量审批入口。
|
||||
6. 客商档案先例:审批后推送异常被吞(Controller 层 try/catch + Service 层落 failed),**推送失败不影响审批结果**。
|
||||
|
||||
## 3. 总体方案
|
||||
|
||||
```
|
||||
┌─ 手动 POST /xxx/approve ─→ service.approve() ─┐
|
||||
审批通过(两条入口)─┤ ├─ 事务内置 approved + 发布 KingdeeBillApprovedEvent
|
||||
└─ OA 回调 approveFromProcess ─────────────────┘ (OA handler 统一改走 service.approve)
|
||||
│ 事务提交(AFTER_COMMIT)
|
||||
▼
|
||||
KingdeeApprovalSyncListener(域开关判断 + 全量 try/catch 吞异常)
|
||||
│ 复用手动按钮同一 Service 方法
|
||||
┌────────────┬────────────┴──────────┬──────────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
结算单 sync 付款申请 sync 开票申请 sync 收票 push
|
||||
(A102-2/A302-2) (A103-1/A103-3) (A303-1) (A303-2+A302-6)
|
||||
│ │ │ │
|
||||
成功→synced 成功→synced 成功→synced 成功→pushed/assigned
|
||||
失败→failed 失败→failed 失败→failed 失败→failed
|
||||
└────────────┴───────────┬───────────┴──────────────┘
|
||||
▼
|
||||
KingdeeAutoSyncRetryTask(定时,默认关):仅重推 failed
|
||||
前置未就绪(如应付单未生成)→ 预检拦截 → 再次 failed → 下轮继续
|
||||
前置就绪 → 自动续推成功(硬顺序由预检保证,不做级联)
|
||||
```
|
||||
|
||||
三条不变式:
|
||||
1. **审批结果不受推送影响**:事件在审批事务提交后才消费,监听器整体 try/catch,推送异常只落库 + 记日志(客商先例)。
|
||||
2. **只有审批状态真正变为 approved 的那次才发布事件**(重复回调被 approve 的状态校验拒绝,天然不重复推送)。
|
||||
3. **复用手动按钮同一 Service 方法**:自动推送与手动推送走完全相同的校验、预检、回写逻辑,不另写推送路径。
|
||||
|
||||
## 4. 设计要点
|
||||
|
||||
### 4.1 触发点(覆盖双入口)
|
||||
|
||||
- 新增事件 `KingdeeBillApprovedEvent(String bizType, Long id)`,`bizType` 复用 `MkProcessBizHandlers` 既有取值(`formal-settlement` / `payment-application` / `invoice-application` / `invoice-receipt`)。
|
||||
- 4 个实体的 `service.approve()` 在状态置 approved 成功后 `publishEvent`(须确保 approve 方法在事务内执行——均有变更记录写入,已是 @Transactional;无事务时 `@TransactionalEventListener` 默认丢弃事件,实现时核实兜底)。
|
||||
- **统一 OA 回调**:`MkProcessBizHandlers` 中 formal-settlement(:309-318)与 payment-application(:372-380)的 `approveFromProcess` 改为调 `service.approve(request)`,与 invoice-application / invoice-receipt 的 handler 写法对齐。副作用即修复:付款 OA 审批补上票据额度联动 `useBillBalance`、结算 OA 审批补上变更记录。
|
||||
- 客商档案已有自己的审批后推送,不动。
|
||||
|
||||
### 4.2 执行模型:AFTER_COMMIT 同步推送
|
||||
|
||||
- 监听器 `KingdeeApprovalSyncListener` 用 `@TransactionalEventListener(phase = AFTER_COMMIT)`,事务提交后在当前线程调用推送(与客商档案现网行为一致)。正常推送秒级;Feign 超时上限 10s 连接 / 60s 读取。
|
||||
- `ponytail:` 同步推送占用审批请求线程,若 BFI 网关大面积变慢导致审批接口可感知变慢,再把监听器内部改为提交到守护线程池(`MkProcessMessageService` 已有同款裸 `ScheduledExecutorService` 模式,1 处改动)。
|
||||
- 路由:
|
||||
|
||||
| bizType | 调用(与手动按钮相同) |
|
||||
|---|---|
|
||||
| formal-settlement | `formalSettlementService.syncKingdee(id)`(内部按 settlementType 路由 A102-2/A302-2) |
|
||||
| payment-application | `paymentApplicationService.syncKingdee(id)`(内部路由 A103-1/A103-3) |
|
||||
| invoice-application | `invoiceApplicationService.syncKingdee(id)` |
|
||||
| invoice-receipt | `invoiceReceiptService.pushKingdeeReceivedInvoice(List.of(id))` |
|
||||
|
||||
### 4.3 失败统一落 failed(修 11.2-#3)
|
||||
|
||||
- 结算单、开票申请的推送方法改为**"事务回滚后独立事务落 failed + 失败原因"**(沿用付款申请已有 `markKingdeeSyncFailed` 模式),与付款/收票口径统一。无需新列、无需 SQL(状态字段均为 varchar,新增 `failed` 取值即可)。
|
||||
- 收票自动推送遇到推送计划不满足(未恰好分摊 1 张审批通过应付结算单、查验未通过等,现写 `blockedReason` 不动状态):**自动推送路径下置 failed + 原因**,纳入重推循环;前置补齐后重推自动续上。
|
||||
- 前端列表的金蝶状态筛选/展示补 `failed` 选项(结算单/开票)。
|
||||
|
||||
### 4.4 节点先后顺序:预检保证,不做级联
|
||||
|
||||
- 每个节点只在**自身审批通过**时推自己;跨节点硬顺序(A102-2→A103-1/A303-2、A302-2→A303-1)仍由既有 A102-4/A302-4 硬预检强制。
|
||||
- 前置未就绪(上游还没推成)→ 本次推送失败落 failed(原因"前置应付/应收单未生成")→ 重推任务下轮再试 → 上游就绪后自动续推成功。预检与断点续传机制天然支持。
|
||||
- **不做级联**(上游推完自动连推下游):失败传播复杂、且下游有自己的审批节点,级联会绕过"审批通过才推"的语义。
|
||||
|
||||
### 4.5 定时重推任务(兜底)
|
||||
|
||||
- 新增 `KingdeeAutoSyncRetryTask`(`task/` 包,@Component + @Value 开关 + @Scheduled cron,与既有任务同款)。
|
||||
- **只重推 `failed`,不扫描 `unsynced`**——存量"审批通过但从未推送"的历史单不自动补推(避免上线即批量推历史数据),仍走手动按钮。
|
||||
- 每域候选集(均要求未作废):
|
||||
|
||||
| 域 | 查询条件 | 动作 |
|
||||
|---|---|---|
|
||||
| 结算单 | `kingdee_sync_status='failed'` 且 approved | `syncKingdee(id)` |
|
||||
| 付款申请 | `kingdee_status='failed'` 且 approved | `syncKingdee(id)` |
|
||||
| 开票申请 | `kingdee_status='failed'` 且 approved | `syncKingdee(id)` |
|
||||
| 收票登记 | `kingdee_push_status='failed'` 且 approved | `pushKingdeeReceivedInvoice(List.of(id))` |
|
||||
|
||||
- 逐条 try/catch,单条失败不中断;与付款勾稽纠偏(A103-2 查到即纠正 synced)天然兼容:重推前预检命中远端已落单会幂等回填,不会重复生成。
|
||||
- 开关 `thirdParty.bfi.autoSync.retryEnabled` 默认 false,cron 默认 `0 */10 * * * ?`。
|
||||
- `ponytail:` 重推无次数上限,永久性坏数据(如联行号未维护)会每 10 分钟失败一次;噪音可接受后,再加 retry_count 上限列。
|
||||
|
||||
### 4.6 配置开关与灰度
|
||||
|
||||
监听器内按域判断,4 个开关全部默认 false(Nacos 显式打开),与既有定时任务开关风格一致:
|
||||
|
||||
```yaml
|
||||
third-party:
|
||||
bfi:
|
||||
autoSync:
|
||||
settlement: false # 结算单审批后自动推 A102-2/A302-2
|
||||
payment: false # 付款申请审批后自动推 A103-1/A103-3
|
||||
invoiceApply: false # 开票申请审批后自动推 A303-1
|
||||
receipt: false # 收票登记审批后自动推 A303-2+A302-6
|
||||
retryEnabled: false # failed 定时重推
|
||||
```
|
||||
|
||||
> 注意配置前缀:transport 侧读 `third-party.bfi`(BfiProperties 现行前缀,与 nacos 键对齐),落地时以实际前缀为准。
|
||||
> 关闭某域开关 = 该域回到现状(审批不推送、手动按钮照用);重推开关独立,可只关重推保手动。
|
||||
|
||||
### 4.7 保持不变的部分
|
||||
|
||||
- 手动同步按钮**全部保留**(失败重推与人工兜底入口,三入口并存:自动 + 定时 + 手动)。
|
||||
- 结算单汇率未维护 → 照旧"不传不阻断",不在审批侧强校验。
|
||||
- 定时回写任务(PaymentKingdeeResultTask / CustomerKingdeeSyncTask / ReceiptClaimPushTask)不动。
|
||||
- 状态守卫式锁单(synced 禁止作废等)不动;B901 锁单回调本分支未实现,与本设计无耦合。
|
||||
|
||||
## 5. 改动清单(文件级)
|
||||
|
||||
| # | 文件 | 改动 |
|
||||
|---|---|---|
|
||||
| 1 | `transport/event/KingdeeBillApprovedEvent.java` | 新增 record(bizType, id) |
|
||||
| 2 | `transport/listener/KingdeeApprovalSyncListener.java` | 新增:AFTER_COMMIT 监听 + 域开关 + 路由 4 个 sync 方法 + 全量 try/catch |
|
||||
| 3 | `FormalSettlementServiceImpl.approve`(:539) | 末尾发布事件 |
|
||||
| 4 | `PaymentApplicationServiceImpl.approve`(:400) | 末尾发布事件 |
|
||||
| 5 | `InvoiceApplicationServiceImpl.approve`(:483) | 末尾发布事件 |
|
||||
| 6 | `InvoiceReceiptServiceImpl.approve`(:587) | 末尾发布事件 |
|
||||
| 7 | `MkProcessBizHandlers`(formal-settlement :309 / payment-application :372) | `approveFromProcess` 改调 `service.approve(request)`(统一入口 + 修复 useBillBalance 绕过) |
|
||||
| 8 | `FormalSettlementServiceImpl` / `InvoiceApplicationServiceImpl` 推送失败路径 | 事务回滚后独立事务落 `failed` + 原因(沿用 `markKingdeeSyncFailed` 模式) |
|
||||
| 9 | `InvoiceReceiptServiceImpl` 推送计划 blocked 分支 | 自动推送路径 blocked → failed + 原因 |
|
||||
| 10 | `transport/task/KingdeeAutoSyncRetryTask.java` | 新增:4 域 failed 重推,开关 + cron |
|
||||
| 11 | 前端结算单/开票列表 | 金蝶状态筛选/展示补 `failed`(小改) |
|
||||
|
||||
无新表、无 DDL、无新依赖。
|
||||
|
||||
## 6. 边界场景推演
|
||||
|
||||
| 场景 | 行为 |
|
||||
|---|---|
|
||||
| 推送成功但审批事务实际回滚(AFTER_COMMIT 后回滚不存在,理论不发生) | 不适用——事件只在事务成功提交后消费 |
|
||||
| 推送成功、落 failed 前进程崩溃 | 下轮重推预检命中(A102-4/A103-2/A303-4/A801-3)幂等回填,不重复建单 |
|
||||
| 付款审批通过但结算单应付单未生成 | A102-4 硬校验拦截 → failed("前置应付单未生成") → 重推任务续推 |
|
||||
| 结算单推送失败挂 failed,下游付款/收票/开票全failed等它 | 重推任务每轮都重试各域 failed,上游成功后下游自动续上 |
|
||||
| OA 审批通过的单 | OA handler 统一走 service.approve 后同样触发事件,覆盖 |
|
||||
| 同一单重复 OA 回调 | approve 状态校验拒绝重复审批,不发布事件 |
|
||||
| 永久性坏数据(如联行号未维护) | 每轮重推失败落原因,修数后下轮自动成功;重推无上限(见 4.5 天花板) |
|
||||
| 历史存量 approved-unsynced 单 | 不自动补推,手动按钮处理 |
|
||||
|
||||
## 7. 待确认默认值(按推荐默认落地,可改)
|
||||
|
||||
| 默认 | 内容 | 备选 |
|
||||
|---|---|---|
|
||||
| D1 | AFTER_COMMIT 同步推送(同线程) | 改守护线程池异步(1 处改动,见 4.2) |
|
||||
| D2 | OA handler 统一改走 service.approve(顺修 useBillBalance 绕过) | 保持 handler 直置、在各 handler 单独发事件 |
|
||||
| D3 | 重推只扫 failed,存量 unsynced 不补推 | 加"存量补推"一次性开关 |
|
||||
| D4 | 收票 blocked 条件在自动推送时置 failed | 保持 blockedReason 不动状态 + 重推扫 blocked |
|
||||
| D5 | 重推无次数上限 | 加 retry_count 上限 |
|
||||
Reference in New Issue
Block a user