diff --git a/common/common-core/pom.xml b/common/common-core/pom.xml index 8c32b92..5662a73 100644 --- a/common/common-core/pom.xml +++ b/common/common-core/pom.xml @@ -9,7 +9,7 @@ 4.0.0 common-core - 1.0.0 + 1.0.1 common-core jar diff --git a/common/common-generator/pom.xml b/common/common-generator/pom.xml index d95b0b3..008dab7 100644 --- a/common/common-generator/pom.xml +++ b/common/common-generator/pom.xml @@ -17,7 +17,7 @@ apelet common-core - 1.0.0 + 1.0.1 diff --git a/common/common-online/src/main/java/apelet/common/online/controller/OnlineOperationController.java b/common/common-online/src/main/java/apelet/common/online/controller/OnlineOperationController.java index c6d913d..349249e 100644 --- a/common/common-online/src/main/java/apelet/common/online/controller/OnlineOperationController.java +++ b/common/common-online/src/main/java/apelet/common/online/controller/OnlineOperationController.java @@ -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 { 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 { return onlineTableService.getOne(new LambdaQueryWrapper().eq(OnlineTable::getTableName, onlFormHead.getTableName())); } - private GridData exeListPlugin( - Long formId, - List filterDtoList, - MyOrderParam orderParam, - MyPageParam pageParam, - TraceParamDto traceParamDto, - OnlFormHead onlFormHead, - ResponseResult datasourceResult, - OnlineEventPluginExecuteDto onlineEventPluginExecuteDto - ){ - if(onlineEventPluginExecuteDto == null){ - onlineEventPluginExecuteDto = new OnlineEventPluginExecuteDto(); - } - onlineEventPluginExecuteDto.setFormId(formId); - onlineEventPluginExecuteDto.setEventCode(EventEnum.LOADLISTDATA.getCode()); - Map 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 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 filterList) { OnlineDblink dblink = onlineDblinkService.getById(table.getDblinkId()); @@ -1030,9 +977,6 @@ public class OnlineOperationController { } } } - - - /** * 获取表单详情数据 * @param formId diff --git a/common/common-online/src/main/java/apelet/common/online/service/OnlineOperationService.java b/common/common-online/src/main/java/apelet/common/online/service/OnlineOperationService.java index ad2bc45..6a9f057 100644 --- a/common/common-online/src/main/java/apelet/common/online/service/OnlineOperationService.java +++ b/common/common-online/src/main/java/apelet/common/online/service/OnlineOperationService.java @@ -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; 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 { */ void executePluginTransaction(PlugInArgs plugInArgs); + /** + * 执行列表查询插件(LOADLISTDATA),插件未返回 gridData 时回退到 ORM/EAS 列表服务。 + *

由 OnlineOperationController 的私有方法提升为服务接口,供 OpenAPI 等场景复用。

+ * + * @param formId 表单Id。 + * @param filterDtoList 过滤条件列表。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @param traceParamDto 数据追踪参数。 + * @param onlFormHead 表单表头元数据。 + * @param datasourceResult 数据源校验结果。 + * @param onlineEventPluginExecuteDto 插件执行上下文(可空,listParams 会被填充)。 + * @return 列表查询结果。 + */ + GridData exeListPlugin( + Long formId, + List filterDtoList, + MyOrderParam orderParam, + MyPageParam pageParam, + TraceParamDto traceParamDto, + OnlFormHead onlFormHead, + ResponseResult datasourceResult, + OnlineEventPluginExecuteDto onlineEventPluginExecuteDto); Object ormSaveNew(OnlineTable masterTable, JSONObject masterData, ObjectValue objectValue); diff --git a/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineOperationServiceImpl.java b/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineOperationServiceImpl.java index cba3e46..c5a3dbc 100644 --- a/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineOperationServiceImpl.java +++ b/common/common-online/src/main/java/apelet/common/online/service/impl/OnlineOperationServiceImpl.java @@ -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; 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 { plugInArgs.endOperationTransaction(); } + /** + * 执行列表查询插件(LOADLISTDATA),插件未返回 gridData 时回退到 ORM/EAS 列表服务。 + * 由 OnlineOperationController 的私有方法提升到 service。 + */ + @Override + public GridData exeListPlugin( + Long formId, + List filterDtoList, + MyOrderParam orderParam, + MyPageParam pageParam, + TraceParamDto traceParamDto, + OnlFormHead onlFormHead, + ResponseResult datasourceResult, + OnlineEventPluginExecuteDto onlineEventPluginExecuteDto) { + if (onlineEventPluginExecuteDto == null) { + onlineEventPluginExecuteDto = new OnlineEventPluginExecuteDto(); + } + onlineEventPluginExecuteDto.setFormId(formId); + onlineEventPluginExecuteDto.setEventCode(EventEnum.LOADLISTDATA.getCode()); + Map 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 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 pluginList, String billid, String tableName, ObjectValue objectValue , OnlineForm onlineForm) { PlugInArgs plugInArgs = new PlugInArgs(null, onlineForm); diff --git a/common/common-openapi/pom.xml b/common/common-openapi/pom.xml new file mode 100644 index 0000000..75b435e --- /dev/null +++ b/common/common-openapi/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + apelet + common + 1.0.0 + + + common-openapi + 1.0.0 + common-openapi + jar + + + 8 + 8 + UTF-8 + + + + + apelet + common-core + 1.0.1 + + + + apelet + common-online + 1.0.5 + + + + apelet + common-generator + 1.0.5 + + + + apelet + common-redis + 1.0.0 + + + \ No newline at end of file diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/config/OpenApiProperties.java b/common/common-openapi/src/main/java/apelet/common/openapi/config/OpenApiProperties.java new file mode 100644 index 0000000..d5588bd --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/config/OpenApiProperties.java @@ -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; +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/constant/OpenApiFieldSource.java b/common/common-openapi/src/main/java/apelet/common/openapi/constant/OpenApiFieldSource.java new file mode 100644 index 0000000..be7fbcd --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/constant/OpenApiFieldSource.java @@ -0,0 +1,30 @@ +package apelet.common.openapi.constant; + +/** + * OpenAPI 字段来源/条件目标常量(param_config / output_config 中的 source、target 取值)。 + * + * @author chenchuchuan + * @date 2026-09-03 + */ +public final class OpenApiFieldSource { + + /** + * 表单主表字段(onl_form_field,headId=主表)。 + */ + 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() { + } +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/constant/OpenApiParamMode.java b/common/common-openapi/src/main/java/apelet/common/openapi/constant/OpenApiParamMode.java new file mode 100644 index 0000000..05d485f --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/constant/OpenApiParamMode.java @@ -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() { + } +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiAppController.java b/common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiAppController.java new file mode 100644 index 0000000..7268a71 --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiAppController.java @@ -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> list(@MyRequestBody OpenApiApp openApiAppFilter, @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + List 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 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 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 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 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 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); + } +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiAuthController.java b/common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiAuthController.java new file mode 100644 index 0000000..e5fcd38 --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiAuthController.java @@ -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 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()); + } +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiConfigController.java b/common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiConfigController.java new file mode 100644 index 0000000..e593c22 --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiConfigController.java @@ -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> pages(@RequestParam(required = false) String keyword) { + return ResponseResult.success(openApiConfigService.getPageOptionList(keyword)); + } + + /** + * 该页面关联的表单列表(配置步骤2 联动)。 + */ + @GetMapping("/forms") + public ResponseResult> forms(@RequestParam Long pageId) { + return ResponseResult.success(openApiConfigService.getFormOptionList(pageId)); + } + + /** + * 可选操作下拉:查询(query) + operationList 各项(配置步骤3)。 + */ + @GetMapping("/operations") + public ResponseResult> operations(@RequestParam Long formId) { + return ResponseResult.success(openApiConfigService.getOperationList(formId)); + } + + /** + * 表单可配置字段树(FIELD_DIRECT 字段直传专用)。 + */ + @GetMapping("/fields") + public ResponseResult> fields(@RequestParam Long formId) { + return ResponseResult.success(openApiConfigService.getFieldTree(formId)); + } + + /** + * 分页查询开放接口配置。 + */ + @PostMapping("/list") + public ResponseResult> list(@MyRequestBody OpenApiConfig openApiConfigFilter, @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + List configList = openApiConfigService.getOpenApiConfigList(openApiConfigFilter, "create_time DESC"); + return ResponseResult.success(MyPageUtil.makeResponseData(configList)); + } + + /** + * 查看配置详情。 + */ + @GetMapping("/view") + public ResponseResult 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 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 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 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(); + } +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiRouterController.java b/common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiRouterController.java new file mode 100644 index 0000000..69788aa --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiRouterController.java @@ -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})。 + *

鉴权由 OpenApiAuthInterceptor 完成;本控制器按路径定位配置并执行。

+ * + * @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 execute(@PathVariable String pageCode, @PathVariable String formCode, @PathVariable String operationCode, + @RequestBody(required = false) Map requestBody) { + String requestPath = openApiProperties.getUrlPrefix() + "/v1/" + pageCode + "/" + formCode + "/" + operationCode; + return openApiExecService.execute(requestPath, requestBody == null ? Collections.emptyMap() : requestBody); + } +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/dao/OpenApiAppMapper.java b/common/common-openapi/src/main/java/apelet/common/openapi/dao/OpenApiAppMapper.java new file mode 100644 index 0000000..8bcd190 --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/dao/OpenApiAppMapper.java @@ -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 { + + /** + * 获取过滤后的应用列表。 + * + * @param openApiAppFilter 应用过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 应用列表。 + */ + List getOpenApiAppList(@Param("openApiAppFilter") OpenApiApp openApiAppFilter, @Param("orderBy") String orderBy); +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/dao/OpenApiConfigMapper.java b/common/common-openapi/src/main/java/apelet/common/openapi/dao/OpenApiConfigMapper.java new file mode 100644 index 0000000..091667a --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/dao/OpenApiConfigMapper.java @@ -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 { + + /** + * 获取过滤后的配置列表。 + * + * @param openApiConfigFilter 配置过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 配置列表。 + */ + List getOpenApiConfigList( + @Param("openApiConfigFilter") OpenApiConfig openApiConfigFilter, @Param("orderBy") String orderBy); +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/dao/mapper/OpenApiAppMapper.xml b/common/common-openapi/src/main/java/apelet/common/openapi/dao/mapper/OpenApiAppMapper.xml new file mode 100644 index 0000000..12b7c1c --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/dao/mapper/OpenApiAppMapper.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + AND xy_sys_open_app.app_code = #{openApiAppFilter.appCode} + + + + AND xy_sys_open_app.app_name LIKE #{safeAppName} + + + AND xy_sys_open_app.status = #{openApiAppFilter.status} + + + + + + diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/dao/mapper/OpenApiConfigMapper.xml b/common/common-openapi/src/main/java/apelet/common/openapi/dao/mapper/OpenApiConfigMapper.xml new file mode 100644 index 0000000..208fd93 --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/dao/mapper/OpenApiConfigMapper.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AND xy_sys_open_api_config.app_code = #{openApiConfigFilter.appCode} + + + AND xy_sys_open_api_config.page_id = #{openApiConfigFilter.pageId} + + + AND xy_sys_open_api_config.form_code = #{openApiConfigFilter.formCode} + + + AND xy_sys_open_api_config.status = #{openApiConfigFilter.status} + + + + + + diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/document/OpenApiDocumentCustomizer.java b/common/common-openapi/src/main/java/apelet/common/openapi/document/OpenApiDocumentCustomizer.java new file mode 100644 index 0000000..a079094 --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/document/OpenApiDocumentCustomizer.java @@ -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 推导,嵌套结构生成嵌套 schema(knife4j /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 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)); + } + } + + /** + * 请求体 schema:FIELD_DIRECT 取 fields,QUERY/CONDITION_MATCH 取 params;QUERY 追加 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; + } + + /** + * 输出 schema:ResponseResult.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"); + } +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiFieldDto.java b/common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiFieldDto.java new file mode 100644 index 0000000..c8cb5b1 --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiFieldDto.java @@ -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 children; +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiFormOptionDto.java b/common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiFormOptionDto.java new file mode 100644 index 0000000..a122fde --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiFormOptionDto.java @@ -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; +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiOperationDto.java b/common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiOperationDto.java new file mode 100644 index 0000000..06fbc9c --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiOperationDto.java @@ -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; +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiPageOptionDto.java b/common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiPageOptionDto.java new file mode 100644 index 0000000..9d77768 --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiPageOptionDto.java @@ -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; +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiTokenDto.java b/common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiTokenDto.java new file mode 100644 index 0000000..4c9777e --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiTokenDto.java @@ -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; +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiUserInfo.java b/common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiUserInfo.java new file mode 100644 index 0000000..771ebf7 --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiUserInfo.java @@ -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; +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/interceptor/OpenApiAuthInterceptor.java b/common/common-openapi/src/main/java/apelet/common/openapi/interceptor/OpenApiAuthInterceptor.java new file mode 100644 index 0000000..2bd33bb --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/interceptor/OpenApiAuthInterceptor.java @@ -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 校验拦截器。 + *

在 SpringMVC 拦截器链中注册(见接入方 InterceptorConfig),只拦截 {urlPrefix}/v1/**; + * token 签发(/auth/token)与配置管理(/config/**、/app/**,走应用自身登录态)不经过本拦截器。

+ * + * @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; + } +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/model/OpenApiApp.java b/common/common-openapi/src/main/java/apelet/common/openapi/model/OpenApiApp.java new file mode 100644 index 0000000..7fc7b02 --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/model/OpenApiApp.java @@ -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; +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/model/OpenApiConfig.java b/common/common-openapi/src/main/java/apelet/common/openapi/model/OpenApiConfig.java new file mode 100644 index 0000000..4eba81f --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/model/OpenApiConfig.java @@ -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; + /** + * 关联的在线表单主键Id(OnlineForm.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; +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiAppService.java b/common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiAppService.java new file mode 100644 index 0000000..dca490f --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiAppService.java @@ -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 { + + /** + * 获取过滤后的应用列表。 + * + * @param filter 过滤对象。 + * @param orderBy 排序字符串。 + * @return 应用列表。 + */ + List getOpenApiAppList(OpenApiApp filter, String orderBy); + + /** + * 新增应用:appCode 为空时自动生成,appSecret 自动生成并返回明文。 + * + * @param app 应用对象。 + * @return 保存后的应用(含明文 appSecret)。 + */ + OpenApiApp addNew(OpenApiApp app); + + /** + * 重置应用密钥:旧密钥立即失效。 + * + * @param id 应用主键Id。 + * @return 新的明文 appSecret。 + */ + String resetSecret(Long id); +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiAuthService.java b/common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiAuthService.java new file mode 100644 index 0000000..b2890d7 --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiAuthService.java @@ -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 issueToken(String appCode, String appSecret, String username); + + /** + * 校验 Bearer token:验签 + 有效期 + Redis 会话存在性,还原 TokenData。 + * + * @param token Authorization 头值(含 Bearer 前缀)。 + * @return TokenData,无效或已失效返回null。 + */ + TokenData validateToken(String token); +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiConfigService.java b/common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiConfigService.java new file mode 100644 index 0000000..c8977da --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiConfigService.java @@ -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 { + + /** + * 获取过滤后的配置列表。 + * + * @param filter 过滤对象。 + * @param orderBy 排序字符串。 + * @return 配置列表。 + */ + List getOpenApiConfigList(OpenApiConfig filter, String orderBy); + + /** + * 获取在线页面下拉列表(配置步骤1),可按页面名称模糊搜索。 + * + * @param keyword 页面名称关键字,为空返回全部。 + * @return 页面列表。 + */ + List getPageOptionList(String keyword); + + /** + * 获取指定页面关联的表单下拉列表(配置步骤2 联动)。 + * + * @param pageId 页面主键Id。 + * @return 表单列表。 + */ + List getFormOptionList(Long pageId); + + /** + * 获取可选操作下拉:查询(query) + widgetJson.operationList 各项(配置步骤3)。 + * + * @param formId 在线表单主键Id。 + * @return 操作列表。 + */ + List getOperationList(Long formId); + + /** + * 获取表单可配置字段树:主表字段 + 关联字段属性 + 一对多分录附表字段。 + * + * @param formId 在线表单主键Id。 + * @return 字段树。 + */ + List 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); +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiExecService.java b/common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiExecService.java new file mode 100644 index 0000000..e6a49f8 --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiExecService.java @@ -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 execute(String requestPath, Map requestParams); +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiOnlineQueryService.java b/common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiOnlineQueryService.java new file mode 100644 index 0000000..2d4c500 --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiOnlineQueryService.java @@ -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 在线表数据查询辅助接口(运行在在线数据源上下文)。 + *

用于关联字段反查等需要直接查询在线表(zz_online_*)的场景,避免在普通上下文误查默认库。

+ * + * @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 resolveRelatedIds(OnlineForm masterForm, String masterColumnName, String attrColumn, Object attrValue); + + /** + * 查询一对多分录(附表)数据:按 master 行主键查子表关联行(用于输出配置的 ENTRY_ARRAY)。 + * + * @param masterForm 主表单。 + * @param masterId master 行主键值。 + * @param subTableName 子表表名(一对多分录附表)。 + * @return 子表行列表(可能为空,不会为 null)。 + */ + List> querySubTableRows(OnlineForm masterForm, Object masterId, String subTableName); +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiUserService.java b/common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiUserService.java new file mode 100644 index 0000000..443f96c --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiUserService.java @@ -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)提供实现,按登录名查询可模拟身份的用户。 + *

common-openapi 不依赖应用层用户服务,通过 Spring 自动装配该接口;未装配时按用户名签发 token 返回明确错误。

+ * + * @author chenchuchuan + * @date 2026-09-02 + */ +public interface OpenApiUserService { + + /** + * 按登录名查询用户。 + * + * @param loginName 登录名。 + * @param tenantId 应用归属租户Id(为空表示不限租户,全局查)。 + * @return 用户信息。 + */ + ResponseResult getByLoginName(String loginName, Long tenantId); +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiAppServiceImpl.java b/common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiAppServiceImpl.java new file mode 100644 index 0000000..2630b88 --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiAppServiceImpl.java @@ -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 implements OpenApiAppService { + + @Autowired + private OpenApiAppMapper openApiAppMapper; + + @Override + protected BaseDaoMapper mapper() { + return openApiAppMapper; + } + + @Override + public List 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; + } +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiAuthServiceImpl.java b/common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiAuthServiceImpl.java new file mode 100644 index 0000000..4fa7d56 --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiAuthServiceImpl.java @@ -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 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() + .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 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 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 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 bucket = redissonClient.getBucket(sessionIdKey); + bucket.set(sessionData); + bucket.expire(expirationSeconds, TimeUnit.SECONDS); + } +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiConfigServiceImpl.java b/common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiConfigServiceImpl.java new file mode 100644 index 0000000..695999e --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiConfigServiceImpl.java @@ -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 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 mapper() { + return openApiConfigMapper; + } + + @Override + public List getOpenApiConfigList(OpenApiConfig filter, String orderBy) { + if (filter == null) { + filter = new OpenApiConfig(); + } + return openApiConfigMapper.getOpenApiConfigList(filter, orderBy); + } + + @Override + public List getPageOptionList(String keyword) { + // 按页面名称模糊搜索,空关键字返回全部;结果按创建时间倒序 + OnlinePage filter = null; + if (StrUtil.isNotBlank(keyword)) { + filter = new OnlinePage(); + filter.setPageName(keyword); + } + List pageList = onlinePageService.getOnlinePageList(filter, "create_time DESC"); + List 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 getFormOptionList(Long pageId) { + List result = new ArrayList<>(); + if (pageId == null) { + return result; + } + List formList = onlineFormService.list( + new LambdaQueryWrapper().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 getOperationList(Long formId) { + List 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 getFieldTree(Long formId) { + List 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() + .eq(OnlFormHead::getTableName, masterTable.getTableName())); + if (head == null) { + return result; + } + // 主表字段:普通字段 FORM_FIELD,关联字段(refPropert 非空)RELATION_FIELD(按 id/编码/名称 三个属性展开) + List masterFields = onlFormFieldService.list( + new LambdaQueryWrapper().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() + .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 children = new ArrayList<>(); + List subFields = onlFormFieldService.list( + new LambdaQueryWrapper().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().eq(OpenApiConfig::getRequestPath, requestPath)); + } + + /** + * 推导参数模式:query→QUERY;保存/提交→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(); + } +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiExecServiceImpl.java b/common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiExecServiceImpl.java new file mode 100644 index 0000000..e8fd57f --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiExecServiceImpl.java @@ -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 开放接口执行服务实现。 + *

按 param_mode 分派三种执行路径:QUERY 走列表插件、FIELD_DIRECT 走 executePlugin(字段直传)、 + * CONDITION_MATCH 先按条件匹配单据集合再逐条 executePlugin。

+ * + * @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 execute(String requestPath, Map 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 executeQuery(OpenApiConfig config, OnlineForm form, Map 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> filterResult = this.buildFilterDtoList(paramConfig, requestParams, form); + if (!filterResult.isSuccess()) { + return ResponseResult.errorFrom(filterResult); + } + List filterDtoList = filterResult.getData(); + ResponseResult 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 executeFieldDirect(OpenApiConfig config, OnlineForm form, Map 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 executeConditionMatch(OpenApiConfig config, OnlineForm form, Map requestParams) { + JSONObject paramConfig = JSON.parseObject(config.getParamConfig()); + ResponseResult> filterResult = this.buildFilterDtoList(paramConfig, requestParams, form); + if (!filterResult.isSuccess()) { + return ResponseResult.errorFrom(filterResult); + } + List filterDtoList = filterResult.getData(); + ResponseResult 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 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() + .eq(OnlineForm::getPageId, config.getPageId()) + .eq(OnlineForm::getFormCode, config.getFormCode()), false); + } + + /** + * 取表单主表数据源(含 masterTable,供列表插件回退使用)。 + */ + private ResponseResult 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 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 与请求参数构造过滤条件列表(未传参数的条件跳过)。 + * 仅放行在线列表引擎真正支持的条件 op(eq/like/in/between),其余直接拒绝,避免条件被静默丢弃导致 CONDITION_MATCH 放大执行。 + */ + private ResponseResult> buildFilterDtoList(JSONObject paramConfig, Map requestParams, OnlineForm form) { + List 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 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 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 assembleDataList(List> dataList, String outputConfigJson, OnlineForm form) { + List 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 row : dataList) { + JSONObject item = new JSONObject(true); + this.mapRow(item, row, dataFields, form); + result.add(item); + } + return result; + } + + private void mapRow(JSONObject target, Map 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> subRows = openApiOnlineQueryService.querySubTableRows(form, masterId, subTable); + if (subRows == null) { + return out; + } + for (Map 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 到 FieldFilterType(common-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(); + } +} diff --git a/common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiOnlineQueryServiceImpl.java b/common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiOnlineQueryServiceImpl.java new file mode 100644 index 0000000..b81aa2a --- /dev/null +++ b/common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiOnlineQueryServiceImpl.java @@ -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 在线表数据查询辅助实现。 + *

标注在线数据源(COMMON_FLOW_AND_ONLINE),内部 ormGenDataSourceUtil 查询落在在线库。

+ * + * @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 resolveRelatedIds(OnlineForm masterForm, String masterColumnName, String attrColumn, Object attrValue) { + Set 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> querySubTableRows(OnlineForm masterForm, Object masterId, String subTableName) { + List> 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> 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 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> 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() + .eq(OnlFormHead::getTableName, masterTable.getTableName())); + if (masterHead == null) { + return null; + } + List relFields = onlFormFieldService.list(new LambdaQueryWrapper() + .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() + .eq(OnlineTable::getTableName, relHead.getTableName())); + } + + /** + * 查询关联表:SELECT pk FROM 关联表 WHERE attrColumn = attrValue(sorter 传 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); + } +}