@ -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 = 10 00;
private static final int DEFAULT_ PAGE_SIZE = 5 00;
/ * *
* 单次调用最多扫描的记录数 , 兜底防止一次拉爆内存 ; 正常表单远达不到 。
* 单页记录数上限 , 取在线列表引擎上限 。
* /
private static final int SCAN_LIMIT = 50 000;
private static final int MAX_PAGE_SIZE = 1 000;
@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 pageSiz e ) {
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 {