✨ 合并收款认领合规推送,金蝶实际付款后再同步履约
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+4
-1
@@ -213,6 +213,9 @@ public interface IBfiClient {
|
||||
|
||||
/**
|
||||
* A103-4 付款处理查询
|
||||
* <p>
|
||||
* 注意:开始业务日期的请求参数名以金蝶契约为准,为 {@code beigin_bizdate}(并非 begin_bizdate),
|
||||
* 传错会被忽略并返回「参数【beigin_bizdate】必填」。
|
||||
*
|
||||
* @param accessToken 访问令牌,通过 getToken 获取
|
||||
* @param pageNo 页码,从1开始
|
||||
@@ -227,7 +230,7 @@ public interface IBfiClient {
|
||||
BfiResultVO<BfiPageVO<BfiPayBillVO>> payBillQuery(@RequestHeader(BfiConstant.HEADER_ACCESS_TOKEN) String accessToken,
|
||||
@RequestParam("pageNo") Integer pageNo,
|
||||
@RequestParam("pageSize") Integer pageSize,
|
||||
@RequestParam("begin_bizdate") String beginBizdate,
|
||||
@RequestParam("beigin_bizdate") String beginBizdate,
|
||||
@RequestParam("end_bizdate") String endBizdate,
|
||||
@RequestParam(value = "billno", required = false) List<String> billno,
|
||||
@RequestParam("org_number") List<String> orgNumber);
|
||||
|
||||
+2
-1
@@ -51,7 +51,8 @@ public class BfiPayBillQueryDTO implements Serializable {
|
||||
*/
|
||||
private Integer pageSize;
|
||||
/**
|
||||
* 开始业务日期,格式:yyyy-MM-dd,限定时间范围不可超过1个月
|
||||
* 开始业务日期,格式:yyyy-MM-dd,限定时间范围不可超过1个月;
|
||||
* 请求参数名为 beigin_bizdate(金蝶契约拼写如此,非 begin_bizdate)
|
||||
*/
|
||||
private String beginBizdate;
|
||||
/**
|
||||
|
||||
+4
-3
@@ -38,7 +38,7 @@ import java.util.List;
|
||||
* <p>
|
||||
* 请求示例:
|
||||
* <pre>{@code
|
||||
* GET /ierp/kapi/v2/awrp/cas/cas_recbill/queryRecBill?begin_bizdate=2021-09-08&end_bizdate=2026-09-08&org_number=01.01.20.02.04.01&pageSize=10&pageNo=1
|
||||
* GET /ierp/kapi/v2/awrp/cas/cas_recbill/queryRecBill?begin_bizdate=2021-09-08 00:00:00&end_bizdate=2026-09-08 23:59:59&org_number=01.01.20.02.04.01&pageSize=10&pageNo=1
|
||||
* }</pre>
|
||||
*
|
||||
* @author Chill
|
||||
@@ -57,12 +57,13 @@ public class BfiRecBillQueryDTO implements Serializable {
|
||||
*/
|
||||
private Integer pageSize;
|
||||
/**
|
||||
* 【必填】开始业务日期,格式:yyyy-MM-dd,限定时间范围不可超过1个月
|
||||
* 【必填】开始业务日期,格式:yyyy-MM-dd HH:mm:ss,窗口天数由调用方控制;
|
||||
* 传纯日期 yyyy-MM-dd 会被金蝶拒绝(bos.filterSqlBuilder 时间格式错误)
|
||||
*/
|
||||
@JsonProperty("begin_bizdate")
|
||||
private String beginBizdate;
|
||||
/**
|
||||
* 【必填】结束业务日期,格式:yyyy-MM-dd
|
||||
* 【必填】结束业务日期,格式:yyyy-MM-dd HH:mm:ss
|
||||
*/
|
||||
@JsonProperty("end_bizdate")
|
||||
private String endBizdate;
|
||||
|
||||
+1
-1
@@ -287,7 +287,7 @@ public interface IBfiService {
|
||||
/**
|
||||
* A301-1 收款单查询(收款处理查询)
|
||||
*
|
||||
* @param param 查询参数,业务日期范围不可超过1个月
|
||||
* @param param 查询参数,业务日期格式 yyyy-MM-dd HH:mm:ss,窗口天数由调用方控制
|
||||
* @return 收款处理分页数据
|
||||
*/
|
||||
BfiPageVO<BfiRecBillVO> queryRecBill(BfiRecBillQueryDTO param);
|
||||
|
||||
Vendored
+169
@@ -25,15 +25,19 @@
|
||||
*/
|
||||
package org.springblade.thirdparty.bfi.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiBaseDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
/**
|
||||
@@ -44,6 +48,7 @@ import java.util.function.IntFunction;
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
public class BfiUtil {
|
||||
|
||||
/**
|
||||
@@ -56,6 +61,11 @@ public class BfiUtil {
|
||||
*/
|
||||
public static final int DEFAULT_MAX_PAGES = 50;
|
||||
|
||||
/**
|
||||
* 按 totalCount 推导总页数时的兜底最大页数,仅防金蝶侧返回异常导致死循环
|
||||
*/
|
||||
public static final int DEFAULT_TOTAL_MAX_PAGES = 500;
|
||||
|
||||
private BfiUtil() {
|
||||
}
|
||||
|
||||
@@ -139,4 +149,163 @@ public class BfiUtil {
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* BFI 分页查询通用翻页:按首页返回的 totalCount 推导总页数,自动翻页汇总全量数据行
|
||||
* <p>
|
||||
* 与 {@link #queryAllPages(IntFunction)} 的区别:总页数由 totalCount 决定,固定页数仅作兜底,
|
||||
* 适合业务日期窗口较长、数据量可能超过固定页数上限的查询场景;达到兜底上限或未取满 totalCount 时打印告警,避免静默截断
|
||||
*
|
||||
* @param pageFetcher 页码 -> 分页结果,lambda 内负责设置页码(必要时含页大小)并调用接口
|
||||
* @param <T> 数据行类型
|
||||
* @return 全量数据行
|
||||
*/
|
||||
public static <T> List<T> queryAllPagesByTotal(IntFunction<BfiPageVO<T>> pageFetcher) {
|
||||
return queryAllPagesByTotal(pageFetcher, DEFAULT_TOTAL_MAX_PAGES);
|
||||
}
|
||||
|
||||
/**
|
||||
* BFI 分页查询通用翻页:按首页返回的 totalCount 推导总页数,自动翻页汇总全量数据行
|
||||
* <p>
|
||||
* 终止条件:返回页为空、数据行为空、lastPage=true、数据行不满一页(按返回 pageSize 判断)、取满推导出的总页数
|
||||
* <p>
|
||||
* totalCount 缺失或非正数时退化为 {@link #queryAllPages(IntFunction, int)} 的固定页数上限逻辑
|
||||
*
|
||||
* @param pageFetcher 页码 -> 分页结果,lambda 内负责设置页码(必要时含页大小)并调用接口
|
||||
* @param maxPages 兜底最大页数,防止金蝶侧 totalCount/lastPage 返回异常时死循环
|
||||
* @param <T> 数据行类型
|
||||
* @return 全量数据行
|
||||
*/
|
||||
public static <T> List<T> queryAllPagesByTotal(IntFunction<BfiPageVO<T>> pageFetcher, int maxPages) {
|
||||
List<T> rows = new ArrayList<>();
|
||||
int totalPages = maxPages;
|
||||
Integer totalCount = null;
|
||||
for (int pageNo = 1; pageNo <= totalPages; pageNo++) {
|
||||
BfiPageVO<T> page = pageFetcher.apply(pageNo);
|
||||
if (page == null || Func.isEmpty(page.getRows())) {
|
||||
return rows;
|
||||
}
|
||||
rows.addAll(page.getRows());
|
||||
if (pageNo == 1) {
|
||||
totalCount = page.getTotalCount();
|
||||
totalPages = resolveTotalPages(page, maxPages);
|
||||
}
|
||||
if (Boolean.TRUE.equals(page.getLastPage())) {
|
||||
return rows;
|
||||
}
|
||||
if (page.getPageSize() != null && page.getRows().size() < page.getPageSize()) {
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
if (totalCount != null && rows.size() < totalCount) {
|
||||
log.warn("BFI分页查询未取满,totalCount={},已汇总 {} 条,可能存在数据截断", totalCount, rows.size());
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按首页 totalCount 与 pageSize 推导总页数,超过兜底上限时按上限截断并告警
|
||||
*
|
||||
* @param page 首页分页结果
|
||||
* @param maxPages 兜底最大页数
|
||||
* @return 总页数
|
||||
*/
|
||||
private static int resolveTotalPages(BfiPageVO<?> page, int maxPages) {
|
||||
Integer totalCount = page.getTotalCount();
|
||||
Integer pageSize = page.getPageSize();
|
||||
if (totalCount == null || totalCount <= 0 || pageSize == null || pageSize <= 0) {
|
||||
return maxPages;
|
||||
}
|
||||
int totalPages = (totalCount + pageSize - 1) / pageSize;
|
||||
if (totalPages > maxPages) {
|
||||
log.warn("BFI分页查询需要 {} 页(totalCount={}),超过兜底上限 {} 页,超出部分将被截断", totalPages, totalCount, maxPages);
|
||||
return maxPages;
|
||||
}
|
||||
return totalPages;
|
||||
}
|
||||
|
||||
/**
|
||||
* BFI 时间窗口二分切片续扫翻页,逐页回调处理,窗口数据超过单查询翻页上限时尾部不漏同步
|
||||
* <p>
|
||||
* 适用按时间窗口增量同步且数据量可能超过单查询翻页上限的场景
|
||||
* (A801-1 票池同步按 updateTime、A301-1 收款单同步按业务日期):
|
||||
* <pre>{@code
|
||||
* int sliceCount = BfiUtil.forEachWindowSlice("金蝶全票池同步", windowStart, windowEnd,
|
||||
* PAGE_SIZE, MAX_PAGES,
|
||||
* (sliceStart, sliceEnd, pageNo) -> {
|
||||
* data.setUpdateTime(sliceStart.format(FORMATTER));
|
||||
* data.setUpdateTimeEnd(sliceEnd.format(FORMATTER));
|
||||
* data.setPageNo(pageNo);
|
||||
* data.setPageSize(PAGE_SIZE);
|
||||
* return bfiService.queryInvoicePool(param);
|
||||
* },
|
||||
* rows -> batchUpsert(rows));
|
||||
* }</pre>
|
||||
* <p>
|
||||
* 单片独立翻页(最多 maxPages 页):空页或末页不满一页即该片到底;触达页数保护说明该时间片
|
||||
* 数据超过单查询窗口上限(maxPages × pageSize),从中点二分为 [start, mid] + [mid, end] 两片递归续扫,
|
||||
* 总覆盖不受单查询上限约束。切片边界同刻度的数据可能在相邻两片各拉一次,pageHandler 必须幂等;
|
||||
* 切片细到不足 1 秒仍满页视为远端异常,告警跳过防御死递归。
|
||||
* 终止只认空页与短页,不依赖 lastPage(末页不满一页天然触发短页终止,lastPage 异常提前终止反而漏拉)
|
||||
*
|
||||
* @param bizName 业务名称(日志前缀)
|
||||
* @param windowStart 窗口起点(含)
|
||||
* @param windowEnd 窗口终点(含)
|
||||
* @param pageSize 分页大小(须与 pageFetcher 内实际设置的页大小一致)
|
||||
* @param maxPages 单片最大页数保护
|
||||
* @param pageFetcher 时间片起止 + 页码 → 分页结果,lambda 内负责设置时间窗口与页码并调用接口
|
||||
* @param pageHandler 单页数据行处理(过滤、upsert、统计等),必须幂等
|
||||
* @param <T> 数据行类型
|
||||
* @return 实际处理完的切片数(含空片,异常跳过的不计)
|
||||
*/
|
||||
public static <T> int forEachWindowSlice(String bizName, LocalDateTime windowStart, LocalDateTime windowEnd,
|
||||
int pageSize, int maxPages, WindowPageFetcher<T> pageFetcher, Consumer<List<T>> pageHandler) {
|
||||
// 二分收敛保护:切片区间为空说明时间粒度异常,放弃该片
|
||||
if (!windowStart.isBefore(windowEnd)) {
|
||||
log.error("{}切片区间为空({} ~ {}),时间粒度异常,放弃该切片", bizName, windowStart, windowEnd);
|
||||
return 0;
|
||||
}
|
||||
for (int pageNo = 1; pageNo <= maxPages; pageNo++) {
|
||||
BfiPageVO<T> page = pageFetcher.fetch(windowStart, windowEnd, pageNo);
|
||||
List<T> rows = page == null || page.getRows() == null ? List.of() : page.getRows();
|
||||
if (rows.isEmpty()) {
|
||||
return 1;
|
||||
}
|
||||
pageHandler.accept(rows);
|
||||
// 短页:该片到底,正常终止(不依赖 lastPage,末页不满一页天然触发)
|
||||
if (rows.size() < pageSize) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
// 触顶:该时间片数据超过单查询窗口上限,从中点二分续扫(边界同刻度数据由 pageHandler 幂等兜底)
|
||||
LocalDateTime mid = windowStart.plus(Duration.between(windowStart, windowEnd).dividedBy(2));
|
||||
if (!mid.isAfter(windowStart)) {
|
||||
log.error("{}切片 {} ~ {} 已达最小粒度仍满页(>{} 条),该时间片数据异常,跳过",
|
||||
bizName, windowStart, windowEnd, (long) maxPages * pageSize);
|
||||
return 0;
|
||||
}
|
||||
log.warn("{}切片 {} ~ {} 触达单片翻页保护 {} 页(>{} 条),二分续扫 [{} ~ {}] + [{} ~ {}]",
|
||||
bizName, windowStart, windowEnd, maxPages, (long) maxPages * pageSize, windowStart, mid, mid, windowEnd);
|
||||
return forEachWindowSlice(bizName, windowStart, mid, pageSize, maxPages, pageFetcher, pageHandler)
|
||||
+ forEachWindowSlice(bizName, mid, windowEnd, pageSize, maxPages, pageFetcher, pageHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间窗口切片翻页取数函数:按切片起止时间 + 页码拉取一页数据
|
||||
*
|
||||
* @param <T> 数据行类型
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface WindowPageFetcher<T> {
|
||||
|
||||
/**
|
||||
* 拉取单个时间片的一页数据
|
||||
*
|
||||
* @param sliceStart 切片起点(含)
|
||||
* @param sliceEnd 切片终点(含)
|
||||
* @param pageNo 页码,从 1 开始
|
||||
* @return 分页结果
|
||||
*/
|
||||
BfiPageVO<T> fetch(LocalDateTime sliceStart, LocalDateTime sliceEnd, int pageNo);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user