feat: 全票池同步补绑定回填步骤(39ffac1口径瘦身版)——同页票号批量定位未绑定存量登记补绑+页级事务批量落库+同步流水,绑定关系无变化的已绑定登记零写入(新架构登记无金蝶流水号/状态字段,不做状态跟随刷新)

This commit is contained in:
刘泉佳
2026-10-08 09:50:25 +08:00
parent eb1edd3bbe
commit a1348fd381
@@ -50,7 +50,7 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoicePoolVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiReceivedInvoiceVO;
import org.springblade.thirdparty.bfi.service.IBfiService;
import org.springblade.transport.constant.KingdeeAttachmentConstant;
import org.springblade.transport.constantq.KingdeeAttachmentConstant;
import org.springblade.transport.constant.KingdeeBfiConstant;
import org.springblade.transport.constant.TransportBusiConstant;
import org.springblade.transport.mapper.CustomerArchiveMapper;
@@ -1181,7 +1181,8 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
}
/**
* 同步主体:滚动增量窗口分页拉取 A801-1(按票池更新时间)+ 镜像批量 upsert(全列覆盖)。
* 同步主体(39ffac1 绑定回填口径瘦身版):滚动增量窗口分页拉取 A801-1(按票池更新时间)
* + 镜像批量 upsert(全列覆盖)+ 未绑定存量登记顺带补绑(同页发票号码批量定位登记,页级事务批量落库)。
* 同步窗口内所有可用态票全量重写,无指纹比对(ponytail: 万行级窗口全列重写成本可接受,
* 人工字段清单式指纹有"新增列漏维护即静默不同步"坑;量级上来后升级为整行载荷规范化哈希)。
* 拉取只认正常票(invoiceStatus=0),不推单据使用状态——在用/已用票随 updateTime 进窗口刷新
@@ -1193,9 +1194,20 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
long startMillis = System.currentTimeMillis();
LocalDateTime windowStart = LocalDateTime.now().minusDays(poolIncrementDays);
LocalDateTime windowEnd = LocalDateTime.now();
// 以下为本轮统计口径,全部累计整轮并进入结束摘要
// A801-1 原始拉取张数
int pulledCount = 0;
// 镜像新增条数(票池有、本地无)
int created = 0;
// 镜像重写条数
int updated = 0;
// 登记首次绑定条数(罕见:存量手工数据未绑定时顺带补绑)
int bound = 0;
// 登记无变化跳过条数
int unchanged = 0;
// 页级批量落库失败条数(整页跳过,下轮幂等重试)
int failed = 0;
// 实际处理的翻页数,触达最大页数保护时配合 warn 定位截断
int pageCount = 0;
log.info("金蝶全票池同步开始,增量窗口 {} ~ {},单页 {} 条最多 {} 页",
windowStart.format(POOL_TIME_FORMATTER), windowEnd.format(POOL_TIME_FORMATTER),
@@ -1210,6 +1222,13 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
BatchUpsertResult upsertResult = this.batchUpsertInvoicePool(rows);
created += upsertResult.created();
updated += upsertResult.updated();
// 已登记票跟随刷新(39ffac1 口径):同页发票号码批量定位登记,内存比对变更后页级事务批量落库
if (!upsertResult.mirrors().isEmpty()) {
RefreshResult refreshResult = this.refreshBoundReceipts(pageNo, upsertResult.mirrors());
bound += refreshResult.bound();
unchanged += refreshResult.unchanged();
failed += refreshResult.failed();
}
if (rows.size() < POOL_PAGE_SIZE) {
break;
}
@@ -1220,11 +1239,87 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
MAX_POOL_PAGES, MAX_POOL_PAGES * POOL_PAGE_SIZE);
}
String summary = "金蝶全票池同步:翻页" + pageCount + "页拉取" + pulledCount + "张发票,镜像新增" + created
+ "更新" + updated + ",耗时" + (System.currentTimeMillis() - startMillis) + "ms";
+ "更新" + updated + ",补绑" + bound + ",登记无变化跳过" + unchanged
+ ",失败" + failed + ",耗时" + (System.currentTimeMillis() - startMillis) + "ms";
log.info("金蝶全票池同步结束,{}", summary);
return summary;
}
/** 已登记票跟随刷新结果统计(绑定/刷新/无变化/失败) */
private record RefreshResult(int bound, int unchanged, int failed) {
}
/**
* 同页发票号码批量定位收票登记,绑定关系变更页级事务批量落库(39ffac1 口径瘦身版):
* 新架构下登记绑定在保存时完成且登记无金蝶流水号/状态字段(70d5d34 已删),
* 已绑定登记零写入,仅未绑定存量登记(罕见,人工数据)顺带补绑;批量失败整页跳过待下轮幂等重试
*
* @param pageNo 当前页码(日志定位)
* @param mirrorMap 本页 invoiceNo -> 刚 upsert 的镜像
* @return 刷新结果统计
*/
private RefreshResult refreshBoundReceipts(int pageNo, Map<String, KingdeeInvoicePool> mirrorMap) {
Map<String, List<InvoiceReceipt>> receiptMap = list(Wrappers.<InvoiceReceipt>lambdaQuery()
.in(InvoiceReceipt::getInvoiceNo, mirrorMap.keySet())
.ne(InvoiceReceipt::getApprovalStatus, TransportBusiConstant.VOIDED)).stream()
.filter(receipt -> !Objects.equals(receipt.getIsDeleted(), 1))
.collect(Collectors.groupingBy(InvoiceReceipt::getInvoiceNo));
// 镜像有了却没人登记:本页不产生任何刷新,属正常(票池范围大于我方登记范围)
if (receiptMap.isEmpty()) {
return new RefreshResult(0, 0, 0);
}
List<InvoiceReceipt> changedReceipts = new ArrayList<>();
List<InvoiceReceiptRecord> syncRecords = new ArrayList<>();
int bound = 0;
int unchanged = 0;
for (Map.Entry<String, List<InvoiceReceipt>> entry : receiptMap.entrySet()) {
KingdeeInvoicePool mirror = mirrorMap.get(entry.getKey());
for (InvoiceReceipt receipt : entry.getValue()) {
// 新架构登记无 kingdeeBillNo/kingdeeStatus 字段(70d5d34 已删),仅绑定关系可变:
// 已绑定且指向同一镜像的登记零写入;未绑定存量登记(罕见,人工数据)顺带补绑
boolean unbound = receipt.getKingdeeInvoicePoolId() == null
|| !Objects.equals(receipt.getKingdeeInvoicePoolId(), mirror.getId());
if (!unbound) {
unchanged++;
continue;
}
receipt.setKingdeeInvoicePoolId(mirror.getId());
changedReceipts.add(receipt);
syncRecords.add(this.buildPoolSyncRecord(receipt, "定时绑定金蝶票池发票"));
bound++;
}
}
if (changedReceipts.isEmpty()) {
return new RefreshResult(0, unchanged, 0);
}
try {
// 页级事务批量落库:绑定/状态批量更新 + 同步流水批量写入(JDBC batch,整页原子生效)
Db.updateBatchById(changedReceipts);
if (!syncRecords.isEmpty()) {
Db.saveBatch(syncRecords);
}
log.info("金蝶全票池同步 第{}页落库完成,补绑 {} 条,同步流水 {} 条",
pageNo, changedReceipts.size(), syncRecords.size());
return new RefreshResult(bound, unchanged, 0);
} catch (Exception batchException) {
// 批量落库失败:整页跳过计入失败,下轮任务幂等重试,不做逐单降级(保持全批量链路)
log.error("金蝶全票池同步登记刷新批量落库异常,第{}页 {} 条跳过待下轮重试", pageNo, changedReceipts.size(), batchException);
return new RefreshResult(0, unchanged, changedReceipts.size());
}
}
/** 构建金蝶票池同步流水(操作人以「定时任务」名义记录) */
private InvoiceReceiptRecord buildPoolSyncRecord(InvoiceReceipt receipt, String reason) {
InvoiceReceiptRecord syncRecord = new InvoiceReceiptRecord();
syncRecord.setInvoiceReceiptId(receipt.getId());
syncRecord.setActionType("sync");
syncRecord.setActionName(reason);
syncRecord.setFromStatus(receipt.getApprovalStatus());
syncRecord.setToStatus(receipt.getApprovalStatus());
syncRecord.setOperatorName(AuthUtil.getUserName() == null ? "定时任务" : AuthUtil.getUserName());
return syncRecord;
}
/** A801-1 按票池更新时间窗口分页拉取(只认正常票,不推单据使用状态,票面可用性由本地过滤) */
private List<BfiInvoicePoolVO> pullPoolWindowPage(LocalDateTime windowStart, LocalDateTime windowEnd, int pageNo) {
BfiInvoicePoolQueryDataDTO data = new BfiInvoicePoolQueryDataDTO();