feat: 新增金蝶failed单据定时重推任务(兜底)

- 新增 KingdeeAutoSyncRetryTask:仅重推各域 failed,不扫 unsynced(存量审批通过但从未推送的历史单不自动补推,仍走手动按钮)
- 4域新增 retryKingdeeFailed:逐单独立成败不中断,返回执行摘要;前置未就绪再次failed下轮继续,节点硬顺序由既有 A102-4/A302-4/A303-4/A801-3 预检保证,不做级联
- 收票重推以落库状态 pushed/assigned 判成败(批量推送方法逐单吞异常返回摘要)
- 开关 thirdParty.bfi.autoSync.retryEnabled 默认关闭,cron 默认每10分钟;重推无次数上限(噪音可接受后再加 retry_count)
This commit is contained in:
刘泉佳
2026-10-10 13:50:12 +08:00
parent 03b343102e
commit cfbbc756eb
9 changed files with 254 additions and 0 deletions
@@ -60,6 +60,14 @@ public interface IFormalSettlementService extends BaseService<FormalSettlement>
*/
void markKingdeeSyncFailed(Long id, String reason);
/**
* 重推金蝶生成失败(kingdeeSyncStatus=failed 且审批通过)的正式结算单:逐单独立成败,失败不中断。
* 只扫 failed 不扫 unsynced——存量"审批通过但从未推送"的历史单不自动补推,仍走手动按钮
*
* @return 执行摘要
*/
String retryKingdeeFailed();
String syncKingdeeArResult();
List<FormalSettlementDetailFee> detailFees(Long detailId);
void adjustDetail(PreSettlementDetailAdjustRequest request);
@@ -74,5 +74,13 @@ public interface IInvoiceApplicationService extends BaseService<InvoiceApplicati
*/
void markKingdeeSyncFailed(Long id, String reason);
/**
* 重推金蝶生成失败(kingdeeStatus=failed 且审批通过)的开票申请:逐单独立成败,失败不中断。
* 只扫 failed 不扫 unsynced——存量"审批通过但从未推送"的历史单不自动补推,仍走手动按钮
*
* @return 执行摘要
*/
String retryKingdeeFailed();
String syncKingdeeInvoiceResult();
}
@@ -125,6 +125,14 @@ public interface IInvoiceReceiptService extends BaseService<InvoiceReceipt> {
*/
String pushKingdeeReceivedInvoice(List<Long> ids, boolean autoSync);
/**
* 重推金蝶推送失败(kingdeePushStatus=failed 且审批通过)的收票登记:逐单独立成败,失败不中断。
* 只扫 failed 不扫 unsynced——存量"审批通过但从未推送"的历史单不自动补推,仍走手动按钮
*
* @return 执行摘要
*/
String retryKingdeeFailed();
/**
* 定时回写金蝶收票单结果(A801-3):金蝶侧删除/红冲/作废 → 回收为 failed 提示人工(不自动作废登记);
* pushed(保存成功后中断)→ 指定补偿;assigned 仅补登缺失的收票单号
@@ -73,6 +73,14 @@ public interface IPaymentApplicationService extends BaseService<PaymentApplicati
*/
void markKingdeeSyncFailed(Long id);
/**
* 重推金蝶生成失败(kingdeeStatus=failed 且审批通过)的付款申请:逐单独立成败,失败不中断。
* 只扫 failed 不扫 unsynced——存量"审批通过但从未推送"的历史单不自动补推,仍走手动按钮
*
* @return 执行摘要
*/
String retryKingdeeFailed();
/**
* 金蝶单据解锁(ADR-0005):金蝶侧单据已被反审核删除时解除本地锁定——
* 金蝶状态置生成失败、清空金蝶单据内码与编号(原值日志留档),不自动重推,等待人工核实后重新推送。
@@ -603,6 +603,44 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
}
}
@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 按防重键反查回填;
@@ -553,6 +553,33 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
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 定位金蝶单据;
* 本地缺失(存量数据)跳过待重推补传;附件上传异常独立捕获,绝不影响主单同步结果
@@ -637,6 +637,41 @@ 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,断点可凭防重键续推。
@@ -539,6 +539,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) {
@@ -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);
}
}
}