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