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