Browse Source

feat(mcp): 完善MCP工具参数解析支持嵌套对象数组

- 实现List<E>中E为带@McpToolParam对象时按对象展开items功能
- 添加hasMcpToolParamFields方法判断类型是否需要递归展开
- 优化toObject方法支持Map类型直接返回原参逻辑
- 扩展OnlineQuickFormField模型添加McpToolParam注解映射
- 增强OpenApiApp和OpenApiConfig模型的参数描述细节
- 重构OpenApiAppController集成McpToolClass工具类标注
- 添加LayoutType排版类型常量字典及校验方法
- 更新OnlineFormDto中表单类型和排版类型的详细描述
- 优化OpenApiAuthService中的应用凭证校验逻辑
- 新增removeAppWithRelationCheck服务方法检查关联关系后删除
- 调整OpenApiConfigController返回OpenApiConfigVo视图对象
- 修复MyBatis查询中LIKE语句安全拼接问题
pull/1/head
chenchuchuan 1 week ago
parent
commit
4aa8edb542
  1. 45
      common/common-core/src/main/java/apelet/common/core/mcp/config/McpToolScanner.java
  2. 4
      common/common-core/src/main/java/apelet/common/core/object/TokenData.java
  3. 7
      common/common-online/src/main/java/apelet/common/online/dto/OnlineFormDto.java
  4. 3
      common/common-online/src/main/java/apelet/common/online/model/OnlineQuickForm.java
  5. 23
      common/common-online/src/main/java/apelet/common/online/model/OnlineQuickFormField.java
  6. 50
      common/common-online/src/main/java/apelet/common/online/model/constant/LayoutType.java
  7. 39
      common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiAppController.java
  8. 9
      common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiAuthController.java
  9. 78
      common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiConfigController.java
  10. 18
      common/common-openapi/src/main/java/apelet/common/openapi/controller/OpenApiRouterController.java
  11. 10
      common/common-openapi/src/main/java/apelet/common/openapi/dao/OpenApiConfigMapper.java
  12. 65
      common/common-openapi/src/main/java/apelet/common/openapi/dao/OpenAppApiRelationMapper.java
  13. 2
      common/common-openapi/src/main/java/apelet/common/openapi/dao/mapper/OpenApiAppMapper.xml
  14. 16
      common/common-openapi/src/main/java/apelet/common/openapi/dao/mapper/OpenApiConfigMapper.xml
  15. 38
      common/common-openapi/src/main/java/apelet/common/openapi/dao/mapper/OpenAppApiRelationMapper.xml
  16. 2
      common/common-openapi/src/main/java/apelet/common/openapi/document/OpenApiDocumentCustomizer.java
  17. 30
      common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiConfigFilterDto.java
  18. 5
      common/common-openapi/src/main/java/apelet/common/openapi/dto/OpenApiTokenDto.java
  19. 7
      common/common-openapi/src/main/java/apelet/common/openapi/model/OpenApiApp.java
  20. 47
      common/common-openapi/src/main/java/apelet/common/openapi/model/OpenApiConfig.java
  21. 32
      common/common-openapi/src/main/java/apelet/common/openapi/model/OpenAppApiRelation.java
  22. 8
      common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiAppService.java
  23. 35
      common/common-openapi/src/main/java/apelet/common/openapi/service/OpenApiConfigService.java
  24. 15
      common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiAppServiceImpl.java
  25. 3
      common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiAuthServiceImpl.java
  26. 76
      common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiConfigServiceImpl.java
  27. 25
      common/common-openapi/src/main/java/apelet/common/openapi/service/impl/OpenApiExecServiceImpl.java
  28. 104
      common/common-openapi/src/main/java/apelet/common/openapi/vo/OpenApiConfigVo.java

45
common/common-core/src/main/java/apelet/common/core/mcp/config/McpToolScanner.java

@ -233,14 +233,45 @@ public class McpToolScanner {
schema.put("required", required); schema.put("required", required);
} }
} else if (info.getType() == McpToolParamType.ARRAY && info.getElementType() != null) { } else if (info.getType() == McpToolParamType.ARRAY && info.getElementType() != null) {
// List<E>:E 为带 @McpToolParam 的对象时按对象展开 items,否则按基础类型映射
JSONObject items = new JSONObject(true); JSONObject items = new JSONObject(true);
items.put("type", this.toJsonType(info.getElementType())); if (this.hasMcpToolParamFields(info.getElementType())) {
JSONObject properties = new JSONObject(true);
JSONArray required = new JSONArray();
this.collectObjectSchema(info.getElementType(), properties, required);
items.put("type", "object");
if (!properties.isEmpty()) {
items.put("properties", properties);
}
if (!required.isEmpty()) {
items.put("required", required);
}
} else {
items.put("type", this.toJsonType(info.getElementType()));
}
schema.put("items", items); schema.put("items", items);
} }
return schema; return schema;
} }
/** /**
* 判断类型是否为需要递归展开的对象其声明字段中存在 @McpToolParam 标注
* <p>用于 List&lt;对象&gt; 场景元素类型带注解字段时按嵌套对象组装 schema 与运行时绑定
* 否则String/数值/布尔等按基础类型处理</p>
*/
private boolean hasMcpToolParamFields(Class<?> clazz) {
if (clazz == null) {
return false;
}
for (Field field : clazz.getDeclaredFields()) {
if (field.getAnnotation(McpToolParam.class) != null) {
return true;
}
}
return false;
}
/**
* 收集对象类型中 @McpToolParam 标注字段的嵌套 schema * 收集对象类型中 @McpToolParam 标注字段的嵌套 schema
*/ */
private void collectObjectSchema(Class<?> schemaClass, JSONObject properties, JSONArray required) { private void collectObjectSchema(Class<?> schemaClass, JSONObject properties, JSONArray required) {
@ -391,7 +422,12 @@ public class McpToolScanner {
List<?> source = raw instanceof List ? (List<?>) raw : Arrays.asList((Object[]) raw); List<?> source = raw instanceof List ? (List<?>) raw : Arrays.asList((Object[]) raw);
List<Object> converted = new ArrayList<>(); List<Object> converted = new ArrayList<>();
for (Object item : source) { for (Object item : source) {
converted.add(this.toScalar(item, elementType)); // List<对象>:元素为 Map 且元素类型带 @McpToolParam 字段时,逐条走 toObject 递归组装
if (this.hasMcpToolParamFields(elementType) && item instanceof Map) {
converted.add(this.toObject(item, elementType));
} else {
converted.add(this.toScalar(item, elementType));
}
} }
if (!paramType.isArray()) { if (!paramType.isArray()) {
return converted; return converted;
@ -405,11 +441,16 @@ public class McpToolScanner {
/** /**
* 构造对象实参反射 new schemaClass 实例遍历 @McpToolParam 字段取值转换后 set * 构造对象实参反射 new schemaClass 实例遍历 @McpToolParam 字段取值转换后 set
* <p>schemaClass Map 类型如方法参数是 Map&lt;String,Object&gt; 自由 key-value 结构时不做 POJO 展开
* 直接把已解析好的入参 Map 原样返回由业务方法自行按 key 取值</p>
*/ */
private Object toObject(Object raw, Class<?> schemaClass) { private Object toObject(Object raw, Class<?> schemaClass) {
if (!(raw instanceof Map) || schemaClass == null) { if (!(raw instanceof Map) || schemaClass == null) {
return null; return null;
} }
if (Map.class.isAssignableFrom(schemaClass)) {
return raw;
}
Map<?, ?> map = (Map<?, ?>) raw; Map<?, ?> map = (Map<?, ?>) raw;
try { try {
Object instance = schemaClass.getDeclaredConstructor().newInstance(); Object instance = schemaClass.getDeclaredConstructor().newInstance();

4
common/common-core/src/main/java/apelet/common/core/object/TokenData.java

@ -80,6 +80,10 @@ public class TokenData {
*/ */
private Integer deviceType; private Integer deviceType;
/** /**
* 用户类型
*/
private Integer userType;
/**
* 标识不同登录的会话Id * 标识不同登录的会话Id
*/ */
private String sessionId; private String sessionId;

7
common/common-online/src/main/java/apelet/common/online/dto/OnlineFormDto.java

@ -61,7 +61,7 @@ public class OnlineFormDto {
@Schema(description = "表单类别") @Schema(description = "表单类别")
@NotNull(message = "数据验证失败,表单类别不能为空!") @NotNull(message = "数据验证失败,表单类别不能为空!")
@ConstDictRef(constDictClass = FormKind.class, message = "数据验证失败,表单类别为无效值!") @ConstDictRef(constDictClass = FormKind.class, message = "数据验证失败,表单类别为无效值!")
@McpToolParam(fieldName = "formKind", type = McpToolParamType.INTEGER, description = "表单类别") @McpToolParam(fieldName = "formKind", type = McpToolParamType.INTEGER, description = "表单类别:1=弹框列表,5=跳页类别")
private Integer formKind; private Integer formKind;
/** /**
@ -70,7 +70,7 @@ public class OnlineFormDto {
@Schema(description = "表单类型") @Schema(description = "表单类型")
@NotNull(message = "数据验证失败,表单类型不能为空!") @NotNull(message = "数据验证失败,表单类型不能为空!")
@ConstDictRef(constDictClass = FormType.class, message = "数据验证失败,表单类型为无效值!") @ConstDictRef(constDictClass = FormType.class, message = "数据验证失败,表单类型为无效值!")
@McpToolParam(fieldName = "formType", type = McpToolParamType.INTEGER, description = "表单类型", required = true) @McpToolParam(fieldName = "formType", type = McpToolParamType.INTEGER, description = "表单类型:1=查询表单,2=左树右表表单,3=一对一关联数据查询,5=编辑表单,10=流程表单,11=流程工单表单,15=快捷表单", required = true)
private Integer formType; private Integer formType;
/** /**
@ -113,12 +113,13 @@ public class OnlineFormDto {
* 排版类型快捷表单 * 排版类型快捷表单
*/ */
@Schema(description = "排版类型") @Schema(description = "排版类型")
@McpToolParam(fieldName = "layoutType", type = McpToolParamType.INTEGER, description = "排版类型(快捷表单)") @McpToolParam(fieldName = "layoutType", type = McpToolParamType.INTEGER, description = "排版类型(快捷表单):0=一行一列,1=一行两列,2=一行三列")
private Integer layoutType; private Integer layoutType;
/** /**
* 快捷表单字段列表 * 快捷表单字段列表
*/ */
@Schema(description = "快捷表单字段列表") @Schema(description = "快捷表单字段列表")
@McpToolParam(fieldName = "onlineQuickFormFields", type = McpToolParamType.ARRAY, description = "快捷表单字段列表(每项见 OnlineQuickFormField 字段)")
private List<OnlineQuickFormField> onlineQuickFormFields; private List<OnlineQuickFormField> onlineQuickFormFields;
} }

3
common/common-online/src/main/java/apelet/common/online/model/OnlineQuickForm.java

@ -61,7 +61,8 @@ public class OnlineQuickForm {
@TableField(value = "quick_form_name") @TableField(value = "quick_form_name")
private String quickFormName; private String quickFormName;
/** /**
* 排版类型 * 排版类型 0 一行一列1一行两列2 一行三列
* @see apelet.common.online.model.constant.LayoutType
*/ */
@TableField(value = "layout_type") @TableField(value = "layout_type")
private Integer layoutType; private Integer layoutType;

23
common/common-online/src/main/java/apelet/common/online/model/OnlineQuickFormField.java

@ -1,5 +1,7 @@
package apelet.common.online.model; package apelet.common.online.model;
import apelet.common.core.mcp.annotation.McpToolParam;
import apelet.common.core.mcp.model.McpToolParamType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
@ -33,126 +35,147 @@ public class OnlineQuickFormField {
* 字段名称 * 字段名称
*/ */
@TableField(value = "field_name") @TableField(value = "field_name")
@McpToolParam(fieldName = "fieldName", type = McpToolParamType.STRING, description = "字段名称", required = true)
private String fieldName; private String fieldName;
/** /**
* 字段备注 * 字段备注
*/ */
@TableField(value = "field_remark") @TableField(value = "field_remark")
@McpToolParam(fieldName = "fieldRemark", type = McpToolParamType.STRING, description = "字段备注")
private String fieldRemark; private String fieldRemark;
/** /**
* 字段长度 * 字段长度
*/ */
@TableField(value = "field_length") @TableField(value = "field_length")
@McpToolParam(fieldName = "fieldLength", type = McpToolParamType.INTEGER, description = "字段长度")
private Integer fieldLength; private Integer fieldLength;
/** /**
* 小数点位数 * 小数点位数
*/ */
@TableField(value = "field_point") @TableField(value = "field_point")
@McpToolParam(fieldName = "fieldPoint", type = McpToolParamType.INTEGER, description = "小数点位数")
private Integer fieldPoint; private Integer fieldPoint;
/** /**
* 字段类型前端展示类型即组件widgetType * 字段类型前端展示类型即组件widgetType
*/ */
@TableField(value = "field_type") @TableField(value = "field_type")
@McpToolParam(fieldName = "fieldType", type = McpToolParamType.INTEGER, description = "字段类型(前端展示类型,即组件widgetType)")
private Integer fieldType; private Integer fieldType;
/** /**
* 关联 zz_online_column 列ID前端传入为空=新增字段待建列非空=已有字段仅修改 * 关联 zz_online_column 列ID前端传入为空=新增字段待建列非空=已有字段仅修改
*/ */
@TableField(value = "column_id") @TableField(value = "column_id")
@McpToolParam(fieldName = "columnId", type = McpToolParamType.INTEGER, description = "关联在线列Id(空=新增字段,非空=已有字段修改)")
private Long columnId; private Long columnId;
/** /**
* 是否为主键0-1- * 是否为主键0-1-
*/ */
@TableField(value = "is_key") @TableField(value = "is_key")
@McpToolParam(fieldName = "isKey", type = McpToolParamType.INTEGER, description = "是否主键:0-否,1-是")
private Integer isKey; private Integer isKey;
/** /**
* 默认值 * 默认值
*/ */
@TableField(value = "default_value") @TableField(value = "default_value")
@McpToolParam(fieldName = "defaultValue", type = McpToolParamType.STRING, description = "默认值")
private String defaultValue; private String defaultValue;
/** /**
* 是否允许为空0-1- * 是否允许为空0-1-
*/ */
@TableField(value = "is_empty") @TableField(value = "is_empty")
@McpToolParam(fieldName = "isEmpty", type = McpToolParamType.INTEGER, description = "是否允许为空:0-否,1-是")
private Integer isEmpty; private Integer isEmpty;
/** /**
* 是否为默认字段0-1- * 是否为默认字段0-1-
*/ */
@TableField(value = "is_default") @TableField(value = "is_default")
@McpToolParam(fieldName = "isDefault", type = McpToolParamType.INTEGER, description = "是否默认字段:0-否,1-是")
private Integer isDefault; private Integer isDefault;
/** /**
* 是否列表展示0-1- * 是否列表展示0-1-
*/ */
@TableField(value = "is_list") @TableField(value = "is_list")
@McpToolParam(fieldName = "isList", type = McpToolParamType.INTEGER, description = "是否列表展示:0-否,1-是")
private Integer isList; private Integer isList;
/** /**
* 是否过滤条件0-1- * 是否过滤条件0-1-
*/ */
@TableField(value = "is_filter") @TableField(value = "is_filter")
@McpToolParam(fieldName = "isFilter", type = McpToolParamType.INTEGER, description = "是否过滤条件:0-否,1-是")
private Integer isFilter; private Integer isFilter;
/** /**
* 过滤方案参考FieldFilterType常量 * 过滤方案参考FieldFilterType常量
*/ */
@TableField(value = "filter_type") @TableField(value = "filter_type")
@McpToolParam(fieldName = "filterType", type = McpToolParamType.INTEGER, description = "过滤方案(参考FieldFilterType常量)")
private Integer filterType; private Integer filterType;
/** /**
* 关联元数据ID * 关联元数据ID
*/ */
@TableField(value = "ref_property") @TableField(value = "ref_property")
@McpToolParam(fieldName = "refProperty", type = McpToolParamType.INTEGER, description = "关联元数据Id")
private Long refProperty; private Long refProperty;
/** /**
* 关联数据字典ID * 关联数据字典ID
*/ */
@TableField(value = "ref_dict") @TableField(value = "ref_dict")
@McpToolParam(fieldName = "refDict", type = McpToolParamType.INTEGER, description = "关联数据字典Id")
private Long refDict; private Long refDict;
/** /**
* 关联数据字典名称 * 关联数据字典名称
*/ */
@TableField(value = "ref_dict_name") @TableField(value = "ref_dict_name")
@McpToolParam(fieldName = "refDictName", type = McpToolParamType.STRING, description = "关联数据字典名称")
private String refDictName; private String refDictName;
/** /**
* 关联在线表单idF7关联字段前端传入用于解析关联表不落库 * 关联在线表单idF7关联字段前端传入用于解析关联表不落库
*/ */
@TableField(exist = false) @TableField(exist = false)
@McpToolParam(fieldName = "formId", type = McpToolParamType.INTEGER, description = "关联在线表单Id(F7关联字段)")
private Long formId; private Long formId;
/** /**
* 关联在线表单显示字段F7关联字段前端传入不落库 * 关联在线表单显示字段F7关联字段前端传入不落库
*/ */
@TableField(exist = false) @TableField(exist = false)
@McpToolParam(fieldName = "displayField", type = McpToolParamType.STRING, description = "关联在线表单显示字段(F7关联字段)")
private String displayField; private String displayField;
/** /**
* 关联表头idF7关联字段由关联在线表单解析出 onl_form_head.id用于 F7 控件 slaveTableId onl_form_field.ref_propert 回写不落库 * 关联表头idF7关联字段由关联在线表单解析出 onl_form_head.id用于 F7 控件 slaveTableId onl_form_field.ref_propert 回写不落库
*/ */
@TableField(exist = false) @TableField(exist = false)
@McpToolParam(fieldName = "refTableId", type = McpToolParamType.INTEGER, description = "关联表头Id(F7关联字段)")
private Long refTableId; private Long refTableId;
/** /**
* 关联表名F7关联字段由关联在线表单解析不落库 * 关联表名F7关联字段由关联在线表单解析不落库
*/ */
@TableField(exist = false) @TableField(exist = false)
@McpToolParam(fieldName = "refTableName", type = McpToolParamType.STRING, description = "关联表名(F7关联字段)")
private String refTableName; private String refTableName;
/** /**
* 关联在线表单编码F7关联字段由关联在线表单解析用于 relativeFormCode不落库 * 关联在线表单编码F7关联字段由关联在线表单解析用于 relativeFormCode不落库
*/ */
@TableField(exist = false) @TableField(exist = false)
@McpToolParam(fieldName = "refFormCode", type = McpToolParamType.STRING, description = "关联在线表单编码(F7关联字段)")
private String refFormCode; private String refFormCode;
/** /**

50
common/common-online/src/main/java/apelet/common/online/model/constant/LayoutType.java

@ -0,0 +1,50 @@
package apelet.common.online.model.constant;
import java.util.HashMap;
import java.util.Map;
/**
* 快捷表单排版类型常量字典对象
*
* @author chenchuchuan
* @date 2026-09-08
*/
public final class LayoutType {
/**
* 一行一列
*/
public static final int SINGLE_COLUMN = 0;
/**
* 一行两列
*/
public static final int DOUBLE_COLUMN = 1;
/**
* 一行三列
*/
public static final int TRIPLE_COLUMN = 2;
private static final Map<Object, String> DICT_MAP = new HashMap<>(3);
static {
DICT_MAP.put(SINGLE_COLUMN, "一行一列");
DICT_MAP.put(DOUBLE_COLUMN, "一行两列");
DICT_MAP.put(TRIPLE_COLUMN, "一行三列");
}
/**
* 私有构造函数明确标识该常量类的作用
*/
private LayoutType() {
}
/**
* 判断参数是否为当前常量字典的合法值
*
* @param value 待验证的参数值
* @return 合法返回true否则false
*/
public static boolean isValid(Integer value) {
return value != null && DICT_MAP.containsKey(value);
}
}

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

@ -2,6 +2,9 @@ package apelet.common.openapi.controller;
import apelet.common.core.annotation.MyRequestBody; import apelet.common.core.annotation.MyRequestBody;
import apelet.common.core.constant.ErrorCodeEnum; import apelet.common.core.constant.ErrorCodeEnum;
import apelet.common.core.mcp.annotation.McpToolClass;
import apelet.common.core.mcp.annotation.McpToolParam;
import apelet.common.core.mcp.model.McpToolParamType;
import apelet.common.core.object.MyPageData; import apelet.common.core.object.MyPageData;
import apelet.common.core.object.MyPageParam; import apelet.common.core.object.MyPageParam;
import apelet.common.core.object.ResponseResult; import apelet.common.core.object.ResponseResult;
@ -9,9 +12,7 @@ import apelet.common.core.object.TokenData;
import apelet.common.core.util.MyCommonUtil; import apelet.common.core.util.MyCommonUtil;
import apelet.common.core.util.MyPageUtil; import apelet.common.core.util.MyPageUtil;
import apelet.common.openapi.model.OpenApiApp; import apelet.common.openapi.model.OpenApiApp;
import apelet.common.openapi.model.OpenApiConfig;
import apelet.common.openapi.service.OpenApiAppService; import apelet.common.openapi.service.OpenApiAppService;
import apelet.common.openapi.service.OpenApiConfigService;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.github.pagehelper.page.PageMethod; import com.github.pagehelper.page.PageMethod;
@ -41,14 +42,14 @@ public class OpenApiAppController {
@Autowired @Autowired
private OpenApiAppService openApiAppService; private OpenApiAppService openApiAppService;
@Autowired
private OpenApiConfigService openApiConfigService;
/** /**
* 分页查询应用列表不回显 appSecret * 分页查询应用列表不回显 appSecret
*/ */
@PostMapping("/list") @PostMapping("/list")
public ResponseResult<MyPageData<OpenApiApp>> list(@MyRequestBody OpenApiApp openApiAppFilter, @MyRequestBody MyPageParam pageParam) { @McpToolClass(name = "openapi_app_list", description = "分页查询 OpenAPI 第三方应用列表(appSecret 已打码),可按 appCode/appName/status 过滤")
public ResponseResult<MyPageData<OpenApiApp>> list(@MyRequestBody @McpToolParam(fieldName = "openApiAppFilter", type = McpToolParamType.OBJECT, description = "应用过滤条件", schemaClass = OpenApiApp.class) OpenApiApp openApiAppFilter
, @MyRequestBody @McpToolParam(fieldName = "pageParam", type = McpToolParamType.OBJECT, description = "分页参数(pageNum/pageSize)", schemaClass = MyPageParam.class) MyPageParam pageParam) {
if (pageParam != null) { if (pageParam != null) {
PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
} }
@ -62,7 +63,8 @@ public class OpenApiAppController {
* 新增应用appCode 未填则自动生成appSecret 仅此刻返回明文 * 新增应用appCode 未填则自动生成appSecret 仅此刻返回明文
*/ */
@PostMapping("/add") @PostMapping("/add")
public ResponseResult<OpenApiApp> add(@MyRequestBody OpenApiApp openApiApp) { @McpToolClass(name = "openapi_app_add", description = "新增 OpenAPI 第三方应用(appCode 不传自动生成),返回对象含明文 appSecret,请妥善保存")
public ResponseResult<OpenApiApp> add(@MyRequestBody @McpToolParam(fieldName = "openApiApp", type = McpToolParamType.OBJECT, description = "应用信息(appName 必填)", schemaClass = OpenApiApp.class, required = true) OpenApiApp openApiApp) {
if (openApiApp == null || MyCommonUtil.existBlankArgument(openApiApp.getAppName())) { if (openApiApp == null || MyCommonUtil.existBlankArgument(openApiApp.getAppName())) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST, "应用名称不能为空!"); return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST, "应用名称不能为空!");
} }
@ -76,7 +78,8 @@ public class OpenApiAppController {
* 更新应用不修改密钥 * 更新应用不修改密钥
*/ */
@PostMapping("/update") @PostMapping("/update")
public ResponseResult<Void> update(@MyRequestBody OpenApiApp openApiApp) { @McpToolClass(name = "openapi_app_update", description = "更新 OpenAPI 第三方应用(appCode/appSecret 不可改,需传 id)")
public ResponseResult<Void> update(@MyRequestBody @McpToolParam(fieldName = "openApiApp", type = McpToolParamType.OBJECT, description = "应用信息(id 必填,appName/status 等可改)", schemaClass = OpenApiApp.class, required = true) OpenApiApp openApiApp) {
if (openApiApp == null || MyCommonUtil.existBlankArgument(openApiApp.getId())) { if (openApiApp == null || MyCommonUtil.existBlankArgument(openApiApp.getId())) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
} }
@ -84,7 +87,7 @@ public class OpenApiAppController {
if (original == null) { if (original == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
} }
// 应用编码作为对外唯一标识且被开放接口配置(xy_sys_open_api_config.app_code)引用,不允许修改 // 应用编码作为对外唯一标识且被授权关联表(xy_sys_open_app_api)引用,不允许修改
if (StrUtil.isNotBlank(openApiApp.getAppCode()) if (StrUtil.isNotBlank(openApiApp.getAppCode())
&& !openApiApp.getAppCode().equals(original.getAppCode())) { && !openApiApp.getAppCode().equals(original.getAppCode())) {
return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "应用编码不允许修改!"); return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "应用编码不允许修改!");
@ -107,20 +110,14 @@ public class OpenApiAppController {
* 删除应用已被开放配置引用的应用禁止删除 * 删除应用已被开放配置引用的应用禁止删除
*/ */
@PostMapping("/delete") @PostMapping("/delete")
public ResponseResult<Void> delete(@MyRequestBody Long id) { @McpToolClass(name = "openapi_app_delete", description = "删除 OpenAPI 第三方应用(已被开放接口配置引用时禁止删除),需传 id")
public ResponseResult<Void> delete(@MyRequestBody @McpToolParam(fieldName = "id", type = McpToolParamType.INTEGER, description = "应用主键Id", required = true) Long id) {
if (MyCommonUtil.existBlankArgument(id)) { if (MyCommonUtil.existBlankArgument(id)) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
} }
OpenApiApp app = openApiAppService.getById(id); if (!openApiAppService.removeAppWithRelationCheck(id)) {
if (app == null) { return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "该应用已被分配开放接口,不能删除!");
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(); return ResponseResult.success();
} }
@ -128,7 +125,8 @@ public class OpenApiAppController {
* 重置应用密钥旧密钥立即失效 * 重置应用密钥旧密钥立即失效
*/ */
@PostMapping("/resetSecret") @PostMapping("/resetSecret")
public ResponseResult<JSONObject> resetSecret(@MyRequestBody Long id) { @McpToolClass(name = "openapi_app_reset_secret", description = "重置 OpenAPI 第三方应用密钥(旧密钥立即失效),返回新明文 appSecret")
public ResponseResult<JSONObject> resetSecret(@MyRequestBody @McpToolParam(fieldName = "id", type = McpToolParamType.INTEGER, description = "应用主键Id", required = true) Long id) {
if (MyCommonUtil.existBlankArgument(id)) { if (MyCommonUtil.existBlankArgument(id)) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
} }
@ -145,7 +143,8 @@ public class OpenApiAppController {
* 查看应用密钥明文前端"小眼睛"调用受应用登录鉴权保护 * 查看应用密钥明文前端"小眼睛"调用受应用登录鉴权保护
*/ */
@PostMapping("/viewSecret") @PostMapping("/viewSecret")
public ResponseResult<JSONObject> viewSecret(@MyRequestBody Long id) { @McpToolClass(name = "openapi_app_view_secret", description = "查看 OpenAPI 第三方应用密钥明文,需传 id")
public ResponseResult<JSONObject> viewSecret(@MyRequestBody @McpToolParam(fieldName = "id", type = McpToolParamType.INTEGER, description = "应用主键Id", required = true) Long id) {
if (MyCommonUtil.existBlankArgument(id)) { if (MyCommonUtil.existBlankArgument(id)) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
} }

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

@ -1,6 +1,9 @@
package apelet.common.openapi.controller; package apelet.common.openapi.controller;
import apelet.common.core.constant.ErrorCodeEnum; import apelet.common.core.constant.ErrorCodeEnum;
import apelet.common.core.mcp.annotation.McpToolClass;
import apelet.common.core.mcp.annotation.McpToolParam;
import apelet.common.core.mcp.model.McpToolParamType;
import apelet.common.core.object.ResponseResult; import apelet.common.core.object.ResponseResult;
import apelet.common.openapi.dto.OpenApiTokenDto; import apelet.common.openapi.dto.OpenApiTokenDto;
import apelet.common.openapi.service.OpenApiAuthService; import apelet.common.openapi.service.OpenApiAuthService;
@ -34,11 +37,11 @@ public class OpenApiAuthController {
* 按应用编码 + 密钥 + 用户名换取 token第三方标准扁平 JSON body * 按应用编码 + 密钥 + 用户名换取 token第三方标准扁平 JSON body
*/ */
@PostMapping("/token") @PostMapping("/token")
public ResponseResult<JSONObject> token(@RequestBody(required = false) OpenApiTokenDto tokenDto) { @McpToolClass(name = "openapi_auth_token", description = "按应用编码+密钥+用户名换取 OpenAPI 访问 token(返回 accessToken/expiresIn)")
public ResponseResult<JSONObject> token(@RequestBody(required = false) @McpToolParam(fieldName = "tokenDto", type = McpToolParamType.OBJECT, description = "换 token 请求体(appCode/appSecret/username)", schemaClass = OpenApiTokenDto.class) OpenApiTokenDto tokenDto) {
if (tokenDto == null) { if (tokenDto == null) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
} }
return openApiAuthService.issueToken( return openApiAuthService.issueToken(tokenDto.getAppCode(), tokenDto.getAppSecret(), tokenDto.getUsername());
tokenDto.getAppCode(), tokenDto.getAppSecret(), tokenDto.getUsername());
} }
} }

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

@ -2,17 +2,20 @@ package apelet.common.openapi.controller;
import apelet.common.core.annotation.MyRequestBody; import apelet.common.core.annotation.MyRequestBody;
import apelet.common.core.constant.ErrorCodeEnum; import apelet.common.core.constant.ErrorCodeEnum;
import apelet.common.core.mcp.annotation.McpToolClass;
import apelet.common.core.mcp.annotation.McpToolParam;
import apelet.common.core.mcp.model.McpToolParamType;
import apelet.common.core.object.MyPageData; import apelet.common.core.object.MyPageData;
import apelet.common.core.object.MyPageParam; import apelet.common.core.object.MyPageParam;
import apelet.common.core.object.ResponseResult; import apelet.common.core.object.ResponseResult;
import apelet.common.core.util.MyCommonUtil; import apelet.common.core.util.MyCommonUtil;
import apelet.common.core.util.MyPageUtil; import apelet.common.core.util.MyPageUtil;
import apelet.common.openapi.dto.OpenApiFieldDto; import apelet.common.openapi.dto.*;
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.model.OpenApiConfig;
import apelet.common.openapi.model.OpenAppApiRelation;
import apelet.common.openapi.service.OpenApiConfigService; import apelet.common.openapi.service.OpenApiConfigService;
import apelet.common.openapi.vo.OpenApiConfigVo;
import cn.hutool.core.collection.CollectionUtil;
import com.github.pagehelper.page.PageMethod; import com.github.pagehelper.page.PageMethod;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -20,7 +23,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/** /**
* OpenAPI 开放接口配置管理接口内部供前端开放平台界面调用 * OpenAPI 开放接口配置管理接口内部供前端开放平台界面调用
@ -42,7 +48,8 @@ public class OpenApiConfigController {
* 在线页面列表配置步骤1 选择可按页面名称模糊搜索 * 在线页面列表配置步骤1 选择可按页面名称模糊搜索
*/ */
@GetMapping("/pages") @GetMapping("/pages")
public ResponseResult<List<OpenApiPageOptionDto>> pages(@RequestParam(required = false) String keyword) { @McpToolClass(name = "openapi_config_page_options", description = "OpenAPI 配置步骤1:在线页面下拉,可按页面名称模糊搜索")
public ResponseResult<List<OpenApiPageOptionDto>> pages(@RequestParam(required = false) @McpToolParam(fieldName = "keyword", type = McpToolParamType.STRING, description = "页面名称关键字,空返回全部") String keyword) {
return ResponseResult.success(openApiConfigService.getPageOptionList(keyword)); return ResponseResult.success(openApiConfigService.getPageOptionList(keyword));
} }
@ -50,7 +57,8 @@ public class OpenApiConfigController {
* 该页面关联的表单列表配置步骤2 联动 * 该页面关联的表单列表配置步骤2 联动
*/ */
@GetMapping("/forms") @GetMapping("/forms")
public ResponseResult<List<OpenApiFormOptionDto>> forms(@RequestParam Long pageId) { @McpToolClass(name = "openapi_config_form_options", description = "OpenAPI 配置步骤2:指定页面对应的表单下拉")
public ResponseResult<List<OpenApiFormOptionDto>> forms(@RequestParam @McpToolParam(fieldName = "pageId", type = McpToolParamType.INTEGER, description = "页面主键Id", required = true) Long pageId) {
return ResponseResult.success(openApiConfigService.getFormOptionList(pageId)); return ResponseResult.success(openApiConfigService.getFormOptionList(pageId));
} }
@ -58,7 +66,8 @@ public class OpenApiConfigController {
* 可选操作下拉查询(query) + operationList 各项配置步骤3 * 可选操作下拉查询(query) + operationList 各项配置步骤3
*/ */
@GetMapping("/operations") @GetMapping("/operations")
public ResponseResult<List<OpenApiOperationDto>> operations(@RequestParam Long formId) { @McpToolClass(name = "openapi_config_operation_options", description = "OpenAPI 配置步骤3:指定表单的可选操作(查询 + operationList 各项)")
public ResponseResult<List<OpenApiOperationDto>> operations(@RequestParam @McpToolParam(fieldName = "formId", type = McpToolParamType.INTEGER, description = "在线表单主键Id", required = true) Long formId) {
return ResponseResult.success(openApiConfigService.getOperationList(formId)); return ResponseResult.success(openApiConfigService.getOperationList(formId));
} }
@ -66,42 +75,54 @@ public class OpenApiConfigController {
* 表单可配置字段树FIELD_DIRECT 字段直传专用 * 表单可配置字段树FIELD_DIRECT 字段直传专用
*/ */
@GetMapping("/fields") @GetMapping("/fields")
public ResponseResult<List<OpenApiFieldDto>> fields(@RequestParam Long formId) { @McpToolClass(name = "openapi_config_field_tree", description = "OpenAPI 配置:指定表单的可配置字段树(主表字段/关联字段属性/一对多分录)")
public ResponseResult<List<OpenApiFieldDto>> fields(@RequestParam @McpToolParam(fieldName = "formId", type = McpToolParamType.INTEGER, description = "在线表单主键Id", required = true) Long formId) {
return ResponseResult.success(openApiConfigService.getFieldTree(formId)); return ResponseResult.success(openApiConfigService.getFieldTree(formId));
} }
/** /**
* 分页查询开放接口配置 * 分页查询开放接口配置查询条件应用Id + 状态
*/ */
@PostMapping("/list") @PostMapping("/list")
public ResponseResult<MyPageData<OpenApiConfig>> list(@MyRequestBody OpenApiConfig openApiConfigFilter, @MyRequestBody MyPageParam pageParam) { @McpToolClass(name = "openapi_config_list", description = "分页查询 OpenAPI 开放接口配置,查询条件:openApiConfigFilter.appId(应用Id,传了仅返回该应用已授权的配置)+ openApiConfigFilter.status")
public ResponseResult<MyPageData<OpenApiConfigVo>> list(@MyRequestBody @McpToolParam(fieldName = "openApiConfigFilter", type = McpToolParamType.OBJECT, description = "查询条件(appId/status)", schemaClass = OpenApiConfigFilterDto.class) OpenApiConfigFilterDto openApiConfigFilter, @MyRequestBody @McpToolParam(fieldName = "pageParam", type = McpToolParamType.OBJECT, description = "分页参数(pageNum/pageSize)", schemaClass = MyPageParam.class) MyPageParam pageParam) {
if (pageParam != null) { if (pageParam != null) {
PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
} }
List<OpenApiConfig> configList = openApiConfigService.getOpenApiConfigList(openApiConfigFilter, "create_time DESC"); List<OpenApiConfig> configList = openApiConfigService.getOpenApiConfigListByFilter(openApiConfigFilter, "create_time DESC");
return ResponseResult.success(MyPageUtil.makeResponseData(configList)); MyPageData<OpenApiConfigVo> configVoMyPageData = MyPageUtil.makeResponseData(configList, OpenApiConfig.INSTANCE);
if (CollectionUtil.isNotEmpty(configVoMyPageData.getDataList())) {
// 一次查出本页全部配置的授权关联行,按 api_id 分组回填 appIdList
List<Long> configIdList = configList.stream().map(OpenApiConfig::getId).collect(Collectors.toList());
Map<Long, List<Long>> appIdMap = openApiConfigService.getAppApiRelationListByConfigIdList(configIdList).stream()
.collect(Collectors.groupingBy(OpenAppApiRelation::getApiId, Collectors.mapping(OpenAppApiRelation::getAppId, Collectors.toList())));
configVoMyPageData.getDataList().forEach(configVo -> configVo.setAppIdList(appIdMap.getOrDefault(configVo.getId(), new ArrayList<>())));
}
return ResponseResult.success(configVoMyPageData);
} }
/** /**
* 查看配置详情 * 查看配置详情返回 VO含已授权应用编码集合
*/ */
@GetMapping("/view") @GetMapping("/view")
public ResponseResult<OpenApiConfig> view(@RequestParam Long configId) { @McpToolClass(name = "openapi_config_view", description = "查看 OpenAPI 开放接口配置详情(返回 VO,appCodes 为已授权应用编码集合)")
public ResponseResult<OpenApiConfigVo> view(@RequestParam @McpToolParam(fieldName = "configId", type = McpToolParamType.INTEGER, description = "配置主键Id", required = true) Long configId) {
OpenApiConfig config = openApiConfigService.getById(configId); OpenApiConfig config = openApiConfigService.getById(configId);
if (config == null) { if (config == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
} }
return ResponseResult.success(config); OpenApiConfigVo configVo = OpenApiConfig.INSTANCE.fromModel(config);
configVo.setAppCodes(openApiConfigService.getAppCodesByConfigId(configId));
return ResponseResult.success(configVo);
} }
/** /**
* 新增开放接口配置 * 新增开放接口配置
*/ */
@PostMapping("/add") @PostMapping("/add")
public ResponseResult<OpenApiConfig> add(@MyRequestBody OpenApiConfig openApiConfig) { @McpToolClass(name = "openapi_config_add", description = "新增 OpenAPI 开放接口配置(pageId/formId/formCode/operationCode 必填,自动推导 paramMode 与 requestPath;应用关联通过 assignApps 分配)")
if (openApiConfig == null || MyCommonUtil.existBlankArgument(openApiConfig.getAppCode(), public ResponseResult<OpenApiConfig> add(@MyRequestBody @McpToolParam(fieldName = "openApiConfig", type = McpToolParamType.OBJECT, description = "开放接口配置信息", schemaClass = OpenApiConfig.class, required = true) OpenApiConfig openApiConfig) {
openApiConfig.getPageId(), openApiConfig.getFormId(), openApiConfig.getFormCode(), if (openApiConfig == null || MyCommonUtil.existBlankArgument(openApiConfig.getPageId(), openApiConfig.getFormId(), openApiConfig.getFormCode(), openApiConfig.getOperationCode())) {
openApiConfig.getOperationCode())) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
} }
return ResponseResult.success(openApiConfigService.addNew(openApiConfig)); return ResponseResult.success(openApiConfigService.addNew(openApiConfig));
@ -111,7 +132,8 @@ public class OpenApiConfigController {
* 更新开放接口配置 * 更新开放接口配置
*/ */
@PostMapping("/update") @PostMapping("/update")
public ResponseResult<Void> update(@MyRequestBody OpenApiConfig openApiConfig) { @McpToolClass(name = "openapi_config_update", description = "更新 OpenAPI 开放接口配置(重新推导 paramMode/requestPath,需传 id)")
public ResponseResult<Void> update(@MyRequestBody @McpToolParam(fieldName = "openApiConfig", type = McpToolParamType.OBJECT, description = "开放接口配置信息(id 必填)", schemaClass = OpenApiConfig.class, required = true) OpenApiConfig openApiConfig) {
if (openApiConfig == null || MyCommonUtil.existBlankArgument(openApiConfig.getId())) { if (openApiConfig == null || MyCommonUtil.existBlankArgument(openApiConfig.getId())) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
} }
@ -122,10 +144,24 @@ public class OpenApiConfigController {
} }
/** /**
* 分配应用全量替换该接口配置的应用授权空集合即清空授权
*/
@PostMapping("/assignApps")
@McpToolClass(name = "openapi_config_assign_apps", description = "给 OpenAPI 开放接口配置分配第三方应用(全量替换授权,appIds 空即清空),返回已授权应用Id 集合")
public ResponseResult<List<Long>> assignApps(@MyRequestBody @McpToolParam(fieldName = "configId", type = McpToolParamType.INTEGER, description = "开放接口配置Id", required = true) Long configId,
@MyRequestBody @McpToolParam(fieldName = "appIds", type = McpToolParamType.ARRAY, description = "授权应用Id 集合(全量替换,空集合即清空授权)") List<Long> appIds) {
if (MyCommonUtil.existBlankArgument(configId)) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
}
return ResponseResult.success(openApiConfigService.assignApps(configId, appIds));
}
/**
* 删除开放接口配置 * 删除开放接口配置
*/ */
@PostMapping("/delete") @PostMapping("/delete")
public ResponseResult<Void> delete(@MyRequestBody Long configId) { @McpToolClass(name = "openapi_config_delete", description = "删除 OpenAPI 开放接口配置,需传 configId")
public ResponseResult<Void> delete(@MyRequestBody @McpToolParam(fieldName = "configId", type = McpToolParamType.INTEGER, description = "配置主键Id", required = true) Long configId) {
if (MyCommonUtil.existBlankArgument(configId)) { if (MyCommonUtil.existBlankArgument(configId)) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
} }

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

@ -1,5 +1,8 @@
package apelet.common.openapi.controller; package apelet.common.openapi.controller;
import apelet.common.core.mcp.annotation.McpToolClass;
import apelet.common.core.mcp.annotation.McpToolParam;
import apelet.common.core.mcp.model.McpToolParamType;
import apelet.common.core.object.ResponseResult; import apelet.common.core.object.ResponseResult;
import apelet.common.openapi.config.OpenApiProperties; import apelet.common.openapi.config.OpenApiProperties;
import apelet.common.openapi.service.OpenApiExecService; import apelet.common.openapi.service.OpenApiExecService;
@ -31,9 +34,20 @@ public class OpenApiRouterController {
@Autowired @Autowired
private OpenApiProperties openApiProperties; private OpenApiProperties openApiProperties;
/**
* 执行指定开放接口配置MCP 工具openapi_route_execute
* <p>MCP 入参 requestBody key-value 形式的自由对象key 为该开放接口配置的参数字段 param_config.conditions 里的 paramFIELD_DIRECT fields.aliasvalue 为对应字段值</p>
* <p>注意MCP 直调走的是 McpToolScanner 反射调用method.invoke不经过本控制器的 HTTP 拦截链路
* 因此不会触发 OpenApiAuthInterceptor 的开放 token 校验注入的 TokenData appCode 归属信息
* OpenApiExecServiceImpl#execute junction 授权校验token.appCode app xy_sys_open_app_api必然不通过
* 当前会返回当前应用无权调用该开放接口本工具仅做接口暴露占位供后续补充内部执行入口绕过授权校验后真正可用</p>
*/
@PostMapping("/{pageCode}/{formCode}/{operationCode}") @PostMapping("/{pageCode}/{formCode}/{operationCode}")
public ResponseResult<Object> execute(@PathVariable String pageCode, @PathVariable String formCode, @PathVariable String operationCode, @McpToolClass(name = "openapi_route_execute", description = "执行 OpenAPI 开放接口配置(HTTP 由 token 鉴权;MCP 直调暂返回无权,仅占位)")
@RequestBody(required = false) Map<String, Object> requestBody) { public ResponseResult<Object> execute(@PathVariable @McpToolParam(fieldName = "pageCode", type = McpToolParamType.STRING, description = "页面编码", required = true) String pageCode,
@PathVariable @McpToolParam(fieldName = "formCode", type = McpToolParamType.STRING, description = "表单编码", required = true) String formCode,
@PathVariable @McpToolParam(fieldName = "operationCode", type = McpToolParamType.STRING, description = "操作编码", required = true) String operationCode,
@RequestBody(required = false) @McpToolParam(fieldName = "requestBody", type = McpToolParamType.OBJECT, description = "请求体,key-value 形式(key 为参数字段,value 为字段值)") Map<String, Object> requestBody) {
String requestPath = openApiProperties.getUrlPrefix() + "/v1/" + pageCode + "/" + formCode + "/" + operationCode; String requestPath = openApiProperties.getUrlPrefix() + "/v1/" + pageCode + "/" + formCode + "/" + operationCode;
return openApiExecService.execute(requestPath, requestBody == null ? Collections.emptyMap() : requestBody); return openApiExecService.execute(requestPath, requestBody == null ? Collections.emptyMap() : requestBody);
} }

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

@ -23,4 +23,14 @@ public interface OpenApiConfigMapper extends BaseDaoMapper<OpenApiConfig> {
*/ */
List<OpenApiConfig> getOpenApiConfigList( List<OpenApiConfig> getOpenApiConfigList(
@Param("openApiConfigFilter") OpenApiConfig openApiConfigFilter, @Param("orderBy") String orderBy); @Param("openApiConfigFilter") OpenApiConfig openApiConfigFilter, @Param("orderBy") String orderBy);
/**
* 按应用Id 分页查询其已授权的配置列表join 多对多授权表status 为空时不带状态条件
*
* @param appId 应用Id
* @param status 状态(0: 停用 1: 启用)可空
* @param orderBy 排序字符串order by从句的参数
* @return 配置列表
*/
List<OpenApiConfig> getOpenApiConfigListByAppId(@Param("appId") Long appId, @Param("status") Integer status, @Param("orderBy") String orderBy);
} }

65
common/common-openapi/src/main/java/apelet/common/openapi/dao/OpenAppApiRelationMapper.java

@ -0,0 +1,65 @@
package apelet.common.openapi.dao;
import apelet.common.core.base.dao.BaseDaoMapper;
import apelet.common.openapi.model.OpenAppApiRelation;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* OpenAPI 应用-接口配置多对多关联数据操作访问接口
*
* @author chenchuchuan
* @date 2026-09-08
*/
public interface OpenAppApiRelationMapper extends BaseDaoMapper<OpenAppApiRelation> {
/**
* 批量插入授权关联行
*
* @param list 关联行集合
* @return 插入行数
*/
int batchInsert(@Param("list") List<OpenAppApiRelation> list);
/**
* 删除指定接口配置的全部授权关联行
*
* @param apiId 开放接口配置Id
* @return 删除行数
*/
int deleteByApiId(@Param("apiId") Long apiId);
/**
* 判断授权关联是否存在
*
* @param appId 应用Id
* @param apiId 开放接口配置Id
* @return 存在返回 true
*/
int existByAppIdAndApiId(@Param("appId") Long appId, @Param("apiId") Long apiId);
/**
* 按接口配置Id 集合查询授权关联列表
*
* @param apiIdList 开放接口配置Id 集合
* @return 授权关联列表
*/
List<OpenAppApiRelation> selectByApiIdList(@Param("apiIdList") List<Long> apiIdList);
/**
* 统计某应用的授权关联行数应用删除拦截用
*
* @param appId 应用Id
* @return 授权关联行数
*/
int countByAppId(@Param("appId") Long appId);
/**
* 按应用Id 查询已授权接口配置Id 集合
*
* @param appId 应用Id
* @return 接口配置Id 集合
*/
List<Long> selectApiIdsByAppId(@Param("appId") Long appId);
}

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

@ -21,7 +21,7 @@
AND xy_sys_open_app.app_code = #{openApiAppFilter.appCode} AND xy_sys_open_app.app_code = #{openApiAppFilter.appCode}
</if> </if>
<if test="openApiAppFilter.appName != null and openApiAppFilter.appName != ''"> <if test="openApiAppFilter.appName != null and openApiAppFilter.appName != ''">
<bind name="safeAppName" value="'%' + openApiAppFilter.appName + '%'"/> <bind name="safeAppName" value="openApiAppFilter.appName + '%'"/>
AND xy_sys_open_app.app_name LIKE #{safeAppName} AND xy_sys_open_app.app_name LIKE #{safeAppName}
</if> </if>
<if test="openApiAppFilter.status != null"> <if test="openApiAppFilter.status != null">

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

@ -3,7 +3,6 @@
<mapper namespace="apelet.common.openapi.dao.OpenApiConfigMapper"> <mapper namespace="apelet.common.openapi.dao.OpenApiConfigMapper">
<resultMap id="BaseResultMap" type="apelet.common.openapi.model.OpenApiConfig"> <resultMap id="BaseResultMap" type="apelet.common.openapi.model.OpenApiConfig">
<id column="id" jdbcType="BIGINT" property="id"/> <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_id" jdbcType="BIGINT" property="pageId"/>
<result column="page_code" jdbcType="VARCHAR" property="pageCode"/> <result column="page_code" jdbcType="VARCHAR" property="pageCode"/>
<result column="form_code" jdbcType="VARCHAR" property="formCode"/> <result column="form_code" jdbcType="VARCHAR" property="formCode"/>
@ -25,9 +24,6 @@
<!-- 这里仅包含调用接口输入的配置过滤条件 --> <!-- 这里仅包含调用接口输入的配置过滤条件 -->
<sql id="inputFilterRef"> <sql id="inputFilterRef">
<if test="openApiConfigFilter != null"> <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"> <if test="openApiConfigFilter.pageId != null">
AND xy_sys_open_api_config.page_id = #{openApiConfigFilter.pageId} AND xy_sys_open_api_config.page_id = #{openApiConfigFilter.pageId}
</if> </if>
@ -49,4 +45,16 @@
ORDER BY ${orderBy} ORDER BY ${orderBy}
</if> </if>
</select> </select>
<select id="getOpenApiConfigListByAppId" resultMap="BaseResultMap">
SELECT config.* FROM xy_sys_open_api_config config
JOIN xy_sys_open_app_api rel ON rel.api_id = config.id
WHERE rel.app_id = #{appId}
<if test="status != null">
AND config.status = #{status}
</if>
<if test="orderBy != null and orderBy != ''">
ORDER BY ${orderBy}
</if>
</select>
</mapper> </mapper>

38
common/common-openapi/src/main/java/apelet/common/openapi/dao/mapper/OpenAppApiRelationMapper.xml

@ -0,0 +1,38 @@
<?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.OpenAppApiRelationMapper">
<resultMap id="BaseResultMap" type="apelet.common.openapi.model.OpenAppApiRelation">
<result column="app_id" jdbcType="BIGINT" property="appId"/>
<result column="api_id" jdbcType="BIGINT" property="apiId"/>
</resultMap>
<insert id="batchInsert">
INSERT INTO xy_sys_open_app_api_relation (app_id, api_id) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.appId}, #{item.apiId})
</foreach>
</insert>
<delete id="deleteByApiId">
DELETE FROM xy_sys_open_app_api_relation WHERE api_id = #{apiId}
</delete>
<select id="existByAppIdAndApiId" resultType="int">
SELECT COUNT(1) FROM xy_sys_open_app_api_relation WHERE app_id = #{appId} AND api_id = #{apiId}
</select>
<select id="selectApiIdsByAppId" resultType="java.lang.Long">
SELECT api_id FROM xy_sys_open_app_api_relation WHERE app_id = #{appId}
</select>
<select id="selectByApiIdList" resultMap="BaseResultMap">
SELECT app_id, api_id FROM xy_sys_open_app_api_relation WHERE api_id IN
<foreach collection="apiIdList" item="item" open="(" close=")" separator=",">
#{item}
</foreach>
</select>
<select id="countByAppId" resultType="int">
SELECT COUNT(1) FROM xy_sys_open_app_api_relation WHERE app_id = #{appId}
</select>
</mapper>

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

@ -60,7 +60,7 @@ public class OpenApiDocumentCustomizer implements OpenApiCustomiser {
operation.setOperationId(config.getOperationCode() + "_" + config.getFormCode()); operation.setOperationId(config.getOperationCode() + "_" + config.getFormCode());
operation.setSummary(config.getOperationName()); operation.setSummary(config.getOperationName());
operation.setDescription(config.getOperationName()); operation.setDescription(config.getOperationName());
operation.addTagsItem(config.getAppCode()); operation.addTagsItem(config.getFormCode());
Schema<?> requestSchema = this.buildRequestSchema(config); Schema<?> requestSchema = this.buildRequestSchema(config);
if (requestSchema != null) { if (requestSchema != null) {
operation.setRequestBody(new RequestBody().required(true) operation.setRequestBody(new RequestBody().required(true)

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

@ -0,0 +1,30 @@
package apelet.common.openapi.dto;
import apelet.common.core.mcp.annotation.McpToolParam;
import apelet.common.core.mcp.model.McpToolParamType;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* OpenAPI 开放接口配置列表查询条件Dto对象
*
* @author chenchuchuan
* @date 2026-09-08
*/
@Schema(description = "OpenAPI开放接口配置列表查询条件Dto对象")
@Data
public class OpenApiConfigFilterDto {
/**
* 应用Id传入时仅返回该应用已授权的配置
*/
@Schema(description = "应用Id,传入时仅返回该应用已授权的配置")
@McpToolParam(fieldName = "appId", type = McpToolParamType.INTEGER, description = "应用Id,传入时仅返回该应用已授权的配置")
private Long appId;
/**
* 状态(0: 停用 1: 启用)
*/
@Schema(description = "状态(0: 停用 1: 启用)")
@McpToolParam(fieldName = "status", type = McpToolParamType.INTEGER, description = "状态(0: 停用 1: 启用)")
private Integer status;
}

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

@ -1,5 +1,7 @@
package apelet.common.openapi.dto; package apelet.common.openapi.dto;
import apelet.common.core.mcp.annotation.McpToolParam;
import apelet.common.core.mcp.model.McpToolParamType;
import lombok.Data; import lombok.Data;
/** /**
@ -14,13 +16,16 @@ public class OpenApiTokenDto {
/** /**
* 应用编码 * 应用编码
*/ */
@McpToolParam(fieldName = "appCode", type = McpToolParamType.STRING, description = "应用编码", required = true)
private String appCode; private String appCode;
/** /**
* 应用密钥 * 应用密钥
*/ */
@McpToolParam(fieldName = "appSecret", type = McpToolParamType.STRING, description = "应用密钥", required = true)
private String appSecret; private String appSecret;
/** /**
* 调用用户名 * 调用用户名
*/ */
@McpToolParam(fieldName = "username", type = McpToolParamType.STRING, description = "调用用户名", required = true)
private String username; private String username;
} }

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

@ -1,5 +1,7 @@
package apelet.common.openapi.model; package apelet.common.openapi.model;
import apelet.common.core.mcp.annotation.McpToolParam;
import apelet.common.core.mcp.model.McpToolParamType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
@ -21,11 +23,13 @@ public class OpenApiApp {
* 主键Id * 主键Id
*/ */
@TableId(value = "id") @TableId(value = "id")
@McpToolParam(fieldName = "id", type = McpToolParamType.INTEGER, description = "主键Id(新增时不传)")
private Long id; private Long id;
/** /**
* 应用编码对外暴露唯一 * 应用编码对外暴露唯一
*/ */
@TableField(value = "app_code") @TableField(value = "app_code")
@McpToolParam(fieldName = "appCode", type = McpToolParamType.STRING, description = "应用编码(对外唯一;新增时不传则自动生成)")
private String appCode; private String appCode;
/** /**
* 应用密钥仅后台可见可重置 * 应用密钥仅后台可见可重置
@ -36,16 +40,19 @@ public class OpenApiApp {
* 应用名称 * 应用名称
*/ */
@TableField(value = "app_name") @TableField(value = "app_name")
@McpToolParam(fieldName = "appName", type = McpToolParamType.STRING, description = "应用名称", required = true)
private String appName; private String appName;
/** /**
* 归属租户Id本期默认为空为空表示不限租户 * 归属租户Id本期默认为空为空表示不限租户
*/ */
@TableField(value = "tenant_id") @TableField(value = "tenant_id")
@McpToolParam(fieldName = "tenantId", type = McpToolParamType.INTEGER, description = "归属租户Id(为空表示不限租户)")
private Long tenantId; private Long tenantId;
/** /**
* 状态(0: 停用 1: 启用) * 状态(0: 停用 1: 启用)
*/ */
@TableField(value = "status") @TableField(value = "status")
@McpToolParam(fieldName = "status", type = McpToolParamType.INTEGER, description = "状态(0: 停用 1: 启用)")
private Integer status; private Integer status;
/** /**
* 创建时间 * 创建时间

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

@ -1,9 +1,15 @@
package apelet.common.openapi.model; package apelet.common.openapi.model;
import apelet.common.core.base.mapper.BaseModelMapper;
import apelet.common.core.mcp.annotation.McpToolParam;
import apelet.common.core.mcp.model.McpToolParamType;
import apelet.common.openapi.vo.OpenApiConfigVo;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.Date; import java.util.Date;
@ -21,41 +27,43 @@ public class OpenApiConfig {
* 主键Id * 主键Id
*/ */
@TableId(value = "id") @TableId(value = "id")
@McpToolParam(fieldName = "id", type = McpToolParamType.INTEGER, description = "主键Id(新增时不传)")
private Long id; private Long id;
/** /**
* 授权开放的应用编码关联 xy_sys_open_app.app_code
*/
@TableField(value = "app_code")
private String appCode;
/**
* 在线页面主键OnlinePage.pageId * 在线页面主键OnlinePage.pageId
*/ */
@TableField(value = "page_id") @TableField(value = "page_id")
@McpToolParam(fieldName = "pageId", type = McpToolParamType.INTEGER, description = "在线页面主键Id", required = true)
private Long pageId; private Long pageId;
/** /**
* 页面编码冗余存储用于组装开放路径 * 页面编码冗余存储用于组装开放路径
*/ */
@TableField(value = "page_code") @TableField(value = "page_code")
@McpToolParam(fieldName = "pageCode", type = McpToolParamType.STRING, description = "页面编码(不传时按 pageId 反查)")
private String pageCode; private String pageCode;
/** /**
* 该页面下关联的在线表单编码OnlineForm.formCode用于组装开放路径 URL * 该页面下关联的在线表单编码OnlineForm.formCode用于组装开放路径 URL
*/ */
@TableField(value = "form_code") @TableField(value = "form_code")
@McpToolParam(fieldName = "formCode", type = McpToolParamType.STRING, description = "在线表单编码", required = true)
private String formCode; private String formCode;
/** /**
* 关联的在线表单主键IdOnlineForm.formId执行时优先按 formId 精确定位表单 * 关联的在线表单主键IdOnlineForm.formId执行时优先按 formId 精确定位表单
*/ */
@TableField(value = "form_id") @TableField(value = "form_id")
@McpToolParam(fieldName = "formId", type = McpToolParamType.INTEGER, description = "在线表单主键Id", required = true)
private Long formId; private Long formId;
/** /**
* 操作编码query operationList 中操作的 code * 操作编码query operationList 中操作的 code
*/ */
@TableField(value = "operation_code") @TableField(value = "operation_code")
@McpToolParam(fieldName = "operationCode", type = McpToolParamType.STRING, description = "操作编码:query 或操作按钮的 code", required = true)
private String operationCode; private String operationCode;
/** /**
* 操作名称查询 operationList 中操作的 name * 操作名称查询 operationList 中操作的 name
*/ */
@TableField(value = "operation_name") @TableField(value = "operation_name")
@McpToolParam(fieldName = "operationName", type = McpToolParamType.STRING, description = "操作名称:查询 / 操作按钮 name")
private String operationName; private String operationName;
/** /**
* 参数模式后端自动推导FIELD_DIRECT / CONDITION_MATCH / QUERY * 参数模式后端自动推导FIELD_DIRECT / CONDITION_MATCH / QUERY
@ -71,21 +79,25 @@ public class OpenApiConfig {
* 请求方法 GET / POST * 请求方法 GET / POST
*/ */
@TableField(value = "request_method") @TableField(value = "request_method")
@McpToolParam(fieldName = "requestMethod", type = McpToolParamType.STRING, description = "请求方法(默认 POST)")
private String requestMethod; private String requestMethod;
/** /**
* JSON输入参数+条件配置见设计文档 6.1 * JSON输入参数+条件配置见设计文档 6.1
*/ */
@TableField(value = "param_config") @TableField(value = "param_config")
@McpToolParam(fieldName = "paramConfig", type = McpToolParamType.STRING, description = "参数/条件配置 JSON(字段直传填 fields,条件匹配/查询填 conditions)")
private String paramConfig; private String paramConfig;
/** /**
* JSON输出层级结构见设计文档 6.2 * JSON输出层级结构见设计文档 6.2
*/ */
@TableField(value = "output_config") @TableField(value = "output_config")
@McpToolParam(fieldName = "outputConfig", type = McpToolParamType.STRING, description = "输出层级配置 JSON(dataFields)")
private String outputConfig; private String outputConfig;
/** /**
* 状态(0: 停用 1: 启用) * 状态(0: 停用 1: 启用)
*/ */
@TableField(value = "status") @TableField(value = "status")
@McpToolParam(fieldName = "status", type = McpToolParamType.INTEGER, description = "状态(0: 停用 1: 启用,不传默认 1)")
private Integer status; private Integer status;
/** /**
* 创建时间 * 创建时间
@ -107,4 +119,29 @@ public class OpenApiConfig {
*/ */
@TableField(value = "update_user_id") @TableField(value = "update_user_id")
private Long updateUserId; private Long updateUserId;
/**
* OpenApiConfig OpenApiConfigVo 的转换器MapStructappCodes 引用编码集合由业务层单独填充
*/
@Mapper
public interface OpenApiConfigModelMapper extends BaseModelMapper<OpenApiConfigVo, OpenApiConfig> {
/**
* 转换Vo对象到实体对象
*
* @param openApiConfigVo 域对象
* @return 实体对象
*/
@Override
OpenApiConfig toModel(OpenApiConfigVo openApiConfigVo);
/**
* 转换实体对象到VO对象
*
* @param openApiConfig 实体对象
* @return 域对象
*/
@Override
OpenApiConfigVo fromModel(OpenApiConfig openApiConfig);
}
public static final OpenApiConfigModelMapper INSTANCE = Mappers.getMapper(OpenApiConfigModelMapper.class);
} }

32
common/common-openapi/src/main/java/apelet/common/openapi/model/OpenAppApiRelation.java

@ -0,0 +1,32 @@
package apelet.common.openapi.model;
import apelet.common.core.mcp.annotation.McpToolParam;
import apelet.common.core.mcp.model.McpToolParamType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* OpenAPI 应用-接口配置多对多关联实体对象
* <p>多对多关联表统一极简设计仅业务 id 字段 + 联合主键即唯一索引无自增主键/审计字段</p>
*
* @author chenchuchuan
* @date 2026-09-08
*/
@Data
@TableName(value = "xy_sys_open_app_api_relation")
public class OpenAppApiRelation {
/**
* 应用Idxy_sys_open_app.id
*/
@TableField(value = "app_id")
@McpToolParam(fieldName = "appId", type = McpToolParamType.INTEGER, description = "应用Id")
private Long appId;
/**
* 开放接口配置Idxy_sys_open_api_config.id
*/
@TableField(value = "api_id")
@McpToolParam(fieldName = "apiId", type = McpToolParamType.INTEGER, description = "开放接口配置Id")
private Long apiId;
}

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

@ -37,4 +37,12 @@ public interface OpenApiAppService extends IBaseService<OpenApiApp, Long> {
* @return 新的明文 appSecret * @return 新的明文 appSecret
*/ */
String resetSecret(Long id); String resetSecret(Long id);
/**
* 删除应用已被分配开放接口时拒绝返回 false
*
* @param id 应用主键Id
* @return 删除成功返回 true应用不存在或已被分配返回 false
*/
boolean removeAppWithRelationCheck(Long id);
} }

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

@ -6,6 +6,7 @@ import apelet.common.openapi.dto.OpenApiFormOptionDto;
import apelet.common.openapi.dto.OpenApiOperationDto; import apelet.common.openapi.dto.OpenApiOperationDto;
import apelet.common.openapi.dto.OpenApiPageOptionDto; import apelet.common.openapi.dto.OpenApiPageOptionDto;
import apelet.common.openapi.model.OpenApiConfig; import apelet.common.openapi.model.OpenApiConfig;
import apelet.common.openapi.model.OpenAppApiRelation;
import java.util.List; import java.util.List;
@ -27,6 +28,15 @@ public interface OpenApiConfigService extends IBaseService<OpenApiConfig, Long>
List<OpenApiConfig> getOpenApiConfigList(OpenApiConfig filter, String orderBy); List<OpenApiConfig> getOpenApiConfigList(OpenApiConfig filter, String orderBy);
/** /**
* 列表统一查询入口appId 有值时按应用已授权过滤join junctionSQL 内按需叠加 status否则走普通过滤查询
*
* @param filter 查询条件DtoappId/status 均可空
* @param orderBy 排序字符串
* @return 配置列表
*/
List<OpenApiConfig> getOpenApiConfigListByFilter(apelet.common.openapi.dto.OpenApiConfigFilterDto filter, String orderBy);
/**
* 获取在线页面下拉列表配置步骤1可按页面名称模糊搜索 * 获取在线页面下拉列表配置步骤1可按页面名称模糊搜索
* *
* @param keyword 页面名称关键字为空返回全部 * @param keyword 页面名称关键字为空返回全部
@ -81,4 +91,29 @@ public interface OpenApiConfigService extends IBaseService<OpenApiConfig, Long>
* @return 配置对象 * @return 配置对象
*/ */
OpenApiConfig getByRequestPath(String requestPath); OpenApiConfig getByRequestPath(String requestPath);
/**
* 分配应用全量替换该接口配置的应用授权先删该 configId 全部授权行再按 appIds 插入空集合即清空
*
* @param configId 开放接口配置Id
* @param appIds 授权应用Id 集合自动去重
* @return 替换后该配置已授权的应用Id 集合
*/
List<Long> assignApps(Long configId, List<Long> appIds);
/**
* 查询指定接口配置已授权的应用编码集合详情 VO 回显用
*
* @param configId 开放接口配置Id
* @return 已授权应用的应用编码集合xy_sys_open_app.app_code
*/
List<String> getAppCodesByConfigId(Long configId);
/**
* 批量获取接口配置的授权关联列表app_id + api_id一次查询单个 configId 用单元素集合传入
*
* @param configIdList 开放接口配置Id 集合
* @return 授权关联列表
*/
List<OpenAppApiRelation> getAppApiRelationListByConfigIdList(List<Long> configIdList);
} }

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

@ -4,6 +4,7 @@ import apelet.common.core.base.dao.BaseDaoMapper;
import apelet.common.core.base.service.BaseService; import apelet.common.core.base.service.BaseService;
import apelet.common.core.object.TokenData; import apelet.common.core.object.TokenData;
import apelet.common.openapi.dao.OpenApiAppMapper; import apelet.common.openapi.dao.OpenApiAppMapper;
import apelet.common.openapi.dao.OpenAppApiRelationMapper;
import apelet.common.openapi.model.OpenApiApp; import apelet.common.openapi.model.OpenApiApp;
import apelet.common.openapi.service.OpenApiAppService; import apelet.common.openapi.service.OpenApiAppService;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
@ -28,6 +29,8 @@ public class OpenApiAppServiceImpl extends BaseService<OpenApiApp, Long> impleme
@Autowired @Autowired
private OpenApiAppMapper openApiAppMapper; private OpenApiAppMapper openApiAppMapper;
@Autowired
private OpenAppApiRelationMapper openAppApiRelationMapper;
@Override @Override
protected BaseDaoMapper<OpenApiApp> mapper() { protected BaseDaoMapper<OpenApiApp> mapper() {
@ -79,4 +82,16 @@ public class OpenApiAppServiceImpl extends BaseService<OpenApiApp, Long> impleme
this.updateById(app); this.updateById(app);
return newSecret; return newSecret;
} }
@Override
public boolean removeAppWithRelationCheck(Long id) {
if (id == null || this.getById(id) == null) {
return false;
}
// 配置与应用的关联已全部走授权关联表,存在授权行即禁止删除
if (openAppApiRelationMapper.countByAppId(id) > 0) {
return false;
}
return this.removeById(id);
}
} }

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

@ -63,8 +63,7 @@ public class OpenApiAuthServiceImpl implements OpenApiAuthService {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST, "应用编码、密钥、用户名均不能为空!"); return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST, "应用编码、密钥、用户名均不能为空!");
} }
// ① 校验应用凭证与状态 // ① 校验应用凭证与状态
OpenApiApp app = openApiAppService.getOne(new LambdaQueryWrapper<OpenApiApp>() OpenApiApp app = openApiAppService.getOne(new LambdaQueryWrapper<OpenApiApp>().eq(OpenApiApp::getAppCode, appCode));
.eq(OpenApiApp::getAppCode, appCode));
if (app == null || !app.getAppSecret().equals(appSecret)) { if (app == null || !app.getAppSecret().equals(appSecret)) {
return ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, "应用编码或密钥错误!"); return ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, "应用编码或密钥错误!");
} }

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

@ -17,11 +17,12 @@ import apelet.common.openapi.config.OpenApiProperties;
import apelet.common.openapi.constant.OpenApiFieldSource; import apelet.common.openapi.constant.OpenApiFieldSource;
import apelet.common.openapi.constant.OpenApiParamMode; import apelet.common.openapi.constant.OpenApiParamMode;
import apelet.common.openapi.dao.OpenApiConfigMapper; import apelet.common.openapi.dao.OpenApiConfigMapper;
import apelet.common.openapi.dto.OpenApiFieldDto; import apelet.common.openapi.dao.OpenAppApiRelationMapper;
import apelet.common.openapi.dto.OpenApiFormOptionDto; import apelet.common.openapi.dto.*;
import apelet.common.openapi.dto.OpenApiOperationDto; import apelet.common.openapi.model.OpenApiApp;
import apelet.common.openapi.dto.OpenApiPageOptionDto;
import apelet.common.openapi.model.OpenApiConfig; import apelet.common.openapi.model.OpenApiConfig;
import apelet.common.openapi.model.OpenAppApiRelation;
import apelet.common.openapi.service.OpenApiAppService;
import apelet.common.openapi.service.OpenApiConfigService; import apelet.common.openapi.service.OpenApiConfigService;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
@ -33,9 +34,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList; import java.util.*;
import java.util.Date; import java.util.stream.Collectors;
import java.util.List;
/** /**
* OpenAPI 开放接口配置数据操作服务类 * OpenAPI 开放接口配置数据操作服务类
@ -53,6 +53,10 @@ public class OpenApiConfigServiceImpl extends BaseService<OpenApiConfig, Long> i
@Autowired @Autowired
private OpenApiConfigMapper openApiConfigMapper; private OpenApiConfigMapper openApiConfigMapper;
@Autowired @Autowired
private OpenAppApiRelationMapper openAppApiRelationMapper;
@Autowired
private OpenApiAppService openApiAppService;
@Autowired
private OpenApiProperties openApiProperties; private OpenApiProperties openApiProperties;
@Autowired @Autowired
private OnlinePageService onlinePageService; private OnlinePageService onlinePageService;
@ -79,6 +83,16 @@ public class OpenApiConfigServiceImpl extends BaseService<OpenApiConfig, Long> i
} }
@Override @Override
public List<OpenApiConfig> getOpenApiConfigListByFilter(OpenApiConfigFilterDto filter, String orderBy) {
if (filter == null || filter.getAppId() == null) {
OpenApiConfig configFilter = new OpenApiConfig();
configFilter.setStatus(filter == null ? null : filter.getStatus());
return openApiConfigMapper.getOpenApiConfigList(configFilter, orderBy);
}
return openApiConfigMapper.getOpenApiConfigListByAppId(filter.getAppId(), filter.getStatus(), orderBy);
}
@Override
public List<OpenApiPageOptionDto> getPageOptionList(String keyword) { public List<OpenApiPageOptionDto> getPageOptionList(String keyword) {
// 按页面名称模糊搜索,空关键字返回全部;结果按创建时间倒序 // 按页面名称模糊搜索,空关键字返回全部;结果按创建时间倒序
OnlinePage filter = null; OnlinePage filter = null;
@ -284,6 +298,54 @@ public class OpenApiConfigServiceImpl extends BaseService<OpenApiConfig, Long> i
return openApiConfigMapper.selectOne(new LambdaQueryWrapper<OpenApiConfig>().eq(OpenApiConfig::getRequestPath, requestPath)); return openApiConfigMapper.selectOne(new LambdaQueryWrapper<OpenApiConfig>().eq(OpenApiConfig::getRequestPath, requestPath));
} }
@Override
public List<Long> assignApps(Long configId, List<Long> appIds) {
OpenApiConfig config = this.getById(configId);
if (config == null) {
throw new IllegalArgumentException("数据验证失败,开放接口配置不存在!");
}
Set<Long> appIdSet = appIds == null ? new HashSet<>() : new LinkedHashSet<>(appIds);
// 全量替换:先清空该配置的授权,再按入参重插;空集合即清空授权
openAppApiRelationMapper.deleteByApiId(configId);
if (!appIdSet.isEmpty()) {
List<OpenApiApp> existApps = openApiAppService.list(new LambdaQueryWrapper<OpenApiApp>().in(OpenApiApp::getId, appIdSet));
if (existApps.size() < appIdSet.size()) {
throw new IllegalArgumentException("数据验证失败,存在无效的应用Id!");
}
List<OpenAppApiRelation> relList = new ArrayList<>(appIdSet.size());
for (Long appId : appIdSet) {
OpenAppApiRelation rel = new OpenAppApiRelation();
rel.setAppId(appId);
rel.setApiId(configId);
relList.add(rel);
}
openAppApiRelationMapper.batchInsert(relList);
}
return new ArrayList<>(appIdSet);
}
@Override
public List<String> getAppCodesByConfigId(Long configId) {
if (configId == null) {
return new ArrayList<>();
}
List<OpenAppApiRelation> relationList = this.getAppApiRelationListByConfigIdList(Collections.singletonList(configId));
if (relationList.isEmpty()) {
return new ArrayList<>();
}
List<Long> appIds = relationList.stream().map(OpenAppApiRelation::getAppId).collect(Collectors.toList());
List<OpenApiApp> appList = openApiAppService.list(new LambdaQueryWrapper<OpenApiApp>().in(OpenApiApp::getId, appIds));
return appList.stream().map(OpenApiApp::getAppCode).collect(Collectors.toList());
}
@Override
public List<OpenAppApiRelation> getAppApiRelationListByConfigIdList(List<Long> configIdList) {
if (configIdList == null || configIdList.isEmpty()) {
return new ArrayList<>();
}
return openAppApiRelationMapper.selectByApiIdList(configIdList);
}
/** /**
* 推导参数模式queryQUERY保存/提交FIELD_DIRECT其他CONDITION_MATCH * 推导参数模式queryQUERY保存/提交FIELD_DIRECT其他CONDITION_MATCH
*/ */

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

@ -20,7 +20,10 @@ import apelet.common.online.service.OnlineTableService;
import apelet.common.online.util.OnlineOperationHelper; import apelet.common.online.util.OnlineOperationHelper;
import apelet.common.openapi.constant.OpenApiFieldSource; import apelet.common.openapi.constant.OpenApiFieldSource;
import apelet.common.openapi.constant.OpenApiParamMode; import apelet.common.openapi.constant.OpenApiParamMode;
import apelet.common.openapi.dao.OpenAppApiRelationMapper;
import apelet.common.openapi.model.OpenApiApp;
import apelet.common.openapi.model.OpenApiConfig; import apelet.common.openapi.model.OpenApiConfig;
import apelet.common.openapi.service.OpenApiAppService;
import apelet.common.openapi.service.OpenApiConfigService; import apelet.common.openapi.service.OpenApiConfigService;
import apelet.common.openapi.service.OpenApiExecService; import apelet.common.openapi.service.OpenApiExecService;
import apelet.common.openapi.service.OpenApiOnlineQueryService; import apelet.common.openapi.service.OpenApiOnlineQueryService;
@ -33,7 +36,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.*; import java.util.*;
@ -52,6 +54,10 @@ public class OpenApiExecServiceImpl implements OpenApiExecService {
@Autowired @Autowired
private OpenApiConfigService openApiConfigService; private OpenApiConfigService openApiConfigService;
@Autowired @Autowired
private OpenApiAppService openApiAppService;
@Autowired
private OpenAppApiRelationMapper openAppApiRelationMapper;
@Autowired
private OnlineFormService onlineFormService; private OnlineFormService onlineFormService;
@Autowired @Autowired
private OnlineTableService onlineTableService; private OnlineTableService onlineTableService;
@ -77,7 +83,11 @@ public class OpenApiExecServiceImpl implements OpenApiExecService {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "在线表单不存在!"); return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, "在线表单不存在!");
} }
TokenData tokenData = TokenData.takeFromRequest(); TokenData tokenData = TokenData.takeFromRequest();
if (tokenData == null || !config.getAppCode().equals(tokenData.getAppCode())) { if (tokenData == null || StrUtil.isBlank(tokenData.getAppCode())) {
return ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION, "当前应用无权调用该开放接口!");
}
// 授权校验走多对多关联表:token.appCode → 应用 → junction (app_id, api_id) 存在才放行(一条接口配置可授权多个应用)
if (!this.hasAssignRelation(tokenData.getAppCode(), config.getId())) {
return ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION, "当前应用无权调用该开放接口!"); return ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION, "当前应用无权调用该开放接口!");
} }
// 列表类操作(QUERY / CONDITION_MATCH)依赖 pc.tableWidget 走列表引擎,编辑页未配置列表时给出明确提示而非底层空指针。 // 列表类操作(QUERY / CONDITION_MATCH)依赖 pc.tableWidget 走列表引擎,编辑页未配置列表时给出明确提示而非底层空指针。
@ -242,6 +252,17 @@ public class OpenApiExecServiceImpl implements OpenApiExecService {
// ============ 公共辅助 ============ // ============ 公共辅助 ============
/** /**
* 校验应用与接口配置的授权关系appCode xy_sys_open_app.id xy_sys_open_app_api (app_id, api_id)
*/
private boolean hasAssignRelation(String appCode, Long configId) {
OpenApiApp app = openApiAppService.getOne(new LambdaQueryWrapper<OpenApiApp>().eq(OpenApiApp::getAppCode, appCode));
if (app == null) {
return false;
}
return openAppApiRelationMapper.existByAppIdAndApiId(app.getId(), configId) > 0;
}
/**
* 定位在线表单优先按 formId未配置时回退 pageId + formCode * 定位在线表单优先按 formId未配置时回退 pageId + formCode
*/ */
private OnlineForm findForm(OpenApiConfig config) { private OnlineForm findForm(OpenApiConfig config) {

104
common/common-openapi/src/main/java/apelet/common/openapi/vo/OpenApiConfigVo.java

@ -0,0 +1,104 @@
package apelet.common.openapi.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
import java.util.List;
/**
* OpenAPI 开放接口配置VO对象详情接口返回含关联应用编码集合
*
* @author chenchuchuan
* @date 2026-09-08
*/
@Schema(description = "OpenAPI开放接口配置VO对象")
@Data
public class OpenApiConfigVo {
/**
* 主键Id
*/
@Schema(description = "主键Id")
private Long id;
/**
* 在线页面主键OnlinePage.pageId
*/
@Schema(description = "在线页面主键Id")
private Long pageId;
/**
* 页面编码冗余存储用于组装开放路径
*/
@Schema(description = "页面编码")
private String pageCode;
/**
* 该页面下关联的在线表单编码
*/
@Schema(description = "在线表单编码")
private String formCode;
/**
* 关联的在线表单主键Id
*/
@Schema(description = "在线表单主键Id")
private Long formId;
/**
* 操作编码query operationList 中操作的 code
*/
@Schema(description = "操作编码")
private String operationCode;
/**
* 操作名称
*/
@Schema(description = "操作名称")
private String operationName;
/**
* 参数模式后端自动推导FIELD_DIRECT / CONDITION_MATCH / QUERY
*/
@Schema(description = "参数模式")
private String paramMode;
/**
* 开放路径/openapi/v1/{pageCode}/{formCode}/{operationCode}全局唯一
*/
@Schema(description = "开放路径")
private String requestPath;
/**
* 请求方法 GET / POST
*/
@Schema(description = "请求方法")
private String requestMethod;
/**
* JSON输入参数+条件配置
*/
@Schema(description = "参数/条件配置JSON")
private String paramConfig;
/**
* JSON输出层级结构
*/
@Schema(description = "输出层级配置JSON")
private String outputConfig;
/**
* 状态(0: 停用 1: 启用)
*/
@Schema(description = "状态(0: 停用 1: 启用)")
private Integer status;
/**
* 创建时间
*/
@Schema(description = "创建时间")
private Date createTime;
/**
* 更新时间
*/
@Schema(description = "更新时间")
private Date updateTime;
/**
* 引用编码集合该配置已授权应用的应用编码xy_sys_open_app.app_code xy_sys_open_app_api 反查
*/
@Schema(description = "引用编码集合:已授权应用的应用编码集合")
private List<String> appCodes;
/**
* 引用Id集合该配置已授权应用的应用Idxy_sys_open_app.app_id xy_sys_open_app_api 反查
*/
@Schema(description = "引用Id集合:已授权应用的应用Id集合")
private List<Long> appIdList;
}
Loading…
Cancel
Save