feat: 金蝶票池选票回切镜像读(本地真分页+票号未命中回源+瘦身版全票池定时同步+结算认领确认重查闸门,下线实时游标透传)

- 选票两门面端点改查 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 等实时链路代码
This commit is contained in:
刘泉佳
2026-10-05 17:52:32 +08:00
parent 0dc6c9457a
commit d3f549bf42
11 changed files with 477 additions and 267 deletions
@@ -5,23 +5,22 @@
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 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.
* <p>
* 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.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* 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;
}
@@ -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;
}
@@ -1,66 +0,0 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* 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<KingdeeInvoicePoolVO> records;
/** 当前页码,从 1 开始 */
@Schema(description = "当前页码")
private Long current;
/** 每页条数 */
@Schema(description = "每页条数")
private Long size;
/** 是否还有下一页(服务端有界补页扫描判断) */
@Schema(description = "是否还有下一页")
private Boolean hasMore;
/** 下一页续扫游标("远程页码:已消费过滤行数"),null 表示没有更多;翻下一页时原样回传 */
@Schema(description = "下一页续扫游标,null 表示没有更多")
private String nextCursor;
}
@@ -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<KingdeeInvoicePoolPageVO> invoicePool(KingdeeInvoicePoolQueryDTO query) {
@Operation(summary = "金蝶票据池镜像分页查询(认领查询)")
public R<IPage<KingdeeInvoicePoolVO>> invoicePool(KingdeeInvoicePoolQueryDTO query) {
return R.data(invoiceReceiptService.invoicePoolPage(query));
}
}
@@ -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<KingdeeInvoicePoolPageVO> invoicePool(KingdeeInvoicePoolQueryDTO query) {
@Operation(summary = "金蝶票据池镜像分页查询(含认领/登记标记)")
public R<IPage<KingdeeInvoicePoolVO>> invoicePool(KingdeeInvoicePoolQueryDTO query) {
return R.data(invoiceReceiptService.invoicePoolPage(query));
}
@PostMapping("/sync-invoice-pool")
@ApiOperationSupport(order = 13)
@Operation(summary = "手动触发金蝶全票池发票同步(窗口拉取+镜像全列upsert)")
public R<String> syncInvoicePool() {
return R.data(invoiceReceiptService.syncKingdeeInvoicePool());
}
@GetMapping("/invoice-pool/attachment/download")
@ApiOperationSupport(order = 17)
@Operation(summary = "金蝶票池发票文件下载(A801-2 流式回源)",
@@ -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<KingdeeInvoicePool> {
/**
* 金蝶票据池镜像统一分页查询:仅出可用票(剩余可用金额>0 且未使用),认领/登记标记由 EXISTS 子查询直出
*
* @param page 分页参数
* @param query 统一查询条件(已在上层完成 trim 与日期格式校验)
* @param tenantId 当前租户
* @return 统一视图分页
*/
IPage<KingdeeInvoicePoolVO> selectInvoicePoolPage(IPage<KingdeeInvoicePoolVO> page,
@Param("query") KingdeeInvoicePoolQueryDTO query, @Param("tenantId") String tenantId);
}
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.transport.mapper.KingdeeInvoicePoolMapper">
<!-- 已认领:任一有效正式结算单发票行出现过(结算单作废不删发票行,须关联主单剔除作废与已删;自定义 SQL 无租户插件,手工带租户与逻辑删除) -->
<sql id="claimedExists">
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'))
</sql>
<!-- 已登记:存在非作废收票登记 -->
<sql id="registeredExists">
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)
</sql>
<!-- 统一视图列:价税合计与可认领/默认认领金额同源;剩余可用金额为金蝶可用税额口径(仅展示);镜像 invoice_type 落库即中文名 -->
<sql id="invoicePoolVoColumns">
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,
<include refid="claimedExists"/> AS claimed,
<include refid="registeredExists"/> AS registered
</sql>
<!-- 选票只出可用票:剩余可用金额>0 且金额使用状态未使用(与实时方案 isUsablePoolRow 同口径,本地化执行) -->
<sql id="usableFilter">
AND p.available_amount &gt; 0 AND p.amount_use_status = '0'
</sql>
<select id="selectInvoicePoolPage" resultType="org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO">
SELECT
<include refid="invoicePoolVoColumns"/>
FROM blade_kingdee_invoice_pool p
WHERE p.is_deleted = 0 AND p.status = 1 AND p.tenant_id = #{tenantId}
<include refid="usableFilter"/>
<if test="query.invoiceNo != null and query.invoiceNo != ''">
AND p.invoice_no LIKE CONCAT('%', #{query.invoiceNo}, '%')
</if>
<if test="query.salerName != null and query.salerName != ''">
AND p.issuer_name LIKE CONCAT('%', #{query.salerName}, '%')
</if>
<if test="query.buyerName != null and query.buyerName != ''">
AND p.receiver_name LIKE CONCAT('%', #{query.buyerName}, '%')
</if>
<if test="query.invoiceType != null and query.invoiceType != ''">
AND p.invoice_type_code = #{query.invoiceType}
</if>
<if test="query.salerTaxNo != null and query.salerTaxNo != ''">
AND p.saler_tax_no = #{query.salerTaxNo}
</if>
<if test="query.buyerTaxNo != null and query.buyerTaxNo != ''">
AND p.buyer_tax_no = #{query.buyerTaxNo}
</if>
<if test="query.invoiceDateStart != null and query.invoiceDateStart != ''">
AND p.invoice_date &gt;= #{query.invoiceDateStart}
</if>
<if test="query.invoiceDateEnd != null and query.invoiceDateEnd != ''">
AND p.invoice_date &lt;= #{query.invoiceDateEnd}
</if>
<if test="query.claimed != null and query.claimed">
AND <include refid="claimedExists"/>
</if>
<if test="query.claimed != null and !query.claimed">
AND NOT <include refid="claimedExists"/>
</if>
<if test="query.registered != null and query.registered">
AND <include refid="registeredExists"/>
</if>
<if test="query.registered != null and !query.registered">
AND NOT <include refid="registeredExists"/>
</if>
ORDER BY p.source_updated_time DESC, p.create_time DESC
</select>
</mapper>
@@ -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<InvoiceReceipt> {
InvoiceReceiptVO detail(Long id);
/**
* 金蝶票池实时统一查询(A801-1 游标分页 + 本地认领/登记装饰标记),收票登记选票与正式结算单认领共用
* <p>A801-1 不返回总数,改用「多取 1 条探测」得出 hasMore 驱动上一页/下一页;
* claimed/registered 仅本地展示装饰,不下推金蝶过滤。
* 金蝶票据池镜像统一查询(本地真分页,收票登记选票与正式结算单认领共用)。
* <p>仅出可用票(剩余可用金额>0 且金额使用状态未用),认领/登记三态过滤在 SQL 内直出;
* 票号精确查本地未命中时窄查一次 A801-1 回源入库后重查,兜底增量窗口缝隙内的当日新票。
*
* @param query 统一查询条件
* @return 实时游标分页
* @return 镜像统一视图分页
*/
KingdeeInvoicePoolPageVO invoicePoolPage(KingdeeInvoicePoolQueryDTO query);
IPage<KingdeeInvoicePoolVO> invoicePoolPage(KingdeeInvoicePoolQueryDTO query);
/**
* 金蝶全票池定时同步(瘦身版):滚动增量窗口分页拉取 A801-1 + 镜像批量全列 upsert。
* <p>无绑定回填/漏绑补偿/白名单(保存即写存根绑定,登记侧无需定时绑定);
* single-flight 锁防多副本并发,手动触发与定时任务共用。
*
* @return 执行摘要
*/
String syncKingdeeInvoicePool();
/**
* 结算单认领确认闸门:按票号集合逐张重查 A801-1 并按选票同口径复核可用性,
* 选中到确认之间票被认领/使用/红冲在此拦截(与收票保存重查同闸门)
*
* @param invoiceNos 待认领发票号码集合
*/
void assertInvoicesClaimable(Collection<String> invoiceNos);
/**
* 金蝶票池发票文件下载(A801-2 流式回源,前端查看/下载代理端点)。
@@ -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<FormalSettlementMapper, FormalSettlement>
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<FormalSettlemen
if (!invoiceNumbers.add(invoiceNo)) throw new ServiceException("发票号" + invoiceNo + "重复");
matchedTotal = matchedTotal.add(money(item.getMatchedAmount()));
}
// 认领闸门:确认前按票号重查金蝶复核可用性,选中到确认之间票被认领/使用/红冲在此拦截
invoiceReceiptService.assertInvoicesClaimable(invoiceNumbers);
if (matchedTotal.compareTo(money(settlement.getSettlementAmount())) > 0) {
throw new ServiceException("发票匹配结算单金额合计不能超过结算金额");
}
@@ -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<InvoiceReceiptMap
/** A801-1 rim 契约单页最大条数 */
private static final int POOL_PAGE_SIZE = 500;
/** A801-1 增量窗口翻页保护页数(rim 无总数,防御死循环;触顶说明窗口尾部本轮未处理) */
private static final int MAX_POOL_PAGES = 50;
/** A801-1 窗口时间参数格式 */
private static final java.time.format.DateTimeFormatter POOL_TIME_FORMATTER = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/** 金蝶全票池同步 single-flight 锁键前缀(多副本定时任务与手动触发共用) */
private static final String POOL_SYNC_LOCK_PREFIX = "transport:invoice-receipt:kingdee-pool-sync:";
/** A801-2 文件下载流式拷贝缓冲区大小 */
private static final int DOWNLOAD_BUFFER_SIZE = 8 * 1024;
@@ -159,6 +169,14 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
@Value("${thirdParty.bfi.receivedInvoice.lockLeaseSeconds:1800}")
private long receivedResultLockLeaseSeconds;
/** A801-1 全票池同步增量窗口天数(按票池更新时间滚动,每轮固定近 N 天,幂等覆盖) */
@Value("${thirdParty.bfi.invoiceReceipt.incrementDays:30}")
private int poolIncrementDays;
/** 金蝶全票池同步 single-flight 锁租约秒数,须大于最坏一轮耗时;进程异常退出时锁在租约到期后自动释放 */
@Value("${thirdParty.bfi.invoiceReceipt.lockLeaseSeconds:1800}")
private long poolSyncLockLeaseSeconds;
@Override
public IPage<InvoiceReceiptVO> selectPage(IPage<InvoiceReceipt> page, InvoiceReceiptVO query) {
LambdaQueryWrapper<InvoiceReceipt> wrapper = Wrappers.<InvoiceReceipt>lambdaQuery()
@@ -220,82 +238,61 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
}
@Override
public KingdeeInvoicePoolPageVO invoicePoolPage(KingdeeInvoicePoolQueryDTO query) {
public IPage<KingdeeInvoicePoolVO> 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<BfiInvoicePoolVO> 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<BfiInvoicePoolVO> rawRows = this.queryPoolPage(scanPageNo, fetchSize, query);
remoteCalls++;
List<BfiInvoicePoolVO> 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<KingdeeInvoicePoolVO> 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<BfiInvoicePoolVO> pageRows = window.subList(fromIndex, end);
// 末次拉取页内已消费的过滤行数:翻页游标锚定在该页,页内剩余行下页续用
int consumedInLastPage = Math.max(0, end - Math.max(fromIndex, filteredBeforeLastFetch));
boolean hasMore = !exhausted || end < window.size();
List<KingdeeInvoicePoolVO> 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<BfiInvoicePoolVO> 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<BfiInvoicePoolVO> poolPage = bfiService.queryInvoicePool(param);
return Optional.ofNullable(poolPage.getRows()).orElse(List.of());
/** 本地镜像真分页(可用过滤/认领登记标记均在 SQL 内直出) */
private IPage<KingdeeInvoicePoolVO> 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<BfiInvoicePoolVO> 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.<KingdeeInvoicePool>lambdaQuery()
.eq(KingdeeInvoicePool::getInvoiceNo, invoiceNo));
upsertInvoicePool(existing, poolRow);
log.info("金蝶票池回源入库完成,发票号码={}", invoiceNo);
return true;
}
/**
@@ -313,39 +310,6 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
return parseAmount(row.getInvoiceNo(), row.getAvailableAmount()).compareTo(BigDecimal.ZERO) > 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<InvoiceReceiptMap
}
}
/**
* A801-1 实时行 -> 统一视图:镜像未生成故 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<KingdeeInvoicePoolVO> records) {
if (records.isEmpty()) {
return;
}
List<String> invoiceNos = records.stream().map(KingdeeInvoicePoolVO::getInvoiceNo).distinct().toList();
Set<String> registeredNos = list(Wrappers.<InvoiceReceipt>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<String, Set<Long>> claimIdsByInvoice = formalSettlementInvoiceMapper.selectList(
Wrappers.<FormalSettlementInvoice>lambdaQuery()
.select(FormalSettlementInvoice::getInvoiceNo, FormalSettlementInvoice::getFormalSettlementId)
.in(FormalSettlementInvoice::getInvoiceNo, invoiceNos)).stream()
.collect(Collectors.groupingBy(FormalSettlementInvoice::getInvoiceNo,
Collectors.mapping(FormalSettlementInvoice::getFormalSettlementId, Collectors.toSet())));
Set<Long> allClaimIds = claimIdsByInvoice.values().stream().flatMap(Set::stream).collect(Collectors.toSet());
Set<Long> aliveSettlementIds = allClaimIds.isEmpty() ? Set.of()
: formalSettlementMapper.selectList(Wrappers.<FormalSettlement>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<Map<String, Object>> settlementCandidates(String keyword, Long receiptId) {
List<FormalSettlement> settlements = formalSettlementMapper.selectList(
@@ -1213,7 +1114,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
// 金蝶流水号作金蝶侧唯一标识(A801-1 无单据号概念)
mirror.setSerialNo(row.getSerialNo());
mirror.setKingdeeBillNo(row.getSerialNo());
mirror.setKingdeeStatus(KingdeeBfiConstant.KINGDEE_STATUS_SYNCED);
mirror.setKingdeeStatus(poolKingdeeStatus(row));
mirror.setInvoiceDate(parseDate(row.getInvoiceNo(), row.getInvoiceDate()));
mirror.setInvoiceType(BfiConstant.INVOICE_TYPE_NAMES.getOrDefault(row.getInvoiceType(), row.getInvoiceType()));
// 票易通原始编码随名称一并落库,A303-2 invoicetypef7_number 直接取用(名称反查不可靠)
@@ -1230,9 +1131,18 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
// A801-1 明细行原样序列化落库,A303-2 收票单分录取数来源;源侧无明细时维持库中原值(非空字段更新策略)
mirror.setItemsJson(buildItemsJson(row));
mirror.setSourceUpdatedTime(parseDateTime(row.getInvoiceNo(), row.getBillCreateTime()));
// 选票可用过滤本地化:剩余可用金额与金额使用状态随票池刷新,空金额按 0(不可用)兜底
mirror.setAvailableAmount(parseAmount(row.getInvoiceNo(), row.getAvailableAmount()));
mirror.setAmountUseStatus(trimOrNull(row.getAmountUseStatus()));
return mirror;
}
/** 票池作废映射:A801-1 delete=1(作废删除)置 failed,人工确认;正常票 synced */
private String poolKingdeeStatus(BfiInvoicePoolVO row) {
return "1".equals(row.getDelete()) ? KingdeeBfiConstant.KINGDEE_STATUS_FAILED
: KingdeeBfiConstant.KINGDEE_STATUS_SYNCED;
}
/**
* A801-1 发票明细行序列化(items 为空返回 null,更新时维持库中原值)
*/
@@ -1262,6 +1172,149 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
return JsonUtil.toJson(attachment);
}
// ==================== 金蝶全票池定时同步(瘦身版:窗口拉取 + 全列 upsert,无绑定/补偿/白名单) ====================
@Override
public String syncKingdeeInvoicePool() {
return kingdeeSyncLockSupport.runSingleFlight(POOL_SYNC_LOCK_PREFIX, poolSyncLockLeaseSeconds,
"金蝶全票池同步", this::executePoolSync);
}
/**
* 同步主体:滚动增量窗口分页拉取 A801-1(按票池更新时间)+ 镜像批量 upsert(全列覆盖)。
* 同步窗口内所有可用态票全量重写,无指纹比对(ponytail: 万行级窗口全列重写成本可接受,
* 人工字段清单式指纹有"新增列漏维护即静默不同步"坑;量级上来后升级为整行载荷规范化哈希)。
* 拉取只认正常票(invoiceStatus=0),不推单据使用状态——在用/已用票随 updateTime 进窗口刷新
* 金额使用状态,选票可用过滤在本地 SQL 执行;被删除/红冲票不再出现在拉取结果,由保存/认领重查闸门兜底。
*
* @return 执行摘要
*/
private String executePoolSync() {
long startMillis = System.currentTimeMillis();
LocalDateTime windowStart = LocalDateTime.now().minusDays(poolIncrementDays);
LocalDateTime windowEnd = LocalDateTime.now();
int pulledCount = 0;
int created = 0;
int updated = 0;
int pageCount = 0;
log.info("金蝶全票池同步开始,增量窗口 {} ~ {},单页 {} 条最多 {} 页",
windowStart.format(POOL_TIME_FORMATTER), windowEnd.format(POOL_TIME_FORMATTER),
POOL_PAGE_SIZE, MAX_POOL_PAGES);
for (int pageNo = 1; pageNo <= MAX_POOL_PAGES; pageNo++) {
List<BfiInvoicePoolVO> 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<BfiInvoicePoolVO> 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<BfiInvoicePoolVO> 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<BfiInvoicePoolVO> rows) {
Map<String, KingdeeInvoicePool> mirrorMap = new LinkedHashMap<>();
if (rows.isEmpty()) {
return new BatchUpsertResult(mirrorMap, 0, 0);
}
List<String> invoiceNos = rows.stream()
.map(row -> row.getInvoiceNo().trim())
.distinct()
.toList();
// 同页发票号码一次批量查询既有镜像
Map<String, KingdeeInvoicePool> existingMap = invoicePoolMapper.selectList(
Wrappers.<KingdeeInvoicePool>lambdaQuery()
.in(KingdeeInvoicePool::getInvoiceNo, invoiceNos)).stream()
.collect(Collectors.toMap(KingdeeInvoicePool::getInvoiceNo, Function.identity(), (first, second) -> first));
List<KingdeeInvoicePool> toInsert = new ArrayList<>();
List<KingdeeInvoicePool> 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<String, KingdeeInvoicePool> mirrors, int created, int updated) {
}
// ==================== 结算单认领确认重查闸门 ====================
@Override
public void assertInvoicesClaimable(Collection<String> invoiceNos) {
List<String> 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<InvoiceReceiptMap
return mirrorItem.get();
}
}
KingdeeInvoicePoolQueryDTO query = new KingdeeInvoicePoolQueryDTO();
query.setInvoiceNo(invoiceNo);
List<BfiInvoicePoolVO> rows = queryPoolPage(1, POOL_PAGE_SIZE, query);
List<BfiInvoicePoolVO> rows = queryPoolByInvoiceNo(invoiceNo);
for (BfiInvoicePoolVO row : rows) {
if (row.getInvoiceNo() != null && invoiceNo.equals(row.getInvoiceNo().trim())) {
return kingdeeAttachmentSyncSupport.parseInvoicePoolFile(buildAttachmentJson(row))
@@ -0,0 +1,69 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to the License, and the software
* must not be used for illegal purposes.
* <p>
* 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;
/**
* 收票管理金蝶全票池发票定时同步任务(瘦身版)
* <p>
* 定时按增量窗口分页拉取金蝶发票云全票池发票数据(A801-1,经 BFI 网关 rim 契约透传),
* 镜像表 blade_kingdee_invoice_pool 全列批量 upsert,供选票统一查本地(真分页/名称模糊/认领登记过滤)。
* 业务逻辑收敛在 {@link IInvoiceReceiptService#syncKingdeeInvoicePool()},任务类只做开关与调度,便于手动触发联调。
* <p>
* 调度配置(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);
}
}
}