From f24ad2af1ca6c8c6dae2e6cd48ecd1d787c6be57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Fri, 30 Oct 2026 21:23:53 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E7=BB=93=E7=AE=97=E5=8D=95?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=96=B0=E5=A2=9E=E9=87=91=E8=9D=B6=E5=86=B2?= =?UTF-8?q?=E9=94=80=E7=8A=B6=E6=80=81=EF=BC=88=E5=AE=9A=E6=97=B6=E5=9B=9E?= =?UTF-8?q?=E5=86=99+=E9=A2=84=E6=A3=80/=E9=87=8D=E6=8E=A8=E5=AE=9E?= =?UTF-8?q?=E6=97=B6=E5=9B=9E=E5=86=99=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 正式结算单表新增 kingdee_writeoff_status/kingdee_writeoff_time 字段(可重复执行DDL) - A102-4/A302-4 新增冲销标志批量查询,原单键冲销查询复用批量 - 金蝶结果定时任务新增子流程④:已同步结算单冲销状态批量回写(single-flight 锁) - 结算调整选单预检、重推硬闸门实时查询后顺带回写单笔冲销状态 --- .../pojo/entity/FormalSettlement.java | 8 ++ .../constant/KingdeeBfiConstant.java | 6 ++ .../service/IFormalSettlementService.java | 17 +++ .../impl/FormalSettlementServiceImpl.java | 100 ++++++++++++++++++ .../impl/SettlementAdjustmentServiceImpl.java | 4 + .../support/KingdeeApBillSyncSupport.java | 39 ++++--- .../support/KingdeeArBillSyncSupport.java | 39 ++++--- .../task/PaymentKingdeeResultTask.java | 7 ++ ...l_settlement_kingdee_writeoff_20261030.sql | 31 ++++++ 9 files changed, 227 insertions(+), 24 deletions(-) create mode 100644 doc/sql/transport/lqj/temp/06_blade_formal_settlement_kingdee_writeoff_20261030.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java index 66fbf13d..2a2b36ca 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java @@ -164,6 +164,14 @@ public class FormalSettlement extends TenantEntity { * 金蝶推送失败原因(仅 failed 态展示;重推成功转 synced 后残留原因不再回显) */ private String kingdeeFailReason; + /** + * 金蝶冲销状态:unwrittenoff-未冲销 writtenoff-已冲销(null=未推送金蝶或未校验) + */ + private String kingdeeWriteOffStatus; + /** + * 金蝶冲销状态确认时间(定时回写/预检回写时间) + */ + private LocalDateTime kingdeeWriteOffTime; /** * 附件JSON */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java index b1c45f44..052a50e1 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java @@ -171,6 +171,12 @@ public final class KingdeeBfiConstant { /** 金蝶同步状态:生成失败(推送异常/金蝶返回失败,可重推)。 */ public static final String KINGDEE_STATUS_FAILED = "failed"; + /** 金蝶冲销状态:未冲销(旧应付/应收单在金蝶尚未冲销)。 */ + public static final String KINGDEE_WRITEOFF_UNWRITTEN = "unwrittenoff"; + + /** 金蝶冲销状态:已冲销(旧应付/应收单已在金蝶冲销,部分冲销视同已冲销)。 */ + public static final String KINGDEE_WRITEOFF_WRITTEN = "writtenoff"; + /** 收票单推送状态:未推送(新建单据默认,票池绑定层的 kingdee_status 与此无关)。 */ public static final String RECEIVED_PUSH_UNSYNCED = "unsynced"; 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 daf1d429..34c5649b 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 @@ -80,6 +80,23 @@ public interface IFormalSettlementService extends BaseService String retryKingdeeFailed(); String syncKingdeeArResult(); + + /** + * 金蝶冲销状态定时回写:扫描已同步金蝶的结算单,按防重键批量查询 A102-4/A302-4 的 + * hadwrittenoff(部分冲销视同已冲销),回写冲销状态供结算单列表展示 + * + * @return 执行摘要 + */ + String syncKingdeeWriteOffResult(); + + /** + * 单笔刷新金蝶冲销状态(选单预检/重推闸门实时查询后回写,状态未变化不落库) + * + * @param settlementId 正式结算单id + * @param writtenOff 金蝶侧应付/应收单是否已冲销 + */ + void refreshKingdeeWriteOff(Long settlementId, boolean writtenOff); + List detailFees(Long detailId); void adjustDetail(PreSettlementDetailAdjustRequest request); void refreshPaymentSummary(Long settlementId); 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 0e978b4f..c1751638 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 @@ -154,6 +154,9 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl分批与事务口径同 {@link #executeArResultSync()}:结算明细分批 in 查询、远程整批查询消除逐单 N+1, + * 仅回写状态有变化的结算单并 updateBatchById 批量落库;整轮任务不包外层事务,按防重键幂等可重跑。 + * + * @return 执行摘要 + */ + private String executeWriteOffSync() { + long startMillis = System.currentTimeMillis(); + // 已同步金蝶的结算单才在金蝶存在旧应付/应收单,未同步/生成失败不查询(前端展示 '-') + List settlements = list(Wrappers.lambdaQuery() + .eq(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_SYNCED) + .orderByAsc(FormalSettlement::getId)); + if (Func.isEmpty(settlements)) { + log.info("金蝶冲销状态回写:无已同步结算单"); + return "金蝶冲销状态回写:无已同步结算单"; + } + // 结算明细分批一次查全(消除逐单查询 N+1),按结算单 id 分组后按结算类型组装防重键 + List settlementIds = settlements.stream().map(FormalSettlement::getId).toList(); + Map> detailsBySettlement = new HashMap<>(); + kingdeeApBillSyncSupport.batchForeach(settlementIds, AR_RESULT_DETAIL_QUERY_BATCH, batch -> + detailsBySettlement.putAll(detailMapper.selectList(Wrappers.lambdaQuery() + .in(FormalSettlementDetail::getFormalSettlementId, batch) + .eq(FormalSettlementDetail::getIsDeleted, 0) + .orderByAsc(FormalSettlementDetail::getLineNo)).stream() + .collect(Collectors.groupingBy(FormalSettlementDetail::getFormalSettlementId)))); + Map apSettlementBySrcCode = new LinkedHashMap<>(); + Map arSettlementBySrcCode = new LinkedHashMap<>(); + for (FormalSettlement settlement : settlements) { + List details = detailsBySettlement.getOrDefault(settlement.getId(), List.of()); + if (Func.isEmpty(details)) { + continue; + } + if ("receivable".equals(settlement.getSettlementType())) { + String srcCode = kingdeeArBillSyncSupport.arBillSrcCode(settlement, details); + if (Func.isNotBlank(srcCode)) { + arSettlementBySrcCode.putIfAbsent(srcCode, settlement); + } + } else { + String srcCode = kingdeeApBillSyncSupport.apBillSrcCode(settlement, details); + if (Func.isNotBlank(srcCode)) { + apSettlementBySrcCode.putIfAbsent(srcCode, settlement); + } + } + } + // A102-4/A302-4 整批一次查询,冲销状态有变化的才落库(updateBatchById 批量更新,避免逐单 update) + Map apWriteOffFlags = kingdeeApBillSyncSupport.queryApBillWriteOffFlags(new ArrayList<>(apSettlementBySrcCode.keySet())); + Map arWriteOffFlags = kingdeeArBillSyncSupport.queryArBillWriteOffFlags(new ArrayList<>(arSettlementBySrcCode.keySet())); + List 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); + } + String summary = "金蝶冲销状态回写完成:待查已同步结算单" + (apSettlementBySrcCode.size() + arSettlementBySrcCode.size()) + + "笔,回写" + toUpdateSettlements.size() + "笔"; + log.info("{},耗时{}ms", summary, System.currentTimeMillis() - startMillis); + return summary; + } + + /** 应用单笔冲销查询结果:与本地冲销状态不一致时标记更新(查不到金蝶单据按未冲销处理),一致跳过 */ + private void applyWriteOffFlag(FormalSettlement settlement, Boolean writtenOff, List toUpdateSettlements) { + String status = Boolean.TRUE.equals(writtenOff) ? KingdeeBfiConstant.KINGDEE_WRITEOFF_WRITTEN : KingdeeBfiConstant.KINGDEE_WRITEOFF_UNWRITTEN; + if (status.equals(settlement.getKingdeeWriteOffStatus())) { + return; + } + settlement.setKingdeeWriteOffStatus(status); + settlement.setKingdeeWriteOffTime(LocalDateTime.now()); + toUpdateSettlements.add(settlement); + log.info("金蝶冲销状态回写:正式结算单[{}],{}", settlement.getFormalSettlementNo(), status); + } + @Override @Transactional(rollbackFor = Exception.class) public String applyPayment(FormalSettlementPaymentRequest request) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java index f7dd22e3..8636798d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java @@ -246,6 +246,8 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl { - param.setPageNo(pageNo); - param.setPageSize(PAY_RESULT_PAGE_SIZE); - return bfiService.queryApBill(param); - })) { - if (awrpSrcCode.equals(apBill.getAwrpSrcCode()) && Boolean.TRUE.equals(apBill.getHadwrittenoff())) { - return true; + return queryApBillWriteOffFlags(List.of(awrpSrcCode)).getOrDefault(awrpSrcCode, Boolean.FALSE); + } + + /** + * A102-4 冲销状态批量查询:防重键批量分批查询财务应付单,返回 防重键 -> 是否已冲销 + * (hadwrittenoff=true,部分冲销同样视为已冲销;同键多单取或,查不到的键不在结果中即视为未冲销)。 + * 定时回写结算单金蝶冲销状态用,整批一次查询消除逐单远程查询 N+1 + * + * @param awrpSrcCodes 财务应付单防重键列表 + * @return 防重键 -> 是否已冲销 + */ + public Map queryApBillWriteOffFlags(List awrpSrcCodes) { + Map writeOffFlags = new LinkedHashMap<>(); + batchForeach(awrpSrcCodes, PAY_RESULT_QUERY_BATCH, batch -> { + BfiApBillQueryDTO param = new BfiApBillQueryDTO(); + param.setAwrpSrcCode(batch); + param.setAwrpSrcSys(bfiProperties.getSrcSys()); + for (BfiApBillVO apBill : BfiUtil.queryAllPages(pageNo -> { + param.setPageNo(pageNo); + param.setPageSize(PAY_RESULT_PAGE_SIZE); + return bfiService.queryApBill(param); + })) { + if (Func.isNotEmpty(apBill.getAwrpSrcCode())) { + writeOffFlags.merge(apBill.getAwrpSrcCode(), Boolean.TRUE.equals(apBill.getHadwrittenoff()), Boolean::logicalOr); + } } - } - return false; + }); + return writeOffFlags; } // ==================== A102-2 财务应付单保存 ==================== diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java index f9e214c1..845fc575 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java @@ -150,19 +150,34 @@ public class KingdeeArBillSyncSupport { if (Func.isBlank(arBillSrcCode)) { return false; } - BfiArBillQueryDTO param = new BfiArBillQueryDTO(); - param.setAwrpSrcCode(List.of(arBillSrcCode)); - param.setAwrpSrcSys(bfiProperties.getSrcSys()); - for (BfiArBillVO arBill : BfiUtil.queryAllPages(pageNo -> { - param.setPageNo(pageNo); - param.setPageSize(AR_RESULT_PAGE_SIZE); - return bfiService.queryArBill(param); - })) { - if (arBillSrcCode.equals(arBill.getAwrpSrcCode()) && Boolean.TRUE.equals(arBill.getHadwrittenoff())) { - return true; + return queryArBillWriteOffFlags(List.of(arBillSrcCode)).getOrDefault(arBillSrcCode, Boolean.FALSE); + } + + /** + * A302-4 冲销状态批量查询:防重键批量分批查询财务应收单,返回 防重键 -> 是否已冲销 + * (hadwrittenoff=true,部分冲销同样视为已冲销;同键多单取或,查不到的键不在结果中即视为未冲销)。 + * 定时回写结算单金蝶冲销状态用,整批一次查询消除逐单远程查询 N+1 + * + * @param arBillSrcCodes 财务应收单防重键列表 + * @return 防重键 -> 是否已冲销 + */ + public Map queryArBillWriteOffFlags(List arBillSrcCodes) { + Map writeOffFlags = new LinkedHashMap<>(); + kingdeeApBillSyncSupport.batchForeach(arBillSrcCodes, AR_RESULT_QUERY_BATCH, batch -> { + BfiArBillQueryDTO param = new BfiArBillQueryDTO(); + param.setAwrpSrcCode(batch); + param.setAwrpSrcSys(bfiProperties.getSrcSys()); + for (BfiArBillVO arBill : BfiUtil.queryAllPages(pageNo -> { + param.setPageNo(pageNo); + param.setPageSize(AR_RESULT_PAGE_SIZE); + return bfiService.queryArBill(param); + })) { + if (Func.isNotEmpty(arBill.getAwrpSrcCode())) { + writeOffFlags.merge(arBill.getAwrpSrcCode(), Boolean.TRUE.equals(arBill.getHadwrittenoff()), Boolean::logicalOr); + } } - } - return false; + }); + return writeOffFlags; } // ==================== A302-2 财务应收单保存 ==================== diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java index e5e4fb87..e5160721 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java @@ -86,6 +86,13 @@ 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分钟执行一次 */ diff --git a/doc/sql/transport/lqj/temp/06_blade_formal_settlement_kingdee_writeoff_20261030.sql b/doc/sql/transport/lqj/temp/06_blade_formal_settlement_kingdee_writeoff_20261030.sql new file mode 100644 index 00000000..69f14db3 --- /dev/null +++ b/doc/sql/transport/lqj/temp/06_blade_formal_settlement_kingdee_writeoff_20261030.sql @@ -0,0 +1,31 @@ +-- 正式结算单:新增金蝶冲销状态字段 +-- 结算单列表展示金蝶侧旧应付/应收单冲销状态:定时任务按 A102-4/A302-4 hadwrittenoff 批量回写, +-- 选单预检/重推闸门实时查询后顺带回写;NULL=未推送金蝶或尚未校验,unwrittenoff-未冲销,writtenoff-已冲销(部分冲销视同已冲销) + +-- 以下结构升级支持重复执行 +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_formal_settlement` ADD COLUMN `kingdee_writeoff_status` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶冲销状态:unwrittenoff-未冲销 writtenoff-已冲销(NULL=未推送金蝶或未校验)'' AFTER `kingdee_fail_reason`', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'blade_formal_settlement' + AND COLUMN_NAME = 'kingdee_writeoff_status' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +-- 以下结构升级支持重复执行 +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_formal_settlement` ADD COLUMN `kingdee_writeoff_time` datetime DEFAULT NULL COMMENT ''金蝶冲销状态确认时间(定时回写/预检回写时间)'' AFTER `kingdee_writeoff_status`', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'blade_formal_settlement' + AND COLUMN_NAME = 'kingdee_writeoff_time' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; From fd002ea4796c5cdd4137a667c1724598859437a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Fri, 30 Oct 2026 21:23:53 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=E7=BB=93=E7=AE=97=E5=8D=95?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=96=B0=E5=A2=9E=E9=87=91=E8=9D=B6=E5=86=B2?= =?UTF-8?q?=E9=94=80=E7=8A=B6=E6=80=81=EF=BC=88=E5=AE=9A=E6=97=B6=E5=9B=9E?= =?UTF-8?q?=E5=86=99+=E9=A2=84=E6=A3=80/=E9=87=8D=E6=8E=A8=E5=AE=9E?= =?UTF-8?q?=E6=97=B6=E5=9B=9E=E5=86=99=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 正式结算单表新增 kingdee_writeoff_status/kingdee_writeoff_time 字段(可重复执行DDL) - A102-4/A302-4 新增冲销标志批量查询,原单键冲销查询复用批量 - 金蝶结果定时任务新增子流程④:已同步结算单冲销状态批量回写(single-flight 锁) - 结算调整选单预检、重推硬闸门实时查询后顺带回写单笔冲销状态 --- .../pojo/entity/FormalSettlement.java | 8 ++ .../constant/KingdeeBfiConstant.java | 6 ++ .../service/IFormalSettlementService.java | 17 +++ .../impl/FormalSettlementServiceImpl.java | 100 ++++++++++++++++++ .../impl/SettlementAdjustmentServiceImpl.java | 7 +- .../support/KingdeeApBillSyncSupport.java | 39 ++++--- .../support/KingdeeArBillSyncSupport.java | 39 ++++--- .../task/PaymentKingdeeResultTask.java | 7 ++ ...l_settlement_kingdee_writeoff_20261030.sql | 31 ++++++ 9 files changed, 229 insertions(+), 25 deletions(-) create mode 100644 doc/sql/transport/lqj/temp/06_blade_formal_settlement_kingdee_writeoff_20261030.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java index 66fbf13d..2a2b36ca 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java @@ -164,6 +164,14 @@ public class FormalSettlement extends TenantEntity { * 金蝶推送失败原因(仅 failed 态展示;重推成功转 synced 后残留原因不再回显) */ private String kingdeeFailReason; + /** + * 金蝶冲销状态:unwrittenoff-未冲销 writtenoff-已冲销(null=未推送金蝶或未校验) + */ + private String kingdeeWriteOffStatus; + /** + * 金蝶冲销状态确认时间(定时回写/预检回写时间) + */ + private LocalDateTime kingdeeWriteOffTime; /** * 附件JSON */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java index b1c45f44..052a50e1 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeBfiConstant.java @@ -171,6 +171,12 @@ public final class KingdeeBfiConstant { /** 金蝶同步状态:生成失败(推送异常/金蝶返回失败,可重推)。 */ public static final String KINGDEE_STATUS_FAILED = "failed"; + /** 金蝶冲销状态:未冲销(旧应付/应收单在金蝶尚未冲销)。 */ + public static final String KINGDEE_WRITEOFF_UNWRITTEN = "unwrittenoff"; + + /** 金蝶冲销状态:已冲销(旧应付/应收单已在金蝶冲销,部分冲销视同已冲销)。 */ + public static final String KINGDEE_WRITEOFF_WRITTEN = "writtenoff"; + /** 收票单推送状态:未推送(新建单据默认,票池绑定层的 kingdee_status 与此无关)。 */ public static final String RECEIVED_PUSH_UNSYNCED = "unsynced"; 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 daf1d429..34c5649b 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 @@ -80,6 +80,23 @@ public interface IFormalSettlementService extends BaseService String retryKingdeeFailed(); String syncKingdeeArResult(); + + /** + * 金蝶冲销状态定时回写:扫描已同步金蝶的结算单,按防重键批量查询 A102-4/A302-4 的 + * hadwrittenoff(部分冲销视同已冲销),回写冲销状态供结算单列表展示 + * + * @return 执行摘要 + */ + String syncKingdeeWriteOffResult(); + + /** + * 单笔刷新金蝶冲销状态(选单预检/重推闸门实时查询后回写,状态未变化不落库) + * + * @param settlementId 正式结算单id + * @param writtenOff 金蝶侧应付/应收单是否已冲销 + */ + void refreshKingdeeWriteOff(Long settlementId, boolean writtenOff); + List detailFees(Long detailId); void adjustDetail(PreSettlementDetailAdjustRequest request); void refreshPaymentSummary(Long settlementId); 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 0e978b4f..c1751638 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 @@ -154,6 +154,9 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl分批与事务口径同 {@link #executeArResultSync()}:结算明细分批 in 查询、远程整批查询消除逐单 N+1, + * 仅回写状态有变化的结算单并 updateBatchById 批量落库;整轮任务不包外层事务,按防重键幂等可重跑。 + * + * @return 执行摘要 + */ + private String executeWriteOffSync() { + long startMillis = System.currentTimeMillis(); + // 已同步金蝶的结算单才在金蝶存在旧应付/应收单,未同步/生成失败不查询(前端展示 '-') + List settlements = list(Wrappers.lambdaQuery() + .eq(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_SYNCED) + .orderByAsc(FormalSettlement::getId)); + if (Func.isEmpty(settlements)) { + log.info("金蝶冲销状态回写:无已同步结算单"); + return "金蝶冲销状态回写:无已同步结算单"; + } + // 结算明细分批一次查全(消除逐单查询 N+1),按结算单 id 分组后按结算类型组装防重键 + List settlementIds = settlements.stream().map(FormalSettlement::getId).toList(); + Map> detailsBySettlement = new HashMap<>(); + kingdeeApBillSyncSupport.batchForeach(settlementIds, AR_RESULT_DETAIL_QUERY_BATCH, batch -> + detailsBySettlement.putAll(detailMapper.selectList(Wrappers.lambdaQuery() + .in(FormalSettlementDetail::getFormalSettlementId, batch) + .eq(FormalSettlementDetail::getIsDeleted, 0) + .orderByAsc(FormalSettlementDetail::getLineNo)).stream() + .collect(Collectors.groupingBy(FormalSettlementDetail::getFormalSettlementId)))); + Map apSettlementBySrcCode = new LinkedHashMap<>(); + Map arSettlementBySrcCode = new LinkedHashMap<>(); + for (FormalSettlement settlement : settlements) { + List details = detailsBySettlement.getOrDefault(settlement.getId(), List.of()); + if (Func.isEmpty(details)) { + continue; + } + if ("receivable".equals(settlement.getSettlementType())) { + String srcCode = kingdeeArBillSyncSupport.arBillSrcCode(settlement, details); + if (Func.isNotBlank(srcCode)) { + arSettlementBySrcCode.putIfAbsent(srcCode, settlement); + } + } else { + String srcCode = kingdeeApBillSyncSupport.apBillSrcCode(settlement, details); + if (Func.isNotBlank(srcCode)) { + apSettlementBySrcCode.putIfAbsent(srcCode, settlement); + } + } + } + // A102-4/A302-4 整批一次查询,冲销状态有变化的才落库(updateBatchById 批量更新,避免逐单 update) + Map apWriteOffFlags = kingdeeApBillSyncSupport.queryApBillWriteOffFlags(new ArrayList<>(apSettlementBySrcCode.keySet())); + Map arWriteOffFlags = kingdeeArBillSyncSupport.queryArBillWriteOffFlags(new ArrayList<>(arSettlementBySrcCode.keySet())); + List 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); + } + String summary = "金蝶冲销状态回写完成:待查已同步结算单" + (apSettlementBySrcCode.size() + arSettlementBySrcCode.size()) + + "笔,回写" + toUpdateSettlements.size() + "笔"; + log.info("{},耗时{}ms", summary, System.currentTimeMillis() - startMillis); + return summary; + } + + /** 应用单笔冲销查询结果:与本地冲销状态不一致时标记更新(查不到金蝶单据按未冲销处理),一致跳过 */ + private void applyWriteOffFlag(FormalSettlement settlement, Boolean writtenOff, List toUpdateSettlements) { + String status = Boolean.TRUE.equals(writtenOff) ? KingdeeBfiConstant.KINGDEE_WRITEOFF_WRITTEN : KingdeeBfiConstant.KINGDEE_WRITEOFF_UNWRITTEN; + if (status.equals(settlement.getKingdeeWriteOffStatus())) { + return; + } + settlement.setKingdeeWriteOffStatus(status); + settlement.setKingdeeWriteOffTime(LocalDateTime.now()); + toUpdateSettlements.add(settlement); + log.info("金蝶冲销状态回写:正式结算单[{}],{}", settlement.getFormalSettlementNo(), status); + } + @Override @Transactional(rollbackFor = Exception.class) public String applyPayment(FormalSettlementPaymentRequest request) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java index f7dd22e3..bb3d27f6 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java @@ -242,7 +242,10 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl { - param.setPageNo(pageNo); - param.setPageSize(PAY_RESULT_PAGE_SIZE); - return bfiService.queryApBill(param); - })) { - if (awrpSrcCode.equals(apBill.getAwrpSrcCode()) && Boolean.TRUE.equals(apBill.getHadwrittenoff())) { - return true; + return queryApBillWriteOffFlags(List.of(awrpSrcCode)).getOrDefault(awrpSrcCode, Boolean.FALSE); + } + + /** + * A102-4 冲销状态批量查询:防重键批量分批查询财务应付单,返回 防重键 -> 是否已冲销 + * (hadwrittenoff=true,部分冲销同样视为已冲销;同键多单取或,查不到的键不在结果中即视为未冲销)。 + * 定时回写结算单金蝶冲销状态用,整批一次查询消除逐单远程查询 N+1 + * + * @param awrpSrcCodes 财务应付单防重键列表 + * @return 防重键 -> 是否已冲销 + */ + public Map queryApBillWriteOffFlags(List awrpSrcCodes) { + Map writeOffFlags = new LinkedHashMap<>(); + batchForeach(awrpSrcCodes, PAY_RESULT_QUERY_BATCH, batch -> { + BfiApBillQueryDTO param = new BfiApBillQueryDTO(); + param.setAwrpSrcCode(batch); + param.setAwrpSrcSys(bfiProperties.getSrcSys()); + for (BfiApBillVO apBill : BfiUtil.queryAllPages(pageNo -> { + param.setPageNo(pageNo); + param.setPageSize(PAY_RESULT_PAGE_SIZE); + return bfiService.queryApBill(param); + })) { + if (Func.isNotEmpty(apBill.getAwrpSrcCode())) { + writeOffFlags.merge(apBill.getAwrpSrcCode(), Boolean.TRUE.equals(apBill.getHadwrittenoff()), Boolean::logicalOr); + } } - } - return false; + }); + return writeOffFlags; } // ==================== A102-2 财务应付单保存 ==================== diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java index f9e214c1..845fc575 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java @@ -150,19 +150,34 @@ public class KingdeeArBillSyncSupport { if (Func.isBlank(arBillSrcCode)) { return false; } - BfiArBillQueryDTO param = new BfiArBillQueryDTO(); - param.setAwrpSrcCode(List.of(arBillSrcCode)); - param.setAwrpSrcSys(bfiProperties.getSrcSys()); - for (BfiArBillVO arBill : BfiUtil.queryAllPages(pageNo -> { - param.setPageNo(pageNo); - param.setPageSize(AR_RESULT_PAGE_SIZE); - return bfiService.queryArBill(param); - })) { - if (arBillSrcCode.equals(arBill.getAwrpSrcCode()) && Boolean.TRUE.equals(arBill.getHadwrittenoff())) { - return true; + return queryArBillWriteOffFlags(List.of(arBillSrcCode)).getOrDefault(arBillSrcCode, Boolean.FALSE); + } + + /** + * A302-4 冲销状态批量查询:防重键批量分批查询财务应收单,返回 防重键 -> 是否已冲销 + * (hadwrittenoff=true,部分冲销同样视为已冲销;同键多单取或,查不到的键不在结果中即视为未冲销)。 + * 定时回写结算单金蝶冲销状态用,整批一次查询消除逐单远程查询 N+1 + * + * @param arBillSrcCodes 财务应收单防重键列表 + * @return 防重键 -> 是否已冲销 + */ + public Map queryArBillWriteOffFlags(List arBillSrcCodes) { + Map writeOffFlags = new LinkedHashMap<>(); + kingdeeApBillSyncSupport.batchForeach(arBillSrcCodes, AR_RESULT_QUERY_BATCH, batch -> { + BfiArBillQueryDTO param = new BfiArBillQueryDTO(); + param.setAwrpSrcCode(batch); + param.setAwrpSrcSys(bfiProperties.getSrcSys()); + for (BfiArBillVO arBill : BfiUtil.queryAllPages(pageNo -> { + param.setPageNo(pageNo); + param.setPageSize(AR_RESULT_PAGE_SIZE); + return bfiService.queryArBill(param); + })) { + if (Func.isNotEmpty(arBill.getAwrpSrcCode())) { + writeOffFlags.merge(arBill.getAwrpSrcCode(), Boolean.TRUE.equals(arBill.getHadwrittenoff()), Boolean::logicalOr); + } } - } - return false; + }); + return writeOffFlags; } // ==================== A302-2 财务应收单保存 ==================== diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java index e5e4fb87..e5160721 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java @@ -86,6 +86,13 @@ 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分钟执行一次 */ diff --git a/doc/sql/transport/lqj/temp/06_blade_formal_settlement_kingdee_writeoff_20261030.sql b/doc/sql/transport/lqj/temp/06_blade_formal_settlement_kingdee_writeoff_20261030.sql new file mode 100644 index 00000000..69f14db3 --- /dev/null +++ b/doc/sql/transport/lqj/temp/06_blade_formal_settlement_kingdee_writeoff_20261030.sql @@ -0,0 +1,31 @@ +-- 正式结算单:新增金蝶冲销状态字段 +-- 结算单列表展示金蝶侧旧应付/应收单冲销状态:定时任务按 A102-4/A302-4 hadwrittenoff 批量回写, +-- 选单预检/重推闸门实时查询后顺带回写;NULL=未推送金蝶或尚未校验,unwrittenoff-未冲销,writtenoff-已冲销(部分冲销视同已冲销) + +-- 以下结构升级支持重复执行 +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_formal_settlement` ADD COLUMN `kingdee_writeoff_status` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶冲销状态:unwrittenoff-未冲销 writtenoff-已冲销(NULL=未推送金蝶或未校验)'' AFTER `kingdee_fail_reason`', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'blade_formal_settlement' + AND COLUMN_NAME = 'kingdee_writeoff_status' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +-- 以下结构升级支持重复执行 +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_formal_settlement` ADD COLUMN `kingdee_writeoff_time` datetime DEFAULT NULL COMMENT ''金蝶冲销状态确认时间(定时回写/预检回写时间)'' AFTER `kingdee_writeoff_status`', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'blade_formal_settlement' + AND COLUMN_NAME = 'kingdee_writeoff_time' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; From 70171094448c0632c72468914abce9aac06f3bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Fri, 30 Oct 2026 22:03:30 +0800 Subject: [PATCH 3/5] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E9=87=91?= =?UTF-8?q?=E8=9D=B6=E5=86=B2=E9=94=80=E7=8A=B6=E6=80=81=E8=90=BD=E5=BA=93?= =?UTF-8?q?=E4=B8=8E=E5=88=97=E8=A1=A8=E5=B1=95=E7=A4=BA=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...Ž¥入金蝶设计(未推送自动推-已推送冲销后重推新单).md | 49 ++++++++++++++++++- ...2026-10-17-结算调整单操作流程说明(分场景).md | 24 ++++++++- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).md b/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).md index a33bdcc2..9df66819 100644 --- a/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).md +++ b/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).md @@ -133,6 +133,7 @@ - 预检为**软提示**,不阻断建单(默认建议);是否升级为硬拦截由业务另行确认。 - **降级**:金蝶查询超时/不可用/接口异常时,预检返回"未校验",**不阻断**建单与回带,由后续审批分流与重推闸门兜底。 - 预检结果**只用于提示**,不作为后续环节的结论缓存:审批(操作 2)、重推(操作 3)各自按**当下**状态重新判定。 +- 预检查询结果**顺带回写**正式结算单的冲销状态字段(`kingdee_writeoff_status`,见 §15)供列表展示;异常降级时不回写、保留上次值。 > 为什么不把硬拦截放在选单:冲销是金蝶侧**不可逆/需人工撤销**的对外操作,而审批只是 TMS 内部决策。若选单即要求已冲销,会出现"旧单已冲销、调整单却被驳回/删除"的**应付单空档**(详见第 13 节)。 @@ -165,7 +166,7 @@ `repush(调整单id)` 重写为: 1. **前置校验**:调整单 `approvalStatus = approved`;原单存在。 -2. **冲销校验(硬闸门)**:取调整单号 + 原单号,按旧键(`原结算单号_明细单据号`)查 A102-4(应付)/ A302-4(应收);要求存在且 `hadwrittenoff = true`,否则抛出「原应付/应收单尚未在金蝶冲销,禁止重推」。 +2. **冲销校验(硬闸门)**:取调整单号 + 原单号,按旧键(`原结算单号_明细单据号`)查 A102-4(应付)/ A302-4(应收);要求存在且 `hadwrittenoff = true`,否则抛出「原应付/应收单尚未在金蝶冲销,禁止重推」。**闸门查询结果无论放行还是拦截均回写**正式结算单冲销状态(§15.3③),拦截后列表即同步为"未冲销"。 3. **克隆新正式结算单**:`cloneFormalSettlement(原单, 调整单)` - 新号由 `nextNo` 取正式流水(`FJS`/`SJS`+日期+4 位); - 克隆明细、明细费项、汇总费项、发票、汇率等; @@ -190,7 +191,7 @@ - 只认 `hadwrittenoff = true`(`BfiApBillVO` 158 / `BfiArBillVO` 138)。部分冲销同样放行。 - `iswrittenoff` 不单独作为放行依据。落地前需用 Apifox 实测 A102-4/A302-4 确实回传 `hadwrittenoff`。 -- **两处判定,用途不同**:操作 1 选单预检 = 软提示(第 6.2 节);操作 3 重推 = **硬闸门**(第 8 节)。预检不替代闸门——冲销在金蝶可被撤销,且建单到重推期间状态可能变化。 +- **三处判定,用途不同**:操作 1 选单预检 = 软提示(第 6.2 节);操作 3 重推 = **硬闸门**(第 8 节);定时回写 = 列表展示供数(第 15 节)。预检不替代闸门——冲销在金蝶可被撤销,且建单到重推期间状态可能变化;定时回写值**只用于展示**,闸门放行永远以实时查询为准。 ### 9.3 克隆新正式结算单 @@ -288,3 +289,47 @@ ALTER TABLE blade_formal_settlement ADD COLUMN adjust_status VARCHAR(32) NULL CO 5. **按钮与菜单**:「推送金蝶」与「重新推送」合并为**单一入口**(按金蝶状态分流:未同步/失败→推原结算单;已同步→冲销后重推新单),统一复用既有菜单 `settlement_adjustment_repush`,不新增菜单。 6. **待实测**:A102-4/A302-4 的 `hadwrittenoff` 实际回传(§13.1);克隆新单对 `PreSettlement` 占用的影响(§13.2)。 +--- + +## 15. 金蝶冲销状态落库与列表展示(2026-10-30 新增) + +### 15.1 背景与定位 + +预检(§6.2)与重推闸门(§8)是**事件触发**的单点实时校验:只有建调整单、点重推时才会查金蝶,覆盖不到结算单列表的全量展示;而列表是高频页面,不能每次刷新/翻页都实时查金蝶(外部依赖与延迟会拖垮列表)。因此把冲销状态**持久化 + 定时回写**(与 `PaymentKingdeeResultTask` 既有回写模式一致);决策时刻(预检提示、闸门放行)仍坚持实时查询。三条链路互补:**接口查询保证决策正确,定时回写保证列表有数**。 + +### 15.2 数据模型(DDL) + +脚本 `doc/sql/transport/lqj/temp/06_blade_formal_settlement_kingdee_writeoff_20261030.sql`(可重复执行): + +```sql +ALTER TABLE blade_formal_settlement ADD COLUMN kingdee_writeoff_status VARCHAR(20) NULL COMMENT '金蝶冲销状态:unwrittenoff-未冲销 writtenoff-已冲销(NULL=未推送金蝶或未校验)'; +ALTER TABLE blade_formal_settlement ADD COLUMN kingdee_writeoff_time datetime NULL COMMENT '金蝶冲销状态确认时间(定时回写/预检回写时间)'; +``` + +实体 `FormalSettlement` 同步加 `kingdeeWriteOffStatus` / `kingdeeWriteOffTime`;`FormalSettlementVO` 继承实体自动带出,无需改 VO。 + +### 15.3 三条查询/回写链路 + +| 链路 | 触发 | 行为 | +|---|---|---| +| ① 定时回写(兜底供数) | `PaymentKingdeeResultTask` 子流程④(默认每 10 分钟,single-flight 锁 `kingdee-writeoff-sync`) | 扫 `kingdeeSyncStatus=synced` 的结算单 → 结算明细分批一次查全(消除 N+1)组装防重键 → A102-4/A302-4 整批查询 `hadwrittenoff` → 状态有变化的 `updateBatchById` 批量回写;金蝶异常整轮告警、保持上次值 | +| ② 预检实时回写 | 操作 1 选单预检 | 查询成功即回写该单状态(已冲销/未冲销都存);异常降级"未校验"不回写 | +| ③ 闸门实时回写 | 操作 3 重推硬闸门 | 查询结果无论放行/拦截均回写(拦截=未冲销也存,提示后列表即同步) | + +回写统一走 `FormalSettlementServiceImpl.refreshKingdeeWriteOff(id, writtenOff)`:状态未变化不落库,避免预检反复点击重复写库。 + +### 15.4 列表展示口径 + +| 场景 | 显示 | +|---|---| +| 未推送金蝶(unsynced/failed) | `-` | +| 已推送,尚未回写过(存量上线初期) | 未校验 | +| 已推送,旧单未冲销 | 未冲销(黄) | +| 已推送,旧单已冲销(含部分冲销) | 已冲销(绿) | + +前端:`formalSettlementTable.js` 新增「金蝶冲销状态」列(`formalSettlement.js` COLUMN_EXTRA 标记 `slot`)、`formal-settlement-table-panel.vue` 单元格插槽渲染标签、`utils/kingdee.js` 新增 `kingdeeWriteOffTagType` 颜色映射、`formalSettlementSearch.js` 新增 `kingdeeWriteOffStatusOptions` 文案。 + +### 15.5 与硬闸门的关系(重要) + +定时回写值**只用于展示**,允许滞后一个调度周期(默认 10 分钟);重推放行与否**永远以闸门实时查询为准**(§8.2),不读取定时值——避免拿旧状态放行导致金蝶重复生成应付/应收单。 + diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).md b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).md index 16522c1b..2537dcf6 100644 --- a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).md +++ b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).md @@ -51,6 +51,25 @@ | 已同步 | 已在金蝶生成应付/应收单,调整单上可看到金蝶单据号 | | 生成失败 | 推送失败,可用"推送金蝶"重试或等定时重推 | +### 2.4 正式结算单列表的「金蝶冲销状态」(2026-10-30 新增) + +正式结算单列表在"金蝶单据状态"旁新增一列,显示该单在金蝶侧的旧应付/应收单是否已被财务冲销: + +| 显示 | 含义 | +|---|---| +| 已冲销(绿) | 金蝶侧旧单已冲销(含部分冲销),场景 B 此时可以点"重新推送" | +| 未冲销(黄) | 金蝶侧旧单还在,需先由财务在金蝶完成冲销 | +| 未校验(灰) | 已推送金蝶但系统还没查过(上线初期存量单的初始态,跑一轮定时任务后自动补齐) | +| - | 未推送金蝶的单,没有冲销状态可言 | + +状态由系统在三个途径自动更新,**无需人工维护**: + +1. 定时任务每约 10 分钟批量核对一次金蝶; +2. 新建调整单**选单预检**时即时刷新该单; +3. 点**"重新推送"**时即时刷新该单(被拦截未冲销也会同步为"未冲销")。 + +> 列表状态允许滞后约一个定时周期(10 分钟);点"重新推送"时系统以**当下实时查询**结果为准,不会因列表滞后而误放行。 + --- ## 三、通用操作说明(每个操作:前置条件 → 系统动作 → 结果) @@ -62,7 +81,7 @@ | 入口 | 结算调整单列表 → 新增 | | 前置 | 必须先选定一张**可调整的正式结算单**,条件缺一不可:① 审批通过;② 未完成收款/付款;③ 非尾款结算;④ 未发起尾款付款/付款申请。不满足的单在选单列表里根本搜不到 | | 系统动作 | 选定结算单后自动回带:客商、调整类型(应收/应付)、合同、原结算金额、原单费用明细;同时做一次**金蝶冲销预检(软提示)** | -| 预检提示 | ① 未推送金蝶 → 正常继续;② 已推送且旧单已冲销 → 提示"审批后可重新推送新单";③ 已推送且旧单未冲销 → 醒目提示"审批后需先由财务在金蝶完成冲销,才能重新推送";④ 金蝶查不到/超时 → 提示"未校验",**不阻断建单** | +| 预检提示 | ① 未推送金蝶 → 正常继续;② 已推送且旧单已冲销 → 提示"审批后可重新推送新单";③ 已推送且旧单未冲销 → 醒目提示"审批后需先由财务在金蝶完成冲销,才能重新推送";④ 金蝶查不到/超时 → 提示"未校验",**不阻断建单**。预检查询结果同时回写该结算单列表的「金蝶冲销状态」(见 2.4) | | 结果 | 生成一张草稿,获得单号 | > 预检只是提示,不拦人;真正的拦截在"重新推送"那一步。填单时必须至少录入一条调整费用,否则无法提交。 @@ -129,7 +148,7 @@ |---|---|---|---| | 1 | 业务 | 新增调整单(选单时预检会提示"旧单未冲销"或"已冲销"),提交 | 同场景 A 步骤 1-2 | | 2 | 审批人 | 审批通过 | ① 原结算单业务数据**零改动**,只追加 1 条变更记录("调整单XX审批通过,调整金额XX,待重推金蝶");② 调整单 → 审批通过,金蝶状态保持"已同步"快照("重新推送"按钮因此亮起);③ 前端提示:**"原结算单已推送金蝶,请财务人员在金蝶手工完成旧应付/应收单冲销处理"** | -| 3 | 财务 | **在金蝶系统内手工冲销**旧的应付/应收单 | TMS 不参与,这是金蝶侧的人工操作 | +| 3 | 财务 | **在金蝶系统内手工冲销**旧的应付/应收单 | TMS 不参与,这是金蝶侧的人工操作;冲销完成后**无需通知 TMS**——正式结算单列表的「金蝶冲销状态」约 10 分钟内自动变为"已冲销"(见 2.4),看到变绿即可回 TMS 重推 | | 4 | 业务 | 回到 TMS,点"重新推送(高危)"并二次确认 | 系统依次:① 硬闸门——按原结算单号去金蝶查旧单,**必须已冲销**(部分冲销也算已冲销),未冲销直接拒绝;② **克隆一张新的正式结算单**(全新 FJS/SJS 单号),把调整应用上去,直接置为审批通过(新单不再走审批流);③ 用新单号推送金蝶,生成**全新**的财务应付/应收单 + 落锁 + 附件;④ 调整单改挂到新单,回填"已同步 + 金蝶单据号";⑤ 原单标记"已调整",原单在金蝶的旧锁解锁 | | 5 | 系统 | — | 完成。此时列表里**新旧两张正式结算单并存**,带标识区分(见第六节) | @@ -165,6 +184,7 @@ | 部分冲销 | 视同已冲销,放行重推 | | 审批时原单刚好已被推送到金蝶 | 按**当下**状态走场景 B,选单时的预检结论不作数 | | 选单时金蝶超时/不可用 | 预检降级"未校验",不阻断建单 | +| 列表「金蝶冲销状态」与金蝶实际有出入 | 列表为定时同步值,允许滞后约 10 分钟;重推一律以闸门实时校验为准,不影响拦截准确性 | | 重推成功前原单旧锁残留 | 重推成功时统一解锁 | | 应收结算单 | 与应付完全同一套流程,仅接口不同(应收 A302 系 / 应付 A102 系) | From 6c0f15290a4650a26f03e4539e47228870731856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Fri, 30 Oct 2026 22:33:34 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E9=87=91=E8=9D=B6?= =?UTF-8?q?=E5=86=B2=E9=94=80=E7=8A=B6=E6=80=81=E5=88=97=E5=90=8D=E4=B8=BA?= =?UTF-8?q?kingdee=5Fwrite=5Foff=5Fstatus=E4=B8=8E=E5=AE=9E=E4=BD=93?= =?UTF-8?q?=E9=A9=BC=E5=B3=B0=E6=98=A0=E5=B0=84=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...Ž¥入金蝶设计(未推送自动推-已推送冲销后重推新单).md | 6 ++- ...l_settlement_kingdee_writeoff_20261030.sql | 42 ++++++++++++++++--- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).md b/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).md index 9df66819..9e1b97e4 100644 --- a/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).md +++ b/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).md @@ -302,10 +302,12 @@ ALTER TABLE blade_formal_settlement ADD COLUMN adjust_status VARCHAR(32) NULL CO 脚本 `doc/sql/transport/lqj/temp/06_blade_formal_settlement_kingdee_writeoff_20261030.sql`(可重复执行): ```sql -ALTER TABLE blade_formal_settlement ADD COLUMN kingdee_writeoff_status VARCHAR(20) NULL COMMENT '金蝶冲销状态:unwrittenoff-未冲销 writtenoff-已冲销(NULL=未推送金蝶或未校验)'; -ALTER TABLE blade_formal_settlement ADD COLUMN kingdee_writeoff_time datetime NULL COMMENT '金蝶冲销状态确认时间(定时回写/预检回写时间)'; +ALTER TABLE blade_formal_settlement ADD COLUMN kingdee_write_off_status VARCHAR(20) NULL COMMENT '金蝶冲销状态:unwrittenoff-未冲销 writtenoff-已冲销(NULL=未推送金蝶或未校验)'; +ALTER TABLE blade_formal_settlement ADD COLUMN kingdee_write_off_time datetime NULL COMMENT '金蝶冲销状态确认时间(定时回写/预检回写时间)'; ``` +> 列名遵循 MyBatis-Plus 驼峰转下划线默认映射(`kingdeeWriteOffStatus` → `kingdee_write_off_status`),实体无需 `@TableField`;脚本对早期误建的旧列名(`kingdee_writeoff_status`)会先重命名再补缺,可重复执行。 + 实体 `FormalSettlement` 同步加 `kingdeeWriteOffStatus` / `kingdeeWriteOffTime`;`FormalSettlementVO` 继承实体自动带出,无需改 VO。 ### 15.3 三条查询/回写链路 diff --git a/doc/sql/transport/lqj/temp/06_blade_formal_settlement_kingdee_writeoff_20261030.sql b/doc/sql/transport/lqj/temp/06_blade_formal_settlement_kingdee_writeoff_20261030.sql index 69f14db3..34bb320a 100644 --- a/doc/sql/transport/lqj/temp/06_blade_formal_settlement_kingdee_writeoff_20261030.sql +++ b/doc/sql/transport/lqj/temp/06_blade_formal_settlement_kingdee_writeoff_20261030.sql @@ -1,30 +1,60 @@ -- 正式结算单:新增金蝶冲销状态字段 +-- 列名遵循 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-已冲销(部分冲销视同已冲销) --- 以下结构升级支持重复执行 +-- ==================== kingdee_write_off_status ==================== +-- 旧列名重命名(早期脚本版本,不存在则跳过) +SET @old_col := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_formal_settlement' + 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=未推送金蝶或未校验)''', + 'SELECT 1'); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +-- 缺失则新增(支持重复执行) SET @ddl_sql = ( SELECT IF(COUNT(*) = 0, - 'ALTER TABLE `blade_formal_settlement` ADD COLUMN `kingdee_writeoff_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 NULL COMMENT ''金蝶冲销状态:unwrittenoff-未冲销 writtenoff-已冲销(NULL=未推送金蝶或未校验)'' AFTER `kingdee_fail_reason`', 'SELECT 1') FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_formal_settlement' - AND COLUMN_NAME = 'kingdee_writeoff_status' + AND COLUMN_NAME = 'kingdee_write_off_status' ); PREPARE ddl_stmt FROM @ddl_sql; EXECUTE ddl_stmt; DEALLOCATE PREPARE ddl_stmt; --- 以下结构升级支持重复执行 +-- ==================== kingdee_write_off_time ==================== +-- 旧列名重命名(早期脚本版本,不存在则跳过) +SET @old_col := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_formal_settlement' + AND COLUMN_NAME = 'kingdee_writeoff_time' +); +SET @ddl_sql = IF(@old_col = 1, + 'ALTER TABLE `blade_formal_settlement` CHANGE COLUMN `kingdee_writeoff_time` `kingdee_write_off_time` datetime DEFAULT NULL COMMENT ''金蝶冲销状态确认时间(定时回写/预检回写时间)''', + 'SELECT 1'); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +-- 缺失则新增(支持重复执行) SET @ddl_sql = ( SELECT IF(COUNT(*) = 0, - 'ALTER TABLE `blade_formal_settlement` ADD COLUMN `kingdee_writeoff_time` datetime DEFAULT NULL COMMENT ''金蝶冲销状态确认时间(定时回写/预检回写时间)'' AFTER `kingdee_writeoff_status`', + 'ALTER TABLE `blade_formal_settlement` ADD COLUMN `kingdee_write_off_time` datetime DEFAULT NULL COMMENT ''金蝶冲销状态确认时间(定时回写/预检回写时间)'' AFTER `kingdee_write_off_status`', 'SELECT 1') FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_formal_settlement' - AND COLUMN_NAME = 'kingdee_writeoff_time' + AND COLUMN_NAME = 'kingdee_write_off_time' ); PREPARE ddl_stmt FROM @ddl_sql; EXECUTE ddl_stmt; From 014d254295d804cb904eb687c3352a800eb4cb0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Fri, 30 Oct 2026 23:04:10 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=E9=87=91=E8=9D=B6=E5=86=B2?= =?UTF-8?q?=E9=94=80=E7=8A=B6=E6=80=81=E5=9B=9E=E5=86=99=E6=8B=86=E5=88=86?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1=E5=B9=B6?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=88=86=E6=89=B9=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增KingdeeWriteOffResultTask独立调度(Nacos thirdParty.bfi.writeOffResult.*开关/cron),从付款结果任务第④步拆出 - BfiProperties新增writeOffResult配置:syncEnabled开关、queryBatchSize分批大小默认1000,冲销批量查询与批量落库共用 - 回写仅扫描已同步且未冲销的结算单,已冲销为终态不再复查 - kingdee_write_off_status列默认值改为unwrittenoff,存量已同步NULL数据回填未冲销 - 补充扫描数/防重键数/金蝶返回数/落库数关键日志,summary文案修正为待查未冲销结算单 --- .../transport/config/BfiProperties.java | 23 +++++++ .../impl/FormalSettlementServiceImpl.java | 20 ++++-- .../support/KingdeeApBillSyncSupport.java | 2 +- .../support/KingdeeArBillSyncSupport.java | 2 +- .../task/KingdeeWriteOffResultTask.java | 63 +++++++++++++++++++ .../task/PaymentKingdeeResultTask.java | 7 --- ...l_settlement_kingdee_writeoff_20261030.sql | 13 +++- 7 files changed, 113 insertions(+), 17 deletions(-) create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/task/KingdeeWriteOffResultTask.java diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java index 5fe089fe..3d7b078d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java @@ -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; + } + /** * 金蝶单据锁定补偿配置项 */ 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 c1751638..05276ac2 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 @@ -1172,14 +1172,17 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl settlements = list(Wrappers.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 settlementIds = settlements.stream().map(FormalSettlement::getId).toList(); Map> detailsBySettlement = new HashMap<>(); @@ -1209,17 +1212,24 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl apWriteOffFlags = kingdeeApBillSyncSupport.queryApBillWriteOffFlags(new ArrayList<>(apSettlementBySrcCode.keySet())); Map arWriteOffFlags = kingdeeArBillSyncSupport.queryArBillWriteOffFlags(new ArrayList<>(arSettlementBySrcCode.keySet())); + // 金蝶未返回的键按未冲销处理,返回数与组装数差异即金蝶侧查不到单据的规模,留痕便于对账 + log.info("金蝶冲销状态批量查询完成:应付键返回{}个、应收键返回{}个,未返回键按未冲销处理", + apWriteOffFlags.size(), arWriteOffFlags.size()); List 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; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java index a5d74fa9..11f97ce1 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java @@ -225,7 +225,7 @@ public class KingdeeApBillSyncSupport { */ public Map queryApBillWriteOffFlags(List awrpSrcCodes) { Map 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()); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java index 845fc575..763a3371 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java @@ -163,7 +163,7 @@ public class KingdeeArBillSyncSupport { */ public Map queryArBillWriteOffFlags(List arBillSrcCodes) { Map 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()); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/KingdeeWriteOffResultTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/KingdeeWriteOffResultTask.java new file mode 100644 index 00000000..dd4a0e22 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/KingdeeWriteOffResultTask.java @@ -0,0 +1,63 @@ +/** + * 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.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; + +/** + * 金蝶冲销状态定时回写任务 + *

+ * A102-4/A302-4 批量查询已同步结算单对应金蝶旧应付/应收单的 hadwrittenoff(部分冲销视同已冲销), + * 回写结算单冲销状态,用于结算单列表展示金蝶旧单是否已冲销。 + * 业务逻辑收敛在服务方法(分布式锁 + 分批查询 + 批量落库),便于手动触发联调。 + *

+ * 调度配置(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); + } + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java index e5160721..e5e4fb87 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java @@ -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分钟执行一次 */ diff --git a/doc/sql/transport/lqj/temp/06_blade_formal_settlement_kingdee_writeoff_20261030.sql b/doc/sql/transport/lqj/temp/06_blade_formal_settlement_kingdee_writeoff_20261030.sql index 34bb320a..a5fc952a 100644 --- a/doc/sql/transport/lqj/temp/06_blade_formal_settlement_kingdee_writeoff_20261030.sql +++ b/doc/sql/transport/lqj/temp/06_blade_formal_settlement_kingdee_writeoff_20261030.sql @@ -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 := (