:feat: 金蝶票池同步与展示查询按客商单位过滤
定时同步拉取后按客商发票信息税号集过滤,销/购方税号未同时登记的发票不入镜像(短页判定按原始条数,过滤不影响切片续扫);票池统一分页查询增加双 EXISTS 过滤与同步同开关同口径,收票登记选票与结算单认领两处展示自动生效
This commit is contained in:
+6
-4
@@ -44,11 +44,13 @@ public interface KingdeeInvoicePoolMapper extends BaseMapper<KingdeeInvoicePool>
|
||||
/**
|
||||
* 金蝶票据池镜像统一分页查询:仅出可用票(剩余可用金额>0 且未使用),认领/登记标记由 EXISTS 子查询直出
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param query 统一查询条件(已在上层完成 trim 与日期格式校验)
|
||||
* @param tenantId 当前租户
|
||||
* @param page 分页参数
|
||||
* @param query 统一查询条件(已在上层完成 trim 与日期格式校验)
|
||||
* @param tenantId 当前租户
|
||||
* @param customerUnitsOnly 客商单位过滤开关:开启时只出销/购方税号均登记在客商发票信息中的发票(与定时同步同开关)
|
||||
* @return 统一视图分页
|
||||
*/
|
||||
IPage<KingdeeInvoicePoolVO> selectInvoicePoolPage(IPage<KingdeeInvoicePoolVO> page,
|
||||
@Param("query") KingdeeInvoicePoolQueryDTO query, @Param("tenantId") String tenantId);
|
||||
@Param("query") KingdeeInvoicePoolQueryDTO query, @Param("tenantId") String tenantId,
|
||||
@Param("customerUnitsOnly") boolean customerUnitsOnly);
|
||||
}
|
||||
|
||||
+12
@@ -39,12 +39,24 @@
|
||||
AND p.available_amount > 0 AND p.amount_use_status = '0'
|
||||
</sql>
|
||||
|
||||
<!-- 客商单位过滤:开票单位(销方)与受票单位(购方)税号均须登记在有效客商发票信息中
|
||||
(与定时同步拉取过滤同开关同口径;自定义 SQL 无租户插件,手工带租户与逻辑删除) -->
|
||||
<sql id="customerUnitsFilter">
|
||||
AND EXISTS (SELECT 1 FROM blade_customer_invoice_info ci
|
||||
WHERE ci.is_deleted = 0 AND ci.status = 1 AND ci.tenant_id = p.tenant_id AND ci.tax_no = p.saler_tax_no)
|
||||
AND EXISTS (SELECT 1 FROM blade_customer_invoice_info cj
|
||||
WHERE cj.is_deleted = 0 AND cj.status = 1 AND cj.tenant_id = p.tenant_id AND cj.tax_no = p.buyer_tax_no)
|
||||
</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="customerUnitsOnly">
|
||||
<include refid="customerUnitsFilter"/>
|
||||
</if>
|
||||
<if test="query.invoiceNo != null and query.invoiceNo != ''">
|
||||
AND p.invoice_no LIKE CONCAT('%', #{query.invoiceNo}, '%')
|
||||
</if>
|
||||
|
||||
+56
-15
@@ -63,6 +63,7 @@ import org.springblade.transport.mapper.FormalSettlementMapper;
|
||||
import org.springblade.transport.mapper.InvoiceReceiptMapper;
|
||||
import org.springblade.transport.mapper.InvoiceReceiptRecordMapper;
|
||||
import org.springblade.transport.mapper.InvoiceReceiptSettlementMapper;
|
||||
import org.springblade.transport.mapper.CustomerInvoiceInfoMapper;
|
||||
import org.springblade.transport.mapper.KingdeeInvoicePoolMapper;
|
||||
import org.springblade.transport.pojo.dto.InvoiceReceiptSaveRequest;
|
||||
import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest;
|
||||
@@ -70,6 +71,7 @@ import org.springblade.transport.pojo.dto.KingdeeInvoicePoolQueryDTO;
|
||||
import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.CustomerArchive;
|
||||
import org.springblade.transport.pojo.entity.CustomerContact;
|
||||
import org.springblade.transport.pojo.entity.CustomerInvoiceInfo;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlement;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementDetail;
|
||||
import org.springblade.transport.pojo.entity.InvoiceReceipt;
|
||||
@@ -146,6 +148,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
private final FormalSettlementDetailMapper formalSettlementDetailMapper;
|
||||
private final CustomerArchiveMapper customerArchiveMapper;
|
||||
private final CustomerContactMapper customerContactMapper;
|
||||
private final CustomerInvoiceInfoMapper customerInvoiceInfoMapper;
|
||||
private final IBfiService bfiService;
|
||||
private final BfiProperties bfiProperties;
|
||||
private final KingdeeApBillSyncSupport kingdeeApBillSyncSupport;
|
||||
@@ -243,9 +246,10 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
return page;
|
||||
}
|
||||
|
||||
/** 本地镜像真分页(可用过滤/认领登记标记均在 SQL 内直出) */
|
||||
/** 本地镜像真分页(可用过滤/认领登记标记均在 SQL 内直出;客商单位过滤与定时同步同开关同口径) */
|
||||
private IPage<KingdeeInvoicePoolVO> selectLocalPoolPage(KingdeeInvoicePoolQueryDTO query, long current, long size) {
|
||||
return invoicePoolMapper.selectInvoicePoolPage(new Page<>(current, size), query, AuthUtil.getTenantId());
|
||||
return invoicePoolMapper.selectInvoicePoolPage(new Page<>(current, size), query, AuthUtil.getTenantId(),
|
||||
bfiProperties.getInvoiceReceipt().isCustomerUnitsOnly());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1248,20 +1252,25 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
LocalDateTime windowStart = LocalDateTime.now().minusDays(bfiProperties.getInvoiceReceipt().getIncrementDays());
|
||||
LocalDateTime windowEnd = LocalDateTime.now();
|
||||
List<String> invoiceTypes = this.poolInvoiceTypeCodes();
|
||||
// 客商单位过滤集:开关开启时一次性载入有效客商发票信息税号(销/购方税号都须命中才入镜像),整轮共享避免逐页查询
|
||||
Set<String> customerTaxNos = bfiProperties.getInvoiceReceipt().isCustomerUnitsOnly() ? this.customerInvoiceTaxNos() : null;
|
||||
SyncStats stats = new SyncStats();
|
||||
log.info("金蝶全票池同步开始,增量窗口 {} ~ {},发票种类:{},单页 {} 条最多 {} 页(触顶自动二分切片)",
|
||||
log.info("金蝶全票池同步开始,增量窗口 {} ~ {},发票种类:{},客商单位过滤:{},单页 {} 条最多 {} 页(触顶自动二分切片)",
|
||||
windowStart.format(POOL_TIME_FORMATTER), windowEnd.format(POOL_TIME_FORMATTER),
|
||||
invoiceTypes.isEmpty() ? "不限制" : invoiceTypes, POOL_PAGE_SIZE, MAX_POOL_PAGES);
|
||||
invoiceTypes.isEmpty() ? "不限制" : invoiceTypes, customerTaxNos != null ? "开启" : "关闭",
|
||||
POOL_PAGE_SIZE, MAX_POOL_PAGES);
|
||||
if (invoiceTypes.isEmpty()) {
|
||||
this.syncWindowSlice(windowStart, windowEnd, null, stats);
|
||||
this.syncWindowSlice(windowStart, windowEnd, null, customerTaxNos, stats);
|
||||
} else {
|
||||
// 单值参数拆轮:每个票种编码独立完整切片续扫,互不挤占翻页额度
|
||||
for (String invoiceType : invoiceTypes) {
|
||||
this.syncWindowSlice(windowStart, windowEnd, invoiceType, stats);
|
||||
this.syncWindowSlice(windowStart, windowEnd, invoiceType, customerTaxNos, stats);
|
||||
}
|
||||
}
|
||||
String summary = "金蝶全票池同步:翻页" + stats.pageCount + "页切片" + stats.sliceCount + "片拉取"
|
||||
+ stats.pulledCount + "张发票,镜像新增" + stats.created + "更新" + stats.updated + "无变化"
|
||||
+ stats.pulledCount + "张发票"
|
||||
+ (stats.customerFiltered > 0 ? "(客商单位过滤" + stats.customerFiltered + "张)" : "")
|
||||
+ ",镜像新增" + stats.created + "更新" + stats.updated + "无变化"
|
||||
+ stats.mirrorUnchanged + ",补绑" + stats.bound + ",登记无变化跳过" + stats.unchanged
|
||||
+ ",失败" + stats.failed + ",耗时" + (System.currentTimeMillis() - startMillis) + "ms";
|
||||
log.info("金蝶全票池同步结束,{}", summary);
|
||||
@@ -1284,6 +1293,8 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
private static final class SyncStats {
|
||||
/** A801-1 原始拉取张数 */
|
||||
int pulledCount;
|
||||
/** 客商单位过滤剔除张数(销/购方税号未同时登记在客商发票信息中,开关开启时统计) */
|
||||
int customerFiltered;
|
||||
/** 镜像新增条数(票池有、本地无) */
|
||||
int created;
|
||||
/** 镜像有变化并重写条数 */
|
||||
@@ -1307,11 +1318,13 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
* 触顶说明该时间片数据 >2.5 万,从中点二分为两片递归续扫(递归深度受时间粒度自然收敛,
|
||||
* 单片 500 条以内 1 页即短页终止)
|
||||
*
|
||||
* @param sliceStart 切片起点(含)
|
||||
* @param sliceEnd 切片终点(不含,避免与下一片边界重复拉取)
|
||||
* @param stats 整轮统计累加器
|
||||
* @param sliceStart 切片起点(含)
|
||||
* @param sliceEnd 切片终点(不含,避免与下一片边界重复拉取)
|
||||
* @param customerTaxNos 客商发票信息税号集,null 表示客商单位过滤关闭(整轮共享)
|
||||
* @param stats 整轮统计累加器
|
||||
*/
|
||||
private void syncWindowSlice(LocalDateTime sliceStart, LocalDateTime sliceEnd, String invoiceType, SyncStats stats) {
|
||||
private void syncWindowSlice(LocalDateTime sliceStart, LocalDateTime sliceEnd, String invoiceType,
|
||||
Set<String> customerTaxNos, SyncStats stats) {
|
||||
// 二分收敛保护:切片粒度细到 1 秒仍触顶说明远端异常(同秒 >2.5 万条不可能),不再二分直接记漏
|
||||
if (!sliceStart.isBefore(sliceEnd)) {
|
||||
log.error("金蝶全票池同步切片区间为空({} ~ {}),远端 updateTime 粒度异常,放弃该切片", sliceStart, sliceEnd);
|
||||
@@ -1325,6 +1338,13 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
}
|
||||
stats.pageCount++;
|
||||
stats.pulledCount += rows.size();
|
||||
// 客商单位过滤:销/购方税号未同时登记在客商发票信息中的发票不入镜像;
|
||||
// 短页判定用原始条数,过滤后的空页不能提前终止翻页
|
||||
int rawPageSize = rows.size();
|
||||
if (customerTaxNos != null) {
|
||||
rows = rows.stream().filter(row -> this.bothUnitsRegistered(row, customerTaxNos)).toList();
|
||||
stats.customerFiltered += rawPageSize - rows.size();
|
||||
}
|
||||
BatchUpsertResult upsertResult = this.batchUpsertInvoicePool(rows);
|
||||
stats.created += upsertResult.created();
|
||||
stats.updated += upsertResult.updated();
|
||||
@@ -1336,8 +1356,8 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
stats.unchanged += refreshResult.unchanged();
|
||||
stats.failed += refreshResult.failed();
|
||||
}
|
||||
// 远程短页:该片到底,正常终止
|
||||
if (rows.size() < POOL_PAGE_SIZE) {
|
||||
// 远程短页:该片到底,正常终止(按原始拉取条数判断,客商过滤不改变翻页节奏)
|
||||
if (rawPageSize < POOL_PAGE_SIZE) {
|
||||
stats.sliceCount++;
|
||||
return;
|
||||
}
|
||||
@@ -1355,13 +1375,34 @@ 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, invoiceType, stats);
|
||||
this.syncWindowSlice(mid, sliceEnd, invoiceType, stats);
|
||||
this.syncWindowSlice(sliceStart, mid, invoiceType, customerTaxNos, stats);
|
||||
this.syncWindowSlice(mid, sliceEnd, invoiceType, customerTaxNos, stats);
|
||||
return;
|
||||
}
|
||||
stats.sliceCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* 有效客商发票信息税号集(状态正常且未删除,MP 租户插件自动隔离租户):
|
||||
* 客商单位过滤开关开启时,同步拉取与票池展示查询共用的单位白名单
|
||||
*/
|
||||
private Set<String> customerInvoiceTaxNos() {
|
||||
return customerInvoiceInfoMapper.selectList(Wrappers.<CustomerInvoiceInfo>lambdaQuery()
|
||||
.eq(CustomerInvoiceInfo::getStatus, 1)
|
||||
.eq(CustomerInvoiceInfo::getIsDeleted, 0)).stream()
|
||||
.map(CustomerInvoiceInfo::getTaxNo)
|
||||
.filter(StringUtil::isNotBlank)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 开票单位(销方)与受票单位(购方)税号是否均已登记在客商发票信息中;税号缺失视为未登记(开关开启时不入镜像)
|
||||
*/
|
||||
private boolean bothUnitsRegistered(BfiInvoicePoolVO row, Set<String> customerTaxNos) {
|
||||
return StringUtil.isNotBlank(row.getSalerTaxNo()) && StringUtil.isNotBlank(row.getBuyerTaxNo())
|
||||
&& customerTaxNos.contains(row.getSalerTaxNo()) && customerTaxNos.contains(row.getBuyerTaxNo());
|
||||
}
|
||||
|
||||
/** 已登记票跟随刷新结果统计(绑定/刷新/无变化/失败) */
|
||||
private record RefreshResult(int bound, int unchanged, int failed) {
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user