Browse Source
- 新增OpenApiApp实体类及对应的数据库映射 - 实现OpenApiAppService服务接口及具体业务逻辑 - 添加OpenApiAppController控制器提供应用CRUD操作 - 实现OpenApiAuthService服务进行token签发与验证 - 添加OpenApiAuthController提供对外认证接口 - 创建OpenApiAuthInterceptor拦截器进行token校验 - 升级common-core版本至1.0.1并更新依赖引用 - 将exeListPlugin方法从Controller移至OnlineOperationService - 移除OnlineOperationController中的废弃导入和私有方法 - 新增OpenApiConfig模型类用于存储接口配置信息pull/1/head
38 changed files with 2917 additions and 61 deletions
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
<parent> |
||||
<groupId>apelet</groupId> |
||||
<artifactId>common</artifactId> |
||||
<version>1.0.0</version> |
||||
</parent> |
||||
|
||||
<artifactId>common-openapi</artifactId> |
||||
<version>1.0.0</version> |
||||
<name>common-openapi</name> |
||||
<packaging>jar</packaging> |
||||
|
||||
<properties> |
||||
<maven.compiler.source>8</maven.compiler.source> |
||||
<maven.compiler.target>8</maven.compiler.target> |
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
||||
</properties> |
||||
|
||||
<dependencies> |
||||
<dependency> |
||||
<groupId>apelet</groupId> |
||||
<artifactId>common-core</artifactId> |
||||
<version>1.0.1</version> |
||||
</dependency> |
||||
<!-- 在线表单能力:OnlineFormService / OnlineOperationService(含 exeListPlugin) / OnlineDatasourceService 等, |
||||
传递依赖 common-generator(OnlFormHead/OnlFormField)、common-redis、common-swagger(knife4j/springdoc) --> |
||||
<dependency> |
||||
<groupId>apelet</groupId> |
||||
<artifactId>common-online</artifactId> |
||||
<version>1.0.5</version> |
||||
</dependency> |
||||
<!-- 表单元数据:OnlFormHead / OnlFormField / IOnlFormHeadService,开放接口字段清单与关联字段属性换算依赖 --> |
||||
<dependency> |
||||
<groupId>apelet</groupId> |
||||
<artifactId>common-generator</artifactId> |
||||
<version>1.0.5</version> |
||||
</dependency> |
||||
<!-- token 会话存储(可吊销),复用 CommonRedisUtil --> |
||||
<dependency> |
||||
<groupId>apelet</groupId> |
||||
<artifactId>common-redis</artifactId> |
||||
<version>1.0.0</version> |
||||
</dependency> |
||||
</dependencies> |
||||
</project> |
||||
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
package apelet.common.openapi.config; |
||||
|
||||
import lombok.Data; |
||||
import org.springframework.boot.context.properties.ConfigurationProperties; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
/** |
||||
* OpenAPI 平台配置项。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
@Data |
||||
@Component |
||||
@ConfigurationProperties(prefix = "common-openapi") |
||||
public class OpenApiProperties { |
||||
|
||||
/** |
||||
* 对外开放路由前缀(不要以反斜杠结尾),默认 /openapi。 |
||||
*/ |
||||
private String urlPrefix = "/openapi"; |
||||
|
||||
/** |
||||
* 是否启用 OpenAPI 开放平台(false 时所有 Controller 接口不可用)。 |
||||
*/ |
||||
private Boolean operationEnabled = true; |
||||
|
||||
/** |
||||
* JWT 签名密钥(与开放 token 校验使用同一密钥)。 |
||||
*/ |
||||
private String tokenSigningKey = "apelet-openapi-signing-key-default"; |
||||
|
||||
/** |
||||
* token 有效期(秒),默认 7200。 |
||||
*/ |
||||
private Long tokenExpiration = 7200L; |
||||
} |
||||
@ -0,0 +1,30 @@
@@ -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() { |
||||
} |
||||
} |
||||
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
package apelet.common.openapi.constant; |
||||
|
||||
/** |
||||
* OpenAPI 参数模式常量(xy_sys_open_api_config.param_mode 取值)。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-03 |
||||
*/ |
||||
public final class OpenApiParamMode { |
||||
|
||||
/** |
||||
* 基础查询:operation_code=query,走列表插件。 |
||||
*/ |
||||
public static final String QUERY = "QUERY"; |
||||
/** |
||||
* 字段直传:保存/提交操作,走 executePlugin。 |
||||
*/ |
||||
public static final String FIELD_DIRECT = "FIELD_DIRECT"; |
||||
/** |
||||
* 条件匹配:其他操作,先按条件匹配单据集合再执行。 |
||||
*/ |
||||
public static final String CONDITION_MATCH = "CONDITION_MATCH"; |
||||
|
||||
private OpenApiParamMode() { |
||||
} |
||||
} |
||||
@ -0,0 +1,174 @@
@@ -0,0 +1,174 @@
|
||||
package apelet.common.openapi.controller; |
||||
|
||||
import apelet.common.core.annotation.MyRequestBody; |
||||
import apelet.common.core.constant.ErrorCodeEnum; |
||||
import apelet.common.core.object.MyPageData; |
||||
import apelet.common.core.object.MyPageParam; |
||||
import apelet.common.core.object.ResponseResult; |
||||
import apelet.common.core.object.TokenData; |
||||
import apelet.common.core.util.MyCommonUtil; |
||||
import apelet.common.core.util.MyPageUtil; |
||||
import apelet.common.openapi.model.OpenApiApp; |
||||
import apelet.common.openapi.model.OpenApiConfig; |
||||
import apelet.common.openapi.service.OpenApiAppService; |
||||
import apelet.common.openapi.service.OpenApiConfigService; |
||||
import cn.hutool.core.util.StrUtil; |
||||
import com.alibaba.fastjson.JSONObject; |
||||
import com.github.pagehelper.page.PageMethod; |
||||
import io.swagger.v3.oas.annotations.tags.Tag; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
import org.springframework.web.bind.annotation.PostMapping; |
||||
import org.springframework.web.bind.annotation.RequestMapping; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
|
||||
import java.util.Date; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* OpenAPI 第三方应用管理接口(内部,供前端开放平台界面调用)。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
@Tag(name = "OpenAPI第三方应用接口") |
||||
@Slf4j |
||||
@RestController |
||||
@RequestMapping("${common-openapi.urlPrefix:/openapi}/app") |
||||
@ConditionalOnProperty(name = "common-openapi.operationEnabled", havingValue = "true") |
||||
public class OpenApiAppController { |
||||
|
||||
@Autowired |
||||
private OpenApiAppService openApiAppService; |
||||
@Autowired |
||||
private OpenApiConfigService openApiConfigService; |
||||
|
||||
/** |
||||
* 分页查询应用列表(不回显 appSecret)。 |
||||
*/ |
||||
@PostMapping("/list") |
||||
public ResponseResult<MyPageData<OpenApiApp>> list(@MyRequestBody OpenApiApp openApiAppFilter, @MyRequestBody MyPageParam pageParam) { |
||||
if (pageParam != null) { |
||||
PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); |
||||
} |
||||
List<OpenApiApp> appList = openApiAppService.getOpenApiAppList(openApiAppFilter, "create_time DESC"); |
||||
// 列表密钥打码:前3后4明文,中间 *;完整密钥需调 /viewSecret 查看
|
||||
appList.forEach(app -> app.setAppSecret(this.maskSecret(app.getAppSecret()))); |
||||
return ResponseResult.success(MyPageUtil.makeResponseData(appList)); |
||||
} |
||||
|
||||
/** |
||||
* 新增应用:appCode 未填则自动生成,appSecret 仅此刻返回明文。 |
||||
*/ |
||||
@PostMapping("/add") |
||||
public ResponseResult<OpenApiApp> add(@MyRequestBody OpenApiApp openApiApp) { |
||||
if (openApiApp == null || MyCommonUtil.existBlankArgument(openApiApp.getAppName())) { |
||||
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST, "应用名称不能为空!"); |
||||
} |
||||
if (openApiApp.getAppCode() != null && openApiAppService.existOne("appCode", openApiApp.getAppCode())) { |
||||
return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY, "应用编码已存在!"); |
||||
} |
||||
return ResponseResult.success(openApiAppService.addNew(openApiApp)); |
||||
} |
||||
|
||||
/** |
||||
* 更新应用(不修改密钥)。 |
||||
*/ |
||||
@PostMapping("/update") |
||||
public ResponseResult<Void> update(@MyRequestBody OpenApiApp openApiApp) { |
||||
if (openApiApp == null || MyCommonUtil.existBlankArgument(openApiApp.getId())) { |
||||
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); |
||||
} |
||||
OpenApiApp original = openApiAppService.getById(openApiApp.getId()); |
||||
if (original == null) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); |
||||
} |
||||
// 应用编码作为对外唯一标识且被开放接口配置(xy_sys_open_api_config.app_code)引用,不允许修改
|
||||
if (StrUtil.isNotBlank(openApiApp.getAppCode()) |
||||
&& !openApiApp.getAppCode().equals(original.getAppCode())) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "应用编码不允许修改!"); |
||||
} |
||||
openApiApp.setAppCode(original.getAppCode()); |
||||
openApiApp.setAppSecret(original.getAppSecret()); |
||||
openApiApp.setCreateTime(original.getCreateTime()); |
||||
openApiApp.setUpdateTime(new Date()); |
||||
TokenData tokenData = TokenData.takeFromRequest(); |
||||
if (tokenData != null) { |
||||
openApiApp.setUpdateUserId(tokenData.getUserId()); |
||||
} |
||||
if (!openApiAppService.updateById(openApiApp)) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); |
||||
} |
||||
return ResponseResult.success(); |
||||
} |
||||
|
||||
/** |
||||
* 删除应用:已被开放配置引用的应用禁止删除。 |
||||
*/ |
||||
@PostMapping("/delete") |
||||
public ResponseResult<Void> delete(@MyRequestBody Long id) { |
||||
if (MyCommonUtil.existBlankArgument(id)) { |
||||
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); |
||||
} |
||||
OpenApiApp app = openApiAppService.getById(id); |
||||
if (app == null) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); |
||||
} |
||||
OpenApiConfig configFilter = new OpenApiConfig(); |
||||
configFilter.setAppCode(app.getAppCode()); |
||||
if (openApiConfigService.existByFilter(configFilter)) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "该应用已被开放接口配置引用,不能删除!"); |
||||
} |
||||
openApiAppService.removeById(id); |
||||
return ResponseResult.success(); |
||||
} |
||||
|
||||
/** |
||||
* 重置应用密钥:旧密钥立即失效。 |
||||
*/ |
||||
@PostMapping("/resetSecret") |
||||
public ResponseResult<JSONObject> resetSecret(@MyRequestBody Long id) { |
||||
if (MyCommonUtil.existBlankArgument(id)) { |
||||
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); |
||||
} |
||||
String newSecret = openApiAppService.resetSecret(id); |
||||
if (newSecret == null) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); |
||||
} |
||||
JSONObject data = new JSONObject(); |
||||
data.put("appSecret", newSecret); |
||||
return ResponseResult.success(data); |
||||
} |
||||
|
||||
/** |
||||
* 查看应用密钥明文(前端"小眼睛"调用;受应用登录鉴权保护)。 |
||||
*/ |
||||
@PostMapping("/viewSecret") |
||||
public ResponseResult<JSONObject> viewSecret(@MyRequestBody Long id) { |
||||
if (MyCommonUtil.existBlankArgument(id)) { |
||||
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); |
||||
} |
||||
OpenApiApp app = openApiAppService.getById(id); |
||||
if (app == null) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); |
||||
} |
||||
JSONObject data = new JSONObject(); |
||||
data.put("appSecret", app.getAppSecret()); |
||||
return ResponseResult.success(data); |
||||
} |
||||
|
||||
/** |
||||
* 密钥打码:前3后4明文,中间用 * 占位(长度不足 7 时整体打码)。 |
||||
*/ |
||||
private String maskSecret(String secret) { |
||||
if (secret == null) { |
||||
return null; |
||||
} |
||||
int len = secret.length(); |
||||
if (len <= 7) { |
||||
return StrUtil.repeat("*", len); |
||||
} |
||||
return secret.substring(0, 3) + StrUtil.repeat("*", len - 7) + secret.substring(len - 4); |
||||
} |
||||
} |
||||
@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
|
||||
package apelet.common.openapi.controller; |
||||
|
||||
import apelet.common.core.constant.ErrorCodeEnum; |
||||
import apelet.common.core.object.ResponseResult; |
||||
import apelet.common.openapi.dto.OpenApiTokenDto; |
||||
import apelet.common.openapi.service.OpenApiAuthService; |
||||
import com.alibaba.fastjson.JSONObject; |
||||
import io.swagger.v3.oas.annotations.tags.Tag; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
import org.springframework.web.bind.annotation.PostMapping; |
||||
import org.springframework.web.bind.annotation.RequestBody; |
||||
import org.springframework.web.bind.annotation.RequestMapping; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
|
||||
/** |
||||
* OpenAPI 对外鉴权接口(公网)。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
@Tag(name = "OpenAPI对外鉴权接口") |
||||
@Slf4j |
||||
@RestController |
||||
@RequestMapping("${common-openapi.urlPrefix:/openapi}/auth") |
||||
@ConditionalOnProperty(name = "common-openapi.operationEnabled", havingValue = "true") |
||||
public class OpenApiAuthController { |
||||
|
||||
@Autowired |
||||
private OpenApiAuthService openApiAuthService; |
||||
|
||||
/** |
||||
* 按应用编码 + 密钥 + 用户名换取 token(第三方标准扁平 JSON body)。 |
||||
*/ |
||||
@PostMapping("/token") |
||||
public ResponseResult<JSONObject> token(@RequestBody(required = false) OpenApiTokenDto tokenDto) { |
||||
if (tokenDto == null) { |
||||
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); |
||||
} |
||||
return openApiAuthService.issueToken( |
||||
tokenDto.getAppCode(), tokenDto.getAppSecret(), tokenDto.getUsername()); |
||||
} |
||||
} |
||||
@ -0,0 +1,137 @@
@@ -0,0 +1,137 @@
|
||||
package apelet.common.openapi.controller; |
||||
|
||||
import apelet.common.core.annotation.MyRequestBody; |
||||
import apelet.common.core.constant.ErrorCodeEnum; |
||||
import apelet.common.core.object.MyPageData; |
||||
import apelet.common.core.object.MyPageParam; |
||||
import apelet.common.core.object.ResponseResult; |
||||
import apelet.common.core.util.MyCommonUtil; |
||||
import apelet.common.core.util.MyPageUtil; |
||||
import apelet.common.openapi.dto.OpenApiFieldDto; |
||||
import apelet.common.openapi.dto.OpenApiFormOptionDto; |
||||
import apelet.common.openapi.dto.OpenApiOperationDto; |
||||
import apelet.common.openapi.dto.OpenApiPageOptionDto; |
||||
import apelet.common.openapi.model.OpenApiConfig; |
||||
import apelet.common.openapi.service.OpenApiConfigService; |
||||
import com.github.pagehelper.page.PageMethod; |
||||
import io.swagger.v3.oas.annotations.tags.Tag; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
import org.springframework.web.bind.annotation.*; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* OpenAPI 开放接口配置管理接口(内部,供前端开放平台界面调用)。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
@Tag(name = "OpenAPI开放接口配置接口") |
||||
@Slf4j |
||||
@RestController |
||||
@RequestMapping("${common-openapi.urlPrefix:/openapi}/config") |
||||
@ConditionalOnProperty(name = "common-openapi.operationEnabled", havingValue = "true") |
||||
public class OpenApiConfigController { |
||||
|
||||
@Autowired |
||||
private OpenApiConfigService openApiConfigService; |
||||
|
||||
/** |
||||
* 在线页面列表(配置步骤1 选择),可按页面名称模糊搜索。 |
||||
*/ |
||||
@GetMapping("/pages") |
||||
public ResponseResult<List<OpenApiPageOptionDto>> pages(@RequestParam(required = false) String keyword) { |
||||
return ResponseResult.success(openApiConfigService.getPageOptionList(keyword)); |
||||
} |
||||
|
||||
/** |
||||
* 该页面关联的表单列表(配置步骤2 联动)。 |
||||
*/ |
||||
@GetMapping("/forms") |
||||
public ResponseResult<List<OpenApiFormOptionDto>> forms(@RequestParam Long pageId) { |
||||
return ResponseResult.success(openApiConfigService.getFormOptionList(pageId)); |
||||
} |
||||
|
||||
/** |
||||
* 可选操作下拉:查询(query) + operationList 各项(配置步骤3)。 |
||||
*/ |
||||
@GetMapping("/operations") |
||||
public ResponseResult<List<OpenApiOperationDto>> operations(@RequestParam Long formId) { |
||||
return ResponseResult.success(openApiConfigService.getOperationList(formId)); |
||||
} |
||||
|
||||
/** |
||||
* 表单可配置字段树(FIELD_DIRECT 字段直传专用)。 |
||||
*/ |
||||
@GetMapping("/fields") |
||||
public ResponseResult<List<OpenApiFieldDto>> fields(@RequestParam Long formId) { |
||||
return ResponseResult.success(openApiConfigService.getFieldTree(formId)); |
||||
} |
||||
|
||||
/** |
||||
* 分页查询开放接口配置。 |
||||
*/ |
||||
@PostMapping("/list") |
||||
public ResponseResult<MyPageData<OpenApiConfig>> list(@MyRequestBody OpenApiConfig openApiConfigFilter, @MyRequestBody MyPageParam pageParam) { |
||||
if (pageParam != null) { |
||||
PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); |
||||
} |
||||
List<OpenApiConfig> configList = openApiConfigService.getOpenApiConfigList(openApiConfigFilter, "create_time DESC"); |
||||
return ResponseResult.success(MyPageUtil.makeResponseData(configList)); |
||||
} |
||||
|
||||
/** |
||||
* 查看配置详情。 |
||||
*/ |
||||
@GetMapping("/view") |
||||
public ResponseResult<OpenApiConfig> view(@RequestParam Long configId) { |
||||
OpenApiConfig config = openApiConfigService.getById(configId); |
||||
if (config == null) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); |
||||
} |
||||
return ResponseResult.success(config); |
||||
} |
||||
|
||||
/** |
||||
* 新增开放接口配置。 |
||||
*/ |
||||
@PostMapping("/add") |
||||
public ResponseResult<OpenApiConfig> add(@MyRequestBody OpenApiConfig openApiConfig) { |
||||
if (openApiConfig == null || MyCommonUtil.existBlankArgument(openApiConfig.getAppCode(), |
||||
openApiConfig.getPageId(), openApiConfig.getFormId(), openApiConfig.getFormCode(), |
||||
openApiConfig.getOperationCode())) { |
||||
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); |
||||
} |
||||
return ResponseResult.success(openApiConfigService.addNew(openApiConfig)); |
||||
} |
||||
|
||||
/** |
||||
* 更新开放接口配置。 |
||||
*/ |
||||
@PostMapping("/update") |
||||
public ResponseResult<Void> update(@MyRequestBody OpenApiConfig openApiConfig) { |
||||
if (openApiConfig == null || MyCommonUtil.existBlankArgument(openApiConfig.getId())) { |
||||
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); |
||||
} |
||||
if (openApiConfigService.updateConfig(openApiConfig) == null) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); |
||||
} |
||||
return ResponseResult.success(); |
||||
} |
||||
|
||||
/** |
||||
* 删除开放接口配置。 |
||||
*/ |
||||
@PostMapping("/delete") |
||||
public ResponseResult<Void> delete(@MyRequestBody Long configId) { |
||||
if (MyCommonUtil.existBlankArgument(configId)) { |
||||
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); |
||||
} |
||||
if (!openApiConfigService.removeById(configId)) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); |
||||
} |
||||
return ResponseResult.success(); |
||||
} |
||||
} |
||||
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
package apelet.common.openapi.controller; |
||||
|
||||
import apelet.common.core.object.ResponseResult; |
||||
import apelet.common.openapi.config.OpenApiProperties; |
||||
import apelet.common.openapi.service.OpenApiExecService; |
||||
import io.swagger.v3.oas.annotations.tags.Tag; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
import org.springframework.web.bind.annotation.*; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* OpenAPI 对外开放动态路由({urlPrefix}/v1/{pageCode}/{formCode}/{operationCode})。 |
||||
* <p>鉴权由 OpenApiAuthInterceptor 完成;本控制器按路径定位配置并执行。</p> |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
@Tag(name = "OpenAPI对外开放动态路由") |
||||
@Slf4j |
||||
@RestController |
||||
@RequestMapping("${common-openapi.urlPrefix:/openapi}/v1") |
||||
@ConditionalOnProperty(name = "common-openapi.operationEnabled", havingValue = "true") |
||||
public class OpenApiRouterController { |
||||
|
||||
@Autowired |
||||
private OpenApiExecService openApiExecService; |
||||
@Autowired |
||||
private OpenApiProperties openApiProperties; |
||||
|
||||
@PostMapping("/{pageCode}/{formCode}/{operationCode}") |
||||
public ResponseResult<Object> execute(@PathVariable String pageCode, @PathVariable String formCode, @PathVariable String operationCode, |
||||
@RequestBody(required = false) Map<String, Object> requestBody) { |
||||
String requestPath = openApiProperties.getUrlPrefix() + "/v1/" + pageCode + "/" + formCode + "/" + operationCode; |
||||
return openApiExecService.execute(requestPath, requestBody == null ? Collections.emptyMap() : requestBody); |
||||
} |
||||
} |
||||
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
package apelet.common.openapi.dao; |
||||
|
||||
import apelet.common.core.base.dao.BaseDaoMapper; |
||||
import apelet.common.openapi.model.OpenApiApp; |
||||
import org.apache.ibatis.annotations.Param; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* OpenAPI 第三方应用数据操作访问接口。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
public interface OpenApiAppMapper extends BaseDaoMapper<OpenApiApp> { |
||||
|
||||
/** |
||||
* 获取过滤后的应用列表。 |
||||
* |
||||
* @param openApiAppFilter 应用过滤对象。 |
||||
* @param orderBy 排序字符串,order by从句的参数。 |
||||
* @return 应用列表。 |
||||
*/ |
||||
List<OpenApiApp> getOpenApiAppList(@Param("openApiAppFilter") OpenApiApp openApiAppFilter, @Param("orderBy") String orderBy); |
||||
} |
||||
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
package apelet.common.openapi.dao; |
||||
|
||||
import apelet.common.core.base.dao.BaseDaoMapper; |
||||
import apelet.common.openapi.model.OpenApiConfig; |
||||
import org.apache.ibatis.annotations.Param; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* OpenAPI 开放接口配置数据操作访问接口。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
public interface OpenApiConfigMapper extends BaseDaoMapper<OpenApiConfig> { |
||||
|
||||
/** |
||||
* 获取过滤后的配置列表。 |
||||
* |
||||
* @param openApiConfigFilter 配置过滤对象。 |
||||
* @param orderBy 排序字符串,order by从句的参数。 |
||||
* @return 配置列表。 |
||||
*/ |
||||
List<OpenApiConfig> getOpenApiConfigList( |
||||
@Param("openApiConfigFilter") OpenApiConfig openApiConfigFilter, @Param("orderBy") String orderBy); |
||||
} |
||||
@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
||||
<mapper namespace="apelet.common.openapi.dao.OpenApiAppMapper"> |
||||
<resultMap id="BaseResultMap" type="apelet.common.openapi.model.OpenApiApp"> |
||||
<id column="id" jdbcType="BIGINT" property="id"/> |
||||
<result column="app_code" jdbcType="VARCHAR" property="appCode"/> |
||||
<result column="app_secret" jdbcType="VARCHAR" property="appSecret"/> |
||||
<result column="app_name" jdbcType="VARCHAR" property="appName"/> |
||||
<result column="tenant_id" jdbcType="BIGINT" property="tenantId"/> |
||||
<result column="status" jdbcType="INTEGER" property="status"/> |
||||
<result column="create_time" jdbcType="TIMESTAMP" property="createTime"/> |
||||
<result column="create_user_id" jdbcType="BIGINT" property="createUserId"/> |
||||
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/> |
||||
<result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/> |
||||
</resultMap> |
||||
|
||||
<!-- 这里仅包含调用接口输入的应用过滤条件 --> |
||||
<sql id="inputFilterRef"> |
||||
<if test="openApiAppFilter != null"> |
||||
<if test="openApiAppFilter.appCode != null and openApiAppFilter.appCode != ''"> |
||||
AND xy_sys_open_app.app_code = #{openApiAppFilter.appCode} |
||||
</if> |
||||
<if test="openApiAppFilter.appName != null and openApiAppFilter.appName != ''"> |
||||
<bind name="safeAppName" value="'%' + openApiAppFilter.appName + '%'"/> |
||||
AND xy_sys_open_app.app_name LIKE #{safeAppName} |
||||
</if> |
||||
<if test="openApiAppFilter.status != null"> |
||||
AND xy_sys_open_app.status = #{openApiAppFilter.status} |
||||
</if> |
||||
</if> |
||||
</sql> |
||||
|
||||
<select id="getOpenApiAppList" resultMap="BaseResultMap" parameterType="apelet.common.openapi.model.OpenApiApp"> |
||||
SELECT * FROM xy_sys_open_app |
||||
<where> |
||||
<include refid="inputFilterRef"/> |
||||
</where> |
||||
<if test="orderBy != null and orderBy != ''"> |
||||
ORDER BY ${orderBy} |
||||
</if> |
||||
</select> |
||||
</mapper> |
||||
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
||||
<mapper namespace="apelet.common.openapi.dao.OpenApiConfigMapper"> |
||||
<resultMap id="BaseResultMap" type="apelet.common.openapi.model.OpenApiConfig"> |
||||
<id column="id" jdbcType="BIGINT" property="id"/> |
||||
<result column="app_code" jdbcType="VARCHAR" property="appCode"/> |
||||
<result column="page_id" jdbcType="BIGINT" property="pageId"/> |
||||
<result column="page_code" jdbcType="VARCHAR" property="pageCode"/> |
||||
<result column="form_code" jdbcType="VARCHAR" property="formCode"/> |
||||
<result column="form_id" jdbcType="BIGINT" property="formId"/> |
||||
<result column="operation_code" jdbcType="VARCHAR" property="operationCode"/> |
||||
<result column="operation_name" jdbcType="VARCHAR" property="operationName"/> |
||||
<result column="param_mode" jdbcType="VARCHAR" property="paramMode"/> |
||||
<result column="request_path" jdbcType="VARCHAR" property="requestPath"/> |
||||
<result column="request_method" jdbcType="VARCHAR" property="requestMethod"/> |
||||
<result column="param_config" jdbcType="LONGVARCHAR" property="paramConfig"/> |
||||
<result column="output_config" jdbcType="LONGVARCHAR" property="outputConfig"/> |
||||
<result column="status" jdbcType="INTEGER" property="status"/> |
||||
<result column="create_time" jdbcType="TIMESTAMP" property="createTime"/> |
||||
<result column="create_user_id" jdbcType="BIGINT" property="createUserId"/> |
||||
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/> |
||||
<result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/> |
||||
</resultMap> |
||||
|
||||
<!-- 这里仅包含调用接口输入的配置过滤条件 --> |
||||
<sql id="inputFilterRef"> |
||||
<if test="openApiConfigFilter != null"> |
||||
<if test="openApiConfigFilter.appCode != null and openApiConfigFilter.appCode != ''"> |
||||
AND xy_sys_open_api_config.app_code = #{openApiConfigFilter.appCode} |
||||
</if> |
||||
<if test="openApiConfigFilter.pageId != null"> |
||||
AND xy_sys_open_api_config.page_id = #{openApiConfigFilter.pageId} |
||||
</if> |
||||
<if test="openApiConfigFilter.formCode != null and openApiConfigFilter.formCode != ''"> |
||||
AND xy_sys_open_api_config.form_code = #{openApiConfigFilter.formCode} |
||||
</if> |
||||
<if test="openApiConfigFilter.status != null"> |
||||
AND xy_sys_open_api_config.status = #{openApiConfigFilter.status} |
||||
</if> |
||||
</if> |
||||
</sql> |
||||
|
||||
<select id="getOpenApiConfigList" resultMap="BaseResultMap" parameterType="apelet.common.openapi.model.OpenApiConfig"> |
||||
SELECT * FROM xy_sys_open_api_config |
||||
<where> |
||||
<include refid="inputFilterRef"/> |
||||
</where> |
||||
<if test="orderBy != null and orderBy != ''"> |
||||
ORDER BY ${orderBy} |
||||
</if> |
||||
</select> |
||||
</mapper> |
||||
@ -0,0 +1,175 @@
@@ -0,0 +1,175 @@
|
||||
package apelet.common.openapi.document; |
||||
|
||||
import apelet.common.openapi.constant.OpenApiFieldSource; |
||||
import apelet.common.openapi.constant.OpenApiParamMode; |
||||
import apelet.common.openapi.model.OpenApiConfig; |
||||
import apelet.common.openapi.service.OpenApiConfigService; |
||||
import cn.hutool.core.util.StrUtil; |
||||
import com.alibaba.fastjson.JSON; |
||||
import com.alibaba.fastjson.JSONArray; |
||||
import com.alibaba.fastjson.JSONObject; |
||||
import io.swagger.v3.oas.models.OpenAPI; |
||||
import io.swagger.v3.oas.models.Operation; |
||||
import io.swagger.v3.oas.models.PathItem; |
||||
import io.swagger.v3.oas.models.media.ArraySchema; |
||||
import io.swagger.v3.oas.models.media.Content; |
||||
import io.swagger.v3.oas.models.media.MediaType; |
||||
import io.swagger.v3.oas.models.media.Schema; |
||||
import io.swagger.v3.oas.models.parameters.RequestBody; |
||||
import io.swagger.v3.oas.models.responses.ApiResponse; |
||||
import io.swagger.v3.oas.models.responses.ApiResponses; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springdoc.core.customizers.OpenApiCustomiser; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* OpenAPI 文档动态生成器:启动时扫描启用的开放接口配置,为每条配置生成一个 path + operation, |
||||
* schema 由 param_config / output_config 推导,嵌套结构生成嵌套 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<OpenApiConfig> configs = openApiConfigService.getOpenApiConfigList(filter, null); |
||||
for (OpenApiConfig config : configs) { |
||||
this.registerPath(openApi, config); |
||||
} |
||||
log.info("OpenAPI 文档自定义完成,共注册 {} 个开放接口 path", configs.size()); |
||||
} |
||||
|
||||
private void registerPath(OpenAPI openApi, OpenApiConfig config) { |
||||
if (openApi.getPaths() != null && openApi.getPaths().containsKey(config.getRequestPath())) { |
||||
return; |
||||
} |
||||
Operation operation = new Operation(); |
||||
operation.setOperationId(config.getOperationCode() + "_" + config.getFormCode()); |
||||
operation.setSummary(config.getOperationName()); |
||||
operation.setDescription(config.getOperationName()); |
||||
operation.addTagsItem(config.getAppCode()); |
||||
Schema<?> requestSchema = this.buildRequestSchema(config); |
||||
if (requestSchema != null) { |
||||
operation.setRequestBody(new RequestBody().required(true) |
||||
.content(new Content().addMediaType("application/json", |
||||
new MediaType().schema(requestSchema)))); |
||||
} |
||||
Schema<?> responseSchema = new Schema<>().type("object") |
||||
.addProperty("code", new Schema<>().type("integer")) |
||||
.addProperty("msg", new Schema<>().type("string")) |
||||
.addProperty("data", this.buildDataSchema(config.getOutputConfig())); |
||||
operation.responses(new ApiResponses().addApiResponse("200", |
||||
new ApiResponse().description("成功") |
||||
.content(new Content().addMediaType("application/json", |
||||
new MediaType().schema(responseSchema))))); |
||||
PathItem pathItem = new PathItem(); |
||||
if ("GET".equalsIgnoreCase(config.getRequestMethod())) { |
||||
openApi.path(config.getRequestPath(), pathItem.get(operation)); |
||||
} else { |
||||
openApi.path(config.getRequestPath(), pathItem.post(operation)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 请求体 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"); |
||||
} |
||||
} |
||||
@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
|
||||
package apelet.common.openapi.dto; |
||||
|
||||
import lombok.Data; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* OpenAPI 字段树节点(用于开放接口配置时选择字段,支持主表字段/关联字段属性/一对多分录)。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
@Data |
||||
public class OpenApiFieldDto { |
||||
|
||||
/** |
||||
* 字段来源:FORM_FIELD / RELATION_FIELD / ENTRY_ARRAY / ENTRY_FIELD。 |
||||
*/ |
||||
private String source; |
||||
/** |
||||
* 字段名(FORM_FIELD/ENTRY_FIELD),或关联存储字段(RELATION_FIELD)。 |
||||
*/ |
||||
private String field; |
||||
/** |
||||
* RELATION_FIELD 的属性取值:id / number / name。 |
||||
*/ |
||||
private String attr; |
||||
/** |
||||
* ENTRY_ARRAY / ENTRY_FIELD 所属的一对多分录附表表名。 |
||||
*/ |
||||
private String table; |
||||
/** |
||||
* 接口字段名(alias)。 |
||||
*/ |
||||
private String alias; |
||||
/** |
||||
* 展示名称。 |
||||
*/ |
||||
private String label; |
||||
/** |
||||
* ENTRY_ARRAY 下的分录字段列表。 |
||||
*/ |
||||
private List<OpenApiFieldDto> children; |
||||
} |
||||
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
package apelet.common.openapi.dto; |
||||
|
||||
import lombok.Data; |
||||
|
||||
/** |
||||
* OpenAPI 配置页面表单下拉项。 |
||||
* |
||||
* @author chenchuchuan |
||||
*/ |
||||
@Data |
||||
public class OpenApiFormOptionDto { |
||||
|
||||
/** |
||||
* 在线表单主键Id。 |
||||
*/ |
||||
private Long formId; |
||||
/** |
||||
* 表单编码。 |
||||
*/ |
||||
private String formCode; |
||||
/** |
||||
* 表单名称。 |
||||
*/ |
||||
private String formName; |
||||
} |
||||
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
package apelet.common.openapi.dto; |
||||
|
||||
import lombok.Data; |
||||
|
||||
/** |
||||
* OpenAPI 可选操作下拉项(查询 + widgetJson.operationList 各项)。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
@Data |
||||
public class OpenApiOperationDto { |
||||
|
||||
/** |
||||
* 操作编码:query 或 operationList 中操作的 code。 |
||||
*/ |
||||
private String code; |
||||
/** |
||||
* 操作名称:查询 或 operationList 中操作的 name。 |
||||
*/ |
||||
private String name; |
||||
/** |
||||
* 是否启用。 |
||||
*/ |
||||
private Boolean enabled; |
||||
} |
||||
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
package apelet.common.openapi.dto; |
||||
|
||||
import lombok.Data; |
||||
|
||||
/** |
||||
* OpenAPI 配置页面上线页面下拉项。 |
||||
* |
||||
* @author chenchuchuan |
||||
*/ |
||||
@Data |
||||
public class OpenApiPageOptionDto { |
||||
|
||||
/** |
||||
* 在线页面主键Id。 |
||||
*/ |
||||
private Long pageId; |
||||
/** |
||||
* 页面编码。 |
||||
*/ |
||||
private String pageCode; |
||||
/** |
||||
* 页面名称。 |
||||
*/ |
||||
private String pageName; |
||||
} |
||||
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
package apelet.common.openapi.dto; |
||||
|
||||
import lombok.Data; |
||||
|
||||
/** |
||||
* OpenAPI 按用户名换取 token 的请求体。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
@Data |
||||
public class OpenApiTokenDto { |
||||
|
||||
/** |
||||
* 应用编码。 |
||||
*/ |
||||
private String appCode; |
||||
/** |
||||
* 应用密钥。 |
||||
*/ |
||||
private String appSecret; |
||||
/** |
||||
* 调用用户名。 |
||||
*/ |
||||
private String username; |
||||
} |
||||
@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
|
||||
package apelet.common.openapi.dto; |
||||
|
||||
import lombok.Data; |
||||
|
||||
/** |
||||
* OpenAPI 按用户名换取的调用用户信息(由接入方实现 OpenApiUserService 提供)。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
@Data |
||||
public class OpenApiUserInfo { |
||||
|
||||
/** |
||||
* 用户Id。 |
||||
*/ |
||||
private Long userId; |
||||
/** |
||||
* 登录名。 |
||||
*/ |
||||
private String loginName; |
||||
/** |
||||
* 显示名称。 |
||||
*/ |
||||
private String showName; |
||||
/** |
||||
* 租户Id。 |
||||
*/ |
||||
private Long tenantId; |
||||
} |
||||
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
package apelet.common.openapi.interceptor; |
||||
|
||||
import apelet.common.core.constant.ErrorCodeEnum; |
||||
import apelet.common.core.object.ResponseResult; |
||||
import apelet.common.core.object.TokenData; |
||||
import apelet.common.openapi.config.OpenApiProperties; |
||||
import apelet.common.openapi.service.OpenApiAuthService; |
||||
import com.alibaba.fastjson.JSON; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
import org.springframework.stereotype.Component; |
||||
import org.springframework.web.servlet.HandlerInterceptor; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletResponse; |
||||
|
||||
/** |
||||
* OpenAPI 对外开放路由({urlPrefix}/v1/**)的 token 校验拦截器。 |
||||
* <p>在 SpringMVC 拦截器链中注册(见接入方 InterceptorConfig),只拦截 {urlPrefix}/v1/**; |
||||
* token 签发(/auth/token)与配置管理(/config/**、/app/**,走应用自身登录态)不经过本拦截器。</p> |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
@Slf4j |
||||
@Component |
||||
@ConditionalOnProperty(name = "common-openapi.operationEnabled", havingValue = "true") |
||||
public class OpenApiAuthInterceptor implements HandlerInterceptor { |
||||
|
||||
@Autowired |
||||
private OpenApiAuthService openApiAuthService; |
||||
@Autowired |
||||
private OpenApiProperties openApiProperties; |
||||
|
||||
@Override |
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) |
||||
throws Exception { |
||||
// 兜底:即便注册路径放开,也仅对对外开放的动态路由做 token 校验
|
||||
if (!request.getRequestURI().startsWith(openApiProperties.getUrlPrefix() + "/v1/")) { |
||||
return true; |
||||
} |
||||
String token = request.getHeader("Authorization"); |
||||
TokenData tokenData = openApiAuthService.validateToken(token); |
||||
if (tokenData == null) { |
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); |
||||
response.setContentType("application/json;charset=UTF-8"); |
||||
response.getWriter().write(JSON.toJSONString( |
||||
ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, "OpenAPI token 无效或已过期!"))); |
||||
return false; |
||||
} |
||||
TokenData.addToRequest(tokenData); |
||||
return true; |
||||
} |
||||
} |
||||
@ -0,0 +1,70 @@
@@ -0,0 +1,70 @@
|
||||
package apelet.common.openapi.model; |
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField; |
||||
import com.baomidou.mybatisplus.annotation.TableId; |
||||
import com.baomidou.mybatisplus.annotation.TableName; |
||||
import lombok.Data; |
||||
|
||||
import java.util.Date; |
||||
|
||||
/** |
||||
* OpenAPI 第三方应用实体对象。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
@Data |
||||
@TableName(value = "xy_sys_open_app") |
||||
public class OpenApiApp { |
||||
|
||||
/** |
||||
* 主键Id。 |
||||
*/ |
||||
@TableId(value = "id") |
||||
private Long id; |
||||
/** |
||||
* 应用编码(对外暴露唯一)。 |
||||
*/ |
||||
@TableField(value = "app_code") |
||||
private String appCode; |
||||
/** |
||||
* 应用密钥(仅后台可见,可重置)。 |
||||
*/ |
||||
@TableField(value = "app_secret") |
||||
private String appSecret; |
||||
/** |
||||
* 应用名称。 |
||||
*/ |
||||
@TableField(value = "app_name") |
||||
private String appName; |
||||
/** |
||||
* 归属租户Id(本期默认为空,为空表示不限租户)。 |
||||
*/ |
||||
@TableField(value = "tenant_id") |
||||
private Long tenantId; |
||||
/** |
||||
* 状态(0: 停用 1: 启用)。 |
||||
*/ |
||||
@TableField(value = "status") |
||||
private Integer status; |
||||
/** |
||||
* 创建时间。 |
||||
*/ |
||||
@TableField(value = "create_time") |
||||
private Date createTime; |
||||
/** |
||||
* 创建者Id。 |
||||
*/ |
||||
@TableField(value = "create_user_id") |
||||
private Long createUserId; |
||||
/** |
||||
* 更新时间。 |
||||
*/ |
||||
@TableField(value = "update_time") |
||||
private Date updateTime; |
||||
/** |
||||
* 更新者Id。 |
||||
*/ |
||||
@TableField(value = "update_user_id") |
||||
private Long updateUserId; |
||||
} |
||||
@ -0,0 +1,110 @@
@@ -0,0 +1,110 @@
|
||||
package apelet.common.openapi.model; |
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField; |
||||
import com.baomidou.mybatisplus.annotation.TableId; |
||||
import com.baomidou.mybatisplus.annotation.TableName; |
||||
import lombok.Data; |
||||
|
||||
import java.util.Date; |
||||
|
||||
/** |
||||
* OpenAPI 开放接口配置实体对象。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
@Data |
||||
@TableName(value = "xy_sys_open_api_config") |
||||
public class OpenApiConfig { |
||||
|
||||
/** |
||||
* 主键Id。 |
||||
*/ |
||||
@TableId(value = "id") |
||||
private Long id; |
||||
/** |
||||
* 授权开放的应用编码(关联 xy_sys_open_app.app_code)。 |
||||
*/ |
||||
@TableField(value = "app_code") |
||||
private String appCode; |
||||
/** |
||||
* 在线页面主键(OnlinePage.pageId)。 |
||||
*/ |
||||
@TableField(value = "page_id") |
||||
private Long pageId; |
||||
/** |
||||
* 页面编码(冗余存储,用于组装开放路径)。 |
||||
*/ |
||||
@TableField(value = "page_code") |
||||
private String pageCode; |
||||
/** |
||||
* 该页面下关联的在线表单编码(OnlineForm.formCode,用于组装开放路径 URL)。 |
||||
*/ |
||||
@TableField(value = "form_code") |
||||
private String formCode; |
||||
/** |
||||
* 关联的在线表单主键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; |
||||
} |
||||
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
package apelet.common.openapi.service; |
||||
|
||||
import apelet.common.core.base.service.IBaseService; |
||||
import apelet.common.openapi.model.OpenApiApp; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* OpenAPI 第三方应用数据操作服务接口。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
public interface OpenApiAppService extends IBaseService<OpenApiApp, Long> { |
||||
|
||||
/** |
||||
* 获取过滤后的应用列表。 |
||||
* |
||||
* @param filter 过滤对象。 |
||||
* @param orderBy 排序字符串。 |
||||
* @return 应用列表。 |
||||
*/ |
||||
List<OpenApiApp> getOpenApiAppList(OpenApiApp filter, String orderBy); |
||||
|
||||
/** |
||||
* 新增应用:appCode 为空时自动生成,appSecret 自动生成并返回明文。 |
||||
* |
||||
* @param app 应用对象。 |
||||
* @return 保存后的应用(含明文 appSecret)。 |
||||
*/ |
||||
OpenApiApp addNew(OpenApiApp app); |
||||
|
||||
/** |
||||
* 重置应用密钥:旧密钥立即失效。 |
||||
* |
||||
* @param id 应用主键Id。 |
||||
* @return 新的明文 appSecret。 |
||||
*/ |
||||
String resetSecret(Long id); |
||||
} |
||||
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
package apelet.common.openapi.service; |
||||
|
||||
import apelet.common.core.object.ResponseResult; |
||||
import apelet.common.core.object.TokenData; |
||||
import com.alibaba.fastjson.JSONObject; |
||||
|
||||
/** |
||||
* OpenAPI 鉴权服务接口:token 签发与校验。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
public interface OpenApiAuthService { |
||||
|
||||
/** |
||||
* 校验应用凭证 + 调用用户名,签发 token。 |
||||
* |
||||
* @param appCode 应用编码。 |
||||
* @param appSecret 应用密钥。 |
||||
* @param username 调用用户名。 |
||||
* @return data 含 accessToken(不含 Bearer 前缀)与 expiresIn。 |
||||
*/ |
||||
ResponseResult<JSONObject> issueToken(String appCode, String appSecret, String username); |
||||
|
||||
/** |
||||
* 校验 Bearer token:验签 + 有效期 + Redis 会话存在性,还原 TokenData。 |
||||
* |
||||
* @param token Authorization 头值(含 Bearer 前缀)。 |
||||
* @return TokenData,无效或已失效返回null。 |
||||
*/ |
||||
TokenData validateToken(String token); |
||||
} |
||||
@ -0,0 +1,84 @@
@@ -0,0 +1,84 @@
|
||||
package apelet.common.openapi.service; |
||||
|
||||
import apelet.common.core.base.service.IBaseService; |
||||
import apelet.common.openapi.dto.OpenApiFieldDto; |
||||
import apelet.common.openapi.dto.OpenApiFormOptionDto; |
||||
import apelet.common.openapi.dto.OpenApiOperationDto; |
||||
import apelet.common.openapi.dto.OpenApiPageOptionDto; |
||||
import apelet.common.openapi.model.OpenApiConfig; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* OpenAPI 开放接口配置数据操作服务接口。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
public interface OpenApiConfigService extends IBaseService<OpenApiConfig, Long> { |
||||
|
||||
/** |
||||
* 获取过滤后的配置列表。 |
||||
* |
||||
* @param filter 过滤对象。 |
||||
* @param orderBy 排序字符串。 |
||||
* @return 配置列表。 |
||||
*/ |
||||
List<OpenApiConfig> getOpenApiConfigList(OpenApiConfig filter, String orderBy); |
||||
|
||||
/** |
||||
* 获取在线页面下拉列表(配置步骤1),可按页面名称模糊搜索。 |
||||
* |
||||
* @param keyword 页面名称关键字,为空返回全部。 |
||||
* @return 页面列表。 |
||||
*/ |
||||
List<OpenApiPageOptionDto> getPageOptionList(String keyword); |
||||
|
||||
/** |
||||
* 获取指定页面关联的表单下拉列表(配置步骤2 联动)。 |
||||
* |
||||
* @param pageId 页面主键Id。 |
||||
* @return 表单列表。 |
||||
*/ |
||||
List<OpenApiFormOptionDto> getFormOptionList(Long pageId); |
||||
|
||||
/** |
||||
* 获取可选操作下拉:查询(query) + widgetJson.operationList 各项(配置步骤3)。 |
||||
* |
||||
* @param formId 在线表单主键Id。 |
||||
* @return 操作列表。 |
||||
*/ |
||||
List<OpenApiOperationDto> getOperationList(Long formId); |
||||
|
||||
/** |
||||
* 获取表单可配置字段树:主表字段 + 关联字段属性 + 一对多分录附表字段。 |
||||
* |
||||
* @param formId 在线表单主键Id。 |
||||
* @return 字段树。 |
||||
*/ |
||||
List<OpenApiFieldDto> getFieldTree(Long formId); |
||||
|
||||
/** |
||||
* 新增配置:后端推导 paramMode、组装 requestPath。 |
||||
* |
||||
* @param config 配置对象。 |
||||
* @return 保存后的配置。 |
||||
*/ |
||||
OpenApiConfig addNew(OpenApiConfig config); |
||||
|
||||
/** |
||||
* 更新配置:重新推导 paramMode、组装 requestPath。 |
||||
* |
||||
* @param config 配置对象。 |
||||
* @return 更新后的配置,不存在返回null。 |
||||
*/ |
||||
OpenApiConfig updateConfig(OpenApiConfig config); |
||||
|
||||
/** |
||||
* 按开放路径查找启用的配置(动态路由执行用)。 |
||||
* |
||||
* @param requestPath 开放路径。 |
||||
* @return 配置对象。 |
||||
*/ |
||||
OpenApiConfig getByRequestPath(String requestPath); |
||||
} |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
package apelet.common.openapi.service; |
||||
|
||||
import apelet.common.core.object.ResponseResult; |
||||
|
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* OpenAPI 开放接口执行服务接口:按配置定位表单并执行。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
public interface OpenApiExecService { |
||||
|
||||
/** |
||||
* 按开放路径执行开放接口(鉴权由拦截器完成,本方法假定已注入 TokenData)。 |
||||
* |
||||
* @param requestPath 开放路径(/openapi/v1/{pageCode}/{formCode}/{operationCode})。 |
||||
* @param requestParams 请求体参数(扁平 JSON,含 pageNo/pageSize 或字段直传/条件参数)。 |
||||
* @return 执行结果。 |
||||
*/ |
||||
ResponseResult<Object> execute(String requestPath, Map<String, Object> requestParams); |
||||
} |
||||
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
package apelet.common.openapi.service; |
||||
|
||||
import apelet.common.online.model.OnlineForm; |
||||
|
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
|
||||
/** |
||||
* OpenAPI 在线表数据查询辅助接口(运行在在线数据源上下文)。 |
||||
* <p>用于关联字段反查等需要直接查询在线表(zz_online_*)的场景,避免在普通上下文误查默认库。</p> |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-03 |
||||
*/ |
||||
public interface OpenApiOnlineQueryService { |
||||
|
||||
/** |
||||
* 关联字段反查:按 master 表单的关联字段(主表存储列)定位关联表,用 attr 列匹配值,返回关联记录主键。 |
||||
* |
||||
* @param masterForm 主表单(含 masterTableId)。 |
||||
* @param masterColumnName 主表存储列名(onl_form_field.field_name,如 customer_id)。 |
||||
* @param attrColumn 关联表要匹配的列名(如 customer_code / customer_name / id)。 |
||||
* @param attrValue 关联表该列要匹配的值。 |
||||
* @return 关联记录主键;未找到返回 null。 |
||||
*/ |
||||
Object resolveRelatedId(OnlineForm masterForm, String masterColumnName, String attrColumn, Object attrValue); |
||||
|
||||
/** |
||||
* 关联字段批量反查:返回关联表中 attr 列 = attrValue 的**全部**记录主键集合(条件过滤用)。 |
||||
* |
||||
* @param masterForm 主表单(含 masterTableId)。 |
||||
* @param masterColumnName 主表存储列名。 |
||||
* @param attrColumn 关联表要匹配的列名。 |
||||
* @param attrValue 关联表该列要匹配的值。 |
||||
* @return 匹配到的关联记录主键集合(可能为空,不会为 null)。 |
||||
*/ |
||||
Set<String> resolveRelatedIds(OnlineForm masterForm, String masterColumnName, String attrColumn, Object attrValue); |
||||
|
||||
/** |
||||
* 查询一对多分录(附表)数据:按 master 行主键查子表关联行(用于输出配置的 ENTRY_ARRAY)。 |
||||
* |
||||
* @param masterForm 主表单。 |
||||
* @param masterId master 行主键值。 |
||||
* @param subTableName 子表表名(一对多分录附表)。 |
||||
* @return 子表行列表(可能为空,不会为 null)。 |
||||
*/ |
||||
List<Map<String, Object>> querySubTableRows(OnlineForm masterForm, Object masterId, String subTableName); |
||||
} |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
package apelet.common.openapi.service; |
||||
|
||||
import apelet.common.core.object.ResponseResult; |
||||
import apelet.common.openapi.dto.OpenApiUserInfo; |
||||
|
||||
/** |
||||
* OpenAPI 调用用户查询 SPI:由接入方(如 tenant-admin)提供实现,按登录名查询可模拟身份的用户。 |
||||
* <p>common-openapi 不依赖应用层用户服务,通过 Spring 自动装配该接口;未装配时按用户名签发 token 返回明确错误。</p> |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
public interface OpenApiUserService { |
||||
|
||||
/** |
||||
* 按登录名查询用户。 |
||||
* |
||||
* @param loginName 登录名。 |
||||
* @param tenantId 应用归属租户Id(为空表示不限租户,全局查)。 |
||||
* @return 用户信息。 |
||||
*/ |
||||
ResponseResult<OpenApiUserInfo> getByLoginName(String loginName, Long tenantId); |
||||
} |
||||
@ -0,0 +1,82 @@
@@ -0,0 +1,82 @@
|
||||
package apelet.common.openapi.service.impl; |
||||
|
||||
import apelet.common.core.base.dao.BaseDaoMapper; |
||||
import apelet.common.core.base.service.BaseService; |
||||
import apelet.common.core.object.TokenData; |
||||
import apelet.common.openapi.dao.OpenApiAppMapper; |
||||
import apelet.common.openapi.model.OpenApiApp; |
||||
import apelet.common.openapi.service.OpenApiAppService; |
||||
import cn.hutool.core.util.IdUtil; |
||||
import cn.hutool.core.util.RandomUtil; |
||||
import cn.hutool.core.util.StrUtil; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
import java.util.Date; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* OpenAPI 第三方应用数据操作服务类。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
@Slf4j |
||||
@Service("openApiAppService") |
||||
public class OpenApiAppServiceImpl extends BaseService<OpenApiApp, Long> implements OpenApiAppService { |
||||
|
||||
@Autowired |
||||
private OpenApiAppMapper openApiAppMapper; |
||||
|
||||
@Override |
||||
protected BaseDaoMapper<OpenApiApp> mapper() { |
||||
return openApiAppMapper; |
||||
} |
||||
|
||||
@Override |
||||
public List<OpenApiApp> getOpenApiAppList(OpenApiApp filter, String orderBy) { |
||||
if (filter == null) { |
||||
filter = new OpenApiApp(); |
||||
} |
||||
return openApiAppMapper.getOpenApiAppList(filter, orderBy); |
||||
} |
||||
|
||||
@Override |
||||
public OpenApiApp addNew(OpenApiApp app) { |
||||
app.setId(IdUtil.getSnowflakeNextId()); |
||||
if (StrUtil.isBlank(app.getAppCode())) { |
||||
app.setAppCode("open_app_" + RandomUtil.randomString(8)); |
||||
} |
||||
app.setAppSecret(RandomUtil.randomString(32)); |
||||
Date now = new Date(); |
||||
TokenData tokenData = TokenData.takeFromRequest(); |
||||
Long currentUserId = tokenData == null ? null : tokenData.getUserId(); |
||||
app.setCreateTime(now); |
||||
app.setCreateUserId(currentUserId); |
||||
app.setUpdateTime(now); |
||||
app.setUpdateUserId(currentUserId); |
||||
if (app.getStatus() == null) { |
||||
app.setStatus(1); |
||||
} |
||||
this.save(app); |
||||
return app; |
||||
} |
||||
|
||||
@Override |
||||
public String resetSecret(Long id) { |
||||
OpenApiApp app = this.getById(id); |
||||
if (app == null) { |
||||
return null; |
||||
} |
||||
String newSecret = RandomUtil.randomString(32); |
||||
app.setAppSecret(newSecret); |
||||
app.setUpdateTime(new Date()); |
||||
TokenData tokenData = TokenData.takeFromRequest(); |
||||
if (tokenData != null) { |
||||
app.setUpdateUserId(tokenData.getUserId()); |
||||
} |
||||
this.updateById(app); |
||||
return newSecret; |
||||
} |
||||
} |
||||
@ -0,0 +1,137 @@
@@ -0,0 +1,137 @@
|
||||
package apelet.common.openapi.service.impl; |
||||
|
||||
import apelet.common.core.constant.AppDeviceType; |
||||
import apelet.common.core.constant.ErrorCodeEnum; |
||||
import apelet.common.core.object.ResponseResult; |
||||
import apelet.common.core.object.TokenData; |
||||
import apelet.common.core.util.JwtUtil; |
||||
import apelet.common.core.util.RedisKeyUtil; |
||||
import apelet.common.openapi.config.OpenApiProperties; |
||||
import apelet.common.openapi.dto.OpenApiUserInfo; |
||||
import apelet.common.openapi.model.OpenApiApp; |
||||
import apelet.common.openapi.service.OpenApiAppService; |
||||
import apelet.common.openapi.service.OpenApiAuthService; |
||||
import apelet.common.openapi.service.OpenApiUserService; |
||||
import cn.hutool.core.util.IdUtil; |
||||
import cn.hutool.core.util.StrUtil; |
||||
import com.alibaba.fastjson.JSON; |
||||
import com.alibaba.fastjson.JSONObject; |
||||
import com.alibaba.fastjson.serializer.SerializerFeature; |
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
||||
import io.jsonwebtoken.Claims; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.redisson.api.RBucket; |
||||
import org.redisson.api.RedissonClient; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
/** |
||||
* OpenAPI 鉴权服务实现。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
@Slf4j |
||||
@Service("openApiAuthService") |
||||
public class OpenApiAuthServiceImpl implements OpenApiAuthService { |
||||
|
||||
/** |
||||
* JWT claims 中的会话Id键(与应用登录 token 保持一致)。 |
||||
*/ |
||||
private static final String CLAIM_SESSION_ID = "sessionId"; |
||||
/** |
||||
* Bearer 前缀(JwtUtil 生成结果自带,对外返回时去除)。 |
||||
*/ |
||||
private static final String BEARER_PREFIX = "Bearer "; |
||||
|
||||
@Autowired |
||||
private OpenApiAppService openApiAppService; |
||||
@Autowired |
||||
private RedissonClient redissonClient; |
||||
@Autowired |
||||
private OpenApiProperties openApiProperties; |
||||
@Autowired(required = false) |
||||
private OpenApiUserService openApiUserService; |
||||
|
||||
@Override |
||||
public ResponseResult<JSONObject> issueToken(String appCode, String appSecret, String username) { |
||||
if (StrUtil.hasBlank(appCode, appSecret, username)) { |
||||
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST, "应用编码、密钥、用户名均不能为空!"); |
||||
} |
||||
// ① 校验应用凭证与状态
|
||||
OpenApiApp app = openApiAppService.getOne(new LambdaQueryWrapper<OpenApiApp>() |
||||
.eq(OpenApiApp::getAppCode, appCode)); |
||||
if (app == null || !app.getAppSecret().equals(appSecret)) { |
||||
return ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, "应用编码或密钥错误!"); |
||||
} |
||||
if (app.getStatus() == null || app.getStatus() != 1) { |
||||
return ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, "应用已被停用!"); |
||||
} |
||||
// ② 按用户名查询调用用户
|
||||
if (openApiUserService == null) { |
||||
return ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, "未配置 OpenApiUserService 实现,无法按用户名签发 token!"); |
||||
} |
||||
ResponseResult<OpenApiUserInfo> userResult = openApiUserService.getByLoginName(username, app.getTenantId()); |
||||
if (!userResult.isSuccess() || userResult.getData() == null) { |
||||
return ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, "调用用户不存在!"); |
||||
} |
||||
OpenApiUserInfo user = userResult.getData(); |
||||
// ③ 组装 TokenData,签发 JWT(claims 仅放 sessionId,会话状态存 Redis 可吊销)
|
||||
TokenData tokenData = new TokenData(); |
||||
tokenData.setUserId(user.getUserId()); |
||||
tokenData.setLoginName(user.getLoginName()); |
||||
tokenData.setShowName(user.getShowName()); |
||||
tokenData.setTenantId(user.getTenantId()); |
||||
tokenData.setAppCode(appCode); |
||||
tokenData.setDeviceType(AppDeviceType.WEB); |
||||
tokenData.setIsAdmin(false); |
||||
String sessionId = "openapi_" + IdUtil.simpleUUID(); |
||||
tokenData.setSessionId(sessionId); |
||||
Map<String, Object> claims = new HashMap<>(3); |
||||
claims.put(CLAIM_SESSION_ID, sessionId); |
||||
Long expirationSeconds = openApiProperties.getTokenExpiration(); |
||||
String token = JwtUtil.generateToken(claims, expirationSeconds * 1000, openApiProperties.getTokenSigningKey()); |
||||
this.putTokenDataToSessionCache(tokenData, expirationSeconds); |
||||
JSONObject data = new JSONObject(); |
||||
data.put("accessToken", StrUtil.removePrefix(token, BEARER_PREFIX)); |
||||
data.put("expiresIn", expirationSeconds); |
||||
return ResponseResult.success(data); |
||||
} |
||||
|
||||
@Override |
||||
public TokenData validateToken(String token) { |
||||
Claims c = JwtUtil.parseToken(token, openApiProperties.getTokenSigningKey()); |
||||
if (JwtUtil.isNullOrExpired(c)) { |
||||
return null; |
||||
} |
||||
String sessionId = (String) c.get(CLAIM_SESSION_ID); |
||||
if (StrUtil.isBlank(sessionId)) { |
||||
return null; |
||||
} |
||||
String sessionIdKey = RedisKeyUtil.makeSessionIdKey(sessionId); |
||||
RBucket<String> bucket = redissonClient.getBucket(sessionIdKey); |
||||
if (!bucket.isExists()) { |
||||
return null; |
||||
} |
||||
TokenData tokenData = JSON.parseObject(bucket.get(), TokenData.class); |
||||
if (tokenData != null) { |
||||
tokenData.setToken(token); |
||||
} |
||||
return tokenData; |
||||
} |
||||
|
||||
/** |
||||
* TokenData 写入 Redis 会话缓存(key 与应用登录会话共用 RedisKeyUtil.makeSessionIdKey,可吊销)。 |
||||
*/ |
||||
private void putTokenDataToSessionCache(TokenData tokenData, Long expirationSeconds) { |
||||
String sessionIdKey = RedisKeyUtil.makeSessionIdKey(tokenData.getSessionId()); |
||||
String sessionData = JSON.toJSONString(tokenData, SerializerFeature.WriteNonStringValueAsString); |
||||
RBucket<String> bucket = redissonClient.getBucket(sessionIdKey); |
||||
bucket.set(sessionData); |
||||
bucket.expire(expirationSeconds, TimeUnit.SECONDS); |
||||
} |
||||
} |
||||
@ -0,0 +1,312 @@
@@ -0,0 +1,312 @@
|
||||
package apelet.common.openapi.service.impl; |
||||
|
||||
import apelet.common.core.base.dao.BaseDaoMapper; |
||||
import apelet.common.core.base.service.BaseService; |
||||
import apelet.common.core.object.TokenData; |
||||
import apelet.common.generator.model.OnlFormField; |
||||
import apelet.common.generator.model.OnlFormHead; |
||||
import apelet.common.generator.service.IOnlFormFieldService; |
||||
import apelet.common.generator.service.IOnlFormHeadService; |
||||
import apelet.common.online.model.OnlineForm; |
||||
import apelet.common.online.model.OnlinePage; |
||||
import apelet.common.online.model.OnlineTable; |
||||
import apelet.common.online.service.OnlineFormService; |
||||
import apelet.common.online.service.OnlinePageService; |
||||
import apelet.common.online.service.OnlineTableService; |
||||
import apelet.common.openapi.config.OpenApiProperties; |
||||
import apelet.common.openapi.constant.OpenApiFieldSource; |
||||
import apelet.common.openapi.constant.OpenApiParamMode; |
||||
import apelet.common.openapi.dao.OpenApiConfigMapper; |
||||
import apelet.common.openapi.dto.OpenApiFieldDto; |
||||
import apelet.common.openapi.dto.OpenApiFormOptionDto; |
||||
import apelet.common.openapi.dto.OpenApiOperationDto; |
||||
import apelet.common.openapi.dto.OpenApiPageOptionDto; |
||||
import apelet.common.openapi.model.OpenApiConfig; |
||||
import apelet.common.openapi.service.OpenApiConfigService; |
||||
import cn.hutool.core.util.IdUtil; |
||||
import cn.hutool.core.util.StrUtil; |
||||
import com.alibaba.fastjson.JSON; |
||||
import com.alibaba.fastjson.JSONArray; |
||||
import com.alibaba.fastjson.JSONObject; |
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Date; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* OpenAPI 开放接口配置数据操作服务类。 |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
@Slf4j |
||||
@Service("openApiConfigService") |
||||
public class OpenApiConfigServiceImpl extends BaseService<OpenApiConfig, Long> implements OpenApiConfigService { |
||||
|
||||
/** 查询操作的固定 operation_code。 */ |
||||
private static final String QUERY_CODE = "query"; |
||||
|
||||
@Autowired |
||||
private OpenApiConfigMapper openApiConfigMapper; |
||||
@Autowired |
||||
private OpenApiProperties openApiProperties; |
||||
@Autowired |
||||
private OnlinePageService onlinePageService; |
||||
@Autowired |
||||
private OnlineFormService onlineFormService; |
||||
@Autowired |
||||
private OnlineTableService onlineTableService; |
||||
@Autowired |
||||
private IOnlFormHeadService onlFormHeadService; |
||||
@Autowired |
||||
private IOnlFormFieldService onlFormFieldService; |
||||
|
||||
@Override |
||||
protected BaseDaoMapper<OpenApiConfig> mapper() { |
||||
return openApiConfigMapper; |
||||
} |
||||
|
||||
@Override |
||||
public List<OpenApiConfig> getOpenApiConfigList(OpenApiConfig filter, String orderBy) { |
||||
if (filter == null) { |
||||
filter = new OpenApiConfig(); |
||||
} |
||||
return openApiConfigMapper.getOpenApiConfigList(filter, orderBy); |
||||
} |
||||
|
||||
@Override |
||||
public List<OpenApiPageOptionDto> getPageOptionList(String keyword) { |
||||
// 按页面名称模糊搜索,空关键字返回全部;结果按创建时间倒序
|
||||
OnlinePage filter = null; |
||||
if (StrUtil.isNotBlank(keyword)) { |
||||
filter = new OnlinePage(); |
||||
filter.setPageName(keyword); |
||||
} |
||||
List<OnlinePage> pageList = onlinePageService.getOnlinePageList(filter, "create_time DESC"); |
||||
List<OpenApiPageOptionDto> result = new ArrayList<>(); |
||||
if (pageList != null) { |
||||
for (OnlinePage page : pageList) { |
||||
OpenApiPageOptionDto dto = new OpenApiPageOptionDto(); |
||||
dto.setPageId(page.getPageId()); |
||||
dto.setPageCode(page.getPageCode()); |
||||
dto.setPageName(page.getPageName()); |
||||
result.add(dto); |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public List<OpenApiFormOptionDto> getFormOptionList(Long pageId) { |
||||
List<OpenApiFormOptionDto> result = new ArrayList<>(); |
||||
if (pageId == null) { |
||||
return result; |
||||
} |
||||
List<OnlineForm> formList = onlineFormService.list( |
||||
new LambdaQueryWrapper<OnlineForm>().eq(OnlineForm::getPageId, pageId)); |
||||
if (formList != null) { |
||||
for (OnlineForm form : formList) { |
||||
OpenApiFormOptionDto dto = new OpenApiFormOptionDto(); |
||||
dto.setFormId(form.getFormId()); |
||||
dto.setFormCode(form.getFormCode()); |
||||
dto.setFormName(form.getFormName()); |
||||
result.add(dto); |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public List<OpenApiOperationDto> getOperationList(Long formId) { |
||||
List<OpenApiOperationDto> result = new ArrayList<>(); |
||||
// 合成「查询」选项
|
||||
OpenApiOperationDto query = new OpenApiOperationDto(); |
||||
query.setCode(QUERY_CODE); |
||||
query.setName("查询"); |
||||
query.setEnabled(true); |
||||
result.add(query); |
||||
OnlineForm form = formId == null ? null : onlineFormService.getById(formId); |
||||
if (form == null || StrUtil.isBlank(form.getWidgetJson())) { |
||||
return result; |
||||
} |
||||
JSONObject widgetJson = JSON.parseObject(form.getWidgetJson()); |
||||
// 兼容 pc/mobile,取第一个存在 operationList 的设备
|
||||
for (String device : new String[]{"pc", "mobile"}) { |
||||
JSONObject deviceObj = widgetJson.getJSONObject(device); |
||||
if (deviceObj == null) { |
||||
continue; |
||||
} |
||||
JSONArray opList = deviceObj.getJSONArray("operationList"); |
||||
if (opList == null || opList.isEmpty()) { |
||||
continue; |
||||
} |
||||
for (int i = 0; i < opList.size(); i++) { |
||||
JSONObject op = opList.getJSONObject(i); |
||||
if (op == null) { |
||||
continue; |
||||
} |
||||
OpenApiOperationDto dto = new OpenApiOperationDto(); |
||||
dto.setCode(op.getString("code")); |
||||
dto.setName(op.getString("name")); |
||||
dto.setEnabled(op.getBoolean("enabled")); |
||||
result.add(dto); |
||||
} |
||||
break; |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public List<OpenApiFieldDto> getFieldTree(Long formId) { |
||||
List<OpenApiFieldDto> result = new ArrayList<>(); |
||||
OnlineForm form = formId == null ? null : onlineFormService.getById(formId); |
||||
if (form == null || form.getMasterTableId() == null) { |
||||
return result; |
||||
} |
||||
OnlineTable masterTable = onlineTableService.getById(form.getMasterTableId()); |
||||
if (masterTable == null) { |
||||
return result; |
||||
} |
||||
OnlFormHead head = onlFormHeadService.getOne(new LambdaQueryWrapper<OnlFormHead>() |
||||
.eq(OnlFormHead::getTableName, masterTable.getTableName())); |
||||
if (head == null) { |
||||
return result; |
||||
} |
||||
// 主表字段:普通字段 FORM_FIELD,关联字段(refPropert 非空)RELATION_FIELD(按 id/编码/名称 三个属性展开)
|
||||
List<OnlFormField> masterFields = onlFormFieldService.list( |
||||
new LambdaQueryWrapper<OnlFormField>().eq(OnlFormField::getHeadId, head.getId())); |
||||
if (masterFields != null) { |
||||
for (OnlFormField field : masterFields) { |
||||
if (field.getRefPropert() != null) { |
||||
for (String attr : new String[]{"id", "number", "name"}) { |
||||
OpenApiFieldDto dto = new OpenApiFieldDto(); |
||||
dto.setSource(OpenApiFieldSource.RELATION_FIELD); |
||||
dto.setField(field.getFieldName()); |
||||
dto.setAttr(attr); |
||||
dto.setAlias(field.getFieldName()); |
||||
dto.setLabel(field.getFieldRemark()); |
||||
result.add(dto); |
||||
} |
||||
} else { |
||||
OpenApiFieldDto dto = new OpenApiFieldDto(); |
||||
dto.setSource(OpenApiFieldSource.FORM_FIELD); |
||||
dto.setField(field.getFieldName()); |
||||
dto.setAlias(field.getFieldName()); |
||||
dto.setLabel(field.getFieldRemark()); |
||||
result.add(dto); |
||||
} |
||||
} |
||||
} |
||||
// 一对多分录附表(childName 逗号分隔)
|
||||
if (StrUtil.isNotBlank(head.getChildName())) { |
||||
for (String subTableName : head.getChildName().split(",")) { |
||||
subTableName = subTableName.trim(); |
||||
OnlFormHead subHead = onlFormHeadService.getOne(new LambdaQueryWrapper<OnlFormHead>() |
||||
.eq(OnlFormHead::getTableName, subTableName)); |
||||
if (subHead == null) { |
||||
continue; |
||||
} |
||||
OpenApiFieldDto entryArray = new OpenApiFieldDto(); |
||||
entryArray.setSource(OpenApiFieldSource.ENTRY_ARRAY); |
||||
entryArray.setTable(subTableName); |
||||
entryArray.setAlias(subTableName); |
||||
entryArray.setLabel(subTableName); |
||||
List<OpenApiFieldDto> children = new ArrayList<>(); |
||||
List<OnlFormField> subFields = onlFormFieldService.list( |
||||
new LambdaQueryWrapper<OnlFormField>().eq(OnlFormField::getHeadId, subHead.getId())); |
||||
if (subFields != null) { |
||||
for (OnlFormField subField : subFields) { |
||||
OpenApiFieldDto dto = new OpenApiFieldDto(); |
||||
dto.setSource(OpenApiFieldSource.ENTRY_FIELD); |
||||
dto.setTable(subTableName); |
||||
dto.setField(subField.getFieldName()); |
||||
dto.setAlias(subField.getFieldName()); |
||||
dto.setLabel(subField.getFieldRemark()); |
||||
children.add(dto); |
||||
} |
||||
} |
||||
entryArray.setChildren(children); |
||||
result.add(entryArray); |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public OpenApiConfig addNew(OpenApiConfig config) { |
||||
config.setId(IdUtil.getSnowflakeNextId()); |
||||
config.setParamMode(this.resolveParamMode(config)); |
||||
config.setRequestPath(this.buildRequestPath(config)); |
||||
Date now = new Date(); |
||||
TokenData tokenData = TokenData.takeFromRequest(); |
||||
Long currentUserId = tokenData == null ? null : tokenData.getUserId(); |
||||
config.setCreateTime(now); |
||||
config.setCreateUserId(currentUserId); |
||||
config.setUpdateTime(now); |
||||
config.setUpdateUserId(currentUserId); |
||||
if (config.getStatus() == null) { |
||||
config.setStatus(1); |
||||
} |
||||
if (StrUtil.isBlank(config.getRequestMethod())) { |
||||
config.setRequestMethod("POST"); |
||||
} |
||||
this.save(config); |
||||
return config; |
||||
} |
||||
|
||||
@Override |
||||
public OpenApiConfig updateConfig(OpenApiConfig config) { |
||||
OpenApiConfig original = this.getById(config.getId()); |
||||
if (original == null) { |
||||
return null; |
||||
} |
||||
config.setParamMode(this.resolveParamMode(config)); |
||||
config.setRequestPath(this.buildRequestPath(config)); |
||||
config.setCreateTime(original.getCreateTime()); |
||||
config.setUpdateTime(new Date()); |
||||
TokenData tokenData = TokenData.takeFromRequest(); |
||||
if (tokenData != null) { |
||||
config.setUpdateUserId(tokenData.getUserId()); |
||||
} |
||||
this.updateById(config); |
||||
return config; |
||||
} |
||||
|
||||
@Override |
||||
public OpenApiConfig getByRequestPath(String requestPath) { |
||||
if (StrUtil.isBlank(requestPath)) { |
||||
return null; |
||||
} |
||||
return openApiConfigMapper.selectOne(new LambdaQueryWrapper<OpenApiConfig>().eq(OpenApiConfig::getRequestPath, requestPath)); |
||||
} |
||||
|
||||
/** |
||||
* 推导参数模式: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(); |
||||
} |
||||
} |
||||
@ -0,0 +1,566 @@
@@ -0,0 +1,566 @@
|
||||
package apelet.common.openapi.service.impl; |
||||
|
||||
import apelet.common.core.constant.ErrorCodeEnum; |
||||
import apelet.common.core.object.MyPageParam; |
||||
import apelet.common.core.object.ResponseResult; |
||||
import apelet.common.core.object.TokenData; |
||||
import apelet.common.generator.model.OnlFormHead; |
||||
import apelet.common.generator.service.IOnlFormHeadService; |
||||
import apelet.common.online.abstractplugin.model.GridData; |
||||
import apelet.common.online.dto.OnlineFilterDto; |
||||
import apelet.common.online.dto.OnlinePluginExecuteDto; |
||||
import apelet.common.online.model.OnlineDatasource; |
||||
import apelet.common.online.model.OnlineForm; |
||||
import apelet.common.online.model.OnlineTable; |
||||
import apelet.common.online.model.constant.FieldFilterType; |
||||
import apelet.common.online.service.OnlineDatasourceService; |
||||
import apelet.common.online.service.OnlineFormService; |
||||
import apelet.common.online.service.OnlineOperationService; |
||||
import apelet.common.online.service.OnlineTableService; |
||||
import apelet.common.online.util.OnlineOperationHelper; |
||||
import apelet.common.openapi.constant.OpenApiFieldSource; |
||||
import apelet.common.openapi.constant.OpenApiParamMode; |
||||
import apelet.common.openapi.model.OpenApiConfig; |
||||
import apelet.common.openapi.service.OpenApiConfigService; |
||||
import apelet.common.openapi.service.OpenApiExecService; |
||||
import apelet.common.openapi.service.OpenApiOnlineQueryService; |
||||
import cn.hutool.core.util.StrUtil; |
||||
import com.alibaba.fastjson.JSON; |
||||
import com.alibaba.fastjson.JSONArray; |
||||
import com.alibaba.fastjson.JSONObject; |
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
import java.io.Serializable; |
||||
import java.math.BigDecimal; |
||||
import java.util.*; |
||||
|
||||
/** |
||||
* OpenAPI 开放接口执行服务实现。 |
||||
* <p>按 param_mode 分派三种执行路径:QUERY 走列表插件、FIELD_DIRECT 走 executePlugin(字段直传)、 |
||||
* CONDITION_MATCH 先按条件匹配单据集合再逐条 executePlugin。</p> |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-02 |
||||
*/ |
||||
@Slf4j |
||||
@Service("openApiExecService") |
||||
public class OpenApiExecServiceImpl implements OpenApiExecService { |
||||
|
||||
@Autowired |
||||
private OpenApiConfigService openApiConfigService; |
||||
@Autowired |
||||
private OnlineFormService onlineFormService; |
||||
@Autowired |
||||
private OnlineTableService onlineTableService; |
||||
@Autowired |
||||
private IOnlFormHeadService onlFormHeadService; |
||||
@Autowired |
||||
private OnlineDatasourceService onlineDatasourceService; |
||||
@Autowired |
||||
private OnlineOperationService onlineOperationService; |
||||
@Autowired |
||||
private OnlineOperationHelper onlineOperationHelper; |
||||
@Autowired |
||||
private OpenApiOnlineQueryService openApiOnlineQueryService; |
||||
|
||||
@Override |
||||
public ResponseResult<Object> execute(String requestPath, Map<String, Object> requestParams) { |
||||
OpenApiConfig config = openApiConfigService.getByRequestPath(requestPath); |
||||
if (config == null || config.getStatus() == null || config.getStatus() != 1) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "开放接口不存在或已停用!"); |
||||
} |
||||
OnlineForm form = this.findForm(config); |
||||
if (form == null) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "在线表单不存在!"); |
||||
} |
||||
TokenData tokenData = TokenData.takeFromRequest(); |
||||
if (tokenData == null || !config.getAppCode().equals(tokenData.getAppCode())) { |
||||
return ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION, "当前应用无权调用该开放接口!"); |
||||
} |
||||
// 列表类操作(QUERY / CONDITION_MATCH)依赖 pc.tableWidget 走列表引擎,编辑页未配置列表时给出明确提示而非底层空指针。
|
||||
if (OpenApiParamMode.QUERY.equals(config.getParamMode()) |
||||
|| OpenApiParamMode.CONDITION_MATCH.equals(config.getParamMode())) { |
||||
if (!this.hasListTableWidget(form)) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "该表单未配置列表(pc.tableWidget),无法执行查询/条件匹配操作!"); |
||||
} |
||||
} |
||||
// 写类操作(FIELD_DIRECT / CONDITION_MATCH)需目标按钮真实存在且启用并绑定插件,避免误配时 executePlugin 空跑却返回"成功"。
|
||||
if (OpenApiParamMode.FIELD_DIRECT.equals(config.getParamMode()) |
||||
|| OpenApiParamMode.CONDITION_MATCH.equals(config.getParamMode())) { |
||||
if (!this.hasEnabledOperation(form, config.getOperationName())) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, |
||||
"操作按钮[" + config.getOperationName() + "]不存在、未启用或未绑定插件,无法执行!"); |
||||
} |
||||
} |
||||
// 在线引擎按 TokenData.appCode 校验数据源归属(空值表示非第三方应用/租户公共数据)。
|
||||
// 令牌 appCode 是开放应用编码,与在线资源归属编码不是一回事,执行期间先切到该表单数据源真正归属的编码,结束后还原。
|
||||
// 注意:归属编码用"不带归属校验"的直查获取——verifyAndGetDatasource 的归属校验在切换前必然失败,不能拿它取 appCode。
|
||||
String savedAppCode = tokenData.getAppCode(); |
||||
try { |
||||
OnlineDatasource datasource = form.getMasterTableId() == null ? null |
||||
: onlineDatasourceService.getOnlineDatasourceByMasterTableId(form.getMasterTableId()); |
||||
tokenData.setAppCode(datasource == null ? null : datasource.getAppCode()); |
||||
if (OpenApiParamMode.QUERY.equals(config.getParamMode())) { |
||||
return this.executeQuery(config, form, requestParams); |
||||
} |
||||
if (OpenApiParamMode.FIELD_DIRECT.equals(config.getParamMode())) { |
||||
return this.executeFieldDirect(config, form, requestParams); |
||||
} |
||||
return this.executeConditionMatch(config, form, requestParams); |
||||
} finally { |
||||
tokenData.setAppCode(savedAppCode); |
||||
} |
||||
} |
||||
|
||||
// ============ QUERY:列表插件查询 ============
|
||||
|
||||
private ResponseResult<Object> executeQuery(OpenApiConfig config, OnlineForm form, Map<String, Object> requestParams) { |
||||
JSONObject paramConfig = JSON.parseObject(config.getParamConfig()); |
||||
int pageNo = this.toIntParam(requestParams.get("pageNo"), 1, 1, Integer.MAX_VALUE); |
||||
int pageSize = this.toIntParam(requestParams.get("pageSize"), 10, 1, 1000); |
||||
MyPageParam pageParam = new MyPageParam(); |
||||
pageParam.setPageNum(pageNo); |
||||
pageParam.setPageSize(pageSize); |
||||
ResponseResult<List<OnlineFilterDto>> filterResult = this.buildFilterDtoList(paramConfig, requestParams, form); |
||||
if (!filterResult.isSuccess()) { |
||||
return ResponseResult.errorFrom(filterResult); |
||||
} |
||||
List<OnlineFilterDto> filterDtoList = filterResult.getData(); |
||||
ResponseResult<OnlineDatasource> datasourceResult = this.getDatasourceResult(form); |
||||
if (!datasourceResult.isSuccess()) { |
||||
return ResponseResult.errorFrom(datasourceResult); |
||||
} |
||||
OnlFormHead onlFormHead = this.getFormHead(datasourceResult); |
||||
if (onlFormHead == null) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "表单表头不存在!"); |
||||
} |
||||
GridData gridData = onlineOperationService.exeListPlugin(form.getFormId(), filterDtoList, null, pageParam, null, |
||||
onlFormHead, datasourceResult, null); |
||||
if (gridData == null) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_ACCESS_FAILED, "列表查询失败!"); |
||||
} |
||||
JSONObject data = new JSONObject(true); |
||||
data.put("total", gridData.getTotalCount()); |
||||
data.put("pageNo", pageNo); |
||||
data.put("pageSize", pageSize); |
||||
data.put("dataList", this.assembleDataList(gridData.getDataList(), config.getOutputConfig(), form)); |
||||
return ResponseResult.success(data); |
||||
} |
||||
|
||||
// ============ FIELD_DIRECT:字段直传 executePlugin ============
|
||||
|
||||
private ResponseResult<Object> executeFieldDirect(OpenApiConfig config, OnlineForm form, Map<String, Object> requestParams) { |
||||
JSONObject paramConfig = JSON.parseObject(config.getParamConfig()); |
||||
JSONArray fields = paramConfig == null ? null : paramConfig.getJSONArray("fields"); |
||||
if (fields == null) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "未配置字段直传参数!"); |
||||
} |
||||
JSONObject masterData = new JSONObject(true); |
||||
JSONObject slaveData = new JSONObject(true); |
||||
for (int i = 0; i < fields.size(); i++) { |
||||
JSONObject field = fields.getJSONObject(i); |
||||
String source = field.getString("source"); |
||||
String fieldName = field.getString("field"); |
||||
String table = field.getString("table"); |
||||
String alias = field.getString("alias"); |
||||
Object value = requestParams.get(alias); |
||||
if (OpenApiFieldSource.FORM_FIELD.equals(source)) { |
||||
masterData.put(fieldName, value); |
||||
} else if (OpenApiFieldSource.RELATION_FIELD.equals(source)) { |
||||
// attr = 关联表要匹配的列名;attr=id 直存,其他列先按关联表反查主键再写入主表存储列
|
||||
String attr = field.getString("attr"); |
||||
Object writeValue = value; |
||||
if (value != null && StrUtil.isNotBlank(attr) && !"id".equals(attr)) { |
||||
Object relatedId = openApiOnlineQueryService.resolveRelatedId(form, fieldName, attr, value); |
||||
if (relatedId != null) { |
||||
writeValue = relatedId; |
||||
} else { |
||||
log.warn("OpenAPI 关联字段 [{}] 按列 [{}] 未匹配到关联记录,值 [{}] 原样写入", fieldName, attr, value); |
||||
} |
||||
} |
||||
masterData.put(fieldName, writeValue); |
||||
} else if (OpenApiFieldSource.ENTRY_ARRAY.equals(source)) { |
||||
JSONArray entryArray = new JSONArray(); |
||||
if (value instanceof Collection) { |
||||
entryArray.addAll((Collection<?>) value); |
||||
} |
||||
slaveData.put(table, entryArray); |
||||
} |
||||
// ENTRY_FIELD 作为 ENTRY_ARRAY 的子节点,值挂在分录数组元素上,无需单独处理
|
||||
} |
||||
OnlinePluginExecuteDto pluginDto = new OnlinePluginExecuteDto(); |
||||
pluginDto.setButtonName(config.getOperationName()); |
||||
pluginDto.setFormId(form.getFormId()); |
||||
// billId 由系统生成/定位,不作为接口入参
|
||||
Object result = onlineOperationService.executePlugin(pluginDto, masterData, slaveData); |
||||
return ResponseResult.success(result); |
||||
} |
||||
|
||||
// ============ CONDITION_MATCH:条件匹配单据后逐条 executePlugin ============
|
||||
|
||||
private ResponseResult<Object> executeConditionMatch(OpenApiConfig config, OnlineForm form, Map<String, Object> requestParams) { |
||||
JSONObject paramConfig = JSON.parseObject(config.getParamConfig()); |
||||
ResponseResult<List<OnlineFilterDto>> filterResult = this.buildFilterDtoList(paramConfig, requestParams, form); |
||||
if (!filterResult.isSuccess()) { |
||||
return ResponseResult.errorFrom(filterResult); |
||||
} |
||||
List<OnlineFilterDto> filterDtoList = filterResult.getData(); |
||||
ResponseResult<OnlineDatasource> datasourceResult = this.getDatasourceResult(form); |
||||
if (!datasourceResult.isSuccess()) { |
||||
return ResponseResult.errorFrom(datasourceResult); |
||||
} |
||||
OnlFormHead onlFormHead = this.getFormHead(datasourceResult); |
||||
if (onlFormHead == null) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "表单表头不存在!"); |
||||
} |
||||
MyPageParam pageParam = new MyPageParam(); |
||||
pageParam.setPageNum(1); |
||||
pageParam.setPageSize(2000); |
||||
GridData gridData = onlineOperationService.exeListPlugin(form.getFormId(), filterDtoList, null, pageParam, null, |
||||
onlFormHead, datasourceResult, null); |
||||
if (gridData == null) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_ACCESS_FAILED, "单据匹配失败!"); |
||||
} |
||||
OnlinePluginExecuteDto pluginDto = new OnlinePluginExecuteDto(); |
||||
pluginDto.setButtonName(config.getOperationName()); |
||||
pluginDto.setFormId(form.getFormId()); |
||||
JSONArray resultList = new JSONArray(); |
||||
for (Map<String, Object> row : gridData.getDataList()) { |
||||
Object id = row.get("id"); |
||||
if (id == null) { |
||||
continue; |
||||
} |
||||
pluginDto.setBillId(String.valueOf(id)); |
||||
resultList.add(onlineOperationService.executePlugin(pluginDto, null, null)); |
||||
} |
||||
return ResponseResult.success(resultList); |
||||
} |
||||
|
||||
// ============ 公共辅助 ============
|
||||
|
||||
/** |
||||
* 定位在线表单:优先按 formId,未配置时回退 pageId + formCode。 |
||||
*/ |
||||
private OnlineForm findForm(OpenApiConfig config) { |
||||
if (config.getFormId() != null) { |
||||
OnlineForm form = onlineFormService.getById(config.getFormId()); |
||||
if (form != null) { |
||||
return form; |
||||
} |
||||
} |
||||
return onlineFormService.getOne(new LambdaQueryWrapper<OnlineForm>() |
||||
.eq(OnlineForm::getPageId, config.getPageId()) |
||||
.eq(OnlineForm::getFormCode, config.getFormCode()), false); |
||||
} |
||||
|
||||
/** |
||||
* 取表单主表数据源(含 masterTable,供列表插件回退使用)。 |
||||
*/ |
||||
private ResponseResult<OnlineDatasource> getDatasourceResult(OnlineForm form) { |
||||
if (form.getMasterTableId() == null) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "表单主表不存在!"); |
||||
} |
||||
OnlineDatasource datasource = onlineDatasourceService.getOnlineDatasourceByMasterTableId(form.getMasterTableId()); |
||||
if (datasource == null) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "表单数据源不存在!"); |
||||
} |
||||
return onlineOperationHelper.verifyAndGetDatasource(datasource.getDatasourceId()); |
||||
} |
||||
|
||||
/** |
||||
* 取列表查询用的在线表单表头。onl_form_head 以 table_name 为业务键(与 zz_online_form.form_id 不是同一 id), |
||||
* 必须按主表表名定位,而非按 OnlineForm.formId。 |
||||
*/ |
||||
private OnlFormHead getFormHead(ResponseResult<OnlineDatasource> datasourceResult) { |
||||
if (datasourceResult == null || !datasourceResult.isSuccess() || datasourceResult.getData() == null |
||||
|| datasourceResult.getData().getMasterTable() == null) { |
||||
return null; |
||||
} |
||||
return onlFormHeadService.getHeadTableNameCache(datasourceResult.getData().getMasterTable().getTableName()); |
||||
} |
||||
|
||||
/** |
||||
* 判断表单 widget 是否配置了 pc.tableWidget(列表查询/条件匹配的 ORM 回退路径依赖它,缺失会空指针)。 |
||||
*/ |
||||
private boolean hasListTableWidget(OnlineForm form) { |
||||
if (form == null || form.getFormId() == null) { |
||||
return false; |
||||
} |
||||
OnlineForm cached = onlineFormService.getOnlineFormFromCache(form.getFormId()); |
||||
if (cached == null || StrUtil.isBlank(cached.getWidgetJson())) { |
||||
return false; |
||||
} |
||||
JSONObject widget = JSON.parseObject(cached.getWidgetJson()); |
||||
JSONObject pc = widget == null ? null : widget.getJSONObject("pc"); |
||||
return pc != null && pc.containsKey("tableWidget"); |
||||
} |
||||
|
||||
/** |
||||
* 判断表单 widget 是否存在「启用且绑定插件」的同名操作按钮(与 executePlugin 的按钮解析一致,取 pc.operationList)。 |
||||
*/ |
||||
private boolean hasEnabledOperation(OnlineForm form, String buttonName) { |
||||
if (form == null || form.getFormId() == null || StrUtil.isBlank(buttonName)) { |
||||
return false; |
||||
} |
||||
OnlineForm cached = onlineFormService.getOnlineFormFromCache(form.getFormId()); |
||||
if (cached == null || StrUtil.isBlank(cached.getWidgetJson())) { |
||||
return false; |
||||
} |
||||
JSONObject pc = JSON.parseObject(cached.getWidgetJson()).getJSONObject("pc"); |
||||
JSONArray ops = pc == null ? null : pc.getJSONArray("operationList"); |
||||
if (ops == null) { |
||||
return false; |
||||
} |
||||
for (int i = 0; i < ops.size(); i++) { |
||||
JSONObject op = ops.getJSONObject(i); |
||||
if (op == null || !buttonName.equals(op.getString("name"))) { |
||||
continue; |
||||
} |
||||
JSONArray pluginList = op.getJSONArray("pluginList"); |
||||
if (Boolean.TRUE.equals(op.getBoolean("enabled")) && pluginList != null && !pluginList.isEmpty()) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* 安全地把接口入参解析为整数(兼容 1.0 等数值形态),非法/缺省回退默认值,并夹到 [min, max]。 |
||||
*/ |
||||
private int toIntParam(Object value, int defaultValue, int min, int max) { |
||||
if (value == null || StrUtil.isBlank(String.valueOf(value))) { |
||||
return defaultValue; |
||||
} |
||||
try { |
||||
long longValue = new BigDecimal(String.valueOf(value).trim()).longValue(); |
||||
if (longValue < min) { |
||||
return min; |
||||
} |
||||
if (longValue > max) { |
||||
return max; |
||||
} |
||||
return (int) longValue; |
||||
} catch (Exception e) { |
||||
return defaultValue; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 按 param_config.conditions 与请求参数构造过滤条件列表(未传参数的条件跳过)。 |
||||
* 仅放行在线列表引擎真正支持的条件 op(eq/like/in/between),其余直接拒绝,避免条件被静默丢弃导致 CONDITION_MATCH 放大执行。 |
||||
*/ |
||||
private ResponseResult<List<OnlineFilterDto>> buildFilterDtoList(JSONObject paramConfig, Map<String, Object> requestParams, OnlineForm form) { |
||||
List<OnlineFilterDto> result = new ArrayList<>(); |
||||
if (paramConfig == null) { |
||||
return ResponseResult.success(result); |
||||
} |
||||
JSONArray conditions = paramConfig.getJSONArray("conditions"); |
||||
if (conditions == null) { |
||||
return ResponseResult.success(result); |
||||
} |
||||
String masterTableName = this.getMasterTableName(form); |
||||
for (int i = 0; i < conditions.size(); i++) { |
||||
JSONObject cond = conditions.getJSONObject(i); |
||||
String param = cond.getString("param"); |
||||
Object value = requestParams.get(param); |
||||
String target = cond.getString("target"); |
||||
String field = cond.getString("field"); |
||||
String table = cond.getString("table"); |
||||
String attr = cond.getString("attr"); |
||||
String op = cond.getString("op"); |
||||
// between 无单一入参值(靠 columnValueStart/End 两个入参名取上下界),不走单值跳过
|
||||
boolean isBetween = "between".equalsIgnoreCase(op == null ? "" : op.trim()); |
||||
if (!isBetween && (value == null || (value instanceof String && StrUtil.isBlank((String) value)))) { |
||||
continue; |
||||
} |
||||
// RELATION_FIELD 条件:attr 值先反查关联主键集合,再用 主表存储列 IN 过滤
|
||||
if (OpenApiFieldSource.RELATION_FIELD.equals(target) && attr != null && !"id".equals(attr)) { |
||||
result.add(this.buildRelationFilter(form, field, attr, value, masterTableName)); |
||||
continue; |
||||
} |
||||
Integer filterType = this.mapOp(op); |
||||
if (filterType == null) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, |
||||
"暂不支持的条件 op=[" + op + "],仅支持 eq/like/in/between!"); |
||||
} |
||||
OnlineFilterDto filter = new OnlineFilterDto(); |
||||
filter.setTableName(OpenApiFieldSource.ENTRY_FIELD.equals(target) ? table : masterTableName); |
||||
filter.setColumnName(field); |
||||
filter.setFilterType(filterType); |
||||
if (filterType == FieldFilterType.RANGE_FILTER) { |
||||
// between 按 columnValueStart/End 取值(引擎 RANGE 分支读取的就是这两个字段,不是 columnValue)
|
||||
Object startVal = this.rangeValue(requestParams, cond.getString("columnValueStart")); |
||||
Object endVal = this.rangeValue(requestParams, cond.getString("columnValueEnd")); |
||||
if (startVal == null && endVal == null) { |
||||
return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, |
||||
"between 条件需在 conditions 中配置 columnValueStart/columnValueEnd(入参名),且请求需携带对应值!"); |
||||
} |
||||
filter.setColumnValueStart(startVal); |
||||
filter.setColumnValueEnd(endVal); |
||||
} else if (filterType == FieldFilterType.IN_LIST_FILTER) { |
||||
// 引擎 IN_LIST 分支按 getColumnValue() 单值(逗号串)读取
|
||||
filter.setColumnValue(this.joinListValue(value)); |
||||
} else { |
||||
filter.setColumnValue(value); |
||||
} |
||||
result.add(filter); |
||||
} |
||||
return ResponseResult.success(result); |
||||
} |
||||
|
||||
/** |
||||
* 将入参列表拼成逗号串(在线列表引擎的 IN_LIST_FILTER 按 getColumnValue() 单值读取,见 ListDataOrmService#getFilter)。 |
||||
*/ |
||||
private String joinListValue(Object value) { |
||||
if (value instanceof Collection) { |
||||
return StrUtil.join(",", (Collection<?>) value); |
||||
} |
||||
return String.valueOf(value); |
||||
} |
||||
|
||||
/** |
||||
* 取 between 的上/下界值:conditions 里的 columnValueStart/columnValueEnd 记录的是请求体入参名。 |
||||
*/ |
||||
private Object rangeValue(Map<String, Object> requestParams, String paramName) { |
||||
return StrUtil.isBlank(paramName) ? null : requestParams.get(paramName); |
||||
} |
||||
|
||||
/** |
||||
* 关联字段条件过滤:按 attr 列反查关联主键集合,返回 主表存储列 IN (ids) 过滤;无匹配时用占位值保证结果为空。 |
||||
*/ |
||||
private OnlineFilterDto buildRelationFilter(OnlineForm form, String masterColumn, String attr, Object value, String masterTableName) { |
||||
Set<String> relatedIds = new HashSet<>(); |
||||
if (value instanceof Collection) { |
||||
for (Object item : (Collection<?>) value) { |
||||
relatedIds.addAll(openApiOnlineQueryService.resolveRelatedIds(form, masterColumn, attr, item)); |
||||
} |
||||
} else { |
||||
relatedIds.addAll(openApiOnlineQueryService.resolveRelatedIds(form, masterColumn, attr, value)); |
||||
} |
||||
OnlineFilterDto filter = new OnlineFilterDto(); |
||||
filter.setTableName(masterTableName); |
||||
filter.setColumnName(masterColumn); |
||||
filter.setFilterType(FieldFilterType.IN_LIST_FILTER); |
||||
if (relatedIds.isEmpty()) { |
||||
// 无匹配关联 → 用不可能命中的占位值,保证查询结果为空
|
||||
relatedIds.add("-1"); |
||||
} |
||||
// 在线列表引擎按 getColumnValue() 单值读取 IN 条件(逗号串),非 columnValueList
|
||||
filter.setColumnValue(StrUtil.join(",", relatedIds)); |
||||
return filter; |
||||
} |
||||
|
||||
/** |
||||
* 按输出配置裁剪列表行数据(含一对多分录 ENTRY_ARRAY 的逐行组装)。 |
||||
*/ |
||||
private List<JSONObject> assembleDataList(List<Map<String, Object>> dataList, String outputConfigJson, OnlineForm form) { |
||||
List<JSONObject> result = new ArrayList<>(); |
||||
if (dataList == null || StrUtil.isBlank(outputConfigJson)) { |
||||
return result; |
||||
} |
||||
JSONObject outputConfig = JSON.parseObject(outputConfigJson); |
||||
JSONArray dataFields = outputConfig.getJSONArray("dataFields"); |
||||
if (dataFields == null) { |
||||
return result; |
||||
} |
||||
for (Map<String, Object> row : dataList) { |
||||
JSONObject item = new JSONObject(true); |
||||
this.mapRow(item, row, dataFields, form); |
||||
result.add(item); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
private void mapRow(JSONObject target, Map<String, Object> row, JSONArray dataFields, OnlineForm form) { |
||||
for (int i = 0; i < dataFields.size(); i++) { |
||||
JSONObject df = dataFields.getJSONObject(i); |
||||
String alias = df.getString("alias"); |
||||
String field = df.getString("field"); |
||||
String targetType = df.getString("target"); |
||||
if (OpenApiFieldSource.ENTRY_ARRAY.equals(targetType)) { |
||||
// 一对多分录:按 master 行主键查子表关联行,按 children 裁剪字段
|
||||
Object masterId = row.get("id"); |
||||
String subTable = df.getString("table"); |
||||
target.put(alias, this.assembleEntryList(form, masterId, subTable, df.getJSONArray("children"))); |
||||
continue; |
||||
} |
||||
Object value = row.get(field); |
||||
if (value == null) { |
||||
value = row.get(alias); |
||||
} |
||||
if (value == null) { |
||||
value = row.get(StrUtil.toUnderlineCase(field)); |
||||
} |
||||
target.put(alias, value); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 组装一对多分录行:查子表数据并仅保留 children 中配置的字段。 |
||||
*/ |
||||
private JSONArray assembleEntryList(OnlineForm form, Object masterId, String subTable, JSONArray children) { |
||||
JSONArray out = new JSONArray(); |
||||
if (masterId == null || StrUtil.isBlank(subTable) || children == null) { |
||||
return out; |
||||
} |
||||
List<Map<String, Object>> subRows = openApiOnlineQueryService.querySubTableRows(form, masterId, subTable); |
||||
if (subRows == null) { |
||||
return out; |
||||
} |
||||
for (Map<String, Object> subRow : subRows) { |
||||
JSONObject item = new JSONObject(true); |
||||
for (int c = 0; c < children.size(); c++) { |
||||
JSONObject child = children.getJSONObject(c); |
||||
String childAlias = child.getString("alias"); |
||||
String childField = child.getString("field"); |
||||
if (StrUtil.isBlank(childAlias)) { |
||||
continue; |
||||
} |
||||
Object v = subRow.get(childField); |
||||
if (v == null) { |
||||
v = subRow.get(childAlias); |
||||
} |
||||
if (v == null) { |
||||
v = subRow.get(StrUtil.toUnderlineCase(childField)); |
||||
} |
||||
item.put(childAlias, v); |
||||
} |
||||
out.add(item); |
||||
} |
||||
return out; |
||||
} |
||||
|
||||
/** |
||||
* 映射条件 op 到 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(); |
||||
} |
||||
} |
||||
@ -0,0 +1,210 @@
@@ -0,0 +1,210 @@
|
||||
package apelet.common.openapi.service.impl; |
||||
|
||||
import apelet.common.core.annotation.MyDataSourceResolver; |
||||
import apelet.common.core.constant.ApplicationConstant; |
||||
import apelet.common.core.object.MyPageData; |
||||
import apelet.common.core.object.ObjectCollection; |
||||
import apelet.common.core.object.ObjectValue; |
||||
import apelet.common.core.object.ResponseResult; |
||||
import apelet.common.core.util.DefaultDataSourceResolver; |
||||
import apelet.common.generator.model.OnlFormField; |
||||
import apelet.common.generator.model.OnlFormHead; |
||||
import apelet.common.generator.service.IOnlFormFieldService; |
||||
import apelet.common.generator.service.IOnlFormHeadService; |
||||
import apelet.common.generator.utils.OrmGenDataSourceUtil; |
||||
import apelet.common.online.dto.OnlineFilterDto; |
||||
import apelet.common.online.model.OnlineColumn; |
||||
import apelet.common.online.model.OnlineDatasource; |
||||
import apelet.common.online.model.OnlineDatasourceRelation; |
||||
import apelet.common.online.model.OnlineForm; |
||||
import apelet.common.online.model.OnlineTable; |
||||
import apelet.common.online.model.constant.FieldFilterType; |
||||
import apelet.common.online.model.constant.RelationType; |
||||
import apelet.common.online.service.OnlineDatasourceService; |
||||
import apelet.common.online.service.OnlineOperationService; |
||||
import apelet.common.online.service.OnlineTableService; |
||||
import apelet.common.online.util.OnlineOperationHelper; |
||||
import apelet.common.openapi.service.OpenApiOnlineQueryService; |
||||
import apelet.common.orm.impl.Filter; |
||||
import apelet.common.orm.impl.FilterItem; |
||||
import apelet.common.orm.impl.Selector; |
||||
import apelet.common.orm.impl.SelectorItem; |
||||
import cn.hutool.core.util.StrUtil; |
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.HashSet; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
|
||||
/** |
||||
* OpenAPI 在线表数据查询辅助实现。 |
||||
* <p>标注在线数据源(COMMON_FLOW_AND_ONLINE),内部 ormGenDataSourceUtil 查询落在在线库。</p> |
||||
* |
||||
* @author chenchuchuan |
||||
* @date 2026-09-03 |
||||
*/ |
||||
@Slf4j |
||||
@Service("openApiOnlineQueryService") |
||||
@MyDataSourceResolver(resolver = DefaultDataSourceResolver.class, intArg = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE) |
||||
public class OpenApiOnlineQueryServiceImpl implements OpenApiOnlineQueryService { |
||||
|
||||
@Autowired |
||||
private IOnlFormHeadService onlFormHeadService; |
||||
@Autowired |
||||
private IOnlFormFieldService onlFormFieldService; |
||||
@Autowired |
||||
private OnlineTableService onlineTableService; |
||||
@Autowired |
||||
private OnlineDatasourceService onlineDatasourceService; |
||||
@Autowired |
||||
private OnlineOperationService onlineOperationService; |
||||
@Autowired |
||||
private OnlineOperationHelper onlineOperationHelper; |
||||
@Autowired |
||||
private OrmGenDataSourceUtil ormGenDataSourceUtil; |
||||
|
||||
@Override |
||||
public Object resolveRelatedId(OnlineForm masterForm, String masterColumnName, String attrColumn, Object attrValue) { |
||||
if (masterForm == null || masterForm.getMasterTableId() == null || StrUtil.isBlank(masterColumnName) |
||||
|| StrUtil.isBlank(attrColumn) || attrValue == null) { |
||||
return null; |
||||
} |
||||
try { |
||||
OnlineTable relTable = this.findRelTable(masterForm, masterColumnName); |
||||
if (relTable == null || relTable.getPrimaryKeyColumn() == null) { |
||||
return null; |
||||
} |
||||
OnlineColumn pkColumn = relTable.getPrimaryKeyColumn(); |
||||
ObjectCollection collection = this.queryRelTable(relTable, attrColumn, attrValue, pkColumn); |
||||
if (collection == null || collection.isEmpty()) { |
||||
return null; |
||||
} |
||||
ObjectValue obj = collection.getObject(0); |
||||
return obj == null ? null : obj.getString(pkColumn.getColumnName()); |
||||
} catch (Exception e) { |
||||
log.error("OpenAPI 关联字段反查失败:masterColumn={}, attrColumn={}, value={}", masterColumnName, attrColumn, attrValue, e); |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public Set<String> resolveRelatedIds(OnlineForm masterForm, String masterColumnName, String attrColumn, Object attrValue) { |
||||
Set<String> result = new HashSet<>(); |
||||
if (masterForm == null || masterForm.getMasterTableId() == null || StrUtil.isBlank(masterColumnName) |
||||
|| StrUtil.isBlank(attrColumn) || attrValue == null) { |
||||
return result; |
||||
} |
||||
try { |
||||
OnlineTable relTable = this.findRelTable(masterForm, masterColumnName); |
||||
if (relTable == null || relTable.getPrimaryKeyColumn() == null) { |
||||
return result; |
||||
} |
||||
OnlineColumn pkColumn = relTable.getPrimaryKeyColumn(); |
||||
ObjectCollection collection = this.queryRelTable(relTable, attrColumn, attrValue, pkColumn); |
||||
if (collection != null) { |
||||
for (int i = 0; i < collection.size(); i++) { |
||||
ObjectValue obj = collection.getObject(i); |
||||
if (obj != null && obj.getString(pkColumn.getColumnName()) != null) { |
||||
result.add(obj.getString(pkColumn.getColumnName())); |
||||
} |
||||
} |
||||
} |
||||
} catch (Exception e) { |
||||
log.error("OpenAPI 关联字段批量反查失败:masterColumn={}, attrColumn={}, value={}", masterColumnName, attrColumn, attrValue, e); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public List<Map<String, Object>> querySubTableRows(OnlineForm masterForm, Object masterId, String subTableName) { |
||||
List<Map<String, Object>> result = new ArrayList<>(); |
||||
if (masterForm == null || masterForm.getMasterTableId() == null || masterId == null || StrUtil.isBlank(subTableName)) { |
||||
return result; |
||||
} |
||||
try { |
||||
OnlineDatasource datasource = onlineDatasourceService.getOnlineDatasourceByMasterTableId(masterForm.getMasterTableId()); |
||||
if (datasource == null) { |
||||
return result; |
||||
} |
||||
ResponseResult<List<OnlineDatasourceRelation>> relResult = |
||||
onlineOperationHelper.verifyAndGetRelationList(datasource.getDatasourceId(), null); |
||||
if (!relResult.isSuccess() || relResult.getData() == null) { |
||||
return result; |
||||
} |
||||
OnlineDatasourceRelation relation = relResult.getData().stream() |
||||
.filter(r -> r.getRelationType() != null && r.getRelationType() == RelationType.ONE_TO_MANY) |
||||
.filter(r -> subTableName.equals(this.slaveTableName(r))).findFirst().orElse(null); |
||||
if (relation == null || relation.getSlaveColumn() == null) { |
||||
return result; |
||||
} |
||||
// 子表过滤:slaveColumn(附表外键) = master 行主键
|
||||
List<OnlineFilterDto> filterList = new ArrayList<>(); |
||||
OnlineFilterDto filter = new OnlineFilterDto(); |
||||
filter.setTableName(subTableName); |
||||
filter.setColumnName(relation.getSlaveColumn().getColumnName()); |
||||
filter.setColumnValue(masterId); |
||||
filter.setFilterType(FieldFilterType.EQUAL_FILTER); |
||||
filterList.add(filter); |
||||
MyPageData<Map<String, Object>> pageData = |
||||
onlineOperationService.getSlaveDataList(relation, filterList, null, null); |
||||
return pageData == null ? result : pageData.getDataList(); |
||||
} catch (Exception e) { |
||||
log.error("OpenAPI 分录数据查询失败:subTable={}, masterId={}", subTableName, masterId, e); |
||||
return result; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 取关系从表名(slaveTable 未加载时按 slaveTableId 反查)。 |
||||
*/ |
||||
private String slaveTableName(OnlineDatasourceRelation relation) { |
||||
if (relation.getSlaveTable() != null) { |
||||
return relation.getSlaveTable().getTableName(); |
||||
} |
||||
OnlineTable table = onlineTableService.getById(relation.getSlaveTableId()); |
||||
return table == null ? null : table.getTableName(); |
||||
} |
||||
|
||||
/** |
||||
* 定位 master 表单中关联字段(field_name=masterColumnName 且 refPropert 非空)对应的关联表。 |
||||
*/ |
||||
private OnlineTable findRelTable(OnlineForm masterForm, String masterColumnName) { OnlineTable masterTable = onlineTableService.getById(masterForm.getMasterTableId()); |
||||
if (masterTable == null) { |
||||
return null; |
||||
} |
||||
OnlFormHead masterHead = onlFormHeadService.getOne(new LambdaQueryWrapper<OnlFormHead>() |
||||
.eq(OnlFormHead::getTableName, masterTable.getTableName())); |
||||
if (masterHead == null) { |
||||
return null; |
||||
} |
||||
List<OnlFormField> relFields = onlFormFieldService.list(new LambdaQueryWrapper<OnlFormField>() |
||||
.eq(OnlFormField::getHeadId, masterHead.getId()) |
||||
.eq(OnlFormField::getFieldName, masterColumnName) |
||||
.isNotNull(OnlFormField::getRefPropert)); |
||||
if (relFields == null || relFields.isEmpty()) { |
||||
return null; |
||||
} |
||||
OnlFormHead relHead = onlFormHeadService.getById(relFields.get(0).getRefPropert()); |
||||
if (relHead == null) { |
||||
return null; |
||||
} |
||||
return onlineTableService.getOne(new LambdaQueryWrapper<OnlineTable>() |
||||
.eq(OnlineTable::getTableName, relHead.getTableName())); |
||||
} |
||||
|
||||
/** |
||||
* 查询关联表:SELECT pk FROM 关联表 WHERE attrColumn = 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); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue