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;