From d3f549bf42350bdb42a09e6e51ff05e133800898 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Mon, 5 Oct 2026 17:52:32 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=91=E8=9D=B6=E7=A5=A8=E6=B1=A0?=
=?UTF-8?q?=E9=80=89=E7=A5=A8=E5=9B=9E=E5=88=87=E9=95=9C=E5=83=8F=E8=AF=BB?=
=?UTF-8?q?=EF=BC=88=E6=9C=AC=E5=9C=B0=E7=9C=9F=E5=88=86=E9=A1=B5+?=
=?UTF-8?q?=E7=A5=A8=E5=8F=B7=E6=9C=AA=E5=91=BD=E4=B8=AD=E5=9B=9E=E6=BA=90?=
=?UTF-8?q?+=E7=98=A6=E8=BA=AB=E7=89=88=E5=85=A8=E7=A5=A8=E6=B1=A0?=
=?UTF-8?q?=E5=AE=9A=E6=97=B6=E5=90=8C=E6=AD=A5+=E7=BB=93=E7=AE=97?=
=?UTF-8?q?=E8=AE=A4=E9=A2=86=E7=A1=AE=E8=AE=A4=E9=87=8D=E6=9F=A5=E9=97=B8?=
=?UTF-8?q?=E9=97=A8=EF=BC=8C=E4=B8=8B=E7=BA=BF=E5=AE=9E=E6=97=B6=E6=B8=B8?=
=?UTF-8?q?=E6=A0=87=E9=80=8F=E4=BC=A0=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 选票两门面端点改查 blade_kingdee_invoice_pool 本地镜像:Mapper.xml 真分页+total 跳页,
名称模糊/票种(invoice_type_code)/双税号/claimed/registered 三态过滤 SQL 直出,
仅出可用票(available_amount>0 且 amount_use_status='0',本地化执行原 isUsablePoolRow 口径)
- 票号精确查本地未命中→窄查一次 A801-1 回源入库后重查,兜底增量窗口缝隙内当日新票
- 瘦身版同步任务回归:增量窗口分页拉取+镜像全列批量 upsert(无绑定/补偿/白名单/指纹),
复用 KingdeeSyncLockSupport single-flight;手动端点 POST /invoice-receipt/sync-invoice-pool 供 bootstrap
- 镜像实体/表新增 available_amount/amount_use_status,fillInvoicePool 统一回填(存根/回源/同步同口径)
- 结算认领确认补齐服务端按票号重查闸门(与收票保存 persistMirrorFromRemote 同闸门),收票保存重查原样保留
- 删除 KingdeeInvoicePoolPageVO 游标分页契约与 PoolScanCursor/decorateUseFlags 等实时链路代码
---
.../pojo/dto/KingdeeInvoicePoolQueryDTO.java | 51 ++-
.../pojo/entity/KingdeeInvoicePool.java | 8 +
.../pojo/vo/KingdeeInvoicePoolPageVO.java | 66 ---
.../FormalSettlementController.java | 6 +-
.../controller/InvoiceReceiptController.java | 13 +-
.../mapper/KingdeeInvoicePoolMapper.java | 14 +
.../mapper/KingdeeInvoicePoolMapper.xml | 86 ++++
.../service/IInvoiceReceiptService.java | 30 +-
.../impl/FormalSettlementServiceImpl.java | 6 +
.../impl/InvoiceReceiptServiceImpl.java | 395 ++++++++++--------
.../task/InvoiceReceiptKingdeeSyncTask.java | 69 +++
11 files changed, 477 insertions(+), 267 deletions(-)
delete mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/KingdeeInvoicePoolPageVO.java
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeInvoicePoolMapper.xml
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/task/InvoiceReceiptKingdeeSyncTask.java
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/KingdeeInvoicePoolQueryDTO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/KingdeeInvoicePoolQueryDTO.java
index cf5a24e8..4e726481 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/KingdeeInvoicePoolQueryDTO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/KingdeeInvoicePoolQueryDTO.java
@@ -5,23 +5,22 @@
* 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
+ * 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
+ * Using this software signifies agreement to the 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)
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.springblade.transport.pojo.dto;
@@ -32,12 +31,12 @@ import java.io.Serial;
import java.io.Serializable;
/**
- * 金蝶票池实时统一查询条件(收票登记选票与正式结算单认领共用)
+ * 金蝶票据池镜像统一查询条件(收票登记选票与正式结算单认领共用,查本地镜像)
*
* @author Chill
*/
@Data
-@Schema(description = "金蝶票池实时统一查询条件")
+@Schema(description = "金蝶票据池镜像统一查询条件")
public class KingdeeInvoicePoolQueryDTO implements Serializable {
@Serial
@@ -73,6 +72,24 @@ public class KingdeeInvoicePoolQueryDTO implements Serializable {
@Schema(description = "开票日期止 yyyy-MM-dd")
private String invoiceDateEnd;
+ /**
+ * 开票单位(销方名称),模糊
+ */
+ @Schema(description = "开票单位(销方名称,模糊)")
+ private String salerName;
+
+ /**
+ * 受票单位(购方名称),模糊
+ */
+ @Schema(description = "受票单位(购方名称,模糊)")
+ private String buyerName;
+
+ /**
+ * 发票类型编码(票易通字典,如 26/27,镜像按 invoice_type_code 精确匹配)
+ */
+ @Schema(description = "发票类型编码(票易通字典,如 26/27)")
+ private String invoiceType;
+
/**
* 销方税号,精确
*/
@@ -86,14 +103,14 @@ public class KingdeeInvoicePoolQueryDTO implements Serializable {
private String buyerTaxNo;
/**
- * 发票类型编码(票易通字典,如 26/27)
+ * 认领过滤:true 仅已认领,false 仅未认领,null 不过滤(已认领=存在非作废正式结算单发票行)
*/
- @Schema(description = "发票类型编码(票易通字典,如 26/27)")
- private String invoiceType;
+ @Schema(description = "认领过滤:true已认领/false未认领/null不限")
+ private Boolean claimed;
/**
- * 续扫游标:下一页翻页时原样回传上页返回的 nextCursor,空表示按页码锚定重新扫描
+ * 登记过滤:true 仅已登记,false 仅未登记,null 不过滤(已登记=存在非作废收票登记)
*/
- @Schema(description = "续扫游标(下一页请求回传上页 nextCursor)")
- private String cursor;
+ @Schema(description = "登记过滤:true已登记/false未登记/null不限")
+ private Boolean registered;
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeInvoicePool.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeInvoicePool.java
index fbde7a17..79eb12e7 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeInvoicePool.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeInvoicePool.java
@@ -143,4 +143,12 @@ public class KingdeeInvoicePool extends TenantEntity {
* 票池数据更新时间(A801-1 billCreateTime,镜像排序字段)
*/
private LocalDateTime sourceUpdatedTime;
+ /**
+ * 剩余可用金额(A801-1 availableAmount,金蝶可用税额口径,选票本地过滤用:>0 才可选)
+ */
+ private BigDecimal availableAmount;
+ /**
+ * 金额使用状态(A801-1 amountUseStatus,0 未使用可选 / 2 全部使用;选票本地过滤用)
+ */
+ private String amountUseStatus;
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/KingdeeInvoicePoolPageVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/KingdeeInvoicePoolPageVO.java
deleted file mode 100644
index 4e80ad61..00000000
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/KingdeeInvoicePoolPageVO.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * 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.pojo.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-
-import java.io.Serial;
-import java.io.Serializable;
-import java.util.List;
-
-/**
- * 金蝶票池实时查询游标分页返回体(A801-1 无总数:hasMore 驱动上一页/下一页)
- *
- * @author Chill
- */
-@Data
-@Schema(description = "金蝶票池游标分页返回体")
-public class KingdeeInvoicePoolPageVO implements Serializable {
-
- @Serial
- private static final long serialVersionUID = 1L;
-
- /** 本页数据(金蝶实时行 + 本地认领/登记装饰标记) */
- @Schema(description = "本页数据")
- private List records;
-
- /** 当前页码,从 1 开始 */
- @Schema(description = "当前页码")
- private Long current;
-
- /** 每页条数 */
- @Schema(description = "每页条数")
- private Long size;
-
- /** 是否还有下一页(服务端有界补页扫描判断) */
- @Schema(description = "是否还有下一页")
- private Boolean hasMore;
-
- /** 下一页续扫游标("远程页码:已消费过滤行数"),null 表示没有更多;翻下一页时原样回传 */
- @Schema(description = "下一页续扫游标,null 表示没有更多")
- private String nextCursor;
-}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java
index 4ad3efc1..313fe1e0 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java
@@ -30,7 +30,7 @@ import org.springblade.transport.pojo.dto.FormalSettlementInvoiceClaimRequest;
import org.springblade.transport.pojo.dto.KingdeeInvoicePoolQueryDTO;
import org.springblade.transport.pojo.dto.PreSettlementDetailAdjustRequest;
import org.springblade.transport.pojo.entity.FormalSettlementDetailFee;
-import org.springblade.transport.pojo.vo.KingdeeInvoicePoolPageVO;
+import org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO;
import org.springblade.transport.pojo.vo.FormalSettlementVO;
import org.springblade.transport.excel.FormalSettlementExcel;
import org.springblade.transport.pojo.vo.PreSettlementVO;
@@ -262,8 +262,8 @@ public class FormalSettlementController extends BladeController {
@GetMapping("/invoice-pool")
@ApiOperationSupport(order = 21)
- @Operation(summary = "金蝶票池实时分页查询(认领查询)")
- public R invoicePool(KingdeeInvoicePoolQueryDTO query) {
+ @Operation(summary = "金蝶票据池镜像分页查询(认领查询)")
+ public R> invoicePool(KingdeeInvoicePoolQueryDTO query) {
return R.data(invoiceReceiptService.invoicePoolPage(query));
}
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java
index 24cf3584..7dcb6f0a 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/InvoiceReceiptController.java
@@ -40,7 +40,7 @@ import org.springblade.transport.pojo.dto.InvoiceReceiptSaveRequest;
import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest;
import org.springblade.transport.pojo.dto.KingdeeInvoicePoolQueryDTO;
import org.springblade.transport.pojo.vo.InvoiceReceiptVO;
-import org.springblade.transport.pojo.vo.KingdeeInvoicePoolPageVO;
+import org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO;
import org.springblade.transport.service.IInvoiceReceiptService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -82,11 +82,18 @@ public class InvoiceReceiptController extends BladeController {
@GetMapping("/invoice-pool")
@ApiOperationSupport(order = 3)
- @Operation(summary = "金蝶票池实时分页查询(含认领/登记标记)")
- public R invoicePool(KingdeeInvoicePoolQueryDTO query) {
+ @Operation(summary = "金蝶票据池镜像分页查询(含认领/登记标记)")
+ public R> invoicePool(KingdeeInvoicePoolQueryDTO query) {
return R.data(invoiceReceiptService.invoicePoolPage(query));
}
+ @PostMapping("/sync-invoice-pool")
+ @ApiOperationSupport(order = 13)
+ @Operation(summary = "手动触发金蝶全票池发票同步(窗口拉取+镜像全列upsert)")
+ public R syncInvoicePool() {
+ return R.data(invoiceReceiptService.syncKingdeeInvoicePool());
+ }
+
@GetMapping("/invoice-pool/attachment/download")
@ApiOperationSupport(order = 17)
@Operation(summary = "金蝶票池发票文件下载(A801-2 流式回源)",
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeInvoicePoolMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeInvoicePoolMapper.java
index 69663196..29ddf75d 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeInvoicePoolMapper.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeInvoicePoolMapper.java
@@ -26,8 +26,12 @@
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.springblade.transport.pojo.dto.KingdeeInvoicePoolQueryDTO;
import org.springblade.transport.pojo.entity.KingdeeInvoicePool;
+import org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO;
/**
* 金蝶进项发票票据池 Mapper 接口
@@ -37,4 +41,14 @@ import org.springblade.transport.pojo.entity.KingdeeInvoicePool;
@Mapper
public interface KingdeeInvoicePoolMapper extends BaseMapper {
+ /**
+ * 金蝶票据池镜像统一分页查询:仅出可用票(剩余可用金额>0 且未使用),认领/登记标记由 EXISTS 子查询直出
+ *
+ * @param page 分页参数
+ * @param query 统一查询条件(已在上层完成 trim 与日期格式校验)
+ * @param tenantId 当前租户
+ * @return 统一视图分页
+ */
+ IPage selectInvoicePoolPage(IPage page,
+ @Param("query") KingdeeInvoicePoolQueryDTO query, @Param("tenantId") String tenantId);
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeInvoicePoolMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeInvoicePoolMapper.xml
new file mode 100644
index 00000000..b25b0ddc
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeInvoicePoolMapper.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+ EXISTS (SELECT 1 FROM blade_formal_settlement_invoice c
+ WHERE c.is_deleted = 0 AND c.tenant_id = p.tenant_id AND c.invoice_no = p.invoice_no
+ AND EXISTS (SELECT 1 FROM blade_formal_settlement s
+ WHERE s.id = c.formal_settlement_id AND s.is_deleted = 0
+ AND s.approval_status != 'voided'))
+
+
+
+
+ EXISTS (SELECT 1 FROM blade_invoice_receipt r
+ WHERE r.is_deleted = 0 AND r.tenant_id = p.tenant_id
+ AND r.approval_status != 'voided' AND r.invoice_no = p.invoice_no)
+
+
+
+
+ p.id, p.invoice_no, p.serial_no,
+ DATE_FORMAT(p.invoice_date, '%Y-%m-%d') AS invoice_date,
+ p.invoice_type, p.tax_rate, p.invoice_amount, p.tax_amount,
+ p.receiver_name, p.buyer_tax_no, p.issuer_name, p.saler_tax_no,
+ p.bank_name, p.bank_account, p.issuing_bank, p.phone,
+ p.customer_emails, p.department_emails,
+ p.kingdee_bill_no, p.kingdee_status, p.check_status, p.attachments_json,
+ p.invoice_amount AS available_invoice_amount,
+ p.invoice_amount AS matched_amount,
+ p.available_amount AS available_tax_amount,
+ AS claimed,
+ AS registered
+
+
+
+
+ AND p.available_amount > 0 AND p.amount_use_status = '0'
+
+
+
+ SELECT
+
+ FROM blade_kingdee_invoice_pool p
+ WHERE p.is_deleted = 0 AND p.status = 1 AND p.tenant_id = #{tenantId}
+
+
+ AND p.invoice_no LIKE CONCAT('%', #{query.invoiceNo}, '%')
+
+
+ AND p.issuer_name LIKE CONCAT('%', #{query.salerName}, '%')
+
+
+ AND p.receiver_name LIKE CONCAT('%', #{query.buyerName}, '%')
+
+
+ AND p.invoice_type_code = #{query.invoiceType}
+
+
+ AND p.saler_tax_no = #{query.salerTaxNo}
+
+
+ AND p.buyer_tax_no = #{query.buyerTaxNo}
+
+
+ AND p.invoice_date >= #{query.invoiceDateStart}
+
+
+ AND p.invoice_date <= #{query.invoiceDateEnd}
+
+
+ AND
+
+
+ AND NOT
+
+
+ AND
+
+
+ AND NOT
+
+ ORDER BY p.source_updated_time DESC, p.create_time DESC
+
+
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceReceiptService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceReceiptService.java
index 7220003e..a03b78d8 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceReceiptService.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceReceiptService.java
@@ -33,8 +33,9 @@ import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest;
import org.springblade.transport.pojo.dto.KingdeeInvoicePoolQueryDTO;
import org.springblade.transport.pojo.entity.InvoiceReceipt;
import org.springblade.transport.pojo.vo.InvoiceReceiptVO;
-import org.springblade.transport.pojo.vo.KingdeeInvoicePoolPageVO;
+import org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -50,14 +51,31 @@ public interface IInvoiceReceiptService extends BaseService {
InvoiceReceiptVO detail(Long id);
/**
- * 金蝶票池实时统一查询(A801-1 游标分页 + 本地认领/登记装饰标记),收票登记选票与正式结算单认领共用
- * A801-1 不返回总数,改用「多取 1 条探测」得出 hasMore 驱动上一页/下一页;
- * claimed/registered 仅本地展示装饰,不下推金蝶过滤。
+ * 金蝶票据池镜像统一查询(本地真分页,收票登记选票与正式结算单认领共用)。
+ *
仅出可用票(剩余可用金额>0 且金额使用状态未用),认领/登记三态过滤在 SQL 内直出;
+ * 票号精确查本地未命中时窄查一次 A801-1 回源入库后重查,兜底增量窗口缝隙内的当日新票。
*
* @param query 统一查询条件
- * @return 实时游标分页
+ * @return 镜像统一视图分页
*/
- KingdeeInvoicePoolPageVO invoicePoolPage(KingdeeInvoicePoolQueryDTO query);
+ IPage invoicePoolPage(KingdeeInvoicePoolQueryDTO query);
+
+ /**
+ * 金蝶全票池定时同步(瘦身版):滚动增量窗口分页拉取 A801-1 + 镜像批量全列 upsert。
+ * 无绑定回填/漏绑补偿/白名单(保存即写存根绑定,登记侧无需定时绑定);
+ * single-flight 锁防多副本并发,手动触发与定时任务共用。
+ *
+ * @return 执行摘要
+ */
+ String syncKingdeeInvoicePool();
+
+ /**
+ * 结算单认领确认闸门:按票号集合逐张重查 A801-1 并按选票同口径复核可用性,
+ * 选中到确认之间票被认领/使用/红冲在此拦截(与收票保存重查同闸门)
+ *
+ * @param invoiceNos 待认领发票号码集合
+ */
+ void assertInvoicesClaimable(Collection invoiceNos);
/**
* 金蝶票池发票文件下载(A801-2 流式回源,前端查看/下载代理端点)。
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 ff4ab0f2..7ebf7728 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
@@ -81,6 +81,7 @@ import org.springblade.transport.pojo.vo.FormalSettlementVO;
import org.springblade.transport.pojo.vo.PreSettlementVO;
import org.springblade.transport.pojo.vo.SettlementAdjustmentVO;
import org.springblade.transport.service.IFormalSettlementService;
+import org.springblade.transport.service.IInvoiceReceiptService;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.IPreSettlementService;
import org.springblade.transport.service.IWaybillService;
@@ -127,6 +128,9 @@ import java.util.stream.Collectors;
public class FormalSettlementServiceImpl extends BaseServiceImpl
implements IFormalSettlementService {
+ /** 金蝶票池认领闸门(收票服务承载 A801-1 重查与可用性口径) */
+ private final IInvoiceReceiptService invoiceReceiptService;
+
/** 金蝶应收单勾稽 single-flight 锁键前缀(多副本定时任务与手动触发共用) */
private static final String AR_RESULT_SYNC_LOCK_PREFIX = "transport:formal-settlement:kingdee-ar-result-sync:";
@@ -822,6 +826,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl 0) {
throw new ServiceException("发票匹配结算单金额合计不能超过结算金额");
}
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 edac2eb8..3b699a6d 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
@@ -28,6 +28,8 @@ package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.toolkit.Db;
import feign.Response;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
@@ -74,7 +76,6 @@ import org.springblade.transport.pojo.entity.InvoiceReceiptRecord;
import org.springblade.transport.pojo.entity.InvoiceReceiptSettlement;
import org.springblade.transport.pojo.entity.KingdeeInvoicePool;
import org.springblade.transport.pojo.vo.InvoiceReceiptVO;
-import org.springblade.transport.pojo.vo.KingdeeInvoicePoolPageVO;
import org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO;
import org.springblade.transport.service.IInvoiceReceiptService;
import org.springblade.transport.service.KingdeeSyncContext;
@@ -123,6 +124,15 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl selectPage(IPage page, InvoiceReceiptVO query) {
LambdaQueryWrapper wrapper = Wrappers.lambdaQuery()
@@ -220,82 +238,61 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl invoicePoolPage(KingdeeInvoicePoolQueryDTO query) {
long current = query.getCurrent() == null ? 1L : Math.max(query.getCurrent(), 1L);
long size = query.getSize() == null ? 10L : Math.min(Math.max(query.getSize(), 1L), 100L);
- // 查询票号(已 trim 归一,方法级可见):用于对远程模糊命中做全等防御
- String queryInvoiceNo = trimOrNull(query.getInvoiceNo());
- // WHY: 金额可用过滤发生在拉取之后,下一页必须按游标续扫才能跨请求对齐过滤进度,按页码锚定会漏行/重行
- PoolScanCursor cursor = PoolScanCursor.parse(query.getCursor());
- List window = new ArrayList<>();
- // scanPageNo 指向待拉取的远程页;游标续扫固定按 500 行窗口取数,保证"页码:过滤偏移"语义稳定
- long scanPageNo = cursor == null ? current : cursor.pageNo();
- int skip = cursor == null ? 0 : cursor.skip();
- boolean exhausted = false;
- // 末次拉取前的过滤累计行数与末次拉取页码:未消费的过滤行只可能落在末次拉取页内
- int filteredBeforeLastFetch = 0;
- long lastFetchPageNo = scanPageNo;
- int remoteCalls = 0;
- // 有界循环补页:单请求最多 POOL_MAX_REMOTE_CALLS 次远程调用凑满一页,仍不足则返回稀疏页并给出游标
- for (int call = 0; call < KingdeeBfiConstant.POOL_MAX_REMOTE_CALLS && window.size() - skip < size; call++) {
- // 首拉按 size+1 轻量探测,补拉按契约上限 500 行以减少调用次数
- int fetchSize = call == 0 && cursor == null ? (int) size + 1 : POOL_PAGE_SIZE;
- List rawRows = this.queryPoolPage(scanPageNo, fetchSize, query);
- remoteCalls++;
- List usableRows = rawRows.stream()
- .filter(this::isUsablePoolRow)
- // 按票号查询时全等防御:远程为模糊匹配,命中"包含"关系的其他票号不得混入选票结果
- .filter(row -> queryInvoiceNo == null || queryInvoiceNo.equals(row.getInvoiceNo().trim()))
- .toList();
- filteredBeforeLastFetch = window.size();
- window.addAll(usableRows);
- lastFetchPageNo = scanPageNo;
- // WHY: A801-1 无总数,远程短页是唯一耗尽信号
- if (rawRows.size() < fetchSize) {
- exhausted = true;
- break;
- }
- scanPageNo++;
+ query.setCurrent(current);
+ query.setSize(size);
+ query.setInvoiceNo(trimOrNull(query.getInvoiceNo()));
+ query.setSalerName(trimOrNull(query.getSalerName()));
+ query.setBuyerName(trimOrNull(query.getBuyerName()));
+ query.setInvoiceType(trimOrNull(query.getInvoiceType()));
+ query.setSalerTaxNo(trimOrNull(query.getSalerTaxNo()));
+ query.setBuyerTaxNo(trimOrNull(query.getBuyerTaxNo()));
+ query.setInvoiceDateStart(normalizeQueryDate(query.getInvoiceDateStart()));
+ query.setInvoiceDateEnd(normalizeQueryDate(query.getInvoiceDateEnd()));
+ IPage page = this.selectLocalPoolPage(query, current, size);
+ // 票号精确查本地未命中(且无认领/登记过滤干扰)→ 窄查一次 A801-1 回源入库再本地重查:
+ // 兜底增量窗口缝隙内的当日新票;名称模糊本地为空不回源(远程不支持名称条件,回源救不了)
+ if (page.getRecords().isEmpty() && query.getInvoiceNo() != null
+ && query.getClaimed() == null && query.getRegistered() == null
+ && this.fetchInvoicePoolFromRemote(query.getInvoiceNo())) {
+ page = this.selectLocalPoolPage(query, current, size);
}
- // 按游标偏移切页:skip 为游标窗口内已消费的过滤行数
- int fromIndex = Math.min(skip, window.size());
- int end = Math.min(fromIndex + (int) size, window.size());
- List pageRows = window.subList(fromIndex, end);
- // 末次拉取页内已消费的过滤行数:翻页游标锚定在该页,页内剩余行下页续用
- int consumedInLastPage = Math.max(0, end - Math.max(fromIndex, filteredBeforeLastFetch));
- boolean hasMore = !exhausted || end < window.size();
- List records = pageRows.stream().map(this::toPoolViewVO).toList();
- this.decorateUseFlags(records);
- KingdeeInvoicePoolPageVO result = new KingdeeInvoicePoolPageVO();
- result.setCurrent(current);
- result.setSize(size);
- result.setHasMore(hasMore);
- result.setRecords(records);
- result.setNextCursor(hasMore ? PoolScanCursor.encode(lastFetchPageNo, consumedInLastPage) : null);
- log.info("金蝶票池实时查询 第{}页 {}条/页 返回{}条 hasMore:{} 远程调用{}次 游标:{}", current, size,
- records.size(), hasMore, remoteCalls, result.getNextCursor());
- return result;
+ log.info("金蝶票池镜像查询 第{}页 {}条/页 返回{}条 total:{}", current, size, page.getRecords().size(), page.getTotal());
+ return page;
}
- /** A801-1 按页拉取原始行:查询条件清洗与选票口径集中在此,循环补页多次复用 */
- private List queryPoolPage(long pageNo, int pageSize, KingdeeInvoicePoolQueryDTO query) {
- BfiInvoicePoolQueryDataDTO data = new BfiInvoicePoolQueryDataDTO();
- data.setPageNo((int) pageNo);
- data.setPageSize(pageSize);
- // 固定只认正常状态票,作废/异常票不进选票结果
- data.setInvoiceStatus(KingdeeBfiConstant.POOL_INVOICE_STATUS_NORMAL);
- data.setInvoiceNo(trimOrNull(query.getInvoiceNo()));
- data.setSalerTaxNo(trimOrNull(query.getSalerTaxNo()));
- data.setBuyerTaxNo(trimOrNull(query.getBuyerTaxNo()));
- data.setInvoiceType(trimOrNull(query.getInvoiceType()));
- // 单据使用状态固定查未用,不走前端条件
- data.setExpenseStatus(KingdeeBfiConstant.POOL_EXPENSE_STATUS_UNUSED);
- data.setInvoiceDateStart(normalizeQueryDate(query.getInvoiceDateStart()));
- data.setInvoiceDateEnd(normalizeQueryDate(query.getInvoiceDateEnd()));
- BfiInvoicePoolQueryDTO param = new BfiInvoicePoolQueryDTO();
- param.setQueryParam(data);
- BfiPageVO poolPage = bfiService.queryInvoicePool(param);
- return Optional.ofNullable(poolPage.getRows()).orElse(List.of());
+ /** 本地镜像真分页(可用过滤/认领登记标记均在 SQL 内直出) */
+ private IPage selectLocalPoolPage(KingdeeInvoicePoolQueryDTO query, long current, long size) {
+ return invoicePoolMapper.selectInvoicePoolPage(new Page<>(current, size), query, AuthUtil.getTenantId());
+ }
+
+ /**
+ * 按发票号码窄查 A801-1 回源入库(票号全等防御 + 选票同口径可用过滤,不可用票不入镜像)。
+ *
+ * @param invoiceNo 已 trim 的发票号码
+ * @return 是否有可用票入库(false:远程未命中或不可用,调用方维持本地空页)
+ */
+ private boolean fetchInvoicePoolFromRemote(String invoiceNo) {
+ List poolRows = queryPoolByInvoiceNo(invoiceNo);
+ BfiInvoicePoolVO poolRow = poolRows.stream()
+ .filter(row -> StringUtil.isNotBlank(row.getInvoiceNo())
+ && invoiceNo.equals(row.getInvoiceNo().trim()))
+ .findFirst()
+ .orElse(null);
+ if (poolRow == null) {
+ return false;
+ }
+ if (!this.isUsablePoolRow(poolRow)) {
+ log.info("金蝶票池回源命中但不可用,不入镜像,发票号码={}", invoiceNo);
+ return false;
+ }
+ KingdeeInvoicePool existing = invoicePoolMapper.selectOne(Wrappers.lambdaQuery()
+ .eq(KingdeeInvoicePool::getInvoiceNo, invoiceNo));
+ upsertInvoicePool(existing, poolRow);
+ log.info("金蝶票池回源入库完成,发票号码={}", invoiceNo);
+ return true;
}
/**
@@ -313,39 +310,6 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl 0;
}
- /**
- * 票池扫描游标("远程页码:已消费过滤行数"):过滤发生在拉取之后,翻页按游标续扫才能跨请求对齐过滤进度;
- * 游标页码统一按 500 行窗口定位,解析失败降级为按页码锚定(仅存在翻页错位风险,不阻断查询)
- */
- private record PoolScanCursor(long pageNo, int skip) {
-
- static PoolScanCursor parse(String text) {
- if (StringUtil.isBlank(text)) {
- return null;
- }
- String[] parts = text.trim().split(":");
- if (parts.length != 2) {
- log.warn("金蝶票池续扫游标格式非法,忽略后按页码锚定,游标={}", text);
- return null;
- }
- try {
- long pageNo = Long.parseLong(parts[0].trim());
- int skip = Integer.parseInt(parts[1].trim());
- if (pageNo < 1 || skip < 0) {
- throw new NumberFormatException("游标数值越界");
- }
- return new PoolScanCursor(pageNo, skip);
- } catch (NumberFormatException exception) {
- log.warn("金蝶票池续扫游标解析失败,忽略后按页码锚定,游标={}", text);
- return null;
- }
- }
-
- static String encode(long pageNo, int skip) {
- return pageNo + ":" + skip;
- }
- }
-
/** 空白串归一为 null,便于远程查询条件与 MyBatis-Plus 判空 */
private String trimOrNull(String text) {
return StringUtil.isBlank(text) ? null : text.trim();
@@ -363,69 +327,6 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl 统一视图:镜像未生成故 id 为空;金额口径价税合计三值同源;
- * 银行/邮箱/电话远端无来源保持空,与镜像存根同形
- */
- private KingdeeInvoicePoolVO toPoolViewVO(BfiInvoicePoolVO row) {
- KingdeeInvoicePoolVO vo = new KingdeeInvoicePoolVO();
- vo.setInvoiceNo(row.getInvoiceNo().trim());
- vo.setSerialNo(row.getSerialNo());
- vo.setInvoiceDate(row.getInvoiceDate());
- vo.setInvoiceType(BfiConstant.INVOICE_TYPE_NAMES.getOrDefault(row.getInvoiceType(), row.getInvoiceType()));
- vo.setTaxRate(resolveTaxRate(row));
- vo.setTaxAmount(parseAmount(row.getInvoiceNo(), row.getTaxAmount()));
- BigDecimal totalAmount = parseAmount(row.getInvoiceNo(), row.getTotalAmount());
- vo.setInvoiceAmount(totalAmount);
- vo.setAvailableInvoiceAmount(totalAmount);
- vo.setMatchedAmount(totalAmount);
- // 剩余可用金额为金蝶可用税额口径,仅用于选票展示,与可认领金额的价税合计口径不同
- vo.setAvailableTaxAmount(parseAmount(row.getInvoiceNo(), row.getAvailableAmount()));
- vo.setReceiverName(row.getBuyerName());
- vo.setBuyerTaxNo(row.getBuyerTaxNo());
- vo.setIssuerName(row.getSalerName());
- vo.setSalerTaxNo(row.getSalerTaxNo());
- vo.setKingdeeBillNo(row.getSerialNo());
- vo.setCheckStatus(row.getCheckStatus());
- vo.setAttachmentsJson(buildAttachmentJson(row));
- return vo;
- }
-
- /**
- * claimed/registered 只读装饰:本页票号三次批量本地查询内存打标,金蝶不感知、不作过滤下推;
- * claimed=票关联的结算单中存在未作废主单(作废结算单不占坑)
- */
- private void decorateUseFlags(List records) {
- if (records.isEmpty()) {
- return;
- }
- List invoiceNos = records.stream().map(KingdeeInvoicePoolVO::getInvoiceNo).distinct().toList();
- Set registeredNos = list(Wrappers.lambdaQuery()
- .select(InvoiceReceipt::getInvoiceNo)
- .in(InvoiceReceipt::getInvoiceNo, invoiceNos)
- .ne(InvoiceReceipt::getApprovalStatus, TransportBusiConstant.VOIDED)
- .eq(InvoiceReceipt::getIsDeleted, 0)).stream()
- .map(InvoiceReceipt::getInvoiceNo).collect(Collectors.toSet());
- Map> claimIdsByInvoice = formalSettlementInvoiceMapper.selectList(
- Wrappers.lambdaQuery()
- .select(FormalSettlementInvoice::getInvoiceNo, FormalSettlementInvoice::getFormalSettlementId)
- .in(FormalSettlementInvoice::getInvoiceNo, invoiceNos)).stream()
- .collect(Collectors.groupingBy(FormalSettlementInvoice::getInvoiceNo,
- Collectors.mapping(FormalSettlementInvoice::getFormalSettlementId, Collectors.toSet())));
- Set allClaimIds = claimIdsByInvoice.values().stream().flatMap(Set::stream).collect(Collectors.toSet());
- Set aliveSettlementIds = allClaimIds.isEmpty() ? Set.of()
- : formalSettlementMapper.selectList(Wrappers.lambdaQuery()
- .select(FormalSettlement::getId)
- .in(FormalSettlement::getId, allClaimIds)
- .ne(FormalSettlement::getApprovalStatus, TransportBusiConstant.VOIDED)).stream()
- .map(FormalSettlement::getId).collect(Collectors.toSet());
- records.forEach(record -> {
- record.setRegistered(registeredNos.contains(record.getInvoiceNo()));
- record.setClaimed(claimIdsByInvoice.getOrDefault(record.getInvoiceNo(), Set.of()).stream()
- .anyMatch(aliveSettlementIds::contains));
- });
- }
-
@Override
public List> settlementCandidates(String keyword, Long receiptId) {
List settlements = formalSettlementMapper.selectList(
@@ -1213,7 +1114,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl rows = this.pullPoolWindowPage(windowStart, windowEnd, pageNo);
+ if (rows.isEmpty()) {
+ break;
+ }
+ pageCount++;
+ pulledCount += rows.size();
+ BatchUpsertResult upsertResult = this.batchUpsertInvoicePool(rows);
+ created += upsertResult.created();
+ updated += upsertResult.updated();
+ if (rows.size() < POOL_PAGE_SIZE) {
+ break;
+ }
+ }
+ if (pageCount >= MAX_POOL_PAGES) {
+ // 末轮仍满页说明窗口数据量已超过翻页保护,尾部数据本轮未处理(须缩小 incrementDays 或调大保护页数)
+ log.warn("金蝶全票池同步翻页达最大保护 {} 页({} 条)仍未到底,窗口尾部本轮未处理",
+ MAX_POOL_PAGES, MAX_POOL_PAGES * POOL_PAGE_SIZE);
+ }
+ String summary = "金蝶全票池同步:翻页" + pageCount + "页拉取" + pulledCount + "张发票,镜像新增" + created
+ + "更新" + updated + ",耗时" + (System.currentTimeMillis() - startMillis) + "ms";
+ log.info("金蝶全票池同步结束,{}", summary);
+ return summary;
+ }
+
+ /** A801-1 按票池更新时间窗口分页拉取(只认正常票,不推单据使用状态,票面可用性由本地过滤) */
+ private List pullPoolWindowPage(LocalDateTime windowStart, LocalDateTime windowEnd, 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);
+ BfiInvoicePoolQueryDTO param = new BfiInvoicePoolQueryDTO();
+ param.setQueryParam(data);
+ BfiPageVO poolPage = bfiService.queryInvoicePool(param);
+ log.info("金蝶全票池同步 第{}页拉取 {} 条", pageNo, poolPage.getRows() == null ? 0 : poolPage.getRows().size());
+ return poolPage.getRows() == null ? List.of() : poolPage.getRows();
+ }
+
+ /**
+ * 票池镜像批量 upsert:一次查询同页既有镜像,全列覆盖批量写库(新增批量插入、既有批量更新),
+ * 避免逐条查改 N+1;MyBatis-Plus 非空字段更新策略下源侧空字段维持库中原值,与手动同步路径同口径
+ *
+ * @param rows A801-1 窗口拉取的票池发票
+ * @return 批量 upsert 结果
+ */
+ private BatchUpsertResult batchUpsertInvoicePool(List rows) {
+ Map mirrorMap = new LinkedHashMap<>();
+ if (rows.isEmpty()) {
+ return new BatchUpsertResult(mirrorMap, 0, 0);
+ }
+ List invoiceNos = rows.stream()
+ .map(row -> row.getInvoiceNo().trim())
+ .distinct()
+ .toList();
+ // 同页发票号码一次批量查询既有镜像
+ Map existingMap = invoicePoolMapper.selectList(
+ Wrappers.lambdaQuery()
+ .in(KingdeeInvoicePool::getInvoiceNo, invoiceNos)).stream()
+ .collect(Collectors.toMap(KingdeeInvoicePool::getInvoiceNo, Function.identity(), (first, second) -> first));
+ List toInsert = new ArrayList<>();
+ List toUpdate = new ArrayList<>();
+ for (BfiInvoicePoolVO row : rows) {
+ if (Func.isEmpty(row.getInvoiceNo())) {
+ continue;
+ }
+ KingdeeInvoicePool mirror = fillInvoicePool(existingMap.get(row.getInvoiceNo().trim()), row);
+ mirrorMap.put(mirror.getInvoiceNo(), mirror);
+ if (mirror.getId() == null) {
+ toInsert.add(mirror);
+ } else {
+ toUpdate.add(mirror);
+ }
+ }
+ // 批量写库:失败中断本轮,由任务层记录异常、下轮重试(upsert 幂等)
+ if (!toInsert.isEmpty()) {
+ Db.saveBatch(toInsert);
+ }
+ if (!toUpdate.isEmpty()) {
+ Db.updateBatchById(toUpdate);
+ }
+ log.info("金蝶全票池镜像批量写入完成,新增 {} 条,更新 {} 条", toInsert.size(), toUpdate.size());
+ return new BatchUpsertResult(mirrorMap, toInsert.size(), toUpdate.size());
+ }
+
+ /** 票池镜像批量 upsert 结果(invoiceNo -> 镜像,新增条数、更新条数) */
+ private record BatchUpsertResult(Map mirrors, int created, int updated) {
+ }
+
+ // ==================== 结算单认领确认重查闸门 ====================
+
+ @Override
+ public void assertInvoicesClaimable(Collection invoiceNos) {
+ List distinctNos = invoiceNos == null ? List.of() : invoiceNos.stream()
+ .filter(Func::isNotEmpty)
+ .map(String::trim)
+ .distinct()
+ .toList();
+ for (String invoiceNo : distinctNos) {
+ // 窄查 + 票号全等防御 + 选票同口径可用复核:选中到确认之间票被认领/使用/红冲在此拦截
+ BfiInvoicePoolVO poolRow = queryPoolByInvoiceNo(invoiceNo).stream()
+ .filter(row -> StringUtil.isNotBlank(row.getInvoiceNo())
+ && invoiceNo.equals(row.getInvoiceNo().trim()))
+ .findFirst()
+ .orElse(null);
+ if (poolRow == null) {
+ throw new ServiceException("金蝶票池未查询到发票 " + invoiceNo + ",不能认领");
+ }
+ if (!this.isUsablePoolRow(poolRow)) {
+ throw new ServiceException("发票 " + invoiceNo + " 剩余可用金额为0或金额使用状态已变更,不能认领");
+ }
+ }
+ }
+
// ==================== 金蝶票池发票文件下载(A801-2 前端代理,见 2026-09-27 设计文档) ====================
@Override
@@ -1351,9 +1404,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl rows = queryPoolPage(1, POOL_PAGE_SIZE, query);
+ List rows = queryPoolByInvoiceNo(invoiceNo);
for (BfiInvoicePoolVO row : rows) {
if (row.getInvoiceNo() != null && invoiceNo.equals(row.getInvoiceNo().trim())) {
return kingdeeAttachmentSyncSupport.parseInvoicePoolFile(buildAttachmentJson(row))
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/InvoiceReceiptKingdeeSyncTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/InvoiceReceiptKingdeeSyncTask.java
new file mode 100644
index 00000000..ee8a269b
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/InvoiceReceiptKingdeeSyncTask.java
@@ -0,0 +1,69 @@
+/**
+ * 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.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 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 the License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package org.springblade.transport.task;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.transport.service.IInvoiceReceiptService;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * 收票管理金蝶全票池发票定时同步任务(瘦身版)
+ *
+ * 定时按增量窗口分页拉取金蝶发票云全票池发票数据(A801-1,经 BFI 网关 rim 契约透传),
+ * 镜像表 blade_kingdee_invoice_pool 全列批量 upsert,供选票统一查本地(真分页/名称模糊/认领登记过滤)。
+ * 业务逻辑收敛在 {@link IInvoiceReceiptService#syncKingdeeInvoicePool()},任务类只做开关与调度,便于手动触发联调。
+ *
+ * 调度配置(Nacos thirdParty.bfi.invoiceReceipt.*):syncEnabled 开关、cron 表达式、incrementDays 增量窗口。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class InvoiceReceiptKingdeeSyncTask {
+
+ private final IInvoiceReceiptService invoiceReceiptService;
+
+ /** 全票池同步任务开关,默认关闭(上线先手动跑一次 bootstrap 铺底,联调稳定后开启) */
+ @Value("${thirdParty.bfi.invoiceReceipt.syncEnabled:false}")
+ private boolean syncEnabled;
+
+ /** 金蝶全票池同步调度,默认每30分钟执行一次 */
+ @Scheduled(cron = "${thirdParty.bfi.invoiceReceipt.cron:0 */30 * * * ?}")
+ public void syncKingdeeInvoicePool() {
+ if (!syncEnabled) {
+ return;
+ }
+ try {
+ String summary = invoiceReceiptService.syncKingdeeInvoicePool();
+ log.info("定时同步金蝶全票池发票:{}", summary);
+ } catch (Exception exception) {
+ log.error("定时同步金蝶全票池发票异常", exception);
+ }
+ }
+}