feat: 付款申请新增金蝶单据消失解锁兜底(A103-2连续两轮查询消失)

金蝶侧反审核删除付款申请单后,本地停留已生成/付款中且状态门禁禁止重推,单据卡死。
按 ADR-0005 新增 unlockBill():置生成失败、清空金蝶单据内码/编号(日志留档)、不自动重推;
A103-2 定时回写对已推送单据连续两轮未查得(Redis 计数,TTL 覆盖三个回写周期)触发解锁;
failed 单据不参与计数,Redis 异常放弃判定,重推链 A103-2 勾稽预检兜底防重复单据;
B901 UNLOCK 回调后续接入复用同一 unlockBill()
This commit is contained in:
刘泉佳
2026-09-29 01:50:36 +08:00
parent f4f4360e5a
commit 2079da6195
2 changed files with 101 additions and 2 deletions
@@ -73,6 +73,18 @@ public interface IPaymentApplicationService extends BaseService<PaymentApplicati
*/
void markKingdeeSyncFailed(Long id);
/**
* 金蝶单据解锁(ADR-0005):金蝶侧单据已被反审核删除时解除本地锁定——
* 金蝶状态置生成失败、清空金蝶单据内码与编号(原值日志留档),不自动重推,等待人工核实后重新推送。
* 触发来源:① B901 UNLOCK 回调(待建设);② A103-2 定时回写"已同步单据连续两轮查询消失"兜底。
* 仅对已生成/付款中单据生效,终态(已付款/已关闭)与生成失败单据自动跳过;
* 重推时 A103-2 勾稽预检兜底,误判场景(金蝶单据实际存在)自动回填不产生重复单据
*
* @param id 付款申请id
* @param source 解锁来源标识(B901 / A103-2,用于日志追溯)
*/
void unlockBill(Long id, String source);
/**
* 定时回写金蝶付款结果(A103-2 付款申请单查询 / A103-4 付款处理查询)
*
@@ -26,6 +26,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.redis.cache.BladeRedis;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.BeanUtil;
@@ -113,6 +114,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -130,6 +132,12 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
private static final String MOCK_PAYMENT_RECORD_PREFIX = "MOCK-PAY-";
/** 金蝶付款结果回写 single-flight 锁键前缀(多副本定时任务与手动触发共用) */
private static final String PAY_RESULT_SYNC_LOCK_PREFIX = "transport:payment-application:kingdee-pay-result-sync:";
/** A103-2 消失轮次 Redis 计数键前缀(金蝶侧疑似反审核删除的兜底判定,ADR-0005) */
private static final String PAY_RESULT_MISS_KEY_PREFIX = "transport:payment-application:kingdee-pay-miss:";
/** A103-2 消失解锁轮次阈值:连续两轮未查得即解锁置失败态 */
private static final int PAY_RESULT_MISS_UNLOCK_ROUNDS = 2;
/** A103-2 消失轮次计数键过期秒数:覆盖三个回写周期(10分钟/轮),超时未续计则重新累计(保守方向) */
private static final long PAY_RESULT_MISS_KEY_TTL_SECONDS = 2100L;
private final PaymentApplicationInvoiceMapper invoiceMapper;
private final PaymentApplicationRecordMapper recordMapper;
private final PaymentApplicationSettlementMapper settlementRelationMapper;
@@ -152,6 +160,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
private final KingdeeAttachmentSyncSupport kingdeeAttachmentSyncSupport;
private final BfiProperties bfiProperties;
private final KingdeeSyncLockSupport kingdeeSyncLockSupport;
private final BladeRedis bladeRedis;
/**
* 金蝶付款结果回写 single-flight 锁租约秒数,须大于最坏一轮耗时(分批远程查询 + 逐单回写),
@@ -526,6 +535,80 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
.set(PaymentApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void unlockBill(Long id, String source) {
if (id == null) return;
PaymentApplication entity = getById(id);
if (entity == null) return;
clearPayApplyMissKey(id);
if (!KingdeeBfiConstant.KINGDEE_STATUS_SYNCED.equals(entity.getKingdeeStatus())
&& !KingdeeBfiConstant.KINGDEE_STATUS_PAYING.equals(entity.getKingdeeStatus())) {
// 仅已生成/付款中单据处于"金蝶已确认收单"的锁定态;生成失败/终态无需解锁
log.info("付款申请[{}]金蝶单据解锁跳过(来源[{}]):kingdeeStatus=[{}]非锁定态",
entity.getPaymentNo(), source, entity.getKingdeeStatus());
return;
}
// 原金蝶单据编号/内码日志留档后清空:金蝶侧已反审核删除,原值不再有效,
// 附件上传、A103-4 勾稽均依赖单据内码,重推生成新单后自动重新关联
log.warn("付款申请[{}]金蝶单据解锁(来源[{}]):kingdeeStatus[{}]->[生成失败],原单据编号[{}]/内码[{}]已留档并清空,不自动重推,等待人工核实后重新推送",
entity.getPaymentNo(), source, entity.getKingdeeStatus(), entity.getKingdeeBillNo(), entity.getKingdeeBillId());
baseMapper.update(null, Wrappers.<PaymentApplication>lambdaUpdate()
.eq(PaymentApplication::getId, id)
.in(PaymentApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_SYNCED, KingdeeBfiConstant.KINGDEE_STATUS_PAYING)
.set(PaymentApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED)
.set(PaymentApplication::getKingdeeBillId, null)
.set(PaymentApplication::getKingdeeBillNo, null));
}
/**
* A103-2 未查得已推送单据的兜底判定(ADR-0005"连续两轮查询消失"):
* 仅对已生成/付款中单据经 Redis 计数累计消失轮次(生成失败单据金蝶查不到属正常态,等待人工重推,不参与计数),
* 连续达到阈值即解锁置失败态;重推链 A103-2 勾稽预检兜底,误判场景(金蝶单据实际存在)自动回填不产生重复单据
*
* @param application 本轮未查得的付款申请
*/
private void handlePayApplyMissed(PaymentApplication application) {
if (Func.isBlank(application.getPaymentNo())
|| (!KingdeeBfiConstant.KINGDEE_STATUS_SYNCED.equals(application.getKingdeeStatus())
&& !KingdeeBfiConstant.KINGDEE_STATUS_PAYING.equals(application.getKingdeeStatus()))) {
return;
}
String missKey = payApplyMissKey(application.getId());
Long rounds;
try {
rounds = bladeRedis.getStringRedisTemplate().opsForValue().increment(missKey);
bladeRedis.getStringRedisTemplate().expire(missKey, PAY_RESULT_MISS_KEY_TTL_SECONDS, TimeUnit.SECONDS);
} catch (Exception exception) {
// Redis 异常时放弃本轮判定:解锁是破坏性动作(清空金蝶单号),宁可延迟不可误判
log.warn("付款申请[{}]A103-2 消失轮次累计失败,本轮跳过解锁判定", application.getPaymentNo(), exception);
return;
}
if (rounds != null && rounds >= PAY_RESULT_MISS_UNLOCK_ROUNDS) {
log.warn("付款申请[{}]A103-2 连续[{}]轮未查得金蝶付款申请单,判定金蝶侧已反审核删除,执行解锁",
application.getPaymentNo(), rounds);
self().unlockBill(application.getId(), "A103-2 连续" + rounds + "轮查询消失");
} else {
log.warn("付款申请[{}]A103-2 未查得金蝶付款申请单(第[{}]轮),金蝶侧可能已反审核删除,下一轮复核",
application.getPaymentNo(), rounds);
}
}
/** A103-2 消失轮次计数键 */
private String payApplyMissKey(Long id) {
return PAY_RESULT_MISS_KEY_PREFIX + id;
}
/** 清除 A103-2 消失轮次计数(金蝶重新查得或单据解锁后调用) */
private void clearPayApplyMissKey(Long id) {
if (id == null) return;
try {
bladeRedis.getStringRedisTemplate().delete(payApplyMissKey(id));
} catch (Exception exception) {
log.warn("付款申请[{}]清除 A103-2 消失轮次计数失败", id, exception);
}
}
@Override
public List<String> syncKingdeeBatch(List<Long> ids) {
if (ids == null || ids.isEmpty()) throw new ServiceException("请至少选择一条审批通过的付款申请");
@@ -588,8 +671,12 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
for (PaymentApplication application : applications) {
BfiPayApplyVO payApply = payApplyMap.get(application.getPaymentNo());
if (payApply == null) {
// 金蝶侧未查得已推送单据(查询批次其余单据正常返回,排除服务整体故障):
// 累计消失轮次,连续两轮视为金蝶已反审核删除,解锁置失败交人工核实重推
this.handlePayApplyMissed(application);
continue;
}
clearPayApplyMissKey(application.getId());
try {
if (self.applyPayApplyResult(application, payApply)) paidCount++;
updated++;
@@ -957,8 +1044,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
// 往来户.编码:合同乙方关联客商档案客商编码
entry.setEAsstactNumber(kingdeeApBillSyncSupport.bfiAsstactNumber(contract, context));
entry.setEAssacct(entity.getBankAccount());
// 往来银行.编码:与 A103-1 统一口径,取付款申请所选客商收款账户联行号(未维护即拦截)
entry.setEBebankNumber(this.bfiBebankNumber(entity, context));
// 往来银行.编码:与 A103-1 统一口径,取付款申请所选客商收款账户联行号(未维护即拦截)
entry.setEBebankNumber(this.bfiBebankNumber(entity, context));
// 应付金额
// entry.setEPayamount(applyAmount);
// 申请金额