From 9365fd77383ff14f83a611b49e43107dc821a796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Sat, 10 Oct 2026 13:45:45 +0800 Subject: [PATCH 1/7] =?UTF-8?q?refactor:=20MK=E6=B5=81=E7=A8=8B=E7=BB=93?= =?UTF-8?q?=E7=AE=97/=E4=BB=98=E6=AC=BE=E5=AE=A1=E6=89=B9=E5=9B=9E?= =?UTF-8?q?=E8=B0=83=E7=BB=9F=E4=B8=80=E6=94=B9=E8=B0=83service.approve?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 结算/付款 OA handler 不再直置 approved,与开票/收票 handler 对齐走 service.approve 统一入口 - 顺修两处绕过:付款审批补上票据额度联动 useBillBalance,结算审批补上变更记录 - 仅审批中单据可通过,重复 OA 回调被状态校验拒绝,为后续审批事件触发推送保证不重复推送 --- .../transport/mk/MkProcessBizHandlers.java | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java index 8e25176e..918749f6 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java @@ -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 From 03b343102e495f5a111dae600bfee822074c4584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Sat, 10 Oct 2026 13:48:26 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=E6=8E=A8=E9=80=81=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E7=BB=9F=E4=B8=80=E8=90=BDfailed=E5=8F=A3=E5=BE=84?= =?UTF-8?q?=EF=BC=88=E4=BF=AE=E6=80=BB=E7=BA=B211.2-#3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 结算单/开票申请推送失败由整体回滚保持unsynced改为:事务回滚后独立事务落failed+原因(结算写变更记录、开票写操作流水,无新列),与付款/收票口径统一 - 收票新增pushKingdeeReceivedInvoice(ids,autoSync)重载:自动推送路径前置拦截(未绑定镜像/查验未通过/前置应付单未生成等)置failed+原因纳入重推循环,已指定应付的良性跳过不改状态;手动批量路径保持拦截不动状态逐单报因 - 汇率未维护照旧不传不阻断 --- .../service/IFormalSettlementService.java | 9 ++++ .../service/IInvoiceApplicationService.java | 9 ++++ .../service/IInvoiceReceiptService.java | 10 +++++ .../impl/FormalSettlementServiceImpl.java | 35 +++++++++++++++- .../impl/InvoiceApplicationServiceImpl.java | 41 ++++++++++++++++++- .../impl/InvoiceReceiptServiceImpl.java | 39 ++++++++++++++++-- 6 files changed, 136 insertions(+), 7 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java index 2fdd3d55..a341a8eb 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java @@ -51,6 +51,15 @@ public interface IFormalSettlementService extends BaseService */ String doSyncKingdee(Long id); + /** + * 独立事务落"生成失败"状态并记失败原因(在推送事务回滚、行锁释放后执行,与付款/收票口径统一); + * 仅更新审批通过且未同步的单据,未审批/已同步/已删除单据自动跳过,保持原状态 + * + * @param id 正式结算单id + * @param reason 失败原因(写变更记录留档) + */ + void markKingdeeSyncFailed(Long id, String reason); + String syncKingdeeArResult(); List detailFees(Long detailId); void adjustDetail(PreSettlementDetailAdjustRequest request); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java index 92e01890..6c31fd0d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java @@ -65,5 +65,14 @@ public interface IInvoiceApplicationService extends BaseService { */ String pushKingdeeReceivedInvoice(List ids); + /** + * 自动推送路径批量推送金蝶收票单(审批通过事件 / 定时重推专用): + * 前置拦截(未绑定镜像/查验未通过/前置应付单未生成等)一并置 failed + 原因,纳入定时重推循环 + * + * @param ids 收票登记id列表 + * @param autoSync 自动推送标记(前置拦截置 failed) + * @return 执行摘要(逐单失败原因) + */ + String pushKingdeeReceivedInvoice(List ids, boolean autoSync); + /** * 定时回写金蝶收票单结果(A801-3):金蝶侧删除/红冲/作废 → 回收为 failed 提示人工(不自动作废登记); * pushed(保存成功后中断)→ 指定补偿;assigned 仅补登缺失的收票单号 diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java index 7ebf7728..fd6bd5e8 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java @@ -565,13 +565,44 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaUpdate() + .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); + } + } + /** * 附件联带上传(A902):主单推送成功(含幂等预检命中)后调用,以本地 kingdeeBillId 定位金蝶单据; * 本地缺失(存量数据/幂等跳过路径未带回内码)时经 A102-4/A302-4 按防重键反查回填; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java index 4974787e..679d9547 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java @@ -509,13 +509,50 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpllambdaUpdate() + .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; + } + /** * 附件联带上传(A902):主单推送成功(含 A303-4 勾稽回填)后调用,以本地 kingdeeBillId 定位金蝶单据; * 本地缺失(存量数据)跳过待重推补传;附件上传异常独立捕获,绝不影响主单同步结果 diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java index f12665ef..e041053d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java @@ -536,6 +536,24 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl ids) { + // 手动批量推送:前置拦截保持现状(blockedReason 不动推送状态,逐单报因) + return pushKingdeeReceivedInvoice(ids, false); + } + + /** + * 批量推送收票登记至金蝶(图05 环节②③:A303-2 收票单批量保存 + A302-6 指定应付)。 + *

+ * 编排流程:参数去重校验 → 批量组装推送计划(分摊/镜像/结算单/结算明细一次取回防 N+1)→ + * A102-4 应付单已生成预检 → A801-3 幂等预检(金蝶已有收票单则跳过保存续推指定)→ + * 逐单推送(单笔失败不中断整批)→ 汇总成功/失败笔数返回 + * + * @param ids 收票登记主键集合 + * @param autoSync 自动推送路径(审批通过事件/定时重推):前置拦截置 failed + 原因纳入重推循环; + * 手动批量路径保持现状(拦截不改状态,逐单报因) + * @return 推送结果汇总(本批笔数、成功/失败笔数及失败原因明细) + */ + @Override + public String pushKingdeeReceivedInvoice(List ids, boolean autoSync) { if (Func.isEmpty(ids)) { throw new ServiceException("请选择需要推送金蝶的收票登记"); } @@ -603,7 +621,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl 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())) @@ -621,27 +639,42 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl kingdeeRows, - Map apBillNoBySrcCode, KingdeeSyncContext context) { + Map 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() + "未生成财务应付单"); } From cfbbc756ebbdbd5e64f1c77de132c1366ead51da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Sat, 10 Oct 2026 13:50:12 +0800 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E9=87=91?= =?UTF-8?q?=E8=9D=B6failed=E5=8D=95=E6=8D=AE=E5=AE=9A=E6=97=B6=E9=87=8D?= =?UTF-8?q?=E6=8E=A8=E4=BB=BB=E5=8A=A1=EF=BC=88=E5=85=9C=E5=BA=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 KingdeeAutoSyncRetryTask:仅重推各域 failed,不扫 unsynced(存量审批通过但从未推送的历史单不自动补推,仍走手动按钮) - 4域新增 retryKingdeeFailed:逐单独立成败不中断,返回执行摘要;前置未就绪再次failed下轮继续,节点硬顺序由既有 A102-4/A302-4/A303-4/A801-3 预检保证,不做级联 - 收票重推以落库状态 pushed/assigned 判成败(批量推送方法逐单吞异常返回摘要) - 开关 thirdParty.bfi.autoSync.retryEnabled 默认关闭,cron 默认每10分钟;重推无次数上限(噪音可接受后再加 retry_count) --- .../service/IFormalSettlementService.java | 8 ++ .../service/IInvoiceApplicationService.java | 8 ++ .../service/IInvoiceReceiptService.java | 8 ++ .../service/IPaymentApplicationService.java | 8 ++ .../impl/FormalSettlementServiceImpl.java | 38 ++++++++ .../impl/InvoiceApplicationServiceImpl.java | 27 ++++++ .../impl/InvoiceReceiptServiceImpl.java | 35 +++++++ .../impl/PaymentApplicationServiceImpl.java | 27 ++++++ .../task/KingdeeAutoSyncRetryTask.java | 95 +++++++++++++++++++ 9 files changed, 254 insertions(+) create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/task/KingdeeAutoSyncRetryTask.java diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java index a341a8eb..f1fdb1aa 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java @@ -60,6 +60,14 @@ public interface IFormalSettlementService extends BaseService */ void markKingdeeSyncFailed(Long id, String reason); + /** + * 重推金蝶生成失败(kingdeeSyncStatus=failed 且审批通过)的正式结算单:逐单独立成败,失败不中断。 + * 只扫 failed 不扫 unsynced——存量"审批通过但从未推送"的历史单不自动补推,仍走手动按钮 + * + * @return 执行摘要 + */ + String retryKingdeeFailed(); + String syncKingdeeArResult(); List detailFees(Long detailId); void adjustDetail(PreSettlementDetailAdjustRequest request); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java index 6c31fd0d..81647ac5 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java @@ -74,5 +74,13 @@ public interface IInvoiceApplicationService extends BaseService { */ String pushKingdeeReceivedInvoice(List ids, boolean autoSync); + /** + * 重推金蝶推送失败(kingdeePushStatus=failed 且审批通过)的收票登记:逐单独立成败,失败不中断。 + * 只扫 failed 不扫 unsynced——存量"审批通过但从未推送"的历史单不自动补推,仍走手动按钮 + * + * @return 执行摘要 + */ + String retryKingdeeFailed(); + /** * 定时回写金蝶收票单结果(A801-3):金蝶侧删除/红冲/作废 → 回收为 failed 提示人工(不自动作废登记); * pushed(保存成功后中断)→ 指定补偿;assigned 仅补登缺失的收票单号 diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java index 1b5ced1c..6d4807af 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java @@ -73,6 +73,14 @@ public interface IPaymentApplicationService extends BaseService failedIds = list(Wrappers.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 failedIds) { + if (Func.isEmpty(failedIds)) { + return "无生成失败单据"; + } + int success = 0; + List 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 按防重键反查回填; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java index 679d9547..a226d906 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java @@ -553,6 +553,33 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl 500 ? value.substring(0, 500) : value; } + @Override + public String retryKingdeeFailed() { + List failedIds = list(Wrappers.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 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 定位金蝶单据; * 本地缺失(存量数据)跳过待重推补传;附件上传异常独立捕获,绝不影响主单同步结果 diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java index e041053d..157c1b98 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java @@ -637,6 +637,41 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl failedIds = list(Wrappers.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 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,断点可凭防重键续推。 diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java index 41efc41b..419cf0f3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java @@ -539,6 +539,33 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl failedIds = list(Wrappers.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 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) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/KingdeeAutoSyncRetryTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/KingdeeAutoSyncRetryTask.java new file mode 100644 index 00000000..ba488e55 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/KingdeeAutoSyncRetryTask.java @@ -0,0 +1,95 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments from this software for such purposes. + * Copyright of this software remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development. + *

+ * 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—— + * 存量"审批通过但从未推送"的历史单不自动补推(避免上线即批量推历史数据),仍走手动按钮。 + *

+ * 前置未就绪(如上游应付单未生成)→ 预检拦截 → 再次 failed → 下轮继续; + * 前置就绪 → 自动续推成功(节点硬顺序由既有 A102-4/A302-4/A303-4/A801-3 预检保证,不做级联)。 + *

+ * ponytail: 重推无次数上限,永久性坏数据(如联行号未维护)会每轮失败一次; + * 噪音可接受后,再加 retry_count 上限列。 + *

+ * 调度配置(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); + } + } +} From 276dd813d4d64b5124f339996ff7a8de06fdc203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Sat, 10 Oct 2026 13:51:55 +0800 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=E5=AE=A1=E6=89=B9=E9=80=9A?= =?UTF-8?q?=E8=BF=87=E4=BA=8B=E4=BB=B6=E8=A7=A6=E5=8F=91=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E9=87=91=E8=9D=B6=EF=BC=88AFTER=5FCOMMIT+4?= =?UTF-8?q?=E5=9F=9F=E5=BC=80=E5=85=B3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 KingdeeBillApprovedEvent(bizType复用MK处理器取值)与 KingdeeApprovalSyncListener:事务提交后同线程复用手动按钮同一Service方法推送,fallbackExecution兜底无事务场景,全量try/catch吞异常,审批结果不受推送影响 - 4域 approve 置approved成功后发布事件(重复审批被状态校验拒绝天然不重复推送);正式结算单 approve 补 @Transactional(原缺失会致AFTER_COMMIT事件被丢弃,顺带使更新与变更记录原子化) - 域开关 thirdParty.bfi.autoSync.{settlement,payment,invoiceApply,receipt} 默认全关(Nacos显式打开),关闭即回到现状;手动同步按钮全部保留,三入口并存:自动+定时+手动 - 推送失败由上一独立事务落failed+定时重推兜底 --- .../event/KingdeeBillApprovedEvent.java | 44 ++++++ .../listener/KingdeeApprovalSyncListener.java | 133 ++++++++++++++++++ .../impl/FormalSettlementServiceImpl.java | 6 + .../impl/InvoiceApplicationServiceImpl.java | 5 + .../impl/InvoiceReceiptServiceImpl.java | 5 + .../impl/PaymentApplicationServiceImpl.java | 5 + 6 files changed, 198 insertions(+) create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/event/KingdeeBillApprovedEvent.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/listener/KingdeeApprovalSyncListener.java diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/event/KingdeeBillApprovedEvent.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/event/KingdeeBillApprovedEvent.java new file mode 100644 index 00000000..9652742f --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/event/KingdeeBillApprovedEvent.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments from this software for such purposes. + * Copyright of this software remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.event; + +/** + * 单据审批通过事件:审批事务提交后由 {@code KingdeeApprovalSyncListener} 消费, + * 自动推送对应单据至金蝶(星瀚),省掉页面手动点「同步金蝶」。 + *

+ * 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"; +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/KingdeeApprovalSyncListener.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/KingdeeApprovalSyncListener.java new file mode 100644 index 00000000..57216faf --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/KingdeeApprovalSyncListener.java @@ -0,0 +1,133 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments from this software for such purposes. + * Copyright of this software remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development. + *

+ * 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 方法推送, + * 与客商档案审批后自动推送同线程模式。 + *

+ * 不变式:审批结果不受推送影响——监听器整体 try/catch 吞异常,推送异常只落 failed + 日志; + * 失败单据由 {@code KingdeeAutoSyncRetryTask} 定时重推、手动按钮兜底。 + *

+ * ponytail: 同步推送占用审批请求线程,若 BFI 网关大面积变慢导致审批接口可感知变慢, + * 再把路由调用提交到守护线程池(MkProcessMessageService 已有同款裸 ScheduledExecutorService 模式)。 + *

+ * 域开关(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; + + /** + * 事务提交后同步推送。fallbackExecution=true 兜底:approve 未开启事务时事件不再被默认丢弃。 + * + * @param event 审批通过事件(bizType + 单据id) + */ + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true) + public void onBillApproved(KingdeeBillApprovedEvent event) { + 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) { + // 推送失败只记日志(失败状态已在推送方法内独立事务落库),不影响审批结果,交给重推任务与手动按钮兜底 + log.error("审批通过自动同步金蝶失败 bizType={} id={}", event.bizType(), event.id(), exception); + } + } + + 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); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java index fdd377ea..b5048ace 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java @@ -92,11 +92,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; @@ -167,6 +169,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl Date: Sun, 11 Oct 2026 14:35:45 +0800 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20=E5=AE=A1=E6=89=B9=E9=80=9A=E8=BF=87?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=90=8C=E6=AD=A5=E9=87=91=E8=9D=B6=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E5=8E=9F=E5=9B=A0=E6=8F=90=E7=A4=BA=E5=88=B0=E5=89=8D?= =?UTF-8?q?=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FormalSettlementController.java | 15 +++++++++++- .../InvoiceApplicationController.java | 10 ++++++++ .../controller/InvoiceReceiptController.java | 10 ++++++++ .../PaymentApplicationController.java | 15 +++++++++++- .../listener/KingdeeApprovalSyncListener.java | 24 ++++++++++++++++++- 5 files changed, 71 insertions(+), 3 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java index 313fe1e0..83799e6d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java @@ -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; @@ -194,7 +195,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) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java index f7287ead..6d2ab648 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceApplicationController.java @@ -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("审批通过"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java index 7dcb6f0a..59eb30b6 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java @@ -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("审批通过"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java index b81ac131..92a7f5a2 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java @@ -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 = "驳回付款申请") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/KingdeeApprovalSyncListener.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/KingdeeApprovalSyncListener.java index 57216faf..3ed81243 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/KingdeeApprovalSyncListener.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/KingdeeApprovalSyncListener.java @@ -40,6 +40,10 @@ import java.util.List; * 不变式:审批结果不受推送影响——监听器整体 try/catch 吞异常,推送异常只落 failed + 日志; * 失败单据由 {@code KingdeeAutoSyncRetryTask} 定时重推、手动按钮兜底。 *

+ * 推送失败原因通过 {@link #pollSyncFailure()} 传递给审批接口响应:AFTER_COMMIT 监听与 approve + * 同线程同请求,Controller 在 approve 返回后取走原因拼进提示文案;OA 回调路径无人取走, + * 由监听入口清空兜底,不会串单。 + *

* ponytail: 同步推送占用审批请求线程,若 BFI 网关大面积变慢导致审批接口可感知变慢, * 再把路由调用提交到守护线程池(MkProcessMessageService 已有同款裸 ScheduledExecutorService 模式)。 *

@@ -74,6 +78,19 @@ public class KingdeeApprovalSyncListener { @Value("${thirdParty.bfi.autoSync.receipt:false}") private boolean receiptEnabled; + /** 当前审批请求线程的自动推送失败原因,供 Controller 取走提示前端(见类注释线程说明) */ + private static final ThreadLocal SYNC_FAILURE = new ThreadLocal<>(); + + /** + * 取走并清除当前审批请求的自动推送失败原因;null 表示推送成功或未触发推送。 + * 须在 service.approve 返回后立即调用(监听器在审批事务提交时同步执行完毕)。 + */ + public static String pollSyncFailure() { + String failure = SYNC_FAILURE.get(); + SYNC_FAILURE.remove(); + return failure; + } + /** * 事务提交后同步推送。fallbackExecution=true 兜底:approve 未开启事务时事件不再被默认丢弃。 * @@ -81,6 +98,7 @@ public class KingdeeApprovalSyncListener { */ @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()); @@ -90,8 +108,12 @@ public class KingdeeApprovalSyncListener { 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); } } From 5a630bb102115a2d3d1b7e95ee689ff643ed97e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Sun, 11 Oct 2026 14:44:33 +0800 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20=E5=BC=80=E7=A5=A8=E7=94=B3=E8=AF=B7?= =?UTF-8?q?=E5=8D=95=E4=BF=9D=E5=AD=98=E6=97=B6=E9=80=81=E4=B8=8A=E6=B8=B8?= =?UTF-8?q?=E5=8D=95=E6=8D=AE=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/InvoiceApplicationServiceImpl.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java index e67acbda..fa701aa5 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java @@ -874,10 +874,12 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl Date: Sun, 11 Oct 2026 15:51:03 +0800 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20=E5=AE=A1=E6=89=B9=E9=80=9A=E8=BF=87?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=90=8C=E6=AD=A5=E9=87=91=E8=9D=B6=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...‡自动同步金蝶设计(事件触发+定时重推+手动兜底).md | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 doc/docs/业务流程/2026-10-05-审批通过自动同步金蝶设计(事件触发+定时重推+手动兜底).md diff --git a/doc/docs/业务流程/2026-10-05-审批通过自动同步金蝶设计(事件触发+定时重推+手动兜底).md b/doc/docs/业务流程/2026-10-05-审批通过自动同步金蝶设计(事件触发+定时重推+手动兜底).md new file mode 100644 index 00000000..5f50817d --- /dev/null +++ b/doc/docs/业务流程/2026-10-05-审批通过自动同步金蝶设计(事件触发+定时重推+手动兜底).md @@ -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 上限 |