Browse Source

feat(common-openapi): 添加OpenAPI第三方应用管理和认证功能

- 新增OpenApiApp实体类及对应的数据库映射
- 实现OpenApiAppService服务接口及具体业务逻辑
- 添加OpenApiAppController控制器提供应用CRUD操作
- 实现OpenApiAuthService服务进行token签发与验证
- 添加OpenApiAuthController提供对外认证接口
- 创建OpenApiAuthInterceptor拦截器进行token校验
- 升级common-core版本至1.0.1并更新依赖引用
- 将exeListPlugin方法从Controller移至OnlineOperationService
- 移除OnlineOperationController中的废弃导入和私有方法
- 新增OpenApiConfig模型类用于存储接口配置信息
pull/1/head
chenchuchuan 2 weeks ago
parent
commit
a0eb74ecd8
  1. 2
      common/common-core/pom.xml
  2. 2
      common/common-generator/pom.xml
  3. 58
      common/common-online/src/main/java/apelet/common/online/controller/OnlineOperationController.java
  4. 28
      common/common-online/src/main/java/apelet/common/online/service/OnlineOperationService.java
  5. 62
      common/common-online/src/main/java/apelet/common/online/service/impl/OnlineOperationServiceImpl.java
  6. 49
      common/common-openapi/pom.xml
  7. 37
      common/common-openapi/src/main/java/apelet/common/openapi/config/OpenApiProperties.java
  8. 30
      common/common-openapi/src/main/java/apelet/common/openapi/constant/OpenApiFieldSource.java
  9. 26
      common/common-openapi/src/main/java/apelet/common/openapi/constant/OpenApiParamMode.java
  10. 174
      common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiAppController.java
  11. 44
      common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiAuthController.java
  12. 137
      common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiConfigController.java
  13. 40
      common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiRouterController.java
  14. 25
      common/common-openapi/src/main/java/apelet/common/openapi/dao/OpenApiAppMapper.java
  15. 26
      common/common-openapi/src/main/java/apelet/common/openapi/dao/OpenApiConfigMapper.java
  16. 42
      common/common-openapi/src/main/java/apelet/common/openapi/dao/mapper/OpenApiAppMapper.xml
  17. 52
      common/common-openapi/src/main/java/apelet/common/openapi/dao/mapper/OpenApiConfigMapper.xml
  18. 175
      common/common-openapi/src/main/java/apelet/common/openapi/document/OpenApiDocumentCustomizer.java
  19. 44
      common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiFieldDto.java
  20. 25
      common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiFormOptionDto.java
  21. 26
      common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiOperationDto.java
  22. 25
      common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiPageOptionDto.java
  23. 26
      common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiTokenDto.java
  24. 30
      common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiUserInfo.java
  25. 55
      common/common-openapi/src/main/java/apelet/common/openapi/interceptor/OpenApiAuthInterceptor.java
  26. 70
      common/common-openapi/src/main/java/apelet/common/openapi/model/OpenApiApp.java
  27. 110
      common/common-openapi/src/main/java/apelet/common/openapi/model/OpenApiConfig.java
  28. 40
      common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiAppService.java
  29. 32
      common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiAuthService.java
  30. 84
      common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiConfigService.java
  31. 23
      common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiExecService.java
  32. 49
      common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiOnlineQueryService.java
  33. 23
      common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiUserService.java
  34. 82
      common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiAppServiceImpl.java
  35. 137
      common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiAuthServiceImpl.java
  36. 312
      common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiConfigServiceImpl.java
  37. 566
      common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiExecServiceImpl.java
  38. 210
      common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiOnlineQueryServiceImpl.java

2
common/common-core/pom.xml

@ -9,7 +9,7 @@ @@ -9,7 +9,7 @@
<modelVersion>4.0.0</modelVersion>
<artifactId>common-core</artifactId>
<version>1.0.0</version>
<version>1.0.1</version>
<name>common-core</name>
<packaging>jar</packaging>

2
common/common-generator/pom.xml

@ -17,7 +17,7 @@ @@ -17,7 +17,7 @@
<dependency>
<groupId>apelet</groupId>
<artifactId>common-core</artifactId>
<version>1.0.0</version>
<version>1.0.1</version>
</dependency>
<dependency>

58
common/common-online/src/main/java/apelet/common/online/controller/OnlineOperationController.java

@ -21,9 +21,7 @@ import apelet.common.generator.service.IOnlFormHeadService; @@ -21,9 +21,7 @@ import apelet.common.generator.service.IOnlFormHeadService;
import apelet.common.generator.utils.OrmGenDataSourceUtil;
import apelet.common.log.annotation.OperationLog;
import apelet.common.log.model.constant.SysOperationLogType;
import apelet.common.online.abstractplugin.ListDataEasService;
import apelet.common.online.abstractplugin.ListDataOrmService;
import apelet.common.online.abstractplugin.ListDataService;
import apelet.common.online.abstractplugin.model.ExportData;
import apelet.common.online.abstractplugin.model.GridData;
import apelet.common.online.config.OnlineProperties;
@ -848,7 +846,7 @@ public class OnlineOperationController { @@ -848,7 +846,7 @@ public class OnlineOperationController {
listParams.put("metaCode", metaCode);
listParams.put("baseFormId", baseFormId);
OnlFormHead onlFormHead = onlFormHeadService.getFormHeadByDatasource(datasourceId);
GridData pluginData = this.exeListPlugin(formId,filterDtoList,orderParam,pageParam,
GridData pluginData = onlineOperationService.exeListPlugin(formId,filterDtoList,orderParam,pageParam,
traceParamDto,onlFormHead,datasourceResult,onlineEventPluginExecuteDto
);//执行插件查询数据
if(pluginData != null){
@ -960,57 +958,6 @@ public class OnlineOperationController { @@ -960,57 +958,6 @@ public class OnlineOperationController {
return onlineTableService.getOne(new LambdaQueryWrapper<OnlineTable>().eq(OnlineTable::getTableName, onlFormHead.getTableName()));
}
private GridData exeListPlugin(
Long formId,
List<OnlineFilterDto> filterDtoList,
MyOrderParam orderParam,
MyPageParam pageParam,
TraceParamDto traceParamDto,
OnlFormHead onlFormHead,
ResponseResult<OnlineDatasource> datasourceResult,
OnlineEventPluginExecuteDto onlineEventPluginExecuteDto
){
if(onlineEventPluginExecuteDto == null){
onlineEventPluginExecuteDto = new OnlineEventPluginExecuteDto();
}
onlineEventPluginExecuteDto.setFormId(formId);
onlineEventPluginExecuteDto.setEventCode(EventEnum.LOADLISTDATA.getCode());
Map<String,Object> listParams = onlineEventPluginExecuteDto.getListParams();
if (listParams == null) {
listParams = new HashMap<>();
}
listParams.put("filterDtoList",filterDtoList);
listParams.put("pageParam",pageParam);
listParams.put("orderParam",orderParam);
listParams.put("traceParamDto",traceParamDto);
listParams.put("onlFormHead",onlFormHead);
listParams.put("datasourceResult",datasourceResult);
onlineEventPluginExecuteDto.setListParams(listParams);
onlineEventPluginExecuteDto.setModel(new Model());
List<Object> result = onlineFormService.eventPluginExecute(onlineEventPluginExecuteDto);
GridData gridData = null;
if(result != null){
for(Object obj : result){
if(obj instanceof OnlineEventPluginExecuteVo){
OnlineEventPluginExecuteVo vo = (OnlineEventPluginExecuteVo)obj;
if(vo.getWidgetVariableName().equals("gridData")){
gridData = (GridData)vo.getWidgetOperateContent();
}
}
}
}
if(gridData == null){
ListDataService listDataService = null;
if (onlFormHead.getIsomerismMark() != null) {
listDataService = new ListDataEasService(onlineEventPluginExecuteDto);
}else{
listDataService = new ListDataOrmService(onlineEventPluginExecuteDto);
}
gridData = listDataService.loadGridData();
}
return gridData;
}
private void normalizeFilterList(OnlineTable table
, List<OnlineFilterDto> filterList) {
OnlineDblink dblink = onlineDblinkService.getById(table.getDblinkId());
@ -1030,9 +977,6 @@ public class OnlineOperationController { @@ -1030,9 +977,6 @@ public class OnlineOperationController {
}
}
}
/**
* 获取表单详情数据
* @param formId

28
common/common-online/src/main/java/apelet/common/online/service/OnlineOperationService.java

@ -4,8 +4,11 @@ import apelet.common.core.object.*; @@ -4,8 +4,11 @@ import apelet.common.core.object.*;
import apelet.common.core.object.MyPageData;
import apelet.common.core.object.MyPageParam;
import apelet.common.core.object.TraceParamDto;
import apelet.common.online.abstractplugin.model.GridData;
import apelet.common.online.dto.OnlineEventPluginExecuteDto;
import apelet.common.online.dto.OnlineFilterDto;
import apelet.common.online.dto.OnlinePluginExecuteDto;
import apelet.common.generator.model.OnlFormHead;
import apelet.common.online.model.*;
import apelet.common.online.object.TransactionalBusinessData;
import apelet.common.online.plugin.OperationResult;
@ -13,8 +16,6 @@ import apelet.common.online.plugin.PlugInArgs; @@ -13,8 +16,6 @@ import apelet.common.online.plugin.PlugInArgs;
import apelet.common.orm.impl.Filter;
import apelet.common.orm.impl.Selector;
import com.alibaba.fastjson.JSONObject;
import apelet.common.online.dto.OnlineFilterDto;
import apelet.common.online.object.TransactionalBusinessData;
import java.util.List;
import java.util.Map;
@ -304,6 +305,29 @@ public interface OnlineOperationService { @@ -304,6 +305,29 @@ public interface OnlineOperationService {
*/
void executePluginTransaction(PlugInArgs plugInArgs);
/**
* 执行列表查询插件LOADLISTDATA插件未返回 gridData 时回退到 ORM/EAS 列表服务
* <p> OnlineOperationController 的私有方法提升为服务接口 OpenAPI 等场景复用</p>
*
* @param formId 表单Id
* @param filterDtoList 过滤条件列表
* @param orderParam 排序参数
* @param pageParam 分页参数
* @param traceParamDto 数据追踪参数
* @param onlFormHead 表单表头元数据
* @param datasourceResult 数据源校验结果
* @param onlineEventPluginExecuteDto 插件执行上下文可空listParams 会被填充
* @return 列表查询结果
*/
GridData exeListPlugin(
Long formId,
List<OnlineFilterDto> filterDtoList,
MyOrderParam orderParam,
MyPageParam pageParam,
TraceParamDto traceParamDto,
OnlFormHead onlFormHead,
ResponseResult<OnlineDatasource> datasourceResult,
OnlineEventPluginExecuteDto onlineEventPluginExecuteDto);
Object ormSaveNew(OnlineTable masterTable, JSONObject masterData, ObjectValue objectValue);

62
common/common-online/src/main/java/apelet/common/online/service/impl/OnlineOperationServiceImpl.java

@ -20,13 +20,19 @@ import apelet.common.generator.service.IOnlFormFieldService; @@ -20,13 +20,19 @@ import apelet.common.generator.service.IOnlFormFieldService;
import apelet.common.generator.service.IOnlFormHeadService;
import apelet.common.generator.utils.OrmGenDataSourceUtil;
import apelet.common.online.config.OnlineProperties;
import apelet.common.online.abstractplugin.ListDataEasService;
import apelet.common.online.abstractplugin.ListDataOrmService;
import apelet.common.online.abstractplugin.ListDataService;
import apelet.common.online.abstractplugin.model.GridData;
import apelet.common.online.dao.ConvertRelationFOnLineMapper;
import apelet.common.online.dao.OnlineOperationMapper;
import apelet.common.online.dto.OnlineEventPluginExecuteDto;
import apelet.common.online.dto.OnlineFilterDto;
import apelet.common.online.dto.OnlinePluginExecuteDto;
import apelet.common.online.dto.OnlinePluginExecuteSortDto;
import apelet.common.online.exception.OnlineRuntimeException;
import apelet.common.online.model.*;
import apelet.common.online.model.constant.EventEnum;
import apelet.common.online.model.constant.FieldFilterType;
import apelet.common.online.model.constant.FieldKind;
import apelet.common.online.model.constant.RelationType;
@ -42,6 +48,7 @@ import apelet.common.online.plugin.pre.DeleteOperationServicePlugIn; @@ -42,6 +48,7 @@ import apelet.common.online.plugin.pre.DeleteOperationServicePlugIn;
import apelet.common.online.plugin.pre.SaveOperationServicePlugIn;
import apelet.common.online.service.*;
import apelet.common.online.util.*;
import apelet.common.online.vo.OnlineEventPluginExecuteVo;
import apelet.common.orm.impl.*;
import apelet.common.redis.util.CommonRedisUtil;
import apelet.common.sequence.wrapper.IdGeneratorWrapper;
@ -1384,6 +1391,61 @@ public class OnlineOperationServiceImpl implements OnlineOperationService { @@ -1384,6 +1391,61 @@ public class OnlineOperationServiceImpl implements OnlineOperationService {
plugInArgs.endOperationTransaction();
}
/**
* 执行列表查询插件LOADLISTDATA插件未返回 gridData 时回退到 ORM/EAS 列表服务
* OnlineOperationController 的私有方法提升到 service
*/
@Override
public GridData exeListPlugin(
Long formId,
List<OnlineFilterDto> filterDtoList,
MyOrderParam orderParam,
MyPageParam pageParam,
TraceParamDto traceParamDto,
OnlFormHead onlFormHead,
ResponseResult<OnlineDatasource> datasourceResult,
OnlineEventPluginExecuteDto onlineEventPluginExecuteDto) {
if (onlineEventPluginExecuteDto == null) {
onlineEventPluginExecuteDto = new OnlineEventPluginExecuteDto();
}
onlineEventPluginExecuteDto.setFormId(formId);
onlineEventPluginExecuteDto.setEventCode(EventEnum.LOADLISTDATA.getCode());
Map<String, Object> listParams = onlineEventPluginExecuteDto.getListParams();
if (listParams == null) {
listParams = new HashMap<>();
}
listParams.put("filterDtoList", filterDtoList);
listParams.put("pageParam", pageParam);
listParams.put("orderParam", orderParam);
listParams.put("traceParamDto", traceParamDto);
listParams.put("onlFormHead", onlFormHead);
listParams.put("datasourceResult", datasourceResult);
onlineEventPluginExecuteDto.setListParams(listParams);
onlineEventPluginExecuteDto.setModel(new Model());
List<Object> result = onlineFormService.eventPluginExecute(onlineEventPluginExecuteDto);
GridData gridData = null;
if (result != null) {
for (Object obj : result) {
if (obj instanceof OnlineEventPluginExecuteVo) {
OnlineEventPluginExecuteVo vo = (OnlineEventPluginExecuteVo) obj;
if (vo.getWidgetVariableName().equals("gridData")) {
gridData = (GridData) vo.getWidgetOperateContent();
}
}
}
}
if (gridData == null) {
ListDataService listDataService = null;
if (onlFormHead.getIsomerismMark() != null) {
listDataService = new ListDataEasService(onlineEventPluginExecuteDto);
} else {
listDataService = new ListDataOrmService(onlineEventPluginExecuteDto);
}
gridData = listDataService.loadGridData();
}
return gridData;
}
private OperationResult exePlugin(List<OnlinePluginExecuteSortDto> pluginList, String billid, String tableName, ObjectValue objectValue
, OnlineForm onlineForm) {
PlugInArgs plugInArgs = new PlugInArgs(null, onlineForm);

49
common/common-openapi/pom.xml

@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>apelet</groupId>
<artifactId>common</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>common-openapi</artifactId>
<version>1.0.0</version>
<name>common-openapi</name>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>apelet</groupId>
<artifactId>common-core</artifactId>
<version>1.0.1</version>
</dependency>
<!-- 在线表单能力:OnlineFormService / OnlineOperationService(含 exeListPlugin) / OnlineDatasourceService 等,
传递依赖 common-generator(OnlFormHead/OnlFormField)、common-redis、common-swagger(knife4j/springdoc) -->
<dependency>
<groupId>apelet</groupId>
<artifactId>common-online</artifactId>
<version>1.0.5</version>
</dependency>
<!-- 表单元数据:OnlFormHead / OnlFormField / IOnlFormHeadService,开放接口字段清单与关联字段属性换算依赖 -->
<dependency>
<groupId>apelet</groupId>
<artifactId>common-generator</artifactId>
<version>1.0.5</version>
</dependency>
<!-- token 会话存储(可吊销),复用 CommonRedisUtil -->
<dependency>
<groupId>apelet</groupId>
<artifactId>common-redis</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</project>

37
common/common-openapi/src/main/java/apelet/common/openapi/config/OpenApiProperties.java

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
package apelet.common.openapi.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* OpenAPI 平台配置项
*
* @author chenchuchuan
* @date 2026-09-02
*/
@Data
@Component
@ConfigurationProperties(prefix = "common-openapi")
public class OpenApiProperties {
/**
* 对外开放路由前缀不要以反斜杠结尾默认 /openapi
*/
private String urlPrefix = "/openapi";
/**
* 是否启用 OpenAPI 开放平台false 时所有 Controller 接口不可用
*/
private Boolean operationEnabled = true;
/**
* JWT 签名密钥与开放 token 校验使用同一密钥
*/
private String tokenSigningKey = "apelet-openapi-signing-key-default";
/**
* token 有效期默认 7200
*/
private Long tokenExpiration = 7200L;
}

30
common/common-openapi/src/main/java/apelet/common/openapi/constant/OpenApiFieldSource.java

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
package apelet.common.openapi.constant;
/**
* OpenAPI 字段来源/条件目标常量param_config / output_config 中的 sourcetarget 取值
*
* @author chenchuchuan
* @date 2026-09-03
*/
public final class OpenApiFieldSource {
/**
* 表单主表字段onl_form_fieldheadId=主表
*/
public static final String FORM_FIELD = "FORM_FIELD";
/**
* 关联字段属性field=关联存储字段attr=id/编码/名称
*/
public static final String RELATION_FIELD = "RELATION_FIELD";
/**
* 一对多分录明细OnlFormHead.childName 附表值为数组
*/
public static final String ENTRY_ARRAY = "ENTRY_ARRAY";
/**
* 分录明细内的字段附表的 onl_form_field
*/
public static final String ENTRY_FIELD = "ENTRY_FIELD";
private OpenApiFieldSource() {
}
}

26
common/common-openapi/src/main/java/apelet/common/openapi/constant/OpenApiParamMode.java

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
package apelet.common.openapi.constant;
/**
* OpenAPI 参数模式常量xy_sys_open_api_config.param_mode 取值
*
* @author chenchuchuan
* @date 2026-09-03
*/
public final class OpenApiParamMode {
/**
* 基础查询operation_code=query走列表插件
*/
public static final String QUERY = "QUERY";
/**
* 字段直传保存/提交操作 executePlugin
*/
public static final String FIELD_DIRECT = "FIELD_DIRECT";
/**
* 条件匹配其他操作先按条件匹配单据集合再执行
*/
public static final String CONDITION_MATCH = "CONDITION_MATCH";
private OpenApiParamMode() {
}
}

174
common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiAppController.java

@ -0,0 +1,174 @@ @@ -0,0 +1,174 @@
package apelet.common.openapi.controller;
import apelet.common.core.annotation.MyRequestBody;
import apelet.common.core.constant.ErrorCodeEnum;
import apelet.common.core.object.MyPageData;
import apelet.common.core.object.MyPageParam;
import apelet.common.core.object.ResponseResult;
import apelet.common.core.object.TokenData;
import apelet.common.core.util.MyCommonUtil;
import apelet.common.core.util.MyPageUtil;
import apelet.common.openapi.model.OpenApiApp;
import apelet.common.openapi.model.OpenApiConfig;
import apelet.common.openapi.service.OpenApiAppService;
import apelet.common.openapi.service.OpenApiConfigService;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.github.pagehelper.page.PageMethod;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
import java.util.List;
/**
* OpenAPI 第三方应用管理接口内部供前端开放平台界面调用
*
* @author chenchuchuan
* @date 2026-09-02
*/
@Tag(name = "OpenAPI第三方应用接口")
@Slf4j
@RestController
@RequestMapping("${common-openapi.urlPrefix:/openapi}/app")
@ConditionalOnProperty(name = "common-openapi.operationEnabled", havingValue = "true")
public class OpenApiAppController {
@Autowired
private OpenApiAppService openApiAppService;
@Autowired
private OpenApiConfigService openApiConfigService;
/**
* 分页查询应用列表不回显 appSecret
*/
@PostMapping("/list")
public ResponseResult<MyPageData<OpenApiApp>> list(@MyRequestBody OpenApiApp openApiAppFilter, @MyRequestBody MyPageParam pageParam) {
if (pageParam != null) {
PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
}
List<OpenApiApp> appList = openApiAppService.getOpenApiAppList(openApiAppFilter, "create_time DESC");
// 列表密钥打码:前3后4明文,中间 *;完整密钥需调 /viewSecret 查看
appList.forEach(app -> app.setAppSecret(this.maskSecret(app.getAppSecret())));
return ResponseResult.success(MyPageUtil.makeResponseData(appList));
}
/**
* 新增应用appCode 未填则自动生成appSecret 仅此刻返回明文
*/
@PostMapping("/add")
public ResponseResult<OpenApiApp> add(@MyRequestBody OpenApiApp openApiApp) {
if (openApiApp == null || MyCommonUtil.existBlankArgument(openApiApp.getAppName())) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST, "应用名称不能为空!");
}
if (openApiApp.getAppCode() != null && openApiAppService.existOne("appCode", openApiApp.getAppCode())) {
return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY, "应用编码已存在!");
}
return ResponseResult.success(openApiAppService.addNew(openApiApp));
}
/**
* 更新应用不修改密钥
*/
@PostMapping("/update")
public ResponseResult<Void> update(@MyRequestBody OpenApiApp openApiApp) {
if (openApiApp == null || MyCommonUtil.existBlankArgument(openApiApp.getId())) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
}
OpenApiApp original = openApiAppService.getById(openApiApp.getId());
if (original == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
}
// 应用编码作为对外唯一标识且被开放接口配置(xy_sys_open_api_config.app_code)引用,不允许修改
if (StrUtil.isNotBlank(openApiApp.getAppCode())
&& !openApiApp.getAppCode().equals(original.getAppCode())) {
return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "应用编码不允许修改!");
}
openApiApp.setAppCode(original.getAppCode());
openApiApp.setAppSecret(original.getAppSecret());
openApiApp.setCreateTime(original.getCreateTime());
openApiApp.setUpdateTime(new Date());
TokenData tokenData = TokenData.takeFromRequest();
if (tokenData != null) {
openApiApp.setUpdateUserId(tokenData.getUserId());
}
if (!openApiAppService.updateById(openApiApp)) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
}
return ResponseResult.success();
}
/**
* 删除应用已被开放配置引用的应用禁止删除
*/
@PostMapping("/delete")
public ResponseResult<Void> delete(@MyRequestBody Long id) {
if (MyCommonUtil.existBlankArgument(id)) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
}
OpenApiApp app = openApiAppService.getById(id);
if (app == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
}
OpenApiConfig configFilter = new OpenApiConfig();
configFilter.setAppCode(app.getAppCode());
if (openApiConfigService.existByFilter(configFilter)) {
return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "该应用已被开放接口配置引用,不能删除!");
}
openApiAppService.removeById(id);
return ResponseResult.success();
}
/**
* 重置应用密钥旧密钥立即失效
*/
@PostMapping("/resetSecret")
public ResponseResult<JSONObject> resetSecret(@MyRequestBody Long id) {
if (MyCommonUtil.existBlankArgument(id)) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
}
String newSecret = openApiAppService.resetSecret(id);
if (newSecret == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
}
JSONObject data = new JSONObject();
data.put("appSecret", newSecret);
return ResponseResult.success(data);
}
/**
* 查看应用密钥明文前端"小眼睛"调用受应用登录鉴权保护
*/
@PostMapping("/viewSecret")
public ResponseResult<JSONObject> viewSecret(@MyRequestBody Long id) {
if (MyCommonUtil.existBlankArgument(id)) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
}
OpenApiApp app = openApiAppService.getById(id);
if (app == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
}
JSONObject data = new JSONObject();
data.put("appSecret", app.getAppSecret());
return ResponseResult.success(data);
}
/**
* 密钥打码前3后4明文中间用 * 占位长度不足 7 时整体打码
*/
private String maskSecret(String secret) {
if (secret == null) {
return null;
}
int len = secret.length();
if (len <= 7) {
return StrUtil.repeat("*", len);
}
return secret.substring(0, 3) + StrUtil.repeat("*", len - 7) + secret.substring(len - 4);
}
}

44
common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiAuthController.java

@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
package apelet.common.openapi.controller;
import apelet.common.core.constant.ErrorCodeEnum;
import apelet.common.core.object.ResponseResult;
import apelet.common.openapi.dto.OpenApiTokenDto;
import apelet.common.openapi.service.OpenApiAuthService;
import com.alibaba.fastjson.JSONObject;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* OpenAPI 对外鉴权接口公网
*
* @author chenchuchuan
* @date 2026-09-02
*/
@Tag(name = "OpenAPI对外鉴权接口")
@Slf4j
@RestController
@RequestMapping("${common-openapi.urlPrefix:/openapi}/auth")
@ConditionalOnProperty(name = "common-openapi.operationEnabled", havingValue = "true")
public class OpenApiAuthController {
@Autowired
private OpenApiAuthService openApiAuthService;
/**
* 按应用编码 + 密钥 + 用户名换取 token第三方标准扁平 JSON body
*/
@PostMapping("/token")
public ResponseResult<JSONObject> token(@RequestBody(required = false) OpenApiTokenDto tokenDto) {
if (tokenDto == null) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
}
return openApiAuthService.issueToken(
tokenDto.getAppCode(), tokenDto.getAppSecret(), tokenDto.getUsername());
}
}

137
common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiConfigController.java

@ -0,0 +1,137 @@ @@ -0,0 +1,137 @@
package apelet.common.openapi.controller;
import apelet.common.core.annotation.MyRequestBody;
import apelet.common.core.constant.ErrorCodeEnum;
import apelet.common.core.object.MyPageData;
import apelet.common.core.object.MyPageParam;
import apelet.common.core.object.ResponseResult;
import apelet.common.core.util.MyCommonUtil;
import apelet.common.core.util.MyPageUtil;
import apelet.common.openapi.dto.OpenApiFieldDto;
import apelet.common.openapi.dto.OpenApiFormOptionDto;
import apelet.common.openapi.dto.OpenApiOperationDto;
import apelet.common.openapi.dto.OpenApiPageOptionDto;
import apelet.common.openapi.model.OpenApiConfig;
import apelet.common.openapi.service.OpenApiConfigService;
import com.github.pagehelper.page.PageMethod;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* OpenAPI 开放接口配置管理接口内部供前端开放平台界面调用
*
* @author chenchuchuan
* @date 2026-09-02
*/
@Tag(name = "OpenAPI开放接口配置接口")
@Slf4j
@RestController
@RequestMapping("${common-openapi.urlPrefix:/openapi}/config")
@ConditionalOnProperty(name = "common-openapi.operationEnabled", havingValue = "true")
public class OpenApiConfigController {
@Autowired
private OpenApiConfigService openApiConfigService;
/**
* 在线页面列表配置步骤1 选择可按页面名称模糊搜索
*/
@GetMapping("/pages")
public ResponseResult<List<OpenApiPageOptionDto>> pages(@RequestParam(required = false) String keyword) {
return ResponseResult.success(openApiConfigService.getPageOptionList(keyword));
}
/**
* 该页面关联的表单列表配置步骤2 联动
*/
@GetMapping("/forms")
public ResponseResult<List<OpenApiFormOptionDto>> forms(@RequestParam Long pageId) {
return ResponseResult.success(openApiConfigService.getFormOptionList(pageId));
}
/**
* 可选操作下拉查询(query) + operationList 各项配置步骤3
*/
@GetMapping("/operations")
public ResponseResult<List<OpenApiOperationDto>> operations(@RequestParam Long formId) {
return ResponseResult.success(openApiConfigService.getOperationList(formId));
}
/**
* 表单可配置字段树FIELD_DIRECT 字段直传专用
*/
@GetMapping("/fields")
public ResponseResult<List<OpenApiFieldDto>> fields(@RequestParam Long formId) {
return ResponseResult.success(openApiConfigService.getFieldTree(formId));
}
/**
* 分页查询开放接口配置
*/
@PostMapping("/list")
public ResponseResult<MyPageData<OpenApiConfig>> list(@MyRequestBody OpenApiConfig openApiConfigFilter, @MyRequestBody MyPageParam pageParam) {
if (pageParam != null) {
PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
}
List<OpenApiConfig> configList = openApiConfigService.getOpenApiConfigList(openApiConfigFilter, "create_time DESC");
return ResponseResult.success(MyPageUtil.makeResponseData(configList));
}
/**
* 查看配置详情
*/
@GetMapping("/view")
public ResponseResult<OpenApiConfig> view(@RequestParam Long configId) {
OpenApiConfig config = openApiConfigService.getById(configId);
if (config == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
}
return ResponseResult.success(config);
}
/**
* 新增开放接口配置
*/
@PostMapping("/add")
public ResponseResult<OpenApiConfig> add(@MyRequestBody OpenApiConfig openApiConfig) {
if (openApiConfig == null || MyCommonUtil.existBlankArgument(openApiConfig.getAppCode(),
openApiConfig.getPageId(), openApiConfig.getFormId(), openApiConfig.getFormCode(),
openApiConfig.getOperationCode())) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
}
return ResponseResult.success(openApiConfigService.addNew(openApiConfig));
}
/**
* 更新开放接口配置
*/
@PostMapping("/update")
public ResponseResult<Void> update(@MyRequestBody OpenApiConfig openApiConfig) {
if (openApiConfig == null || MyCommonUtil.existBlankArgument(openApiConfig.getId())) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
}
if (openApiConfigService.updateConfig(openApiConfig) == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
}
return ResponseResult.success();
}
/**
* 删除开放接口配置
*/
@PostMapping("/delete")
public ResponseResult<Void> delete(@MyRequestBody Long configId) {
if (MyCommonUtil.existBlankArgument(configId)) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
}
if (!openApiConfigService.removeById(configId)) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
}
return ResponseResult.success();
}
}

40
common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiRouterController.java

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
package apelet.common.openapi.controller;
import apelet.common.core.object.ResponseResult;
import apelet.common.openapi.config.OpenApiProperties;
import apelet.common.openapi.service.OpenApiExecService;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.Map;
/**
* OpenAPI 对外开放动态路由{urlPrefix}/v1/{pageCode}/{formCode}/{operationCode}
* <p>鉴权由 OpenApiAuthInterceptor 完成本控制器按路径定位配置并执行</p>
*
* @author chenchuchuan
* @date 2026-09-02
*/
@Tag(name = "OpenAPI对外开放动态路由")
@Slf4j
@RestController
@RequestMapping("${common-openapi.urlPrefix:/openapi}/v1")
@ConditionalOnProperty(name = "common-openapi.operationEnabled", havingValue = "true")
public class OpenApiRouterController {
@Autowired
private OpenApiExecService openApiExecService;
@Autowired
private OpenApiProperties openApiProperties;
@PostMapping("/{pageCode}/{formCode}/{operationCode}")
public ResponseResult<Object> execute(@PathVariable String pageCode, @PathVariable String formCode, @PathVariable String operationCode,
@RequestBody(required = false) Map<String, Object> requestBody) {
String requestPath = openApiProperties.getUrlPrefix() + "/v1/" + pageCode + "/" + formCode + "/" + operationCode;
return openApiExecService.execute(requestPath, requestBody == null ? Collections.emptyMap() : requestBody);
}
}

25
common/common-openapi/src/main/java/apelet/common/openapi/dao/OpenApiAppMapper.java

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
package apelet.common.openapi.dao;
import apelet.common.core.base.dao.BaseDaoMapper;
import apelet.common.openapi.model.OpenApiApp;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* OpenAPI 第三方应用数据操作访问接口
*
* @author chenchuchuan
* @date 2026-09-02
*/
public interface OpenApiAppMapper extends BaseDaoMapper<OpenApiApp> {
/**
* 获取过滤后的应用列表
*
* @param openApiAppFilter 应用过滤对象
* @param orderBy 排序字符串order by从句的参数
* @return 应用列表
*/
List<OpenApiApp> getOpenApiAppList(@Param("openApiAppFilter") OpenApiApp openApiAppFilter, @Param("orderBy") String orderBy);
}

26
common/common-openapi/src/main/java/apelet/common/openapi/dao/OpenApiConfigMapper.java

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
package apelet.common.openapi.dao;
import apelet.common.core.base.dao.BaseDaoMapper;
import apelet.common.openapi.model.OpenApiConfig;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* OpenAPI 开放接口配置数据操作访问接口
*
* @author chenchuchuan
* @date 2026-09-02
*/
public interface OpenApiConfigMapper extends BaseDaoMapper<OpenApiConfig> {
/**
* 获取过滤后的配置列表
*
* @param openApiConfigFilter 配置过滤对象
* @param orderBy 排序字符串order by从句的参数
* @return 配置列表
*/
List<OpenApiConfig> getOpenApiConfigList(
@Param("openApiConfigFilter") OpenApiConfig openApiConfigFilter, @Param("orderBy") String orderBy);
}

42
common/common-openapi/src/main/java/apelet/common/openapi/dao/mapper/OpenApiAppMapper.xml

@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="apelet.common.openapi.dao.OpenApiAppMapper">
<resultMap id="BaseResultMap" type="apelet.common.openapi.model.OpenApiApp">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="app_code" jdbcType="VARCHAR" property="appCode"/>
<result column="app_secret" jdbcType="VARCHAR" property="appSecret"/>
<result column="app_name" jdbcType="VARCHAR" property="appName"/>
<result column="tenant_id" jdbcType="BIGINT" property="tenantId"/>
<result column="status" jdbcType="INTEGER" property="status"/>
<result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
<result column="create_user_id" jdbcType="BIGINT" property="createUserId"/>
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
<result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/>
</resultMap>
<!-- 这里仅包含调用接口输入的应用过滤条件 -->
<sql id="inputFilterRef">
<if test="openApiAppFilter != null">
<if test="openApiAppFilter.appCode != null and openApiAppFilter.appCode != ''">
AND xy_sys_open_app.app_code = #{openApiAppFilter.appCode}
</if>
<if test="openApiAppFilter.appName != null and openApiAppFilter.appName != ''">
<bind name="safeAppName" value="'%' + openApiAppFilter.appName + '%'"/>
AND xy_sys_open_app.app_name LIKE #{safeAppName}
</if>
<if test="openApiAppFilter.status != null">
AND xy_sys_open_app.status = #{openApiAppFilter.status}
</if>
</if>
</sql>
<select id="getOpenApiAppList" resultMap="BaseResultMap" parameterType="apelet.common.openapi.model.OpenApiApp">
SELECT * FROM xy_sys_open_app
<where>
<include refid="inputFilterRef"/>
</where>
<if test="orderBy != null and orderBy != ''">
ORDER BY ${orderBy}
</if>
</select>
</mapper>

52
common/common-openapi/src/main/java/apelet/common/openapi/dao/mapper/OpenApiConfigMapper.xml

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="apelet.common.openapi.dao.OpenApiConfigMapper">
<resultMap id="BaseResultMap" type="apelet.common.openapi.model.OpenApiConfig">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="app_code" jdbcType="VARCHAR" property="appCode"/>
<result column="page_id" jdbcType="BIGINT" property="pageId"/>
<result column="page_code" jdbcType="VARCHAR" property="pageCode"/>
<result column="form_code" jdbcType="VARCHAR" property="formCode"/>
<result column="form_id" jdbcType="BIGINT" property="formId"/>
<result column="operation_code" jdbcType="VARCHAR" property="operationCode"/>
<result column="operation_name" jdbcType="VARCHAR" property="operationName"/>
<result column="param_mode" jdbcType="VARCHAR" property="paramMode"/>
<result column="request_path" jdbcType="VARCHAR" property="requestPath"/>
<result column="request_method" jdbcType="VARCHAR" property="requestMethod"/>
<result column="param_config" jdbcType="LONGVARCHAR" property="paramConfig"/>
<result column="output_config" jdbcType="LONGVARCHAR" property="outputConfig"/>
<result column="status" jdbcType="INTEGER" property="status"/>
<result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
<result column="create_user_id" jdbcType="BIGINT" property="createUserId"/>
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
<result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/>
</resultMap>
<!-- 这里仅包含调用接口输入的配置过滤条件 -->
<sql id="inputFilterRef">
<if test="openApiConfigFilter != null">
<if test="openApiConfigFilter.appCode != null and openApiConfigFilter.appCode != ''">
AND xy_sys_open_api_config.app_code = #{openApiConfigFilter.appCode}
</if>
<if test="openApiConfigFilter.pageId != null">
AND xy_sys_open_api_config.page_id = #{openApiConfigFilter.pageId}
</if>
<if test="openApiConfigFilter.formCode != null and openApiConfigFilter.formCode != ''">
AND xy_sys_open_api_config.form_code = #{openApiConfigFilter.formCode}
</if>
<if test="openApiConfigFilter.status != null">
AND xy_sys_open_api_config.status = #{openApiConfigFilter.status}
</if>
</if>
</sql>
<select id="getOpenApiConfigList" resultMap="BaseResultMap" parameterType="apelet.common.openapi.model.OpenApiConfig">
SELECT * FROM xy_sys_open_api_config
<where>
<include refid="inputFilterRef"/>
</where>
<if test="orderBy != null and orderBy != ''">
ORDER BY ${orderBy}
</if>
</select>
</mapper>

175
common/common-openapi/src/main/java/apelet/common/openapi/document/OpenApiDocumentCustomizer.java

@ -0,0 +1,175 @@ @@ -0,0 +1,175 @@
package apelet.common.openapi.document;
import apelet.common.openapi.constant.OpenApiFieldSource;
import apelet.common.openapi.constant.OpenApiParamMode;
import apelet.common.openapi.model.OpenApiConfig;
import apelet.common.openapi.service.OpenApiConfigService;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.OpenApiCustomiser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* OpenAPI 文档动态生成器启动时扫描启用的开放接口配置为每条配置生成一个 path + operation
* schema param_config / output_config 推导嵌套结构生成嵌套 schemaknife4j /v3/api-docs 展示
*
* @author chenchuchuan
* @date 2026-09-02
*/
@Slf4j
@Component
@ConditionalOnProperty(name = "common-openapi.operationEnabled", havingValue = "true")
public class OpenApiDocumentCustomizer implements OpenApiCustomiser {
@Autowired
private OpenApiConfigService openApiConfigService;
@Override
public void customise(OpenAPI openApi) {
OpenApiConfig filter = new OpenApiConfig();
filter.setStatus(1);
List<OpenApiConfig> configs = openApiConfigService.getOpenApiConfigList(filter, null);
for (OpenApiConfig config : configs) {
this.registerPath(openApi, config);
}
log.info("OpenAPI 文档自定义完成,共注册 {} 个开放接口 path", configs.size());
}
private void registerPath(OpenAPI openApi, OpenApiConfig config) {
if (openApi.getPaths() != null && openApi.getPaths().containsKey(config.getRequestPath())) {
return;
}
Operation operation = new Operation();
operation.setOperationId(config.getOperationCode() + "_" + config.getFormCode());
operation.setSummary(config.getOperationName());
operation.setDescription(config.getOperationName());
operation.addTagsItem(config.getAppCode());
Schema<?> requestSchema = this.buildRequestSchema(config);
if (requestSchema != null) {
operation.setRequestBody(new RequestBody().required(true)
.content(new Content().addMediaType("application/json",
new MediaType().schema(requestSchema))));
}
Schema<?> responseSchema = new Schema<>().type("object")
.addProperty("code", new Schema<>().type("integer"))
.addProperty("msg", new Schema<>().type("string"))
.addProperty("data", this.buildDataSchema(config.getOutputConfig()));
operation.responses(new ApiResponses().addApiResponse("200",
new ApiResponse().description("成功")
.content(new Content().addMediaType("application/json",
new MediaType().schema(responseSchema)))));
PathItem pathItem = new PathItem();
if ("GET".equalsIgnoreCase(config.getRequestMethod())) {
openApi.path(config.getRequestPath(), pathItem.get(operation));
} else {
openApi.path(config.getRequestPath(), pathItem.post(operation));
}
}
/**
* 请求体 schemaFIELD_DIRECT fieldsQUERY/CONDITION_MATCH paramsQUERY 追加 pageNo/pageSize
*/
private Schema<?> buildRequestSchema(OpenApiConfig config) {
if (StrUtil.isBlank(config.getParamConfig())) {
return null;
}
JSONObject paramConfig = JSON.parseObject(config.getParamConfig());
Schema<?> root = new Schema<>().type("object");
JSONArray props = OpenApiParamMode.FIELD_DIRECT.equals(config.getParamMode())
? paramConfig.getJSONArray("fields")
: paramConfig.getJSONArray("params");
if (props != null) {
for (int i = 0; i < props.size(); i++) {
JSONObject p = props.getJSONObject(i);
String name = p.getString("alias");
if (name == null) {
name = p.getString("name");
}
if (StrUtil.isBlank(name)) {
continue;
}
Schema<?> schema = this.buildSchema(p.getString("fieldType"), p.getString("source"));
if (StrUtil.isNotBlank(p.getString("description"))) {
schema.setDescription(p.getString("description"));
}
root.addProperty(name, schema);
}
}
if (OpenApiParamMode.QUERY.equals(config.getParamMode())) {
root.addProperty("pageNo", new Schema<>().type("integer"));
root.addProperty("pageSize", new Schema<>().type("integer"));
}
return root;
}
/**
* 输出 schemaResponseResult.data 的层级结构dataFields支持一对多分录嵌套
*/
private Schema<?> buildDataSchema(String outputConfigJson) {
Schema<?> root = new Schema<>().type("object");
if (StrUtil.isBlank(outputConfigJson)) {
return root;
}
JSONObject outputConfig = JSON.parseObject(outputConfigJson);
JSONArray dataFields = outputConfig.getJSONArray("dataFields");
if (dataFields == null) {
return root;
}
for (int i = 0; i < dataFields.size(); i++) {
JSONObject df = dataFields.getJSONObject(i);
String alias = df.getString("alias");
if (StrUtil.isBlank(alias)) {
continue;
}
if (OpenApiFieldSource.ENTRY_ARRAY.equals(df.getString("target"))) {
Schema<?> itemSchema = this.buildEntryItemSchema(df.getJSONArray("children"));
root.addProperty(alias, new ArraySchema().items(itemSchema));
} else {
root.addProperty(alias, new Schema<>().type("string"));
}
}
return root;
}
private Schema<?> buildEntryItemSchema(JSONArray children) {
Schema<?> item = new Schema<>().type("object");
if (children != null) {
for (int i = 0; i < children.size(); i++) {
JSONObject child = children.getJSONObject(i);
String alias = child.getString("alias");
if (StrUtil.isBlank(alias)) {
continue;
}
item.addProperty(alias, new Schema<>().type("string"));
}
}
return item;
}
private Schema<?> buildSchema(String fieldType, String source) {
if ("array".equals(fieldType) || OpenApiFieldSource.ENTRY_ARRAY.equals(source)) {
return new ArraySchema().items(new Schema<>());
}
if ("integer".equals(fieldType) || "number".equals(fieldType) || "boolean".equals(fieldType)) {
return new Schema<>().type(fieldType);
}
return new Schema<>().type("string");
}
}

44
common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiFieldDto.java

@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
package apelet.common.openapi.dto;
import lombok.Data;
import java.util.List;
/**
* OpenAPI 字段树节点用于开放接口配置时选择字段支持主表字段/关联字段属性/一对多分录
*
* @author chenchuchuan
* @date 2026-09-02
*/
@Data
public class OpenApiFieldDto {
/**
* 字段来源FORM_FIELD / RELATION_FIELD / ENTRY_ARRAY / ENTRY_FIELD
*/
private String source;
/**
* 字段名FORM_FIELD/ENTRY_FIELD或关联存储字段RELATION_FIELD
*/
private String field;
/**
* RELATION_FIELD 的属性取值id / number / name
*/
private String attr;
/**
* ENTRY_ARRAY / ENTRY_FIELD 所属的一对多分录附表表名
*/
private String table;
/**
* 接口字段名alias
*/
private String alias;
/**
* 展示名称
*/
private String label;
/**
* ENTRY_ARRAY 下的分录字段列表
*/
private List<OpenApiFieldDto> children;
}

25
common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiFormOptionDto.java

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
package apelet.common.openapi.dto;
import lombok.Data;
/**
* OpenAPI 配置页面表单下拉项
*
* @author chenchuchuan
*/
@Data
public class OpenApiFormOptionDto {
/**
* 在线表单主键Id
*/
private Long formId;
/**
* 表单编码
*/
private String formCode;
/**
* 表单名称
*/
private String formName;
}

26
common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiOperationDto.java

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
package apelet.common.openapi.dto;
import lombok.Data;
/**
* OpenAPI 可选操作下拉项查询 + widgetJson.operationList 各项
*
* @author chenchuchuan
* @date 2026-09-02
*/
@Data
public class OpenApiOperationDto {
/**
* 操作编码query operationList 中操作的 code
*/
private String code;
/**
* 操作名称查询 operationList 中操作的 name
*/
private String name;
/**
* 是否启用
*/
private Boolean enabled;
}

25
common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiPageOptionDto.java

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
package apelet.common.openapi.dto;
import lombok.Data;
/**
* OpenAPI 配置页面上线页面下拉项
*
* @author chenchuchuan
*/
@Data
public class OpenApiPageOptionDto {
/**
* 在线页面主键Id
*/
private Long pageId;
/**
* 页面编码
*/
private String pageCode;
/**
* 页面名称
*/
private String pageName;
}

26
common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiTokenDto.java

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
package apelet.common.openapi.dto;
import lombok.Data;
/**
* OpenAPI 按用户名换取 token 的请求体
*
* @author chenchuchuan
* @date 2026-09-02
*/
@Data
public class OpenApiTokenDto {
/**
* 应用编码
*/
private String appCode;
/**
* 应用密钥
*/
private String appSecret;
/**
* 调用用户名
*/
private String username;
}

30
common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiUserInfo.java

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
package apelet.common.openapi.dto;
import lombok.Data;
/**
* OpenAPI 按用户名换取的调用用户信息由接入方实现 OpenApiUserService 提供
*
* @author chenchuchuan
* @date 2026-09-02
*/
@Data
public class OpenApiUserInfo {
/**
* 用户Id
*/
private Long userId;
/**
* 登录名
*/
private String loginName;
/**
* 显示名称
*/
private String showName;
/**
* 租户Id
*/
private Long tenantId;
}

55
common/common-openapi/src/main/java/apelet/common/openapi/interceptor/OpenApiAuthInterceptor.java

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
package apelet.common.openapi.interceptor;
import apelet.common.core.constant.ErrorCodeEnum;
import apelet.common.core.object.ResponseResult;
import apelet.common.core.object.TokenData;
import apelet.common.openapi.config.OpenApiProperties;
import apelet.common.openapi.service.OpenApiAuthService;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* OpenAPI 对外开放路由{urlPrefix}/v1/** token 校验拦截器
* <p> SpringMVC 拦截器链中注册见接入方 InterceptorConfig只拦截 {urlPrefix}/v1/**
* token 签发/auth/token与配置管理/config/**/app/**走应用自身登录态不经过本拦截器</p>
*
* @author chenchuchuan
* @date 2026-09-02
*/
@Slf4j
@Component
@ConditionalOnProperty(name = "common-openapi.operationEnabled", havingValue = "true")
public class OpenApiAuthInterceptor implements HandlerInterceptor {
@Autowired
private OpenApiAuthService openApiAuthService;
@Autowired
private OpenApiProperties openApiProperties;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 兜底:即便注册路径放开,也仅对对外开放的动态路由做 token 校验
if (!request.getRequestURI().startsWith(openApiProperties.getUrlPrefix() + "/v1/")) {
return true;
}
String token = request.getHeader("Authorization");
TokenData tokenData = openApiAuthService.validateToken(token);
if (tokenData == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(
ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, "OpenAPI token 无效或已过期!")));
return false;
}
TokenData.addToRequest(tokenData);
return true;
}
}

70
common/common-openapi/src/main/java/apelet/common/openapi/model/OpenApiApp.java

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
package apelet.common.openapi.model;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
* OpenAPI 第三方应用实体对象
*
* @author chenchuchuan
* @date 2026-09-02
*/
@Data
@TableName(value = "xy_sys_open_app")
public class OpenApiApp {
/**
* 主键Id
*/
@TableId(value = "id")
private Long id;
/**
* 应用编码对外暴露唯一
*/
@TableField(value = "app_code")
private String appCode;
/**
* 应用密钥仅后台可见可重置
*/
@TableField(value = "app_secret")
private String appSecret;
/**
* 应用名称
*/
@TableField(value = "app_name")
private String appName;
/**
* 归属租户Id本期默认为空为空表示不限租户
*/
@TableField(value = "tenant_id")
private Long tenantId;
/**
* 状态(0: 停用 1: 启用)
*/
@TableField(value = "status")
private Integer status;
/**
* 创建时间
*/
@TableField(value = "create_time")
private Date createTime;
/**
* 创建者Id
*/
@TableField(value = "create_user_id")
private Long createUserId;
/**
* 更新时间
*/
@TableField(value = "update_time")
private Date updateTime;
/**
* 更新者Id
*/
@TableField(value = "update_user_id")
private Long updateUserId;
}

110
common/common-openapi/src/main/java/apelet/common/openapi/model/OpenApiConfig.java

@ -0,0 +1,110 @@ @@ -0,0 +1,110 @@
package apelet.common.openapi.model;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
* OpenAPI 开放接口配置实体对象
*
* @author chenchuchuan
* @date 2026-09-02
*/
@Data
@TableName(value = "xy_sys_open_api_config")
public class OpenApiConfig {
/**
* 主键Id
*/
@TableId(value = "id")
private Long id;
/**
* 授权开放的应用编码关联 xy_sys_open_app.app_code
*/
@TableField(value = "app_code")
private String appCode;
/**
* 在线页面主键OnlinePage.pageId
*/
@TableField(value = "page_id")
private Long pageId;
/**
* 页面编码冗余存储用于组装开放路径
*/
@TableField(value = "page_code")
private String pageCode;
/**
* 该页面下关联的在线表单编码OnlineForm.formCode用于组装开放路径 URL
*/
@TableField(value = "form_code")
private String formCode;
/**
* 关联的在线表单主键IdOnlineForm.formId执行时优先按 formId 精确定位表单
*/
@TableField(value = "form_id")
private Long formId;
/**
* 操作编码query operationList 中操作的 code
*/
@TableField(value = "operation_code")
private String operationCode;
/**
* 操作名称查询 operationList 中操作的 name
*/
@TableField(value = "operation_name")
private String operationName;
/**
* 参数模式后端自动推导FIELD_DIRECT / CONDITION_MATCH / QUERY
*/
@TableField(value = "param_mode")
private String paramMode;
/**
* 开放路径/openapi/v1/{pageCode}/{formCode}/{operationCode}全局唯一
*/
@TableField(value = "request_path")
private String requestPath;
/**
* 请求方法 GET / POST
*/
@TableField(value = "request_method")
private String requestMethod;
/**
* JSON输入参数+条件配置见设计文档 6.1
*/
@TableField(value = "param_config")
private String paramConfig;
/**
* JSON输出层级结构见设计文档 6.2
*/
@TableField(value = "output_config")
private String outputConfig;
/**
* 状态(0: 停用 1: 启用)
*/
@TableField(value = "status")
private Integer status;
/**
* 创建时间
*/
@TableField(value = "create_time")
private Date createTime;
/**
* 创建者Id
*/
@TableField(value = "create_user_id")
private Long createUserId;
/**
* 更新时间
*/
@TableField(value = "update_time")
private Date updateTime;
/**
* 更新者Id
*/
@TableField(value = "update_user_id")
private Long updateUserId;
}

40
common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiAppService.java

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
package apelet.common.openapi.service;
import apelet.common.core.base.service.IBaseService;
import apelet.common.openapi.model.OpenApiApp;
import java.util.List;
/**
* OpenAPI 第三方应用数据操作服务接口
*
* @author chenchuchuan
* @date 2026-09-02
*/
public interface OpenApiAppService extends IBaseService<OpenApiApp, Long> {
/**
* 获取过滤后的应用列表
*
* @param filter 过滤对象
* @param orderBy 排序字符串
* @return 应用列表
*/
List<OpenApiApp> getOpenApiAppList(OpenApiApp filter, String orderBy);
/**
* 新增应用appCode 为空时自动生成appSecret 自动生成并返回明文
*
* @param app 应用对象
* @return 保存后的应用含明文 appSecret
*/
OpenApiApp addNew(OpenApiApp app);
/**
* 重置应用密钥旧密钥立即失效
*
* @param id 应用主键Id
* @return 新的明文 appSecret
*/
String resetSecret(Long id);
}

32
common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiAuthService.java

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
package apelet.common.openapi.service;
import apelet.common.core.object.ResponseResult;
import apelet.common.core.object.TokenData;
import com.alibaba.fastjson.JSONObject;
/**
* OpenAPI 鉴权服务接口token 签发与校验
*
* @author chenchuchuan
* @date 2026-09-02
*/
public interface OpenApiAuthService {
/**
* 校验应用凭证 + 调用用户名签发 token
*
* @param appCode 应用编码
* @param appSecret 应用密钥
* @param username 调用用户名
* @return data accessToken不含 Bearer 前缀 expiresIn
*/
ResponseResult<JSONObject> issueToken(String appCode, String appSecret, String username);
/**
* 校验 Bearer token验签 + 有效期 + Redis 会话存在性还原 TokenData
*
* @param token Authorization 头值 Bearer 前缀
* @return TokenData无效或已失效返回null
*/
TokenData validateToken(String token);
}

84
common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiConfigService.java

@ -0,0 +1,84 @@ @@ -0,0 +1,84 @@
package apelet.common.openapi.service;
import apelet.common.core.base.service.IBaseService;
import apelet.common.openapi.dto.OpenApiFieldDto;
import apelet.common.openapi.dto.OpenApiFormOptionDto;
import apelet.common.openapi.dto.OpenApiOperationDto;
import apelet.common.openapi.dto.OpenApiPageOptionDto;
import apelet.common.openapi.model.OpenApiConfig;
import java.util.List;
/**
* OpenAPI 开放接口配置数据操作服务接口
*
* @author chenchuchuan
* @date 2026-09-02
*/
public interface OpenApiConfigService extends IBaseService<OpenApiConfig, Long> {
/**
* 获取过滤后的配置列表
*
* @param filter 过滤对象
* @param orderBy 排序字符串
* @return 配置列表
*/
List<OpenApiConfig> getOpenApiConfigList(OpenApiConfig filter, String orderBy);
/**
* 获取在线页面下拉列表配置步骤1可按页面名称模糊搜索
*
* @param keyword 页面名称关键字为空返回全部
* @return 页面列表
*/
List<OpenApiPageOptionDto> getPageOptionList(String keyword);
/**
* 获取指定页面关联的表单下拉列表配置步骤2 联动
*
* @param pageId 页面主键Id
* @return 表单列表
*/
List<OpenApiFormOptionDto> getFormOptionList(Long pageId);
/**
* 获取可选操作下拉查询(query) + widgetJson.operationList 各项配置步骤3
*
* @param formId 在线表单主键Id
* @return 操作列表
*/
List<OpenApiOperationDto> getOperationList(Long formId);
/**
* 获取表单可配置字段树主表字段 + 关联字段属性 + 一对多分录附表字段
*
* @param formId 在线表单主键Id
* @return 字段树
*/
List<OpenApiFieldDto> getFieldTree(Long formId);
/**
* 新增配置后端推导 paramMode组装 requestPath
*
* @param config 配置对象
* @return 保存后的配置
*/
OpenApiConfig addNew(OpenApiConfig config);
/**
* 更新配置重新推导 paramMode组装 requestPath
*
* @param config 配置对象
* @return 更新后的配置不存在返回null
*/
OpenApiConfig updateConfig(OpenApiConfig config);
/**
* 按开放路径查找启用的配置动态路由执行用
*
* @param requestPath 开放路径
* @return 配置对象
*/
OpenApiConfig getByRequestPath(String requestPath);
}

23
common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiExecService.java

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
package apelet.common.openapi.service;
import apelet.common.core.object.ResponseResult;
import java.util.Map;
/**
* OpenAPI 开放接口执行服务接口按配置定位表单并执行
*
* @author chenchuchuan
* @date 2026-09-02
*/
public interface OpenApiExecService {
/**
* 按开放路径执行开放接口鉴权由拦截器完成本方法假定已注入 TokenData
*
* @param requestPath 开放路径/openapi/v1/{pageCode}/{formCode}/{operationCode}
* @param requestParams 请求体参数扁平 JSON pageNo/pageSize 或字段直传/条件参数
* @return 执行结果
*/
ResponseResult<Object> execute(String requestPath, Map<String, Object> requestParams);
}

49
common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiOnlineQueryService.java

@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
package apelet.common.openapi.service;
import apelet.common.online.model.OnlineForm;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* OpenAPI 在线表数据查询辅助接口运行在在线数据源上下文
* <p>用于关联字段反查等需要直接查询在线表zz_online_*的场景避免在普通上下文误查默认库</p>
*
* @author chenchuchuan
* @date 2026-09-03
*/
public interface OpenApiOnlineQueryService {
/**
* 关联字段反查 master 表单的关联字段主表存储列定位关联表 attr 列匹配值返回关联记录主键
*
* @param masterForm 主表单 masterTableId
* @param masterColumnName 主表存储列名onl_form_field.field_name customer_id
* @param attrColumn 关联表要匹配的列名 customer_code / customer_name / id
* @param attrValue 关联表该列要匹配的值
* @return 关联记录主键未找到返回 null
*/
Object resolveRelatedId(OnlineForm masterForm, String masterColumnName, String attrColumn, Object attrValue);
/**
* 关联字段批量反查返回关联表中 attr = attrValue **全部**记录主键集合条件过滤用
*
* @param masterForm 主表单 masterTableId
* @param masterColumnName 主表存储列名
* @param attrColumn 关联表要匹配的列名
* @param attrValue 关联表该列要匹配的值
* @return 匹配到的关联记录主键集合可能为空不会为 null
*/
Set<String> resolveRelatedIds(OnlineForm masterForm, String masterColumnName, String attrColumn, Object attrValue);
/**
* 查询一对多分录附表数据 master 行主键查子表关联行用于输出配置的 ENTRY_ARRAY
*
* @param masterForm 主表单
* @param masterId master 行主键值
* @param subTableName 子表表名一对多分录附表
* @return 子表行列表可能为空不会为 null
*/
List<Map<String, Object>> querySubTableRows(OnlineForm masterForm, Object masterId, String subTableName);
}

23
common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiUserService.java

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
package apelet.common.openapi.service;
import apelet.common.core.object.ResponseResult;
import apelet.common.openapi.dto.OpenApiUserInfo;
/**
* OpenAPI 调用用户查询 SPI由接入方 tenant-admin提供实现按登录名查询可模拟身份的用户
* <p>common-openapi 不依赖应用层用户服务通过 Spring 自动装配该接口未装配时按用户名签发 token 返回明确错误</p>
*
* @author chenchuchuan
* @date 2026-09-02
*/
public interface OpenApiUserService {
/**
* 按登录名查询用户
*
* @param loginName 登录名
* @param tenantId 应用归属租户Id为空表示不限租户全局查
* @return 用户信息
*/
ResponseResult<OpenApiUserInfo> getByLoginName(String loginName, Long tenantId);
}

82
common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiAppServiceImpl.java

@ -0,0 +1,82 @@ @@ -0,0 +1,82 @@
package apelet.common.openapi.service.impl;
import apelet.common.core.base.dao.BaseDaoMapper;
import apelet.common.core.base.service.BaseService;
import apelet.common.core.object.TokenData;
import apelet.common.openapi.dao.OpenApiAppMapper;
import apelet.common.openapi.model.OpenApiApp;
import apelet.common.openapi.service.OpenApiAppService;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
/**
* OpenAPI 第三方应用数据操作服务类
*
* @author chenchuchuan
* @date 2026-09-02
*/
@Slf4j
@Service("openApiAppService")
public class OpenApiAppServiceImpl extends BaseService<OpenApiApp, Long> implements OpenApiAppService {
@Autowired
private OpenApiAppMapper openApiAppMapper;
@Override
protected BaseDaoMapper<OpenApiApp> mapper() {
return openApiAppMapper;
}
@Override
public List<OpenApiApp> getOpenApiAppList(OpenApiApp filter, String orderBy) {
if (filter == null) {
filter = new OpenApiApp();
}
return openApiAppMapper.getOpenApiAppList(filter, orderBy);
}
@Override
public OpenApiApp addNew(OpenApiApp app) {
app.setId(IdUtil.getSnowflakeNextId());
if (StrUtil.isBlank(app.getAppCode())) {
app.setAppCode("open_app_" + RandomUtil.randomString(8));
}
app.setAppSecret(RandomUtil.randomString(32));
Date now = new Date();
TokenData tokenData = TokenData.takeFromRequest();
Long currentUserId = tokenData == null ? null : tokenData.getUserId();
app.setCreateTime(now);
app.setCreateUserId(currentUserId);
app.setUpdateTime(now);
app.setUpdateUserId(currentUserId);
if (app.getStatus() == null) {
app.setStatus(1);
}
this.save(app);
return app;
}
@Override
public String resetSecret(Long id) {
OpenApiApp app = this.getById(id);
if (app == null) {
return null;
}
String newSecret = RandomUtil.randomString(32);
app.setAppSecret(newSecret);
app.setUpdateTime(new Date());
TokenData tokenData = TokenData.takeFromRequest();
if (tokenData != null) {
app.setUpdateUserId(tokenData.getUserId());
}
this.updateById(app);
return newSecret;
}
}

137
common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiAuthServiceImpl.java

@ -0,0 +1,137 @@ @@ -0,0 +1,137 @@
package apelet.common.openapi.service.impl;
import apelet.common.core.constant.AppDeviceType;
import apelet.common.core.constant.ErrorCodeEnum;
import apelet.common.core.object.ResponseResult;
import apelet.common.core.object.TokenData;
import apelet.common.core.util.JwtUtil;
import apelet.common.core.util.RedisKeyUtil;
import apelet.common.openapi.config.OpenApiProperties;
import apelet.common.openapi.dto.OpenApiUserInfo;
import apelet.common.openapi.model.OpenApiApp;
import apelet.common.openapi.service.OpenApiAppService;
import apelet.common.openapi.service.OpenApiAuthService;
import apelet.common.openapi.service.OpenApiUserService;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* OpenAPI 鉴权服务实现
*
* @author chenchuchuan
* @date 2026-09-02
*/
@Slf4j
@Service("openApiAuthService")
public class OpenApiAuthServiceImpl implements OpenApiAuthService {
/**
* JWT claims 中的会话Id键与应用登录 token 保持一致
*/
private static final String CLAIM_SESSION_ID = "sessionId";
/**
* Bearer 前缀JwtUtil 生成结果自带对外返回时去除
*/
private static final String BEARER_PREFIX = "Bearer ";
@Autowired
private OpenApiAppService openApiAppService;
@Autowired
private RedissonClient redissonClient;
@Autowired
private OpenApiProperties openApiProperties;
@Autowired(required = false)
private OpenApiUserService openApiUserService;
@Override
public ResponseResult<JSONObject> issueToken(String appCode, String appSecret, String username) {
if (StrUtil.hasBlank(appCode, appSecret, username)) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST, "应用编码、密钥、用户名均不能为空!");
}
// ① 校验应用凭证与状态
OpenApiApp app = openApiAppService.getOne(new LambdaQueryWrapper<OpenApiApp>()
.eq(OpenApiApp::getAppCode, appCode));
if (app == null || !app.getAppSecret().equals(appSecret)) {
return ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, "应用编码或密钥错误!");
}
if (app.getStatus() == null || app.getStatus() != 1) {
return ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, "应用已被停用!");
}
// ② 按用户名查询调用用户
if (openApiUserService == null) {
return ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, "未配置 OpenApiUserService 实现,无法按用户名签发 token!");
}
ResponseResult<OpenApiUserInfo> userResult = openApiUserService.getByLoginName(username, app.getTenantId());
if (!userResult.isSuccess() || userResult.getData() == null) {
return ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, "调用用户不存在!");
}
OpenApiUserInfo user = userResult.getData();
// ③ 组装 TokenData,签发 JWT(claims 仅放 sessionId,会话状态存 Redis 可吊销)
TokenData tokenData = new TokenData();
tokenData.setUserId(user.getUserId());
tokenData.setLoginName(user.getLoginName());
tokenData.setShowName(user.getShowName());
tokenData.setTenantId(user.getTenantId());
tokenData.setAppCode(appCode);
tokenData.setDeviceType(AppDeviceType.WEB);
tokenData.setIsAdmin(false);
String sessionId = "openapi_" + IdUtil.simpleUUID();
tokenData.setSessionId(sessionId);
Map<String, Object> claims = new HashMap<>(3);
claims.put(CLAIM_SESSION_ID, sessionId);
Long expirationSeconds = openApiProperties.getTokenExpiration();
String token = JwtUtil.generateToken(claims, expirationSeconds * 1000, openApiProperties.getTokenSigningKey());
this.putTokenDataToSessionCache(tokenData, expirationSeconds);
JSONObject data = new JSONObject();
data.put("accessToken", StrUtil.removePrefix(token, BEARER_PREFIX));
data.put("expiresIn", expirationSeconds);
return ResponseResult.success(data);
}
@Override
public TokenData validateToken(String token) {
Claims c = JwtUtil.parseToken(token, openApiProperties.getTokenSigningKey());
if (JwtUtil.isNullOrExpired(c)) {
return null;
}
String sessionId = (String) c.get(CLAIM_SESSION_ID);
if (StrUtil.isBlank(sessionId)) {
return null;
}
String sessionIdKey = RedisKeyUtil.makeSessionIdKey(sessionId);
RBucket<String> bucket = redissonClient.getBucket(sessionIdKey);
if (!bucket.isExists()) {
return null;
}
TokenData tokenData = JSON.parseObject(bucket.get(), TokenData.class);
if (tokenData != null) {
tokenData.setToken(token);
}
return tokenData;
}
/**
* TokenData 写入 Redis 会话缓存key 与应用登录会话共用 RedisKeyUtil.makeSessionIdKey可吊销
*/
private void putTokenDataToSessionCache(TokenData tokenData, Long expirationSeconds) {
String sessionIdKey = RedisKeyUtil.makeSessionIdKey(tokenData.getSessionId());
String sessionData = JSON.toJSONString(tokenData, SerializerFeature.WriteNonStringValueAsString);
RBucket<String> bucket = redissonClient.getBucket(sessionIdKey);
bucket.set(sessionData);
bucket.expire(expirationSeconds, TimeUnit.SECONDS);
}
}

312
common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiConfigServiceImpl.java

@ -0,0 +1,312 @@ @@ -0,0 +1,312 @@
package apelet.common.openapi.service.impl;
import apelet.common.core.base.dao.BaseDaoMapper;
import apelet.common.core.base.service.BaseService;
import apelet.common.core.object.TokenData;
import apelet.common.generator.model.OnlFormField;
import apelet.common.generator.model.OnlFormHead;
import apelet.common.generator.service.IOnlFormFieldService;
import apelet.common.generator.service.IOnlFormHeadService;
import apelet.common.online.model.OnlineForm;
import apelet.common.online.model.OnlinePage;
import apelet.common.online.model.OnlineTable;
import apelet.common.online.service.OnlineFormService;
import apelet.common.online.service.OnlinePageService;
import apelet.common.online.service.OnlineTableService;
import apelet.common.openapi.config.OpenApiProperties;
import apelet.common.openapi.constant.OpenApiFieldSource;
import apelet.common.openapi.constant.OpenApiParamMode;
import apelet.common.openapi.dao.OpenApiConfigMapper;
import apelet.common.openapi.dto.OpenApiFieldDto;
import apelet.common.openapi.dto.OpenApiFormOptionDto;
import apelet.common.openapi.dto.OpenApiOperationDto;
import apelet.common.openapi.dto.OpenApiPageOptionDto;
import apelet.common.openapi.model.OpenApiConfig;
import apelet.common.openapi.service.OpenApiConfigService;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* OpenAPI 开放接口配置数据操作服务类
*
* @author chenchuchuan
* @date 2026-09-02
*/
@Slf4j
@Service("openApiConfigService")
public class OpenApiConfigServiceImpl extends BaseService<OpenApiConfig, Long> implements OpenApiConfigService {
/** 查询操作的固定 operation_code。 */
private static final String QUERY_CODE = "query";
@Autowired
private OpenApiConfigMapper openApiConfigMapper;
@Autowired
private OpenApiProperties openApiProperties;
@Autowired
private OnlinePageService onlinePageService;
@Autowired
private OnlineFormService onlineFormService;
@Autowired
private OnlineTableService onlineTableService;
@Autowired
private IOnlFormHeadService onlFormHeadService;
@Autowired
private IOnlFormFieldService onlFormFieldService;
@Override
protected BaseDaoMapper<OpenApiConfig> mapper() {
return openApiConfigMapper;
}
@Override
public List<OpenApiConfig> getOpenApiConfigList(OpenApiConfig filter, String orderBy) {
if (filter == null) {
filter = new OpenApiConfig();
}
return openApiConfigMapper.getOpenApiConfigList(filter, orderBy);
}
@Override
public List<OpenApiPageOptionDto> getPageOptionList(String keyword) {
// 按页面名称模糊搜索,空关键字返回全部;结果按创建时间倒序
OnlinePage filter = null;
if (StrUtil.isNotBlank(keyword)) {
filter = new OnlinePage();
filter.setPageName(keyword);
}
List<OnlinePage> pageList = onlinePageService.getOnlinePageList(filter, "create_time DESC");
List<OpenApiPageOptionDto> result = new ArrayList<>();
if (pageList != null) {
for (OnlinePage page : pageList) {
OpenApiPageOptionDto dto = new OpenApiPageOptionDto();
dto.setPageId(page.getPageId());
dto.setPageCode(page.getPageCode());
dto.setPageName(page.getPageName());
result.add(dto);
}
}
return result;
}
@Override
public List<OpenApiFormOptionDto> getFormOptionList(Long pageId) {
List<OpenApiFormOptionDto> result = new ArrayList<>();
if (pageId == null) {
return result;
}
List<OnlineForm> formList = onlineFormService.list(
new LambdaQueryWrapper<OnlineForm>().eq(OnlineForm::getPageId, pageId));
if (formList != null) {
for (OnlineForm form : formList) {
OpenApiFormOptionDto dto = new OpenApiFormOptionDto();
dto.setFormId(form.getFormId());
dto.setFormCode(form.getFormCode());
dto.setFormName(form.getFormName());
result.add(dto);
}
}
return result;
}
@Override
public List<OpenApiOperationDto> getOperationList(Long formId) {
List<OpenApiOperationDto> result = new ArrayList<>();
// 合成「查询」选项
OpenApiOperationDto query = new OpenApiOperationDto();
query.setCode(QUERY_CODE);
query.setName("查询");
query.setEnabled(true);
result.add(query);
OnlineForm form = formId == null ? null : onlineFormService.getById(formId);
if (form == null || StrUtil.isBlank(form.getWidgetJson())) {
return result;
}
JSONObject widgetJson = JSON.parseObject(form.getWidgetJson());
// 兼容 pc/mobile,取第一个存在 operationList 的设备
for (String device : new String[]{"pc", "mobile"}) {
JSONObject deviceObj = widgetJson.getJSONObject(device);
if (deviceObj == null) {
continue;
}
JSONArray opList = deviceObj.getJSONArray("operationList");
if (opList == null || opList.isEmpty()) {
continue;
}
for (int i = 0; i < opList.size(); i++) {
JSONObject op = opList.getJSONObject(i);
if (op == null) {
continue;
}
OpenApiOperationDto dto = new OpenApiOperationDto();
dto.setCode(op.getString("code"));
dto.setName(op.getString("name"));
dto.setEnabled(op.getBoolean("enabled"));
result.add(dto);
}
break;
}
return result;
}
@Override
public List<OpenApiFieldDto> getFieldTree(Long formId) {
List<OpenApiFieldDto> result = new ArrayList<>();
OnlineForm form = formId == null ? null : onlineFormService.getById(formId);
if (form == null || form.getMasterTableId() == null) {
return result;
}
OnlineTable masterTable = onlineTableService.getById(form.getMasterTableId());
if (masterTable == null) {
return result;
}
OnlFormHead head = onlFormHeadService.getOne(new LambdaQueryWrapper<OnlFormHead>()
.eq(OnlFormHead::getTableName, masterTable.getTableName()));
if (head == null) {
return result;
}
// 主表字段:普通字段 FORM_FIELD,关联字段(refPropert 非空)RELATION_FIELD(按 id/编码/名称 三个属性展开)
List<OnlFormField> masterFields = onlFormFieldService.list(
new LambdaQueryWrapper<OnlFormField>().eq(OnlFormField::getHeadId, head.getId()));
if (masterFields != null) {
for (OnlFormField field : masterFields) {
if (field.getRefPropert() != null) {
for (String attr : new String[]{"id", "number", "name"}) {
OpenApiFieldDto dto = new OpenApiFieldDto();
dto.setSource(OpenApiFieldSource.RELATION_FIELD);
dto.setField(field.getFieldName());
dto.setAttr(attr);
dto.setAlias(field.getFieldName());
dto.setLabel(field.getFieldRemark());
result.add(dto);
}
} else {
OpenApiFieldDto dto = new OpenApiFieldDto();
dto.setSource(OpenApiFieldSource.FORM_FIELD);
dto.setField(field.getFieldName());
dto.setAlias(field.getFieldName());
dto.setLabel(field.getFieldRemark());
result.add(dto);
}
}
}
// 一对多分录附表(childName 逗号分隔)
if (StrUtil.isNotBlank(head.getChildName())) {
for (String subTableName : head.getChildName().split(",")) {
subTableName = subTableName.trim();
OnlFormHead subHead = onlFormHeadService.getOne(new LambdaQueryWrapper<OnlFormHead>()
.eq(OnlFormHead::getTableName, subTableName));
if (subHead == null) {
continue;
}
OpenApiFieldDto entryArray = new OpenApiFieldDto();
entryArray.setSource(OpenApiFieldSource.ENTRY_ARRAY);
entryArray.setTable(subTableName);
entryArray.setAlias(subTableName);
entryArray.setLabel(subTableName);
List<OpenApiFieldDto> children = new ArrayList<>();
List<OnlFormField> subFields = onlFormFieldService.list(
new LambdaQueryWrapper<OnlFormField>().eq(OnlFormField::getHeadId, subHead.getId()));
if (subFields != null) {
for (OnlFormField subField : subFields) {
OpenApiFieldDto dto = new OpenApiFieldDto();
dto.setSource(OpenApiFieldSource.ENTRY_FIELD);
dto.setTable(subTableName);
dto.setField(subField.getFieldName());
dto.setAlias(subField.getFieldName());
dto.setLabel(subField.getFieldRemark());
children.add(dto);
}
}
entryArray.setChildren(children);
result.add(entryArray);
}
}
return result;
}
@Override
public OpenApiConfig addNew(OpenApiConfig config) {
config.setId(IdUtil.getSnowflakeNextId());
config.setParamMode(this.resolveParamMode(config));
config.setRequestPath(this.buildRequestPath(config));
Date now = new Date();
TokenData tokenData = TokenData.takeFromRequest();
Long currentUserId = tokenData == null ? null : tokenData.getUserId();
config.setCreateTime(now);
config.setCreateUserId(currentUserId);
config.setUpdateTime(now);
config.setUpdateUserId(currentUserId);
if (config.getStatus() == null) {
config.setStatus(1);
}
if (StrUtil.isBlank(config.getRequestMethod())) {
config.setRequestMethod("POST");
}
this.save(config);
return config;
}
@Override
public OpenApiConfig updateConfig(OpenApiConfig config) {
OpenApiConfig original = this.getById(config.getId());
if (original == null) {
return null;
}
config.setParamMode(this.resolveParamMode(config));
config.setRequestPath(this.buildRequestPath(config));
config.setCreateTime(original.getCreateTime());
config.setUpdateTime(new Date());
TokenData tokenData = TokenData.takeFromRequest();
if (tokenData != null) {
config.setUpdateUserId(tokenData.getUserId());
}
this.updateById(config);
return config;
}
@Override
public OpenApiConfig getByRequestPath(String requestPath) {
if (StrUtil.isBlank(requestPath)) {
return null;
}
return openApiConfigMapper.selectOne(new LambdaQueryWrapper<OpenApiConfig>().eq(OpenApiConfig::getRequestPath, requestPath));
}
/**
* 推导参数模式queryQUERY保存/提交FIELD_DIRECT其他CONDITION_MATCH
*/
private String resolveParamMode(OpenApiConfig config) {
if (QUERY_CODE.equals(config.getOperationCode())) {
return OpenApiParamMode.QUERY;
}
if (StrUtil.equalsAny(config.getOperationName(), "保存", "提交")) {
return OpenApiParamMode.FIELD_DIRECT;
}
return OpenApiParamMode.CONDITION_MATCH;
}
/**
* 组装开放路径{urlPrefix}/v1/{pageCode}/{formCode}/{operationCode}pageCode 为空时按 pageId 反查
*/
private String buildRequestPath(OpenApiConfig config) {
if (StrUtil.isBlank(config.getPageCode()) && config.getPageId() != null) {
OnlinePage page = onlinePageService.getById(config.getPageId());
if (page != null) {
config.setPageCode(page.getPageCode());
}
}
return openApiProperties.getUrlPrefix() + "/v1/" + config.getPageCode() + "/" + config.getFormCode() + "/" + config.getOperationCode();
}
}

566
common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiExecServiceImpl.java

@ -0,0 +1,566 @@ @@ -0,0 +1,566 @@
package apelet.common.openapi.service.impl;
import apelet.common.core.constant.ErrorCodeEnum;
import apelet.common.core.object.MyPageParam;
import apelet.common.core.object.ResponseResult;
import apelet.common.core.object.TokenData;
import apelet.common.generator.model.OnlFormHead;
import apelet.common.generator.service.IOnlFormHeadService;
import apelet.common.online.abstractplugin.model.GridData;
import apelet.common.online.dto.OnlineFilterDto;
import apelet.common.online.dto.OnlinePluginExecuteDto;
import apelet.common.online.model.OnlineDatasource;
import apelet.common.online.model.OnlineForm;
import apelet.common.online.model.OnlineTable;
import apelet.common.online.model.constant.FieldFilterType;
import apelet.common.online.service.OnlineDatasourceService;
import apelet.common.online.service.OnlineFormService;
import apelet.common.online.service.OnlineOperationService;
import apelet.common.online.service.OnlineTableService;
import apelet.common.online.util.OnlineOperationHelper;
import apelet.common.openapi.constant.OpenApiFieldSource;
import apelet.common.openapi.constant.OpenApiParamMode;
import apelet.common.openapi.model.OpenApiConfig;
import apelet.common.openapi.service.OpenApiConfigService;
import apelet.common.openapi.service.OpenApiExecService;
import apelet.common.openapi.service.OpenApiOnlineQueryService;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.*;
/**
* OpenAPI 开放接口执行服务实现
* <p> param_mode 分派三种执行路径QUERY 走列表插件FIELD_DIRECT executePlugin字段直传
* CONDITION_MATCH 先按条件匹配单据集合再逐条 executePlugin</p>
*
* @author chenchuchuan
* @date 2026-09-02
*/
@Slf4j
@Service("openApiExecService")
public class OpenApiExecServiceImpl implements OpenApiExecService {
@Autowired
private OpenApiConfigService openApiConfigService;
@Autowired
private OnlineFormService onlineFormService;
@Autowired
private OnlineTableService onlineTableService;
@Autowired
private IOnlFormHeadService onlFormHeadService;
@Autowired
private OnlineDatasourceService onlineDatasourceService;
@Autowired
private OnlineOperationService onlineOperationService;
@Autowired
private OnlineOperationHelper onlineOperationHelper;
@Autowired
private OpenApiOnlineQueryService openApiOnlineQueryService;
@Override
public ResponseResult<Object> execute(String requestPath, Map<String, Object> requestParams) {
OpenApiConfig config = openApiConfigService.getByRequestPath(requestPath);
if (config == null || config.getStatus() == null || config.getStatus() != 1) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "开放接口不存在或已停用!");
}
OnlineForm form = this.findForm(config);
if (form == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "在线表单不存在!");
}
TokenData tokenData = TokenData.takeFromRequest();
if (tokenData == null || !config.getAppCode().equals(tokenData.getAppCode())) {
return ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION, "当前应用无权调用该开放接口!");
}
// 列表类操作(QUERY / CONDITION_MATCH)依赖 pc.tableWidget 走列表引擎,编辑页未配置列表时给出明确提示而非底层空指针。
if (OpenApiParamMode.QUERY.equals(config.getParamMode())
|| OpenApiParamMode.CONDITION_MATCH.equals(config.getParamMode())) {
if (!this.hasListTableWidget(form)) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "该表单未配置列表(pc.tableWidget),无法执行查询/条件匹配操作!");
}
}
// 写类操作(FIELD_DIRECT / CONDITION_MATCH)需目标按钮真实存在且启用并绑定插件,避免误配时 executePlugin 空跑却返回"成功"。
if (OpenApiParamMode.FIELD_DIRECT.equals(config.getParamMode())
|| OpenApiParamMode.CONDITION_MATCH.equals(config.getParamMode())) {
if (!this.hasEnabledOperation(form, config.getOperationName())) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST,
"操作按钮[" + config.getOperationName() + "]不存在、未启用或未绑定插件,无法执行!");
}
}
// 在线引擎按 TokenData.appCode 校验数据源归属(空值表示非第三方应用/租户公共数据)。
// 令牌 appCode 是开放应用编码,与在线资源归属编码不是一回事,执行期间先切到该表单数据源真正归属的编码,结束后还原。
// 注意:归属编码用"不带归属校验"的直查获取——verifyAndGetDatasource 的归属校验在切换前必然失败,不能拿它取 appCode。
String savedAppCode = tokenData.getAppCode();
try {
OnlineDatasource datasource = form.getMasterTableId() == null ? null
: onlineDatasourceService.getOnlineDatasourceByMasterTableId(form.getMasterTableId());
tokenData.setAppCode(datasource == null ? null : datasource.getAppCode());
if (OpenApiParamMode.QUERY.equals(config.getParamMode())) {
return this.executeQuery(config, form, requestParams);
}
if (OpenApiParamMode.FIELD_DIRECT.equals(config.getParamMode())) {
return this.executeFieldDirect(config, form, requestParams);
}
return this.executeConditionMatch(config, form, requestParams);
} finally {
tokenData.setAppCode(savedAppCode);
}
}
// ============ QUERY:列表插件查询 ============
private ResponseResult<Object> executeQuery(OpenApiConfig config, OnlineForm form, Map<String, Object> requestParams) {
JSONObject paramConfig = JSON.parseObject(config.getParamConfig());
int pageNo = this.toIntParam(requestParams.get("pageNo"), 1, 1, Integer.MAX_VALUE);
int pageSize = this.toIntParam(requestParams.get("pageSize"), 10, 1, 1000);
MyPageParam pageParam = new MyPageParam();
pageParam.setPageNum(pageNo);
pageParam.setPageSize(pageSize);
ResponseResult<List<OnlineFilterDto>> filterResult = this.buildFilterDtoList(paramConfig, requestParams, form);
if (!filterResult.isSuccess()) {
return ResponseResult.errorFrom(filterResult);
}
List<OnlineFilterDto> filterDtoList = filterResult.getData();
ResponseResult<OnlineDatasource> datasourceResult = this.getDatasourceResult(form);
if (!datasourceResult.isSuccess()) {
return ResponseResult.errorFrom(datasourceResult);
}
OnlFormHead onlFormHead = this.getFormHead(datasourceResult);
if (onlFormHead == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "表单表头不存在!");
}
GridData gridData = onlineOperationService.exeListPlugin(form.getFormId(), filterDtoList, null, pageParam, null,
onlFormHead, datasourceResult, null);
if (gridData == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_ACCESS_FAILED, "列表查询失败!");
}
JSONObject data = new JSONObject(true);
data.put("total", gridData.getTotalCount());
data.put("pageNo", pageNo);
data.put("pageSize", pageSize);
data.put("dataList", this.assembleDataList(gridData.getDataList(), config.getOutputConfig(), form));
return ResponseResult.success(data);
}
// ============ FIELD_DIRECT:字段直传 executePlugin ============
private ResponseResult<Object> executeFieldDirect(OpenApiConfig config, OnlineForm form, Map<String, Object> requestParams) {
JSONObject paramConfig = JSON.parseObject(config.getParamConfig());
JSONArray fields = paramConfig == null ? null : paramConfig.getJSONArray("fields");
if (fields == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "未配置字段直传参数!");
}
JSONObject masterData = new JSONObject(true);
JSONObject slaveData = new JSONObject(true);
for (int i = 0; i < fields.size(); i++) {
JSONObject field = fields.getJSONObject(i);
String source = field.getString("source");
String fieldName = field.getString("field");
String table = field.getString("table");
String alias = field.getString("alias");
Object value = requestParams.get(alias);
if (OpenApiFieldSource.FORM_FIELD.equals(source)) {
masterData.put(fieldName, value);
} else if (OpenApiFieldSource.RELATION_FIELD.equals(source)) {
// attr = 关联表要匹配的列名;attr=id 直存,其他列先按关联表反查主键再写入主表存储列
String attr = field.getString("attr");
Object writeValue = value;
if (value != null && StrUtil.isNotBlank(attr) && !"id".equals(attr)) {
Object relatedId = openApiOnlineQueryService.resolveRelatedId(form, fieldName, attr, value);
if (relatedId != null) {
writeValue = relatedId;
} else {
log.warn("OpenAPI 关联字段 [{}] 按列 [{}] 未匹配到关联记录,值 [{}] 原样写入", fieldName, attr, value);
}
}
masterData.put(fieldName, writeValue);
} else if (OpenApiFieldSource.ENTRY_ARRAY.equals(source)) {
JSONArray entryArray = new JSONArray();
if (value instanceof Collection) {
entryArray.addAll((Collection<?>) value);
}
slaveData.put(table, entryArray);
}
// ENTRY_FIELD 作为 ENTRY_ARRAY 的子节点,值挂在分录数组元素上,无需单独处理
}
OnlinePluginExecuteDto pluginDto = new OnlinePluginExecuteDto();
pluginDto.setButtonName(config.getOperationName());
pluginDto.setFormId(form.getFormId());
// billId 由系统生成/定位,不作为接口入参
Object result = onlineOperationService.executePlugin(pluginDto, masterData, slaveData);
return ResponseResult.success(result);
}
// ============ CONDITION_MATCH:条件匹配单据后逐条 executePlugin ============
private ResponseResult<Object> executeConditionMatch(OpenApiConfig config, OnlineForm form, Map<String, Object> requestParams) {
JSONObject paramConfig = JSON.parseObject(config.getParamConfig());
ResponseResult<List<OnlineFilterDto>> filterResult = this.buildFilterDtoList(paramConfig, requestParams, form);
if (!filterResult.isSuccess()) {
return ResponseResult.errorFrom(filterResult);
}
List<OnlineFilterDto> filterDtoList = filterResult.getData();
ResponseResult<OnlineDatasource> datasourceResult = this.getDatasourceResult(form);
if (!datasourceResult.isSuccess()) {
return ResponseResult.errorFrom(datasourceResult);
}
OnlFormHead onlFormHead = this.getFormHead(datasourceResult);
if (onlFormHead == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "表单表头不存在!");
}
MyPageParam pageParam = new MyPageParam();
pageParam.setPageNum(1);
pageParam.setPageSize(2000);
GridData gridData = onlineOperationService.exeListPlugin(form.getFormId(), filterDtoList, null, pageParam, null,
onlFormHead, datasourceResult, null);
if (gridData == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_ACCESS_FAILED, "单据匹配失败!");
}
OnlinePluginExecuteDto pluginDto = new OnlinePluginExecuteDto();
pluginDto.setButtonName(config.getOperationName());
pluginDto.setFormId(form.getFormId());
JSONArray resultList = new JSONArray();
for (Map<String, Object> row : gridData.getDataList()) {
Object id = row.get("id");
if (id == null) {
continue;
}
pluginDto.setBillId(String.valueOf(id));
resultList.add(onlineOperationService.executePlugin(pluginDto, null, null));
}
return ResponseResult.success(resultList);
}
// ============ 公共辅助 ============
/**
* 定位在线表单优先按 formId未配置时回退 pageId + formCode
*/
private OnlineForm findForm(OpenApiConfig config) {
if (config.getFormId() != null) {
OnlineForm form = onlineFormService.getById(config.getFormId());
if (form != null) {
return form;
}
}
return onlineFormService.getOne(new LambdaQueryWrapper<OnlineForm>()
.eq(OnlineForm::getPageId, config.getPageId())
.eq(OnlineForm::getFormCode, config.getFormCode()), false);
}
/**
* 取表单主表数据源 masterTable供列表插件回退使用
*/
private ResponseResult<OnlineDatasource> getDatasourceResult(OnlineForm form) {
if (form.getMasterTableId() == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "表单主表不存在!");
}
OnlineDatasource datasource = onlineDatasourceService.getOnlineDatasourceByMasterTableId(form.getMasterTableId());
if (datasource == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "表单数据源不存在!");
}
return onlineOperationHelper.verifyAndGetDatasource(datasource.getDatasourceId());
}
/**
* 取列表查询用的在线表单表头onl_form_head table_name 为业务键 zz_online_form.form_id 不是同一 id
* 必须按主表表名定位而非按 OnlineForm.formId
*/
private OnlFormHead getFormHead(ResponseResult<OnlineDatasource> datasourceResult) {
if (datasourceResult == null || !datasourceResult.isSuccess() || datasourceResult.getData() == null
|| datasourceResult.getData().getMasterTable() == null) {
return null;
}
return onlFormHeadService.getHeadTableNameCache(datasourceResult.getData().getMasterTable().getTableName());
}
/**
* 判断表单 widget 是否配置了 pc.tableWidget列表查询/条件匹配的 ORM 回退路径依赖它缺失会空指针
*/
private boolean hasListTableWidget(OnlineForm form) {
if (form == null || form.getFormId() == null) {
return false;
}
OnlineForm cached = onlineFormService.getOnlineFormFromCache(form.getFormId());
if (cached == null || StrUtil.isBlank(cached.getWidgetJson())) {
return false;
}
JSONObject widget = JSON.parseObject(cached.getWidgetJson());
JSONObject pc = widget == null ? null : widget.getJSONObject("pc");
return pc != null && pc.containsKey("tableWidget");
}
/**
* 判断表单 widget 是否存在启用且绑定插件的同名操作按钮 executePlugin 的按钮解析一致 pc.operationList
*/
private boolean hasEnabledOperation(OnlineForm form, String buttonName) {
if (form == null || form.getFormId() == null || StrUtil.isBlank(buttonName)) {
return false;
}
OnlineForm cached = onlineFormService.getOnlineFormFromCache(form.getFormId());
if (cached == null || StrUtil.isBlank(cached.getWidgetJson())) {
return false;
}
JSONObject pc = JSON.parseObject(cached.getWidgetJson()).getJSONObject("pc");
JSONArray ops = pc == null ? null : pc.getJSONArray("operationList");
if (ops == null) {
return false;
}
for (int i = 0; i < ops.size(); i++) {
JSONObject op = ops.getJSONObject(i);
if (op == null || !buttonName.equals(op.getString("name"))) {
continue;
}
JSONArray pluginList = op.getJSONArray("pluginList");
if (Boolean.TRUE.equals(op.getBoolean("enabled")) && pluginList != null && !pluginList.isEmpty()) {
return true;
}
}
return false;
}
/**
* 安全地把接口入参解析为整数兼容 1.0 等数值形态非法/缺省回退默认值并夹到 [min, max]
*/
private int toIntParam(Object value, int defaultValue, int min, int max) {
if (value == null || StrUtil.isBlank(String.valueOf(value))) {
return defaultValue;
}
try {
long longValue = new BigDecimal(String.valueOf(value).trim()).longValue();
if (longValue < min) {
return min;
}
if (longValue > max) {
return max;
}
return (int) longValue;
} catch (Exception e) {
return defaultValue;
}
}
/**
* param_config.conditions 与请求参数构造过滤条件列表未传参数的条件跳过
* 仅放行在线列表引擎真正支持的条件 opeq/like/in/between其余直接拒绝避免条件被静默丢弃导致 CONDITION_MATCH 放大执行
*/
private ResponseResult<List<OnlineFilterDto>> buildFilterDtoList(JSONObject paramConfig, Map<String, Object> requestParams, OnlineForm form) {
List<OnlineFilterDto> result = new ArrayList<>();
if (paramConfig == null) {
return ResponseResult.success(result);
}
JSONArray conditions = paramConfig.getJSONArray("conditions");
if (conditions == null) {
return ResponseResult.success(result);
}
String masterTableName = this.getMasterTableName(form);
for (int i = 0; i < conditions.size(); i++) {
JSONObject cond = conditions.getJSONObject(i);
String param = cond.getString("param");
Object value = requestParams.get(param);
String target = cond.getString("target");
String field = cond.getString("field");
String table = cond.getString("table");
String attr = cond.getString("attr");
String op = cond.getString("op");
// between 无单一入参值(靠 columnValueStart/End 两个入参名取上下界),不走单值跳过
boolean isBetween = "between".equalsIgnoreCase(op == null ? "" : op.trim());
if (!isBetween && (value == null || (value instanceof String && StrUtil.isBlank((String) value)))) {
continue;
}
// RELATION_FIELD 条件:attr 值先反查关联主键集合,再用 主表存储列 IN 过滤
if (OpenApiFieldSource.RELATION_FIELD.equals(target) && attr != null && !"id".equals(attr)) {
result.add(this.buildRelationFilter(form, field, attr, value, masterTableName));
continue;
}
Integer filterType = this.mapOp(op);
if (filterType == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED,
"暂不支持的条件 op=[" + op + "],仅支持 eq/like/in/between!");
}
OnlineFilterDto filter = new OnlineFilterDto();
filter.setTableName(OpenApiFieldSource.ENTRY_FIELD.equals(target) ? table : masterTableName);
filter.setColumnName(field);
filter.setFilterType(filterType);
if (filterType == FieldFilterType.RANGE_FILTER) {
// between 按 columnValueStart/End 取值(引擎 RANGE 分支读取的就是这两个字段,不是 columnValue)
Object startVal = this.rangeValue(requestParams, cond.getString("columnValueStart"));
Object endVal = this.rangeValue(requestParams, cond.getString("columnValueEnd"));
if (startVal == null && endVal == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED,
"between 条件需在 conditions 中配置 columnValueStart/columnValueEnd(入参名),且请求需携带对应值!");
}
filter.setColumnValueStart(startVal);
filter.setColumnValueEnd(endVal);
} else if (filterType == FieldFilterType.IN_LIST_FILTER) {
// 引擎 IN_LIST 分支按 getColumnValue() 单值(逗号串)读取
filter.setColumnValue(this.joinListValue(value));
} else {
filter.setColumnValue(value);
}
result.add(filter);
}
return ResponseResult.success(result);
}
/**
* 将入参列表拼成逗号串在线列表引擎的 IN_LIST_FILTER getColumnValue() 单值读取 ListDataOrmService#getFilter
*/
private String joinListValue(Object value) {
if (value instanceof Collection) {
return StrUtil.join(",", (Collection<?>) value);
}
return String.valueOf(value);
}
/**
* between 的上/下界值conditions 里的 columnValueStart/columnValueEnd 记录的是请求体入参名
*/
private Object rangeValue(Map<String, Object> requestParams, String paramName) {
return StrUtil.isBlank(paramName) ? null : requestParams.get(paramName);
}
/**
* 关联字段条件过滤 attr 列反查关联主键集合返回 主表存储列 IN (ids) 过滤无匹配时用占位值保证结果为空
*/
private OnlineFilterDto buildRelationFilter(OnlineForm form, String masterColumn, String attr, Object value, String masterTableName) {
Set<String> relatedIds = new HashSet<>();
if (value instanceof Collection) {
for (Object item : (Collection<?>) value) {
relatedIds.addAll(openApiOnlineQueryService.resolveRelatedIds(form, masterColumn, attr, item));
}
} else {
relatedIds.addAll(openApiOnlineQueryService.resolveRelatedIds(form, masterColumn, attr, value));
}
OnlineFilterDto filter = new OnlineFilterDto();
filter.setTableName(masterTableName);
filter.setColumnName(masterColumn);
filter.setFilterType(FieldFilterType.IN_LIST_FILTER);
if (relatedIds.isEmpty()) {
// 无匹配关联 → 用不可能命中的占位值,保证查询结果为空
relatedIds.add("-1");
}
// 在线列表引擎按 getColumnValue() 单值读取 IN 条件(逗号串),非 columnValueList
filter.setColumnValue(StrUtil.join(",", relatedIds));
return filter;
}
/**
* 按输出配置裁剪列表行数据含一对多分录 ENTRY_ARRAY 的逐行组装
*/
private List<JSONObject> assembleDataList(List<Map<String, Object>> dataList, String outputConfigJson, OnlineForm form) {
List<JSONObject> result = new ArrayList<>();
if (dataList == null || StrUtil.isBlank(outputConfigJson)) {
return result;
}
JSONObject outputConfig = JSON.parseObject(outputConfigJson);
JSONArray dataFields = outputConfig.getJSONArray("dataFields");
if (dataFields == null) {
return result;
}
for (Map<String, Object> row : dataList) {
JSONObject item = new JSONObject(true);
this.mapRow(item, row, dataFields, form);
result.add(item);
}
return result;
}
private void mapRow(JSONObject target, Map<String, Object> row, JSONArray dataFields, OnlineForm form) {
for (int i = 0; i < dataFields.size(); i++) {
JSONObject df = dataFields.getJSONObject(i);
String alias = df.getString("alias");
String field = df.getString("field");
String targetType = df.getString("target");
if (OpenApiFieldSource.ENTRY_ARRAY.equals(targetType)) {
// 一对多分录:按 master 行主键查子表关联行,按 children 裁剪字段
Object masterId = row.get("id");
String subTable = df.getString("table");
target.put(alias, this.assembleEntryList(form, masterId, subTable, df.getJSONArray("children")));
continue;
}
Object value = row.get(field);
if (value == null) {
value = row.get(alias);
}
if (value == null) {
value = row.get(StrUtil.toUnderlineCase(field));
}
target.put(alias, value);
}
}
/**
* 组装一对多分录行查子表数据并仅保留 children 中配置的字段
*/
private JSONArray assembleEntryList(OnlineForm form, Object masterId, String subTable, JSONArray children) {
JSONArray out = new JSONArray();
if (masterId == null || StrUtil.isBlank(subTable) || children == null) {
return out;
}
List<Map<String, Object>> subRows = openApiOnlineQueryService.querySubTableRows(form, masterId, subTable);
if (subRows == null) {
return out;
}
for (Map<String, Object> subRow : subRows) {
JSONObject item = new JSONObject(true);
for (int c = 0; c < children.size(); c++) {
JSONObject child = children.getJSONObject(c);
String childAlias = child.getString("alias");
String childField = child.getString("field");
if (StrUtil.isBlank(childAlias)) {
continue;
}
Object v = subRow.get(childField);
if (v == null) {
v = subRow.get(childAlias);
}
if (v == null) {
v = subRow.get(StrUtil.toUnderlineCase(childField));
}
item.put(childAlias, v);
}
out.add(item);
}
return out;
}
/**
* 映射条件 op FieldFilterTypecommon-online 版常量op 忽略大小写与首尾空白
* 仅映射在线列表引擎真正支持的 op不支持not_in/is_null/not_null 返回 null由调用方拒绝
*/
private Integer mapOp(String op) {
if (StrUtil.isBlank(op)) {
return FieldFilterType.EQUAL_FILTER;
}
switch (op.trim().toLowerCase()) {
case "eq":
return FieldFilterType.EQUAL_FILTER;
case "like":
return FieldFilterType.LIKE_FILTER;
case "in":
return FieldFilterType.IN_LIST_FILTER;
case "between":
return FieldFilterType.RANGE_FILTER;
default:
log.warn("OpenAPI 不支持的在线列表条件 op=[{}],拒绝执行", op);
return null;
}
}
private String getMasterTableName(OnlineForm form) {
if (form.getMasterTableId() == null) {
return null;
}
OnlineTable table = onlineTableService.getById(form.getMasterTableId());
return table == null ? null : table.getTableName();
}
}

210
common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiOnlineQueryServiceImpl.java

@ -0,0 +1,210 @@ @@ -0,0 +1,210 @@
package apelet.common.openapi.service.impl;
import apelet.common.core.annotation.MyDataSourceResolver;
import apelet.common.core.constant.ApplicationConstant;
import apelet.common.core.object.MyPageData;
import apelet.common.core.object.ObjectCollection;
import apelet.common.core.object.ObjectValue;
import apelet.common.core.object.ResponseResult;
import apelet.common.core.util.DefaultDataSourceResolver;
import apelet.common.generator.model.OnlFormField;
import apelet.common.generator.model.OnlFormHead;
import apelet.common.generator.service.IOnlFormFieldService;
import apelet.common.generator.service.IOnlFormHeadService;
import apelet.common.generator.utils.OrmGenDataSourceUtil;
import apelet.common.online.dto.OnlineFilterDto;
import apelet.common.online.model.OnlineColumn;
import apelet.common.online.model.OnlineDatasource;
import apelet.common.online.model.OnlineDatasourceRelation;
import apelet.common.online.model.OnlineForm;
import apelet.common.online.model.OnlineTable;
import apelet.common.online.model.constant.FieldFilterType;
import apelet.common.online.model.constant.RelationType;
import apelet.common.online.service.OnlineDatasourceService;
import apelet.common.online.service.OnlineOperationService;
import apelet.common.online.service.OnlineTableService;
import apelet.common.online.util.OnlineOperationHelper;
import apelet.common.openapi.service.OpenApiOnlineQueryService;
import apelet.common.orm.impl.Filter;
import apelet.common.orm.impl.FilterItem;
import apelet.common.orm.impl.Selector;
import apelet.common.orm.impl.SelectorItem;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* OpenAPI 在线表数据查询辅助实现
* <p>标注在线数据源COMMON_FLOW_AND_ONLINE内部 ormGenDataSourceUtil 查询落在在线库</p>
*
* @author chenchuchuan
* @date 2026-09-03
*/
@Slf4j
@Service("openApiOnlineQueryService")
@MyDataSourceResolver(resolver = DefaultDataSourceResolver.class, intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE)
public class OpenApiOnlineQueryServiceImpl implements OpenApiOnlineQueryService {
@Autowired
private IOnlFormHeadService onlFormHeadService;
@Autowired
private IOnlFormFieldService onlFormFieldService;
@Autowired
private OnlineTableService onlineTableService;
@Autowired
private OnlineDatasourceService onlineDatasourceService;
@Autowired
private OnlineOperationService onlineOperationService;
@Autowired
private OnlineOperationHelper onlineOperationHelper;
@Autowired
private OrmGenDataSourceUtil ormGenDataSourceUtil;
@Override
public Object resolveRelatedId(OnlineForm masterForm, String masterColumnName, String attrColumn, Object attrValue) {
if (masterForm == null || masterForm.getMasterTableId() == null || StrUtil.isBlank(masterColumnName)
|| StrUtil.isBlank(attrColumn) || attrValue == null) {
return null;
}
try {
OnlineTable relTable = this.findRelTable(masterForm, masterColumnName);
if (relTable == null || relTable.getPrimaryKeyColumn() == null) {
return null;
}
OnlineColumn pkColumn = relTable.getPrimaryKeyColumn();
ObjectCollection collection = this.queryRelTable(relTable, attrColumn, attrValue, pkColumn);
if (collection == null || collection.isEmpty()) {
return null;
}
ObjectValue obj = collection.getObject(0);
return obj == null ? null : obj.getString(pkColumn.getColumnName());
} catch (Exception e) {
log.error("OpenAPI 关联字段反查失败:masterColumn={}, attrColumn={}, value={}", masterColumnName, attrColumn, attrValue, e);
return null;
}
}
@Override
public Set<String> resolveRelatedIds(OnlineForm masterForm, String masterColumnName, String attrColumn, Object attrValue) {
Set<String> result = new HashSet<>();
if (masterForm == null || masterForm.getMasterTableId() == null || StrUtil.isBlank(masterColumnName)
|| StrUtil.isBlank(attrColumn) || attrValue == null) {
return result;
}
try {
OnlineTable relTable = this.findRelTable(masterForm, masterColumnName);
if (relTable == null || relTable.getPrimaryKeyColumn() == null) {
return result;
}
OnlineColumn pkColumn = relTable.getPrimaryKeyColumn();
ObjectCollection collection = this.queryRelTable(relTable, attrColumn, attrValue, pkColumn);
if (collection != null) {
for (int i = 0; i < collection.size(); i++) {
ObjectValue obj = collection.getObject(i);
if (obj != null && obj.getString(pkColumn.getColumnName()) != null) {
result.add(obj.getString(pkColumn.getColumnName()));
}
}
}
} catch (Exception e) {
log.error("OpenAPI 关联字段批量反查失败:masterColumn={}, attrColumn={}, value={}", masterColumnName, attrColumn, attrValue, e);
}
return result;
}
@Override
public List<Map<String, Object>> querySubTableRows(OnlineForm masterForm, Object masterId, String subTableName) {
List<Map<String, Object>> result = new ArrayList<>();
if (masterForm == null || masterForm.getMasterTableId() == null || masterId == null || StrUtil.isBlank(subTableName)) {
return result;
}
try {
OnlineDatasource datasource = onlineDatasourceService.getOnlineDatasourceByMasterTableId(masterForm.getMasterTableId());
if (datasource == null) {
return result;
}
ResponseResult<List<OnlineDatasourceRelation>> relResult =
onlineOperationHelper.verifyAndGetRelationList(datasource.getDatasourceId(), null);
if (!relResult.isSuccess() || relResult.getData() == null) {
return result;
}
OnlineDatasourceRelation relation = relResult.getData().stream()
.filter(r -> r.getRelationType() != null && r.getRelationType() == RelationType.ONE_TO_MANY)
.filter(r -> subTableName.equals(this.slaveTableName(r))).findFirst().orElse(null);
if (relation == null || relation.getSlaveColumn() == null) {
return result;
}
// 子表过滤:slaveColumn(附表外键) = master 行主键
List<OnlineFilterDto> filterList = new ArrayList<>();
OnlineFilterDto filter = new OnlineFilterDto();
filter.setTableName(subTableName);
filter.setColumnName(relation.getSlaveColumn().getColumnName());
filter.setColumnValue(masterId);
filter.setFilterType(FieldFilterType.EQUAL_FILTER);
filterList.add(filter);
MyPageData<Map<String, Object>> pageData =
onlineOperationService.getSlaveDataList(relation, filterList, null, null);
return pageData == null ? result : pageData.getDataList();
} catch (Exception e) {
log.error("OpenAPI 分录数据查询失败:subTable={}, masterId={}", subTableName, masterId, e);
return result;
}
}
/**
* 取关系从表名slaveTable 未加载时按 slaveTableId 反查
*/
private String slaveTableName(OnlineDatasourceRelation relation) {
if (relation.getSlaveTable() != null) {
return relation.getSlaveTable().getTableName();
}
OnlineTable table = onlineTableService.getById(relation.getSlaveTableId());
return table == null ? null : table.getTableName();
}
/**
* 定位 master 表单中关联字段field_name=masterColumnName refPropert 非空对应的关联表
*/
private OnlineTable findRelTable(OnlineForm masterForm, String masterColumnName) { OnlineTable masterTable = onlineTableService.getById(masterForm.getMasterTableId());
if (masterTable == null) {
return null;
}
OnlFormHead masterHead = onlFormHeadService.getOne(new LambdaQueryWrapper<OnlFormHead>()
.eq(OnlFormHead::getTableName, masterTable.getTableName()));
if (masterHead == null) {
return null;
}
List<OnlFormField> relFields = onlFormFieldService.list(new LambdaQueryWrapper<OnlFormField>()
.eq(OnlFormField::getHeadId, masterHead.getId())
.eq(OnlFormField::getFieldName, masterColumnName)
.isNotNull(OnlFormField::getRefPropert));
if (relFields == null || relFields.isEmpty()) {
return null;
}
OnlFormHead relHead = onlFormHeadService.getById(relFields.get(0).getRefPropert());
if (relHead == null) {
return null;
}
return onlineTableService.getOne(new LambdaQueryWrapper<OnlineTable>()
.eq(OnlineTable::getTableName, relHead.getTableName()));
}
/**
* 查询关联表SELECT pk FROM 关联表 WHERE attrColumn = attrValuesorter null
*/
private ObjectCollection queryRelTable(OnlineTable relTable, String attrColumn, Object attrValue, OnlineColumn pkColumn) {
Filter filter = new Filter();
filter.add(new FilterItem(attrColumn, FilterItem.equals, attrValue));
Selector selector = new Selector();
selector.getList().add(new SelectorItem(pkColumn.getColumnName()));
return ormGenDataSourceUtil.query(relTable.getTableName(), filter, selector, null);
}
}
Loading…
Cancel
Save