feat: 金蝶冲销状态回写拆分独立定时任务并支持分批配置

- 新增KingdeeWriteOffResultTask独立调度(Nacos thirdParty.bfi.writeOffResult.*开关/cron),从付款结果任务第④步拆出
- BfiProperties新增writeOffResult配置:syncEnabled开关、queryBatchSize分批大小默认1000,冲销批量查询与批量落库共用
- 回写仅扫描已同步且未冲销的结算单,已冲销为终态不再复查
- kingdee_write_off_status列默认值改为unwrittenoff,存量已同步NULL数据回填未冲销
- 补充扫描数/防重键数/金蝶返回数/落库数关键日志,summary文案修正为待查未冲销结算单
This commit is contained in:
刘泉佳
2026-10-30 23:04:10 +08:00
parent 6c0f15290a
commit 014d254295
7 changed files with 113 additions and 17 deletions
@@ -115,6 +115,11 @@ public class BfiProperties {
*/
private ArResult arResult = new ArResult();
/**
* 金蝶冲销状态回写配置(A102-4/A302-4)
*/
private WriteOffResult writeOffResult = new WriteOffResult();
/**
* 金蝶单据锁定补偿配置
*/
@@ -374,6 +379,24 @@ public class BfiProperties {
private long lockLeaseSeconds = 1800;
}
/**
* 金蝶冲销状态回写配置项(A102-4/A302-4):定时回写开关、防重键批量查询与结算单批量更新的分批大小,
* 防止单批过大超出金蝶查询接口与批量落库承载能力
*/
@Data
public static class WriteOffResult {
/**
* 冲销状态回写任务开关,默认关闭(安全兜底),由 Nacos 按环境显式开启
*/
private boolean syncEnabled = false;
/**
* 冲销状态批量查询/批量更新分批大小,默认 1000
*/
private int queryBatchSize = 1000;
}
/**
* 金蝶单据锁定补偿配置项
*/
@@ -1172,14 +1172,17 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
*/
private String executeWriteOffSync() {
long startMillis = System.currentTimeMillis();
// 已同步金蝶的结算单才在金蝶存在旧应付/应收单,未同步/生成失败不查询(前端展示 '-')
// 已同步金蝶的结算单才在金蝶存在旧应付/应收单,未同步/生成失败不查询(前端展示 '-');
// 已冲销为终态不再复查,仅扫未冲销单据(存量 NULL 由 06 脚本回填默认未冲销)
List<FormalSettlement> settlements = list(Wrappers.<FormalSettlement>lambdaQuery()
.eq(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_SYNCED)
.eq(FormalSettlement::getKingdeeWriteOffStatus, KingdeeBfiConstant.KINGDEE_WRITEOFF_UNWRITTEN)
.orderByAsc(FormalSettlement::getId));
if (Func.isEmpty(settlements)) {
log.info("金蝶冲销状态回写:无已同步结算单");
return "金蝶冲销状态回写:无已同步结算单";
log.info("金蝶冲销状态回写:无待查未冲销结算单");
return "金蝶冲销状态回写:无待查未冲销结算单";
}
log.info("金蝶冲销状态回写:本轮扫描待查未冲销结算单{}笔", settlements.size());
// 结算明细分批一次查全(消除逐单查询 N+1),按结算单 id 分组后按结算类型组装防重键
List<Long> settlementIds = settlements.stream().map(FormalSettlement::getId).toList();
Map<Long, List<FormalSettlementDetail>> detailsBySettlement = new HashMap<>();
@@ -1209,17 +1212,24 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
}
}
// A102-4/A302-4 整批一次查询,冲销状态有变化的才落库(updateBatchById 批量更新,避免逐单 update)
log.info("金蝶冲销状态回写:防重键组装完成,应付{}个、应收{}个,按批次大小{}分批查询",
apSettlementBySrcCode.size(), arSettlementBySrcCode.size(), bfiProperties.getWriteOffResult().getQueryBatchSize());
Map<String, Boolean> apWriteOffFlags = kingdeeApBillSyncSupport.queryApBillWriteOffFlags(new ArrayList<>(apSettlementBySrcCode.keySet()));
Map<String, Boolean> arWriteOffFlags = kingdeeArBillSyncSupport.queryArBillWriteOffFlags(new ArrayList<>(arSettlementBySrcCode.keySet()));
// 金蝶未返回的键按未冲销处理,返回数与组装数差异即金蝶侧查不到单据的规模,留痕便于对账
log.info("金蝶冲销状态批量查询完成:应付键返回{}个、应收键返回{}个,未返回键按未冲销处理",
apWriteOffFlags.size(), arWriteOffFlags.size());
List<FormalSettlement> toUpdateSettlements = new ArrayList<>();
apSettlementBySrcCode.forEach((srcCode, settlement) ->
this.applyWriteOffFlag(settlement, apWriteOffFlags.get(srcCode), toUpdateSettlements));
arSettlementBySrcCode.forEach((srcCode, settlement) ->
this.applyWriteOffFlag(settlement, arWriteOffFlags.get(srcCode), toUpdateSettlements));
if (Func.isNotEmpty(toUpdateSettlements)) {
updateBatchById(toUpdateSettlements);
// 批量落库按同一分批大小拆分,避免单批过大
updateBatchById(toUpdateSettlements, bfiProperties.getWriteOffResult().getQueryBatchSize());
log.info("金蝶冲销状态回写:批量落库完成,本批更新{}笔", toUpdateSettlements.size());
}
String summary = "金蝶冲销状态回写完成:待查已同步结算单" + (apSettlementBySrcCode.size() + arSettlementBySrcCode.size())
String summary = "金蝶冲销状态回写完成:待查未冲销结算单" + (apSettlementBySrcCode.size() + arSettlementBySrcCode.size())
+ "笔,回写" + toUpdateSettlements.size() + "笔";
log.info("{},耗时{}ms", summary, System.currentTimeMillis() - startMillis);
return summary;
@@ -225,7 +225,7 @@ public class KingdeeApBillSyncSupport {
*/
public Map<String, Boolean> queryApBillWriteOffFlags(List<String> awrpSrcCodes) {
Map<String, Boolean> writeOffFlags = new LinkedHashMap<>();
batchForeach(awrpSrcCodes, PAY_RESULT_QUERY_BATCH, batch -> {
batchForeach(awrpSrcCodes, bfiProperties.getWriteOffResult().getQueryBatchSize(), batch -> {
BfiApBillQueryDTO param = new BfiApBillQueryDTO();
param.setAwrpSrcCode(batch);
param.setAwrpSrcSys(bfiProperties.getSrcSys());
@@ -163,7 +163,7 @@ public class KingdeeArBillSyncSupport {
*/
public Map<String, Boolean> queryArBillWriteOffFlags(List<String> arBillSrcCodes) {
Map<String, Boolean> writeOffFlags = new LinkedHashMap<>();
kingdeeApBillSyncSupport.batchForeach(arBillSrcCodes, AR_RESULT_QUERY_BATCH, batch -> {
kingdeeApBillSyncSupport.batchForeach(arBillSrcCodes, bfiProperties.getWriteOffResult().getQueryBatchSize(), batch -> {
BfiArBillQueryDTO param = new BfiArBillQueryDTO();
param.setAwrpSrcCode(batch);
param.setAwrpSrcSys(bfiProperties.getSrcSys());
@@ -0,0 +1,63 @@
/**
* 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.config.BfiProperties;
import org.springblade.transport.service.IFormalSettlementService;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 金蝶冲销状态定时回写任务
* <p>
* A102-4/A302-4 批量查询已同步结算单对应金蝶旧应付/应收单的 hadwrittenoff(部分冲销视同已冲销),
* 回写结算单冲销状态,用于结算单列表展示金蝶旧单是否已冲销。
* 业务逻辑收敛在服务方法(分布式锁 + 分批查询 + 批量落库),便于手动触发联调。
* <p>
* 调度配置(Nacos thirdParty.bfi.writeOffResult.*):syncEnabled 开关、cron 表达式、queryBatchSize 分批大小。
*
* @author Chill
*/
@Slf4j
@Component
@RefreshScope
@RequiredArgsConstructor
public class KingdeeWriteOffResultTask {
private final IFormalSettlementService formalSettlementService;
private final BfiProperties bfiProperties;
/** 金蝶冲销状态回写调度,默认每10分钟执行一次(分钟位与付款结果回写错峰) */
@Scheduled(cron = "${thirdParty.bfi.writeOffResult.cron:0 4/10 * * * ?}")
public void syncKingdeeWriteOffResult() {
if (!bfiProperties.getWriteOffResult().isSyncEnabled()) {
return;
}
try {
String summary = formalSettlementService.syncKingdeeWriteOffResult();
log.info("定时回写金蝶冲销状态:{}", summary);
} catch (Exception exception) {
log.error("定时回写金蝶冲销状态异常", exception);
}
}
}
@@ -86,13 +86,6 @@ public class PaymentKingdeeResultTask {
} catch (Exception exception) {
log.error("定时回写金蝶开票结果异常", exception);
}
// ④ A102-4/A302-4 冲销状态回写(结算单列表展示金蝶旧单是否已冲销)
try {
String summary = formalSettlementService.syncKingdeeWriteOffResult();
log.info("定时回写金蝶冲销状态:{}", summary);
} catch (Exception exception) {
log.error("定时回写金蝶冲销状态异常", exception);
}
}
/** 金蝶收票单结果回写调度(异常回收/指定补偿/单号补登),默认每10分钟执行一次 */
@@ -2,7 +2,7 @@
-- 列名遵循 MyBatis-Plus 驼峰转下划线默认映射(kingdeeWriteOffStatus -> kingdee_write_off_status,kingdeeWriteOffTime -> kingdee_write_off_time);
-- 若存在早期脚本误建的旧列名 kingdee_writeoff_status / kingdee_writeoff_time 则先重命名,否则按缺失新增
-- 结算单列表展示金蝶侧旧应付/应收单冲销状态:定时任务按 A102-4/A302-4 hadwrittenoff 批量回写,
-- 选单预检/重推闸门实时查询后顺带回写;NULL=未推送金蝶或尚未校验,unwrittenoff-未冲销,writtenoff-已冲销(部分冲销视同已冲销)
-- 选单预检/重推闸门实时查询后顺带回写;默认 unwrittenoff-未冲销(金蝶单据在未冲销前即视为未冲销,避免首跑全量初始化回写),writtenoff-已冲销(部分冲销视同已冲销)
-- ==================== kingdee_write_off_status ====================
-- 旧列名重命名(早期脚本版本,不存在则跳过)
@@ -12,7 +12,7 @@ SET @old_col := (
AND COLUMN_NAME = 'kingdee_writeoff_status'
);
SET @ddl_sql = IF(@old_col = 1,
'ALTER TABLE `blade_formal_settlement` CHANGE COLUMN `kingdee_writeoff_status` `kingdee_write_off_status` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶冲销状态:unwrittenoff-未冲销 writtenoff-已冲销(NULL=未推送金蝶或未校验)''',
'ALTER TABLE `blade_formal_settlement` CHANGE COLUMN `kingdee_writeoff_status` `kingdee_write_off_status` varchar(20) COLLATE utf8mb4_general_ci DEFAULT ''unwrittenoff'' COMMENT ''金蝶冲销状态:unwrittenoff-未冲销(默认) writtenoff-已冲销''',
'SELECT 1');
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
@@ -21,7 +21,7 @@ DEALLOCATE PREPARE ddl_stmt;
-- 缺失则新增(支持重复执行)
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_formal_settlement` ADD COLUMN `kingdee_write_off_status` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶冲销状态:unwrittenoff-未冲销 writtenoff-已冲销(NULL=未推送金蝶或未校验)'' AFTER `kingdee_fail_reason`',
'ALTER TABLE `blade_formal_settlement` ADD COLUMN `kingdee_write_off_status` varchar(20) COLLATE utf8mb4_general_ci DEFAULT ''unwrittenoff'' COMMENT ''金蝶冲销状态:unwrittenoff-未冲销(默认) writtenoff-已冲销'' AFTER `kingdee_fail_reason`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
@@ -32,6 +32,13 @@ PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- 存量已同步结算单回填默认未冲销(可重复执行:仅补 NULL 行;未同步单据保持默认值即可,前端按同步状态展示'-')
UPDATE `blade_formal_settlement`
SET `kingdee_write_off_status` = 'unwrittenoff'
WHERE `kingdee_write_off_status` IS NULL
AND `kingdee_sync_status` = 'synced'
AND `is_deleted` = 0;
-- ==================== kingdee_write_off_time ====================
-- 旧列名重命名(早期脚本版本,不存在则跳过)
SET @old_col := (