Browse Source

feat(openapi): 按表单分页查询文件清单接口改造

- 将文件清单查询接口改为分页模式,支持pageNo和pageSize参数
- 返回数据结构从列表改为分页对象,包含list/pageNo/pageSize/total/hasMore字段
- 添加分页查询逻辑,每次只返回一页数据,通过hasMore判断是否继续翻页
- 移除原来的全量查询和扫描限制机制,改为标准分页实现
- 保留所有主表记录,无附件记录的files字段为空数组而非被丢弃
- 添加页码和页面大小参数验证,默认值为1和500,上限1000
feature/2026-08/0812-ccc-dev
chenchuchuan 1 day ago
parent
commit
fec35cd7d5
  1. 14
      common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiKbController.java
  2. 8
      common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiKbFileQueryDto.java
  3. 14
      common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiKbService.java
  4. 110
      common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiKbServiceImpl.java

14
common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiKbController.java

@ -2,7 +2,7 @@ package apelet.common.openapi.controller; @@ -2,7 +2,7 @@ package apelet.common.openapi.controller;
import apelet.common.core.constant.ErrorCodeEnum;
import apelet.common.core.object.ResponseResult;
import apelet.common.openapi.dto.OpenApiKbFileNodeDto;
import apelet.common.openapi.dto.OpenApiKbFilePageDto;
import apelet.common.openapi.dto.OpenApiKbFileQueryDto;
import apelet.common.openapi.dto.OpenApiKbPageNodeDto;
import apelet.common.openapi.dto.OpenApiKbTreeQueryDto;
@ -56,17 +56,19 @@ public class OpenApiKbController { @@ -56,17 +56,19 @@ public class OpenApiKbController {
}
/**
* 按表单(可选按附件字段)查询全部文件清单,供消费侧做增量比对后再逐个下载。清单不分页。
* 按表单(可选按附件字段)分页查询文件清单,供消费侧做增量比对后再逐个下载。
* <p>每次只返回一页,翻页由调用方用 pageNo 递增驱动,翻页只认 hasMore。返回的记录含全部主表记录,
* 没有附件的记录 files 为空数组(不是被丢弃),因此本页条数与 pageSize 一致(末页除外)。</p>
*
* @param queryDto 查询入参(formId 必填,fieldName 可缺省)。
* @return data 直接为记录节点数组(元素含 files 子数组)。
* @param queryDto 查询入参(formId 必填,fieldName/pageNo/pageSize 可缺省)。
* @return data 为分页对象(list/pageNo/pageSize/total/hasMore)。
*/
@PostMapping("/files")
public ResponseResult<List<OpenApiKbFileNodeDto>> files(@RequestBody(required = false) OpenApiKbFileQueryDto queryDto) {
public ResponseResult<OpenApiKbFilePageDto> files(@RequestBody(required = false) OpenApiKbFileQueryDto queryDto) {
if (queryDto == null) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST, "请求体不能为空!");
}
return openApiKbService.getFileList(queryDto.getFormId(), queryDto.getFieldName());
return openApiKbService.getFileList(queryDto.getFormId(), queryDto.getFieldName(), queryDto.getPageNo(), queryDto.getPageSize());
}
/**

8
common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiKbFileQueryDto.java

@ -18,4 +18,12 @@ public class OpenApiKbFileQueryDto { @@ -18,4 +18,12 @@ public class OpenApiKbFileQueryDto {
* 附件字段的数据库列名,选填;不传时返回该表单全部文件字段的附件。
*/
private String fieldName;
/**
* 页码,选填,从 1 开始;不传或非法按 1 处理。
*/
private Integer pageNo;
/**
* 单页记录数,选填;不传或非法按 500 处理,上限 1000。
*/
private Integer pageSize;
}

14
common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiKbService.java

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
package apelet.common.openapi.service;
import apelet.common.core.object.ResponseResult;
import apelet.common.openapi.dto.OpenApiKbFileNodeDto;
import apelet.common.openapi.dto.OpenApiKbFilePageDto;
import apelet.common.openapi.dto.OpenApiKbPageNodeDto;
import javax.servlet.http.HttpServletResponse;
@ -26,15 +26,17 @@ public interface OpenApiKbService { @@ -26,15 +26,17 @@ public interface OpenApiKbService {
List<OpenApiKbPageNodeDto> getPageFormTree(String keyword, Boolean onlyReady);
/**
* 按表单(可选按附件字段)查询全部文件清单,供消费侧做增量比对后再逐个下载。
* <p>清单不分页:消费侧靠它做增量比对,只给一页会让其余记录的文档被误判成"已删除"。内部按 1000 条一页翻完,
* 记录数超过 {@code SCAN_LIMIT} 时截断并记 warn 日志。</p>
* 按表单(可选按附件字段)分页查询文件清单,每次只返回一页,翻页由调用方驱动。
* <p>只认返回的 hasMore 判断是否到底,不要用"本页条数不足 pageSize"或"本页为空"判断。返回的记录含全部主表记录,
* 没有附件的记录 files 为空数组,因此本页条数与 pageSize 一致(末页除外)。</p>
*
* @param formId 在线表单主键Id。
* @param fieldName 附件字段的数据库列名,为空时返回该表单全部文件字段的附件。
* @return 记录节点列表(含 files 子数组),直接作为响应的 data。
* @param pageNo 页码,从 1 开始;为空或非法按 1 处理。
* @param pageSize 单页记录数;为空或非法按 500 处理,上限 1000。
* @return 分页对象(list/pageNo/pageSize/total/hasMore)作为响应的 data。
*/
ResponseResult<List<OpenApiKbFileNodeDto>> getFileList(Long formId, String fieldName);
ResponseResult<OpenApiKbFilePageDto> getFileList(Long formId, String fieldName, Integer pageNo, Integer pageSize);
/**
* 下载指定记录指定附件字段下的单个附件,直接写出二进制流。

110
common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiKbServiceImpl.java

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
package apelet.common.openapi.service.impl;
import apelet.common.core.constant.ErrorCodeEnum;
import apelet.common.core.object.MyOrderParam;
import apelet.common.core.object.MyPageParam;
import apelet.common.core.object.ObjectValue;
import apelet.common.core.object.ResponseResult;
@ -47,13 +48,13 @@ public class OpenApiKbServiceImpl implements OpenApiKbService { @@ -47,13 +48,13 @@ public class OpenApiKbServiceImpl implements OpenApiKbService {
*/
private static final String UPDATE_TIME_COLUMN = "update_time";
/**
* 单次翻页大小,取在线列表引擎上限。
* 缺省单页记录数,与接口文档示例一致。
*/
private static final int PAGE_SIZE = 1000;
private static final int DEFAULT_PAGE_SIZE = 500;
/**
* 单次调用最多扫描的记录数,兜底防止一次拉爆内存;正常表单远达不到。
* 单页记录数上限,取在线列表引擎上限。
*/
private static final int SCAN_LIMIT = 50000;
private static final int MAX_PAGE_SIZE = 1000;
@Autowired
private OnlinePageService onlinePageService;
@ -270,10 +271,12 @@ public class OpenApiKbServiceImpl implements OpenApiKbService { @@ -270,10 +271,12 @@ public class OpenApiKbServiceImpl implements OpenApiKbService {
// ============ 接口2:文件清单 ============
@Override
public ResponseResult<List<OpenApiKbFileNodeDto>> getFileList(Long formId, String fieldName) {
public ResponseResult<OpenApiKbFilePageDto> getFileList(Long formId, String fieldName, Integer pageNo, Integer pageSize) {
if (formId == null) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST, "formId 不能为空!");
}
int effectivePageNo = this.toIntParam(pageNo, 1, 1, Integer.MAX_VALUE);
int effectivePageSize = this.toIntParam(pageSize, DEFAULT_PAGE_SIZE, 1, MAX_PAGE_SIZE);
OnlineForm form = onlineFormService.getOnlineFormFromCache(formId);
if (form == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "在线表单不存在!");
@ -311,60 +314,79 @@ public class OpenApiKbServiceImpl implements OpenApiKbService { @@ -311,60 +314,79 @@ public class OpenApiKbServiceImpl implements OpenApiKbService {
if (datasourceResult.getData() == null || datasourceResult.getData().getMasterTable() == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "表单主表不存在!");
}
List<Map<String, Object>> rawList = this.queryAllRows(form, datasourceResult, primaryKeyColumn);
if (rawList == null) {
GridData gridData = this.queryPage(form, datasourceResult, primaryKeyColumn, effectivePageNo, effectivePageSize);
// 查询失败必须报错,不能返回 hasMore=false + 空列表:消费侧会把"出错"当成"拉完了",静默少拉一批。
if (gridData == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_ACCESS_FAILED, "列表查询失败!");
}
List<Map<String, Object>> rawList = gridData.getDataList();
// 列表路径的 SELECT 列来自列表设计器配置,并不强制包含主键;而 fillFileColumn 是靠主键回 zz_online_file 捞附件的,
// 主键缺失时附件会全部回填不出来,接口却照常返回 200。这里显式校验,把静默失败变成显式失败。
if (CollUtil.isNotEmpty(rawList) && !this.hasPrimaryKeyValue(rawList, primaryKeyColumn.getColumnName())) {
return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "该表单的列表未配置主键列,无法回填附件,请在表单列表中加入主键字段后重试!");
}
return ResponseResult.success(this.assembleFileNodeList(rawList, primaryKeyColumn.getColumnName(), fileColumnList));
return ResponseResult.success(this.buildPageData(rawList, primaryKeyColumn.getColumnName(), fileColumnList,
effectivePageNo, effectivePageSize, gridData.getTotalCount()));
} finally {
tokenData.setAppCode(savedAppCode);
}
}
/**
* 一次翻页拉完全部记录。
* <p>清单必须完整:消费侧靠它做增量比对(updateTime 变没变、记录还在不在),只取一页会导致其余记录的文档被判成"已删除"。
* 每页 1000 条(引擎上限),循环翻到累计条数达到 total 为止。真正的安全阀是 scanLimit,单次调用最多扫这么多个记录节点。</p>
* 只翻一页。页号由调用方给,内部不再循环,也不再有扫描行数上限——翻到 hasMore=false 即是全量。
*
* @return 全部记录行;查询失败返回 null。
* @return 该页的查询结果;查询失败返回 null(由调用方转成报错,避免被消费侧当成"拉完了")。
*/
private List<Map<String, Object>> queryAllRows(OnlineForm form, ResponseResult<OnlineDatasource> datasourceResult, OnlineColumn primaryKeyColumn) {
private GridData queryPage(OnlineForm form, ResponseResult<OnlineDatasource> datasourceResult, OnlineColumn primaryKeyColumn,
int pageNo, int pageSize) {
OnlFormHead onlFormHead = onlFormHeadService.getHeadTableNameCache(datasourceResult.getData().getMasterTable().getTableName());
if (onlFormHead == null) {
log.warn("OpenAPI 知识库清单:表单 [{}] 的表头不存在", form.getFormId());
return null;
}
List<Map<String, Object>> allRows = new ArrayList<>();
int pageNum = 1;
while (true) {
MyPageParam pageParam = new MyPageParam();
pageParam.setPageNum(pageNum);
pageParam.setPageSize(PAGE_SIZE);
GridData gridData = onlineOperationService.exeListPlugin(form.getFormId(), new ArrayList<>(), null, pageParam, null, onlFormHead, datasourceResult, null);
if (gridData == null) {
return null;
}
List<Map<String, Object>> rows = gridData.getDataList();
if (CollUtil.isEmpty(rows)) {
break;
}
allRows.addAll(rows);
// 本页不足一页,或已拉满 total,说明到底了
if (rows.size() < PAGE_SIZE || allRows.size() >= gridData.getTotalCount()) {
break;
}
if (allRows.size() >= SCAN_LIMIT) {
log.warn("OpenAPI 知识库清单:表单 [{}] 记录数超过单次扫描上限 {},本次结果可能不完整,请指定 fieldName 缩小范围或分批调用", form.getFormId(), SCAN_LIMIT);
break;
}
pageNum++;
MyPageParam pageParam = new MyPageParam();
pageParam.setPageNum(pageNo);
pageParam.setPageSize(pageSize);
return onlineOperationService.exeListPlugin(form.getFormId(), new ArrayList<>(), this.buildOrderParam(onlFormHead, primaryKeyColumn),
pageParam, null, onlFormHead, datasourceResult, null);
}
/**
* 构造 KB 清单专用排序:主键降序。
* <p>列表路径的默认排序兜底只有 create_time DESC(见 ListDataOrmService#loadGridData),同一秒创建的记录相对顺序不稳定,
* OFFSET 分页按"第 N 行"切片就会切错位置,导致某行在两页各出现一次、或两页都不出现。主键唯一,补上它即得到全序。
* 该排序与列表 UI 的排序语义无关,KB 只需要一次完整稳定的遍历。</p>
*
* @return 排序参数;EAS 异构表单返回 null,见方法内注释。
*/
private MyOrderParam buildOrderParam(OnlFormHead onlFormHead, OnlineColumn primaryKeyColumn) {
// EAS 异构表单:ListDataEasService 会把 orderParam 的字段名映射到 onlFormField,映射不到直接空指针
// (见 ListDataEasService#loadGridData),且它自带 number DESC 排序,因此这里不传排序。
if (onlFormHead.getIsomerismMark() != null) {
return null;
}
return allRows;
MyOrderParam orderParam = new MyOrderParam();
MyOrderParam.OrderInfo orderInfo = new MyOrderParam.OrderInfo();
// 取小写列名,与 ORM 选择列及 create_time 兜底的写法保持同一形态。
orderInfo.setFieldName(primaryKeyColumn.getColumnName().toLowerCase());
orderInfo.setAsc(false);
orderParam.add(orderInfo);
return orderParam;
}
/**
* 组装分页应答:记录节点 + 回显分页参数 + hasMore。
* <p>hasMore 按分页那一层(主表记录数)算,与本页返回的清单条数无关——即使本页记录全都没有附件,也不影响翻页。</p>
*/
private OpenApiKbFilePageDto buildPageData(List<Map<String, Object>> rawList, String primaryKeyColumnName,
List<OnlineColumn> fileColumnList, int pageNo, int pageSize, int totalCount) {
OpenApiKbFilePageDto pageDto = new OpenApiKbFilePageDto();
pageDto.setList(this.assembleFileNodeList(rawList, primaryKeyColumnName, fileColumnList));
pageDto.setPageNo(pageNo);
pageDto.setPageSize(pageSize);
pageDto.setTotal((long) totalCount);
pageDto.setHasMore((long) pageNo * pageSize < totalCount);
return pageDto;
}
private boolean hasPrimaryKeyValue(List<Map<String, Object>> rawList, String primaryKeyColumnName) {
@ -372,9 +394,9 @@ public class OpenApiKbServiceImpl implements OpenApiKbService { @@ -372,9 +394,9 @@ public class OpenApiKbServiceImpl implements OpenApiKbService {
}
/**
* 把查询结果裁剪成文件清单:每个记录节点只保留主键、记录更新时间与附件条目,并丢弃一个附件都没有的记录。
* <p>丢弃无附件记录既省流量,也让消费侧的 changeKey diff 语义自洽:某条记录从清单里消失,
* 就代表它不再有附件,应当删除对应文档。</p>
* 把查询结果裁剪成文件清单:每个记录节点只保留主键、记录更新时间与附件条目。
* <p>记录一律保留,某个字段没有附件时 files 为空数组。这样页内条数与本次请求的 pageSize 一致
* (末页除外),消费侧不需要靠"本页条数"做任何推断;翻页只认 hasMore。</p>
*/
private List<OpenApiKbFileNodeDto> assembleFileNodeList(List<Map<String, Object>> rawList, String primaryKeyColumnName, List<OnlineColumn> fileColumnList) {
List<OpenApiKbFileNodeDto> result = new ArrayList<>();
@ -394,9 +416,6 @@ public class OpenApiKbServiceImpl implements OpenApiKbService { @@ -394,9 +416,6 @@ public class OpenApiKbServiceImpl implements OpenApiKbService {
}
files.addAll(this.parseFileItems(fileJson, column.getColumnName()));
}
if (files.isEmpty()) {
continue;
}
OpenApiKbFileNodeDto node = new OpenApiKbFileNodeDto();
node.setRecordId(this.toLongId(String.valueOf(recordId)));
String updateTime = this.toPlainString(this.getRowValue(row, UPDATE_TIME_COLUMN));
@ -532,7 +551,8 @@ public class OpenApiKbServiceImpl implements OpenApiKbService { @@ -532,7 +551,8 @@ public class OpenApiKbServiceImpl implements OpenApiKbService {
/**
* 安全地把入参解析为整数(兼容 1.0 等数值形态),非法/缺省回退默认值,并夹到 [min, max]。
*/
private int toIntParam(Object value, int defaultValue, int min, int max) { if (value == null || StrUtil.isBlank(String.valueOf(value))) {
private int toIntParam(Object value, int defaultValue, int min, int max) {
if (value == null || StrUtil.isBlank(String.valueOf(value))) {
return defaultValue;
}
try {

Loading…
Cancel
Save