diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java index 2483416f..f12665ef 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java @@ -1179,12 +1179,18 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl切片续扫(ponytail: 单查询窗口有 MAX_POOL_PAGES×500=2.5 万硬上限,窗口数据超限尾部永久漏同步; * 切片让每片各自独立受翻页保护,总覆盖不受限):把大窗口切成小片独立翻页,某片触顶说明该时间片内 * 数据 >2.5 万,二分为 [start, mid) [mid, end) 两片递归处理;updateTime 单调,切片边界不重不漏。 + *

发票种类白名单**下推远端**(invoiceType 为单值参数,按白名单编码逐轮查询,每轮独立切片额度), + * 白名单为空退化为单轮全量——只同步数电普票/专票时不把他票种的额度消耗进来。 *

拉取只认正常票(invoiceStatus=0),不推单据使用状态——在用/已用票随 updateTime 进窗口刷新 * 金额使用状态,选票可用过滤在本地 SQL 执行;被删除/红冲票不再出现在拉取结果,由保存/认领重查闸门兜底。 * @@ -1194,11 +1200,19 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl invoiceTypes = this.poolInvoiceTypeCodes(); SyncStats stats = new SyncStats(); - log.info("金蝶全票池同步开始,增量窗口 {} ~ {},单页 {} 条最多 {} 页(触顶自动二分切片)", + log.info("金蝶全票池同步开始,增量窗口 {} ~ {},发票种类:{},单页 {} 条最多 {} 页(触顶自动二分切片)", windowStart.format(POOL_TIME_FORMATTER), windowEnd.format(POOL_TIME_FORMATTER), - POOL_PAGE_SIZE, MAX_POOL_PAGES); - this.syncWindowSlice(windowStart, windowEnd, stats); + invoiceTypes.isEmpty() ? "不限制" : invoiceTypes, POOL_PAGE_SIZE, MAX_POOL_PAGES); + if (invoiceTypes.isEmpty()) { + this.syncWindowSlice(windowStart, windowEnd, null, stats); + } else { + // 单值参数拆轮:每个票种编码独立完整切片续扫,互不挤占翻页额度 + for (String invoiceType : invoiceTypes) { + this.syncWindowSlice(windowStart, windowEnd, invoiceType, stats); + } + } String summary = "金蝶全票池同步:翻页" + stats.pageCount + "页切片" + stats.sliceCount + "片拉取" + stats.pulledCount + "张发票,镜像新增" + stats.created + "更新" + stats.updated + "无变化" + stats.mirrorUnchanged + ",补绑" + stats.bound + ",登记无变化跳过" + stats.unchanged @@ -1207,6 +1221,18 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl poolInvoiceTypeCodes() { + if (StringUtil.isBlank(poolInvoiceTypes)) { + return List.of(); + } + return java.util.Arrays.stream(poolInvoiceTypes.split(",")) + .map(String::trim) + .filter(StringUtil::isNotBlank) + .distinct() + .toList(); + } + /** 整轮同步累计统计(各切片共享累加,结束进摘要) */ private static final class SyncStats { /** A801-1 原始拉取张数 */ @@ -1238,7 +1264,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl2.5 万条不可能),不再二分直接记漏 if (!sliceStart.isBefore(sliceEnd)) { log.error("金蝶全票池同步切片区间为空({} ~ {}),远端 updateTime 粒度异常,放弃该切片", sliceStart, sliceEnd); @@ -1246,7 +1272,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl rows = this.pullPoolWindowPage(sliceStart, sliceEnd, pageNo); + List rows = this.pullPoolWindowPage(sliceStart, sliceEnd, invoiceType, pageNo); if (rows.isEmpty()) { break; } @@ -1282,8 +1308,8 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl pullPoolWindowPage(LocalDateTime windowStart, LocalDateTime windowEnd, int pageNo) { + /** A801-1 按票池更新时间窗口分页拉取(只认正常票,不推单据使用状态;invoiceType 单值下推,null 不限) */ + private List pullPoolWindowPage(LocalDateTime windowStart, LocalDateTime windowEnd, + String invoiceType, int pageNo) { BfiInvoicePoolQueryDataDTO data = new BfiInvoicePoolQueryDataDTO(); data.setPageNo(pageNo); data.setPageSize(POOL_PAGE_SIZE); data.setUpdateTime(windowStart.format(POOL_TIME_FORMATTER)); data.setUpdateTimeEnd(windowEnd.format(POOL_TIME_FORMATTER)); data.setInvoiceStatus(KingdeeBfiConstant.POOL_INVOICE_STATUS_NORMAL); + data.setInvoiceType(invoiceType); BfiInvoicePoolQueryDTO param = new BfiInvoicePoolQueryDTO(); param.setQueryParam(data); BfiPageVO poolPage = bfiService.queryInvoicePool(param); diff --git a/doc/docs/业务流程/2026-10-05-金蝶票池选票回切镜像读(本地真分页+票号回源+瘦身同步+认领闸门)业务流程.md b/doc/docs/业务流程/2026-10-05-金蝶票池选票回切镜像读(本地真分页+票号回源+瘦身同步+认领闸门)业务流程.md index fe81d101..385d2a98 100644 --- a/doc/docs/业务流程/2026-10-05-金蝶票池选票回切镜像读(本地真分页+票号回源+瘦身同步+认领闸门)业务流程.md +++ b/doc/docs/业务流程/2026-10-05-金蝶票池选票回切镜像读(本地真分页+票号回源+瘦身同步+认领闸门)业务流程.md @@ -47,10 +47,15 @@ ``` InvoiceReceiptKingdeeSyncTask(@Scheduled cron,thirdParty.bfi.invoiceReceipt.cron 默认每30分钟,syncEnabled 默认关闭) → IInvoiceReceiptService.syncKingdeeInvoicePool()(KingdeeSyncLockSupport.runSingleFlight 单飞,锁键按租户) - → executePoolSync():滚动近 incrementDays 天(默认30)窗口按 updateTime 分页拉取(pageSize 500,最多 50 页保护) + → executePoolSync():滚动近 incrementDays 天(默认30)窗口 + ① 发票种类白名单下推(thirdParty.bfi.invoiceReceipt.invoiceTypes,单值参数按编码拆轮,每轮独立额度;空不限制) + ② updateTime 切片续扫(syncWindowSlice):单查询窗口受 50页×500条=2.5万硬上限,触顶自动二分 + [start,mid)+[mid,end) 递归续扫(右片不含 mid 不重不漏;细到 1 秒仍满页防御报 error 跳过) 出站口径 invoiceStatus="0",不推 expenseStatus(在用/已用票随 updateTime 进窗口刷新使用状态,本地过滤自然排除) - → batchUpsertInvoicePool:同页号码一次批量查既有镜像 → fillInvoicePool 全列填充 → Db.saveBatch / Db.updateBatchById - (全列覆盖、无指纹比对;源侧空字段维持库中原值——MP 非空字段更新策略,与手动/回源路径同口径) + → batchUpsertInvoicePool:同页号码一次批量查既有镜像 → fillInvoicePool 全列填充 → 有变化才写: + 轻量比对 serialNo/价税合计/税额/剩余可用金额/金额使用状态(compareTo 比金额,空对空不算变化), + 无变化跳过 UPDATE(消整窗重复拉取的无效写);有变化 Db.updateBatchById、新增 Db.saveBatch + → 已登记票跟随补绑:同页票号批量定位登记,仅未绑定存量登记补绑+写 sync 流水(页级事务批量落库) → 失败中断本轮由任务层记日志,下轮幂等重试 ``` @@ -63,7 +68,7 @@ InvoiceReceiptKingdeeSyncTask(@Scheduled cron,thirdParty.bfi.invoiceReceipt. | --- | --- | | 镜像表新增列 | `available_amount` decimal(18,2)、`amount_use_status` varchar(8),幂等脚本 `doc/sql/transport/blade_kingdee_invoice_pool_available_20261005.sql`(可重跑) | | 新增索引 | `blade_formal_settlement_invoice (tenant_id, invoice_no)`(认领过滤 EXISTS 走索引) | -| Nacos | `third-party-api.yaml` 新增 `thirdParty.bfi.invoiceReceipt.*`:`syncEnabled`(false)/`cron`(每30分钟)/`incrementDays`(30)/`lockLeaseSeconds`(1800) | +| Nacos | `third-party-api.yaml` 新增 `thirdParty.bfi.invoiceReceipt.*`:`syncEnabled`(false)/`cron`(每30分钟)/`incrementDays`(30)/`invoiceTypes`(白名单编码,如 26,27,下推远端按编码拆轮)/`lockLeaseSeconds`(1800) | | 存量数据 | 存量存根行两列为 NULL,不参与选票(可用过滤排除),随增量窗口同步自然回填;回切当天手动跑一次 `POST /invoice-receipt/sync-invoice-pool` bootstrap 铺底 | ## 6. 前端统一组件(`src/components/kingdee-invoice-pool-dialog/main.vue`)