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 @@ |
|||||||
|
<?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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
<?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 @@ |
|||||||
|
<?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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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