feat: 全票池同步发票种类白名单下推远端——invoiceTypes单值参数按编码拆轮独立切片续扫,不再拉回后内存过滤消耗翻页额度;配置键 thirdParty.bfi.invoiceReceipt.invoiceTypes
This commit is contained in:
+37
-9
@@ -1179,12 +1179,18 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
"金蝶全票池同步", this::executePoolSync);
|
||||
}
|
||||
|
||||
/** 发票种类白名单(thirdParty.bfi.invoiceReceipt.invoiceTypes 逗号分隔票易通编码,如 26,27),为空不限制 */
|
||||
@Value("${thirdParty.bfi.invoiceReceipt.invoiceTypes:}")
|
||||
private String poolInvoiceTypes;
|
||||
|
||||
/**
|
||||
* 同步主体(39ffac1 绑定回填口径瘦身版):滚动增量窗口按 updateTime **切片续扫** + 镜像批量 upsert(有变化才写)
|
||||
* + 未绑定存量登记顺带补绑(同页发票号码批量定位登记,页级事务批量落库)。
|
||||
* <p>切片续扫(ponytail: 单查询窗口有 MAX_POOL_PAGES×500=2.5 万硬上限,窗口数据超限尾部永久漏同步;
|
||||
* 切片让每片各自独立受翻页保护,总覆盖不受限):把大窗口切成小片独立翻页,某片触顶说明该时间片内
|
||||
* 数据 >2.5 万,二分为 [start, mid) [mid, end) 两片递归处理;updateTime 单调,切片边界不重不漏。
|
||||
* <p>发票种类白名单**下推远端**(invoiceType 为单值参数,按白名单编码逐轮查询,每轮独立切片额度),
|
||||
* 白名单为空退化为单轮全量——只同步数电普票/专票时不把他票种的额度消耗进来。
|
||||
* <p>拉取只认正常票(invoiceStatus=0),不推单据使用状态——在用/已用票随 updateTime 进窗口刷新
|
||||
* 金额使用状态,选票可用过滤在本地 SQL 执行;被删除/红冲票不再出现在拉取结果,由保存/认领重查闸门兜底。
|
||||
*
|
||||
@@ -1194,11 +1200,19 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
long startMillis = System.currentTimeMillis();
|
||||
LocalDateTime windowStart = LocalDateTime.now().minusDays(poolIncrementDays);
|
||||
LocalDateTime windowEnd = LocalDateTime.now();
|
||||
List<String> 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<InvoiceReceiptMap
|
||||
return summary;
|
||||
}
|
||||
|
||||
/** 白名单编码集:逗号分隔配置解析 trim,空配置返回空集(不限制) */
|
||||
private List<String> 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 BaseServiceImpl<InvoiceReceiptMap
|
||||
* @param sliceEnd 切片终点(不含,避免与下一片边界重复拉取)
|
||||
* @param stats 整轮统计累加器
|
||||
*/
|
||||
private void syncWindowSlice(LocalDateTime sliceStart, LocalDateTime sliceEnd, SyncStats stats) {
|
||||
private void syncWindowSlice(LocalDateTime sliceStart, LocalDateTime sliceEnd, String invoiceType, SyncStats stats) {
|
||||
// 二分收敛保护:切片粒度细到 1 秒仍触顶说明远端异常(同秒 >2.5 万条不可能),不再二分直接记漏
|
||||
if (!sliceStart.isBefore(sliceEnd)) {
|
||||
log.error("金蝶全票池同步切片区间为空({} ~ {}),远端 updateTime 粒度异常,放弃该切片", sliceStart, sliceEnd);
|
||||
@@ -1246,7 +1272,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
}
|
||||
boolean truncated = false;
|
||||
for (int pageNo = 1; pageNo <= MAX_POOL_PAGES; pageNo++) {
|
||||
List<BfiInvoicePoolVO> rows = this.pullPoolWindowPage(sliceStart, sliceEnd, pageNo);
|
||||
List<BfiInvoicePoolVO> rows = this.pullPoolWindowPage(sliceStart, sliceEnd, invoiceType, pageNo);
|
||||
if (rows.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
@@ -1282,8 +1308,8 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
}
|
||||
log.warn("金蝶全票池同步切片 {} ~ {} 触达翻页保护 {} 页({} 条),二分续扫 [{} ~ {}) + [{} ~ {})",
|
||||
sliceStart, sliceEnd, MAX_POOL_PAGES, MAX_POOL_PAGES * POOL_PAGE_SIZE, sliceStart, mid, mid, sliceEnd);
|
||||
this.syncWindowSlice(sliceStart, mid, stats);
|
||||
this.syncWindowSlice(mid, sliceEnd, stats);
|
||||
this.syncWindowSlice(sliceStart, mid, invoiceType, stats);
|
||||
this.syncWindowSlice(mid, sliceEnd, invoiceType, stats);
|
||||
return;
|
||||
}
|
||||
stats.sliceCount++;
|
||||
@@ -1364,14 +1390,16 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
return syncRecord;
|
||||
}
|
||||
|
||||
/** A801-1 按票池更新时间窗口分页拉取(只认正常票,不推单据使用状态,票面可用性由本地过滤) */
|
||||
private List<BfiInvoicePoolVO> pullPoolWindowPage(LocalDateTime windowStart, LocalDateTime windowEnd, int pageNo) {
|
||||
/** A801-1 按票池更新时间窗口分页拉取(只认正常票,不推单据使用状态;invoiceType 单值下推,null 不限) */
|
||||
private List<BfiInvoicePoolVO> 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<BfiInvoicePoolVO> poolPage = bfiService.queryInvoicePool(param);
|
||||
|
||||
@@ -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`)
|
||||
|
||||
Reference in New Issue
Block a user