Browse Source

feat(mcp): 添加MCP工具框架支持包括流式HTTP传输和注解扫描功能

- 新增 McpStreamableController 提供流式HTTP单端点支持 /tenantadmin/mcp
- 实现 StreamableHttpServerTransportProvider 支持MCP协议的流式HTTP传输
- 添加 @McpToolClass 和 @McpToolParam 注解用于标记MCP工具方法和参数
- 开发 McpToolScanner 扫描器自动注册带有注解的工具方法
- 集成 mcp-java8-sdk 依赖支持MCP协议服务器功能
- 新增 McpToolArgInfo 和 McpToolParamType 模型类处理工具参数元信息
- 配置 McpToolConfig 自动装配SSE和流式HTTP两种传输方式
- 为 MyPageParam 添加 MCP 参数注解支持分页参数映射
pull/1/head
chenchuchuan 1 month ago
parent
commit
3e25f370b8
  1. 15
      common/common-core/pom.xml
  2. 30
      common/common-core/src/main/java/apelet/common/core/mcp/annotation/McpToolClass.java
  3. 48
      common/common-core/src/main/java/apelet/common/core/mcp/annotation/McpToolParam.java
  4. 91
      common/common-core/src/main/java/apelet/common/core/mcp/config/McpToolConfig.java
  5. 499
      common/common-core/src/main/java/apelet/common/core/mcp/config/McpToolScanner.java
  6. 47
      common/common-core/src/main/java/apelet/common/core/mcp/controller/McpStreamableController.java
  7. 71
      common/common-core/src/main/java/apelet/common/core/mcp/model/McpToolArgInfo.java
  8. 35
      common/common-core/src/main/java/apelet/common/core/mcp/model/McpToolParamType.java
  9. 230
      common/common-core/src/main/java/apelet/common/core/mcp/transport/StreamableHttpServerTransportProvider.java
  10. 5
      common/common-core/src/main/java/apelet/common/core/object/MyPageParam.java
  11. 8
      common/common-online/src/main/java/apelet/common/online/controller/OnlineColumnController.java
  12. 8
      common/common-online/src/main/java/apelet/common/online/controller/OnlineDatasourceRelationController.java
  13. 21
      common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java
  14. 32
      common/common-online/src/main/java/apelet/common/online/controller/OnlinePageController.java
  15. 27
      common/common-online/src/main/java/apelet/common/online/dto/OnlineColumnDto.java
  16. 8
      common/common-online/src/main/java/apelet/common/online/dto/OnlineDatasourceDto.java
  17. 14
      common/common-online/src/main/java/apelet/common/online/dto/OnlineDatasourceRelationDto.java
  18. 14
      common/common-online/src/main/java/apelet/common/online/dto/OnlineFormDto.java
  19. 11
      common/common-online/src/main/java/apelet/common/online/dto/OnlinePageDto.java

15
common/common-core/pom.xml

@ -117,5 +117,20 @@ @@ -117,5 +117,20 @@
<version>4.4.0</version>
<scope>compile</scope>
</dependency>
<!-- MCP 接口框架(注解/扫描器/SSE+Streamable server 装配),供各模块用 @McpToolClass 暴露工具 -->
<dependency>
<groupId>com.github.krrr</groupId>
<artifactId>mcp-java8-sdk</artifactId>
<version>0.8.1</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>
</project>

30
common/common-core/src/main/java/apelet/common/core/mcp/annotation/McpToolClass.java

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
package apelet.common.core.mcp.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* MCP 工具方法注解:标注在业务方法上,启动时由 McpToolScanner 扫描自动生成 MCP Tool 并注册。
* <p>name 与 description 即 MCP Tool 的 name / description,inputSchema 由方法参数上的 @McpToolParam 组装。</p>
*
* @author chenchuchuan
* @date 2026-08-31
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface McpToolClass {
/**
* MCP Tool 名称,客户端 tools/list 看到的 name。
*/
String name();
/**
* MCP Tool 描述,供大模型理解用途。
*/
String description();
}

48
common/common-core/src/main/java/apelet/common/core/mcp/annotation/McpToolParam.java

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
package apelet.common.core.mcp.annotation;
import apelet.common.core.mcp.model.McpToolParamType;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* MCP 工具方法参数注解:标注在 @McpToolClass 方法的参数上,以及 type=OBJECT 的 schemaClass 字段上。
* <p>启动扫描时按注解属性组装 inputSchema;调用工具时按 fieldName 从入参取值绑定到方法实参。</p>
*
* @author chenchuchuan
* @date 2026-08-31
*/
@Documented
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface McpToolParam {
/**
* 入参字段名,即客户端传参时使用的 key。
* 未配置时取 Java 参数名(默认编译带调试信息,可通过 DefaultParameterNameDiscoverer 获取)。
*/
String fieldName() default "";
/**
* 字段类型(JSON Schema 的 type),默认字符串。
*/
McpToolParamType type() default McpToolParamType.STRING;
/**
* 字段描述,供大模型理解入参含义。
*/
String description() default "";
/**
* 是否必填,默认 false。
*/
boolean required() default false;
/**
* type=OBJECT 时必填,指向的对象类型,其 @McpToolParam 标注字段会递归组装成嵌套 schema。
*/
Class<?> schemaClass() default Void.class;
}

91
common/common-core/src/main/java/apelet/common/core/mcp/config/McpToolConfig.java

@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
package apelet.common.core.mcp.config;
import apelet.common.core.mcp.transport.StreamableHttpServerTransportProvider;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
import java.util.List;
/**
* MCP Server 装配:把业务工具暴露为 MCP tool,供大模型(客户端)调用。
* 基于 mcp-java8-sdk 0.8.1(构造器 API,非新版 builder())。
* 该 SDK 不提供 listTools(),故注册时同步收集一份工具清单供调试接口读取(见 McpToolScanner.getTools())。
* 双传输并存:SSE(/mcp/sse + /mcp/message)+ Streamable HTTP(/mcp 单端点)。
* 工具由 McpToolScanner 启动时扫描 @McpToolClass 自动生成并统一注入。
*
* @author chenchuchuan
* @date 2026-08-27
*/
@Configuration
public class McpToolConfig {
/**
* MCP 消息专用 ObjectMapper:应用注入的 ObjectMapper 在序列化 @JsonTypeInfo 多态对象
* (如 CallToolResult.content 的 TextContent)时丢失 type 判别符,导致客户端反序列化失败,
* 故 MCP 的序列化统一走此普通 mapper(普通 mapper 会正常输出 "type":"text")。
*/
private static final ObjectMapper MCP_OBJECT_MAPPER = new ObjectMapper();
@Autowired
private McpToolScanner mcpToolScanner;
/**
* MCP 传输层(Spring MVC SSE):SSE 长连接端点 + JSON-RPC message 端点。
* 端点路径带 /tenantadmin 前缀(本应用无 servlet context-path,模块 urlPrefix 统一用 /tenantadmin/xxx)。
*/
@Bean
public WebMvcSseServerTransportProvider mcpTransport() {
return new WebMvcSseServerTransportProvider(MCP_OBJECT_MAPPER, "/tenantadmin/mcp/message", "/tenantadmin/mcp/sse");
}
/**
* 把 SSE transport 内部的 RouterFunction(SSE/message 两个端点)注册进 Spring MVC。
*/
@Bean
public RouterFunction<ServerResponse> mcpEndpoints(WebMvcSseServerTransportProvider mcpTransport) {
return mcpTransport.getRouterFunction();
}
/**
* 构建同步 MCP Server(SSE 传输),并注册业务工具。
*/
@Bean
public McpSyncServer mcpSyncServer(WebMvcSseServerTransportProvider mcpTransport) {
return registerTools(McpServer.sync(mcpTransport).serverInfo("business-mcp-server", "1.0.0")).build();
}
/**
* MCP 传输层(Streamable HTTP,单端点 POST /tenantadmin/mcp)。
* 端点由 McpStreamableController 提供,不走 RouterFunction(避免应用转换器二次加工响应)。
*/
@Bean
public StreamableHttpServerTransportProvider streamableHttpTransport() {
return new StreamableHttpServerTransportProvider(MCP_OBJECT_MAPPER);
}
/**
* 构建同步 MCP Server(Streamable HTTP 传输),注册同一套业务工具。
*/
@Bean
public McpSyncServer mcpStreamableSyncServer(StreamableHttpServerTransportProvider streamableHttpTransport) {
return registerTools(McpServer.sync(streamableHttpTransport).serverInfo("business-mcp-server", "1.0.0")).build();
}
/**
* 注册 MCP 工具(SSE 与 Streamable 两个 server 共用同一套注册逻辑)。
* 工具清单由 McpToolScanner 扫描 @McpToolClass 自动组装,这里批量注入。
*/
private McpServer.SyncSpecification registerTools(McpServer.SyncSpecification spec) {
List<McpServerFeatures.SyncToolSpecification> specifications = mcpToolScanner.getToolSpecifications();
return specifications.isEmpty() ? spec : spec.tools(specifications);
}
}

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

@ -0,0 +1,499 @@ @@ -0,0 +1,499 @@
package apelet.common.core.mcp.config;
import apelet.common.core.mcp.annotation.McpToolClass;
import apelet.common.core.mcp.annotation.McpToolParam;
import apelet.common.core.mcp.model.McpToolArgInfo;
import apelet.common.core.mcp.model.McpToolParamType;
import apelet.common.core.object.TokenData;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.*;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
/**
* MCP 工具扫描注册器:启动时从 Spring 容器取 @RestController/@Controller/@Component 等组件 bean,
* 扫描其上 @McpToolClass 标注的方法,自动组装 McpSchema.Tool 并生成调用 handler。
* <p>直接基于容器 bean 扫描(getBeanNamesForAnnotation),不做 classpath 资源扫描,
* 避免 fat jar 嵌套 jar 里包名通配(**)扫不到的问题,也无需 scan-packages 等配置。
* 组装结果以 SyncToolSpecification 列表暴露给 McpToolConfig 注入 SSE + Streamable 两个 server。</p>
*
* @author chenchuchuan
* @date 2026-08-31
*/
@Slf4j
@Component
public class McpToolScanner {
/**
* 方法参数名发现器:优先 -parameters,兜底 LocalVariableTable(默认编译带调试信息)。
*/
private static final DefaultParameterNameDiscoverer PARAM_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
private final ApplicationContext applicationContext;
/**
* MCP 调用使用的默认用户Id(MCP 无 HTTP 登录态,服务内 TokenData.takeFromRequest() 会 NPE,调用前注入)。
*/
@Value("${apelet.mcp.default-user-id:0}")
private Long defaultUserId;
/**
* 已组装好的工具注册项(供 spec.tools 批量注入)。
*/
@Getter
private List<McpServerFeatures.SyncToolSpecification> toolSpecifications = Collections.emptyList();
/**
* 已注册的工具清单(SDK 不提供 listTools(),供调试接口读取)。
*/
private List<McpSchema.Tool> toolList = Collections.emptyList();
public McpToolScanner(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
/**
* 按返回类型给动态代理一个安全的默认值(对象 null、字符串空、集合空、数组空、数值 0、布尔 false)。
*/
private static Object defaultValue(Class<?> returnType) {
if (returnType == String.class) {
return "";
}
if (returnType == boolean.class) {
return false;
}
if (returnType == int.class) {
return 0;
}
if (returnType == long.class) {
return 0L;
}
if (returnType == List.class || returnType == Map.class || returnType == Set.class) {
return Collections.emptyMap();
}
if (returnType.isArray()) {
return Array.newInstance(returnType.getComponentType(), 0);
}
return null;
}
@PostConstruct
public void init() {
List<McpServerFeatures.SyncToolSpecification> specifications = new ArrayList<>();
List<McpSchema.Tool> tools = new ArrayList<>();
// 直接从 Spring 容器取组件 bean 扫描,不做 classpath 资源扫描。
// 原因:ClassPathScanningCandidateComponentProvider 走 PathMatchingResourcePatternResolver 递归匹配 classpath 资源,
// 应用打成 fat jar 后 @McpToolClass 类在 BOOT-INF/lib/*.jar 嵌套 jar 里,包名带 `**` 时匹配不上,导致扫不到;
// 改为从容器取 bean 则完全不受打包方式影响。@RestController/@Controller 都被 @Component 元注解标注,
// getBeanNamesForAnnotation(Component.class) 全部命中,@Service/@Repository 等组件同样覆盖。
Set<Class<?>> scanned = new HashSet<>();
for (String beanName : applicationContext.getBeanNamesForAnnotation(RestController.class)) {
Class<?> clazz = applicationContext.getType(beanName);
if (clazz == null) {
continue;
}
clazz = ClassUtils.getUserClass(clazz);
if (!scanned.add(clazz)) {
continue;
}
this.scanClassMethods(specifications, tools, clazz, beanName);
}
this.toolSpecifications = specifications;
this.toolList = tools;
log.info("MCP 工具扫描完成,共注册 {} 个 tool:{}", tools.size(),
tools.stream().map(McpSchema.Tool::getName).collect(Collectors.toList()));
}
/**
* 扫描单个 bean 类的 @McpToolClass 方法并注册。
*/
private void scanClassMethods(List<McpServerFeatures.SyncToolSpecification> specifications,
List<McpSchema.Tool> tools, Class<?> clazz, String beanName) {
for (Method method : clazz.getDeclaredMethods()) {
// 只暴露 public 方法为 MCP 工具,私有/受保护方法不注册。
if (!Modifier.isPublic(method.getModifiers())) {
continue;
}
McpToolClass ann = method.getAnnotation(McpToolClass.class);
if (ann == null) {
continue;
}
this.registerTool(specifications, tools, clazz, beanName, method, ann);
}
}
public List<McpSchema.Tool> getTools() {
return toolList;
}
/**
* 组装单个工具:inputSchema + Tool + 调用 handler。
*/
private void registerTool(List<McpServerFeatures.SyncToolSpecification> specifications,
List<McpSchema.Tool> tools, Class<?> clazz, String beanName, Method method, McpToolClass ann) {
List<McpToolArgInfo> argInfos = this.buildArgInfos(method);
String inputSchema = this.buildInputSchema(argInfos);
McpSchema.Tool tool = new McpSchema.Tool(ann.name(), ann.description(), inputSchema);
// 懒取 bean:只有真正带 @McpToolClass 方法的类才触发初始化;拿到的是代理实例,invoke 时走 AOP。
Object bean = applicationContext.getBean(beanName);
McpServerFeatures.SyncToolSpecification specification = new McpServerFeatures.SyncToolSpecification();
specification.setTool(tool);
specification.setCall((exchange, args) -> this.invoke(bean, method, argInfos, args));
specifications.add(specification);
tools.add(tool);
log.info("MCP 工具已注册:{} (inputSchema={})", tool.getName(), inputSchema);
}
/**
* 组装方法参数绑定信息:@McpToolParam 未配置 fieldName 时取 Java 参数名。
*/
private List<McpToolArgInfo> buildArgInfos(Method method) {
Parameter[] parameters = method.getParameters();
String[] paramNames = PARAM_NAME_DISCOVERER.getParameterNames(method);
List<McpToolArgInfo> argInfos = new ArrayList<>();
for (int i = 0; i < parameters.length; i++) {
McpToolParam ann = parameters[i].getAnnotation(McpToolParam.class);
if (ann == null) {
log.warn("MCP 工具方法 [{}#{}] 参数 [{}] 缺少 @McpToolParam,跳过该参数", method.getDeclaringClass().getSimpleName(), method.getName(), i);
continue;
}
String fieldName = StrUtil.isNotBlank(ann.fieldName()) ? ann.fieldName()
: (paramNames != null && paramNames.length > i ? paramNames[i] : "arg" + i);
argInfos.add(this.toArgInfo(i, parameters[i].getType(), parameters[i].getParameterizedType(), ann, fieldName));
}
return argInfos;
}
/**
* 由注解 + Java 类型组装元信息(方法参数与对象字段共用)。
*/
private McpToolArgInfo toArgInfo(int paramIndex, Class<?> paramType, Type genericType, McpToolParam ann, String fieldName) {
Class<?> elementType = ann.type() == McpToolParamType.ARRAY ? this.inferElementType(genericType, paramType) : null;
Class<?> schemaClass = ann.type() == McpToolParamType.OBJECT ? (ann.schemaClass() == Void.class ? paramType : ann.schemaClass()) : null;
return new McpToolArgInfo(paramIndex, fieldName, ann.type(), ann.description(), paramType, elementType, schemaClass, ann.required());
}
/**
* 组装 inputSchema:{"type":"object","properties":{...},"required":[...]}。
*/
private String buildInputSchema(List<McpToolArgInfo> argInfos) {
JSONObject root = new JSONObject(true);
root.put("type", "object");
JSONObject properties = new JSONObject(true);
JSONArray required = new JSONArray();
for (McpToolArgInfo info : argInfos) {
properties.put(info.getFieldName(), this.buildElementSchema(info));
if (info.isRequired()) {
required.add(info.getFieldName());
}
}
if (!properties.isEmpty()) {
root.put("properties", properties);
}
if (!required.isEmpty()) {
root.put("required", required);
}
return root.toJSONString();
}
/**
* 组装单个参数/字段的 JSON Schema;OBJECT 递归展开 schemaClass 的 @McpToolParam 字段。
*/
private JSONObject buildElementSchema(McpToolArgInfo info) {
JSONObject schema = new JSONObject(true);
schema.put("type", info.getType().getJsonType());
if (StrUtil.isNotBlank(info.getDescription())) {
schema.put("description", info.getDescription());
}
if (info.getType() == McpToolParamType.OBJECT && info.getSchemaClass() != null) {
JSONObject properties = new JSONObject(true);
JSONArray required = new JSONArray();
this.collectObjectSchema(info.getSchemaClass(), properties, required);
if (!properties.isEmpty()) {
schema.put("properties", properties);
}
if (!required.isEmpty()) {
schema.put("required", required);
}
} else if (info.getType() == McpToolParamType.ARRAY && info.getElementType() != null) {
JSONObject items = new JSONObject(true);
items.put("type", this.toJsonType(info.getElementType()));
schema.put("items", items);
}
return schema;
}
/**
* 收集对象类型中 @McpToolParam 标注字段的嵌套 schema。
*/
private void collectObjectSchema(Class<?> schemaClass, JSONObject properties, JSONArray required) {
for (Field field : schemaClass.getDeclaredFields()) {
McpToolParam ann = field.getAnnotation(McpToolParam.class);
if (ann == null) {
continue;
}
String fieldName = StrUtil.isNotBlank(ann.fieldName()) ? ann.fieldName() : field.getName();
properties.put(fieldName, this.buildElementSchema(this.toArgInfo(-1, field.getType(), field.getGenericType(), ann, fieldName)));
if (ann.required()) {
required.add(fieldName);
}
}
}
/**
* 推断数组元素类型:List<E> 泛型实参或数组组件类型,未知默认 String。
*/
private Class<?> inferElementType(Type genericType, Class<?> rawType) {
if (genericType instanceof ParameterizedType) {
Type actual = ((ParameterizedType) genericType).getActualTypeArguments()[0];
if (actual instanceof Class) {
return (Class<?>) actual;
}
}
return rawType.isArray() ? rawType.getComponentType() : String.class;
}
/**
* Java 类型 → JSON Schema type。
*/
private String toJsonType(Class<?> javaType) {
if (javaType == BigDecimal.class || javaType == Double.class || javaType == Float.class
|| javaType == double.class || javaType == float.class) {
return "number";
}
if (javaType == Long.class || javaType == Integer.class || javaType == Short.class || javaType == Byte.class
|| javaType == long.class || javaType == int.class) {
return "integer";
}
if (javaType == Boolean.class || javaType == boolean.class) {
return "boolean";
}
return "string";
}
/**
* 调用工具方法:按元信息从入参 map 取参、转换类型、反射调用,结果包成 CallToolResult。
* <p>MCP 调用运行在 reactor 调度线程,无 servlet 请求上下文,需先挂一个动态代理的 request/response
* (承载 TokenData)再调用,否则 @OperationLog 等切面在 RequestContextHolder 上强转 NPE。</p>
*/
private McpSchema.CallToolResult invoke(Object bean, Method method, List<McpToolArgInfo> argInfos, Map<String, Object> args) {
try {
//todo 这里要求带上token MCP 无 HTTP 登录态,注入默认用户 TokenData,避免服务内 takeFromRequest() 拿到 null 抛 NPE。
TokenData tokenData = new TokenData();
tokenData.setUserId(this.defaultUserId);
return this.withRequestContext(tokenData, () -> {
Object[] invokeArgs = this.bindArgs(args, argInfos, method.getParameterCount());
Object result = method.invoke(bean, invokeArgs);
String text = result == null ? "" : (result instanceof String ? (String) result : JSON.toJSONString(result));
return new McpSchema.CallToolResult(Collections.singletonList(new McpSchema.TextContent(text)), false);
});
} catch (Exception e) {
Throwable cause = e instanceof InvocationTargetException ? ((InvocationTargetException) e).getTargetException() : e;
log.error("MCP 工具 [{}] 调用失败", method.getName(), cause);
return new McpSchema.CallToolResult(Collections.singletonList(new McpSchema.TextContent(cause.getMessage())), true);
}
}
/**
* 在指定 TokenData 的请求上下文下执行动作:当前线程没有请求上下文时挂动态代理的 request/response,
* 执行完恢复原样。
*/
private <T> T withRequestContext(TokenData tokenData, ThrowingSupplier<T> action) throws Exception {
RequestAttributes previous = RequestContextHolder.getRequestAttributes();
boolean created = previous == null;
if (created) {
ServletRequestAttributes attributes = new ServletRequestAttributes(mcpMockRequest(), mcpMockResponse());
RequestContextHolder.setRequestAttributes(attributes);
}
TokenData.addToRequest(tokenData);
try {
return action.get();
} finally {
if (created) {
RequestContextHolder.resetRequestAttributes();
}
}
}
/**
* 动态代理的 HttpServletRequest:仅承载 getAttribute/setAttribute(供 TokenData),其余方法返回安全默认值。
*/
private HttpServletRequest mcpMockRequest() {
return (HttpServletRequest) Proxy.newProxyInstance(this.getClass().getClassLoader(),
new Class<?>[]{HttpServletRequest.class}, new MockRequestHandler());
}
/**
* 动态代理的 HttpServletResponse:setHeader 等方法空操作。
*/
private HttpServletResponse mcpMockResponse() {
return (HttpServletResponse) Proxy.newProxyInstance(this.getClass().getClassLoader(),
new Class<?>[]{HttpServletResponse.class}, (proxy, method, args1) -> defaultValue(method.getReturnType()));
}
/**
* 按 argInfos 的 paramIndex 填充方法实参数组(数组长度按方法形参总数,未标注解的参数保持 null)。
*/
private Object[] bindArgs(Map<String, Object> args, List<McpToolArgInfo> argInfos, int parameterCount) {
Object[] invokeArgs = new Object[parameterCount];
for (McpToolArgInfo info : argInfos) {
invokeArgs[info.getParamIndex()] = this.convertValue(args.get(info.getFieldName()), info);
}
return invokeArgs;
}
/**
* 按元信息把入参 JSON 值转换为 Java 值(简单类型 / 数组 / 对象递归)。
*/
private Object convertValue(Object raw, McpToolArgInfo info) {
if (raw == null) {
return null;
}
switch (info.getType()) {
case STRING:
return String.valueOf(raw);
case BOOLEAN:
return Boolean.valueOf(raw.toString());
case INTEGER:
return this.toScalar(raw, info.getParamType());
case NUMBER:
return this.toScalar(raw, info.getParamType());
case ARRAY:
return this.toArray(raw, info.getParamType(), info.getElementType());
case OBJECT:
return this.toObject(raw, info.getSchemaClass());
default:
return raw;
}
}
/**
* 构造数组实参:支持 List<E> 与 E[],元素按元素类型转换。
*/
private Object toArray(Object raw, Class<?> paramType, Class<?> elementType) {
List<?> source = raw instanceof List ? (List<?>) raw : Arrays.asList((Object[]) raw);
List<Object> converted = new ArrayList<>();
for (Object item : source) {
converted.add(this.toScalar(item, elementType));
}
if (!paramType.isArray()) {
return converted;
}
Object array = Array.newInstance(elementType, converted.size());
for (int i = 0; i < converted.size(); i++) {
Array.set(array, i, converted.get(i));
}
return array;
}
/**
* 构造对象实参:反射 new schemaClass 实例,遍历 @McpToolParam 字段取值转换后 set 入。
*/
private Object toObject(Object raw, Class<?> schemaClass) {
if (!(raw instanceof Map) || schemaClass == null) {
return null;
}
Map<?, ?> map = (Map<?, ?>) raw;
try {
Object instance = schemaClass.getDeclaredConstructor().newInstance();
for (Field field : schemaClass.getDeclaredFields()) {
McpToolParam ann = field.getAnnotation(McpToolParam.class);
if (ann == null) {
continue;
}
String fieldName = StrUtil.isNotBlank(ann.fieldName()) ? ann.fieldName() : field.getName();
Object value = map.get(fieldName);
if (value == null) {
continue;
}
McpToolArgInfo fieldInfo = this.toArgInfo(-1, field.getType(), field.getGenericType(), ann, fieldName);
field.setAccessible(true);
field.set(instance, this.convertValue(value, fieldInfo));
}
return instance;
} catch (Exception e) {
log.error("MCP 对象参数 [{}] 组装失败", schemaClass.getSimpleName(), e);
return null;
}
}
/**
* 标量值按目标 Java 类型转换(String/数值/布尔)。
*/
private Object toScalar(Object raw, Class<?> type) {
if (type == String.class) {
return String.valueOf(raw);
}
if (type == Long.class || type == long.class) {
return Long.valueOf(raw.toString());
}
if (type == Integer.class || type == int.class) {
return Integer.valueOf(raw.toString());
}
if (type == BigDecimal.class) {
return new BigDecimal(raw.toString());
}
if (type == Double.class || type == double.class) {
return Double.valueOf(raw.toString());
}
if (type == Float.class || type == float.class) {
return Float.valueOf(raw.toString());
}
if (type == Boolean.class || type == boolean.class) {
return Boolean.valueOf(raw.toString());
}
return raw;
}
/**
* 允许抛受检异常的 Supplier(method.invoke 抛 IllegalAccessException/InvocationTargetException)。
*/
@FunctionalInterface
private interface ThrowingSupplier<T> {
T get() throws Exception;
}
/**
* 动态代理 request 处理器:getAttribute/setAttribute 落到内存 map,其余方法返回安全默认值。
*/
private static class MockRequestHandler implements InvocationHandler {
private final Map<String, Object> attributes = new HashMap<>();
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
String name = method.getName();
if ("getAttribute".equals(name)) {
return attributes.get(args[0]);
}
if ("setAttribute".equals(name)) {
attributes.put((String) args[0], args[1]);
return null;
}
if ("getHeader".equals(name)) {
return attributes.get(args[0]);
}
if ("getRequestURI".equals(name)) {
return "/mcp";
}
return defaultValue(method.getReturnType());
}
}
}

47
common/common-core/src/main/java/apelet/common/core/mcp/controller/McpStreamableController.java

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
package apelet.common.core.mcp.controller;
import apelet.common.core.mcp.transport.StreamableHttpServerTransportProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* Streamable HTTP 单端点(POST /tenantadmin/mcp)。
* 直接读写 HttpServletRequest/Response,序列化由 provider 完成并原样写入,
* 不经过 Spring 消息转换器(应用 ObjectMapper 会丢 @JsonTypeInfo 的 type 判别符 / 双重编码 JsonNode)。
*
* @author chenchuchuan
* @date 2026-08-28
*/
@RestController
@RequestMapping("/tenantadmin/mcp")
public class McpStreamableController {
@Autowired
private StreamableHttpServerTransportProvider streamableHttpTransport;
@PostMapping
public void handle(HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Mcp-Session-Id", required = false) String sessionHeader) throws IOException {
String body = new String(StreamUtils.copyToByteArray(request.getInputStream()), StandardCharsets.UTF_8);
StreamableHttpServerTransportProvider.McpProcessResult result =
streamableHttpTransport.processMcpRequest(body, sessionHeader);
response.setStatus(result.httpStatus);
response.setContentType("application/json;charset=UTF-8");
if (result.sessionId != null) {
response.setHeader(StreamableHttpServerTransportProvider.SESSION_HEADER, result.sessionId);
}
String json = result.responseJson != null ? result.responseJson : result.errorJson;
if (json != null) {
response.getWriter().write(json);
}
}
}

71
common/common-core/src/main/java/apelet/common/core/mcp/model/McpToolArgInfo.java

@ -0,0 +1,71 @@ @@ -0,0 +1,71 @@
package apelet.common.core.mcp.model;
/**
* 单个 MCP 工具方法参数/对象字段的元信息:组装 inputSchema 与调用时绑定实参共用。
*
* @author chenchuchuan
* @date 2026-08-31
*/
public class McpToolArgInfo {
/** 参数在方法形参列表中的下标;对象字段为 -1。 */
private final int paramIndex;
/** 入参字段名(注解 fieldName 未配置时为 Java 参数名/属性名)。 */
private final String fieldName;
/** 注解声明的类型(决定 inputSchema 的 type 与转换分支)。 */
private final McpToolParamType type;
/** 字段描述。 */
private final String description;
/** Java 类型(形参或字段类型)。 */
private final Class<?> paramType;
/** ARRAY 类型的元素类型(由泛型实参或数组组件类型推断),非 ARRAY 为 null。 */
private final Class<?> elementType;
/** OBJECT 类型指向的对象类型,非 OBJECT 为 null。 */
private final Class<?> schemaClass;
/** 是否必填(进 inputSchema 的 required 数组)。 */
private final boolean required;
public McpToolArgInfo(int paramIndex, String fieldName, McpToolParamType type, String description,
Class<?> paramType, Class<?> elementType, Class<?> schemaClass, boolean required) {
this.paramIndex = paramIndex;
this.fieldName = fieldName;
this.type = type;
this.description = description;
this.paramType = paramType;
this.elementType = elementType;
this.schemaClass = schemaClass;
this.required = required;
}
public int getParamIndex() {
return paramIndex;
}
public String getFieldName() {
return fieldName;
}
public McpToolParamType getType() {
return type;
}
public String getDescription() {
return description;
}
public Class<?> getParamType() {
return paramType;
}
public Class<?> getElementType() {
return elementType;
}
public Class<?> getSchemaClass() {
return schemaClass;
}
public boolean isRequired() {
return required;
}
}

35
common/common-core/src/main/java/apelet/common/core/mcp/model/McpToolParamType.java

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
package apelet.common.core.mcp.model;
import lombok.Getter;
/**
* MCP 参数类型枚举:value 即 JSON Schema 的 type 值。
* <p>Java 侧实际的转换类型由方法参数声明决定,枚举只负责 inputSchema 的 type 与转换分支。</p>
*
* @author chenchuchuan
* @date 2026-08-31
*/
@Getter
public enum McpToolParamType {
/** 字符串 */
STRING("string"),
/** 数字(整数或浮点),对应 BigDecimal/Double/Float */
NUMBER("number"),
/** 整数,对应 Integer/Long */
INTEGER("integer"),
/** 布尔值 */
BOOLEAN("boolean"),
/** 数组,元素类型由 List 泛型实参或数组组件类型推断 */
ARRAY("array"),
/** 对象,需要配合 schemaClass 指定类型 */
OBJECT("object");
/** JSON Schema 的 type 值 */
private final String jsonType;
McpToolParamType(String jsonType) {
this.jsonType = jsonType;
}
}

230
common/common-core/src/main/java/apelet/common/core/mcp/transport/StreamableHttpServerTransportProvider.java

@ -0,0 +1,230 @@ @@ -0,0 +1,230 @@
package apelet.common.core.mcp.transport;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransport;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* Streamable HTTP 传输层(MCP spec 2025-03-26 单端点 POST 传输)。
* 基于 mcp-java8-sdk 0.8.1 的 SPI(McpServerTransportProvider + McpServerTransport)自研,
* 因该 fork 不提供 Streamable HTTP Provider(官方 SDK 0.9.0 才引入)。
* 会话通过 Mcp-Session-Id 头管理:initialize 创建,后续请求携带。
* 响应序列化由调用方(McpStreamableController)直接写入 HTTP 响应,
* 不经过 Spring 消息转换器,避免应用 ObjectMapper 丢失 @JsonTypeInfo 的 type 判别符
* 或对 JsonNode 双重编码。
*
* @author chenchuchuan
* @date 2026-08-28
*/
public class StreamableHttpServerTransportProvider implements McpServerTransportProvider {
private static final Logger logger = LoggerFactory.getLogger(StreamableHttpServerTransportProvider.class);
public static final String SESSION_HEADER = "Mcp-Session-Id";
private final ObjectMapper objectMapper;
private final ConcurrentHashMap<String, McpServerSession> sessions = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, StreamableHttpSessionTransport> transports = new ConcurrentHashMap<>();
private volatile boolean isClosing;
private McpServerSession.Factory sessionFactory;
public StreamableHttpServerTransportProvider(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void setSessionFactory(McpServerSession.Factory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@Override
public Mono<Void> notifyClients(String method, Map<String, Object> params) {
return Flux.fromIterable(sessions.values())
.flatMap(session -> session.sendNotification(method, params))
.then();
}
@Override
public Mono<Void> closeGracefully() {
isClosing = true;
return Flux.fromIterable(sessions.values())
.flatMap(McpServerSession::closeGracefully)
.then();
}
/**
* 处理一条 MCP 请求:JSON-RPC 解析 + Mcp-Session-Id 会话路由 + 业务处理。
* 返回序列化好的响应 JSON(含 type 判别符),由调用方原样写入 HTTP 响应。
*/
public McpProcessResult processMcpRequest(String body, String sessionHeader) {
if (isClosing) {
return McpProcessResult.error(503, null, "Server is shutting down");
}
McpSchema.JSONRPCMessage message;
try {
message = McpSchema.deserializeJsonRpcMessage(objectMapper, body);
} catch (Exception e) {
logger.error("Failed to deserialize MCP message: {}", e.getMessage());
return McpProcessResult.error(400, null, "Invalid message format");
}
String sessionId = sessionHeader;
boolean isInitialize = message instanceof McpSchema.JSONRPCRequest
&& "initialize".equals(((McpSchema.JSONRPCRequest) message).getMethod());
McpServerSession session;
StreamableHttpSessionTransport transport;
if (isInitialize && sessionId == null) {
sessionId = UUID.randomUUID().toString();
transport = new StreamableHttpSessionTransport(sessionId, objectMapper);
session = sessionFactory.create(transport);
sessions.put(sessionId, session);
transports.put(sessionId, transport);
} else if (sessionId != null) {
transport = transports.get(sessionId);
session = sessions.get(sessionId);
if (session == null || transport == null) {
return McpProcessResult.error(404, sessionId, "Session not found: " + sessionId);
}
} else {
return McpProcessResult.error(400, null, "Session ID missing in message endpoint");
}
logger.info("MCP 请求 method={} sessionId={}",
message instanceof McpSchema.JSONRPCRequest ? ((McpSchema.JSONRPCRequest) message).getMethod()
: (message instanceof McpSchema.JSONRPCNotification ? ((McpSchema.JSONRPCNotification) message).getMethod() : "unknown"),
sessionId);
try {
session.handle(message).block(Duration.ofSeconds(30));
// 协议上客户端应在 initialize 后发送 notifications/initialized 才会进入 INITIALIZED 状态,
// 但直接 HTTP 调用(Postman / 平台扫描)往往省略该通知,导致后续 tools/list 等请求
// 因 exchange 未就绪而在 exchangeSink.asMono() 上无限阻塞超时,故服务端自动补发。
if (isInitialize) {
session.handle(new McpSchema.JSONRPCNotification("2.0", "notifications/initialized", Collections.emptyMap()))
.block(Duration.ofSeconds(30));
}
} catch (Exception e) {
logger.error("Failed to process MCP message: {}", e.getMessage());
return McpProcessResult.error(500, sessionId, "Failed to process MCP message: " + e.getMessage());
}
McpSchema.JSONRPCMessage response = null;
if (message instanceof McpSchema.JSONRPCRequest) {
try {
response = transport.pollResponse();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return McpProcessResult.error(500, sessionId, "Interrupted while waiting for response");
}
}
try {
return new McpProcessResult(200, sessionId, response != null ? objectMapper.writeValueAsString(response) : null, null);
} catch (Exception e) {
logger.error("Failed to serialize MCP response: {}", e.getMessage());
return McpProcessResult.error(500, sessionId, "Failed to serialize MCP response: " + e.getMessage());
}
}
/**
* MCP 请求处理结果,供控制器原样写入 HTTP 响应。
*/
public static class McpProcessResult {
/** HTTP 状态码 */
public final int httpStatus;
/** 会话 id(可能为 null) */
public final String sessionId;
/** 正常响应 JSON(含 type 判别符);通知类请求为 null */
public final String responseJson;
/** 错误响应 JSON;正常时为 null */
public final String errorJson;
McpProcessResult(int httpStatus, String sessionId, String responseJson, String errorJson) {
this.httpStatus = httpStatus;
this.sessionId = sessionId;
this.responseJson = responseJson;
this.errorJson = errorJson;
}
static McpProcessResult error(int httpStatus, String sessionId, String message) {
return new McpProcessResult(httpStatus, sessionId, null,
"{\"error\":\"" + message.replace("\"", "\\\"") + "\"}");
}
}
/**
* 每会话的传输层:sendMessage 把出站消息放入队列,由 processMcpRequest 取走序列化写回 HTTP 响应。
*/
public static class StreamableHttpSessionTransport implements McpServerTransport {
private final String sessionId;
private final ObjectMapper objectMapper;
private final BlockingQueue<McpSchema.JSONRPCMessage> outboundMessages = new LinkedBlockingQueue<>();
private volatile boolean isClosed;
StreamableHttpSessionTransport(String sessionId, ObjectMapper objectMapper) {
this.sessionId = sessionId;
this.objectMapper = objectMapper;
}
@Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
return Mono.fromRunnable(() -> {
try {
if (isClosed) {
logger.warn("Failed to send message to session {}: transport is closed", sessionId);
return;
}
outboundMessages.offer(message);
logger.debug("Message sent to session {}", sessionId);
} catch (Exception e) {
logger.error("Failed to send message to session {}: {}", sessionId, e.getMessage());
}
});
}
@Override
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
return objectMapper.convertValue(data, typeRef);
}
@Override
public Mono<Void> closeGracefully() {
return Mono.fromRunnable(() -> {
isClosed = true;
outboundMessages.clear();
});
}
@Override
public void close() {
isClosed = true;
outboundMessages.clear();
}
/**
* 取出一条出站消息(通常是对当前请求的 JSON-RPC 响应)。
*/
McpSchema.JSONRPCMessage pollResponse() throws InterruptedException {
return isClosed ? null : outboundMessages.poll(1, TimeUnit.SECONDS);
}
}
}

5
common/common-core/src/main/java/apelet/common/core/object/MyPageParam.java

@ -1,5 +1,7 @@ @@ -1,5 +1,7 @@
package apelet.common.core.object;
import apelet.common.core.mcp.annotation.McpToolParam;
import apelet.common.core.mcp.model.McpToolParamType;
import lombok.Getter;
/**
@ -18,16 +20,19 @@ public class MyPageParam { @@ -18,16 +20,19 @@ public class MyPageParam {
/**
* 分页号码,从1开始计数。
*/
@McpToolParam(fieldName = "pageNum", type = McpToolParamType.INTEGER, description = "分页页号,从1开始")
private Integer pageNum;
/**
* 每页大小。
*/
@McpToolParam(fieldName = "pageSize", type = McpToolParamType.INTEGER, description = "每页大小")
private Integer pageSize;
/**
* 是否统计totalCount
*/
@McpToolParam(fieldName = "count", type = McpToolParamType.BOOLEAN, description = "是否统计 totalCount", required = false)
private Boolean count = true;
/**

8
common/common-online/src/main/java/apelet/common/online/controller/OnlineColumnController.java

@ -3,6 +3,9 @@ package apelet.common.online.controller; @@ -3,6 +3,9 @@ package apelet.common.online.controller;
import apelet.common.core.annotation.MyRequestBody;
import apelet.common.core.constant.ErrorCodeEnum;
import apelet.common.core.constant.MaskFieldTypeEnum;
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.*;
import apelet.common.core.util.MyCommonUtil;
import apelet.common.core.util.MyModelUtil;
@ -176,9 +179,10 @@ public class OnlineColumnController { @@ -176,9 +179,10 @@ public class OnlineColumnController {
* @return 应答结果对象,包含查询结果集。
*/
@PostMapping("/list")
@McpToolClass(name = "online_column_list", description = "分页查询在线表字段列表,pageParam 传 pageNum/pageSize")
public ResponseResult<MyPageData<OnlineColumnVo>> list(
@MyRequestBody OnlineColumnDto onlineColumnDtoFilter,
@MyRequestBody MyPageParam pageParam) {
@MyRequestBody @McpToolParam(fieldName = "onlineColumnDtoFilter", type = McpToolParamType.OBJECT, description = "字段过滤条件", schemaClass = OnlineColumnDto.class) OnlineColumnDto onlineColumnDtoFilter,
@MyRequestBody @McpToolParam(fieldName = "pageParam", type = McpToolParamType.OBJECT, description = "分页参数(pageNum/pageSize)", schemaClass = MyPageParam.class) MyPageParam pageParam) {
if (pageParam != null) {
PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
}

8
common/common-online/src/main/java/apelet/common/online/controller/OnlineDatasourceRelationController.java

@ -2,6 +2,9 @@ package apelet.common.online.controller; @@ -2,6 +2,9 @@ package apelet.common.online.controller;
import apelet.common.core.annotation.MyRequestBody;
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.*;
import apelet.common.core.util.MyCommonUtil;
import apelet.common.core.util.MyModelUtil;
@ -203,10 +206,11 @@ public class OnlineDatasourceRelationController { @@ -203,10 +206,11 @@ public class OnlineDatasourceRelationController {
* @return 应答结果对象,包含查询结果集。
*/
@PostMapping("/list")
@McpToolClass(name = "online_datasource_relation_list", description = "分页查询数据源关联列表,pageParam 传 pageNum/pageSize")
public ResponseResult<MyPageData<OnlineDatasourceRelationVo>> list(
@MyRequestBody OnlineDatasourceRelationDto onlineDatasourceRelationDtoFilter,
@MyRequestBody @McpToolParam(fieldName = "onlineDatasourceRelationDtoFilter", type = McpToolParamType.OBJECT, description = "关联过滤条件", schemaClass = OnlineDatasourceRelationDto.class) OnlineDatasourceRelationDto onlineDatasourceRelationDtoFilter,
@MyRequestBody MyOrderParam orderParam,
@MyRequestBody MyPageParam pageParam) {
@MyRequestBody @McpToolParam(fieldName = "pageParam", type = McpToolParamType.OBJECT, description = "分页参数(pageNum/pageSize)", schemaClass = MyPageParam.class) MyPageParam pageParam) {
if (pageParam != null) {
PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
}

21
common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java

@ -7,6 +7,9 @@ import apelet.common.core.cache.CacheKey; @@ -7,6 +7,9 @@ import apelet.common.core.cache.CacheKey;
import apelet.common.core.constant.AppDeviceType;
import apelet.common.core.constant.ErrorCodeEnum;
import apelet.common.core.constant.GlobalDeletedFlag;
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.*;
import apelet.common.core.util.MyCommonUtil;
import apelet.common.core.util.MyModelUtil;
@ -149,7 +152,8 @@ public class OnlineFormController { @@ -149,7 +152,8 @@ public class OnlineFormController {
@OperationLog(type = SysOperationLogType.ADD)
@PostMapping("/add")
@Transactional(rollbackFor = Exception.class)
public ResponseResult<Long> add(@MyRequestBody OnlineFormDto onlineFormDto) {
@McpToolClass(name = "online_form_add", description = "新增在线表单")
public ResponseResult<Long> add(@MyRequestBody @McpToolParam(fieldName = "onlineFormDto", type = McpToolParamType.OBJECT, description = "表单信息", schemaClass = OnlineFormDto.class, required = true) OnlineFormDto onlineFormDto) {
// 快捷表单:走快捷表单保存逻辑
if (onlineFormDto.getFormType() != null && onlineFormDto.getFormType() == FormType.QUICK_FORM) {
return ResponseResult.success(this.quickFormSave(onlineFormDto, null));
@ -193,7 +197,8 @@ public class OnlineFormController { @@ -193,7 +197,8 @@ public class OnlineFormController {
@OperationLog(type = SysOperationLogType.UPDATE)
@PostMapping("/update")
@Transactional(rollbackFor = Exception.class)
public ResponseResult<Void> update(@MyRequestBody OnlineFormDto onlineFormDto) {
@McpToolClass(name = "online_form_update", description = "修改在线表单")
public ResponseResult<Void> update(@MyRequestBody @McpToolParam(fieldName = "onlineFormDto", type = McpToolParamType.OBJECT, description = "表单信息", schemaClass = OnlineFormDto.class, required = true) OnlineFormDto onlineFormDto) {
// 快捷表单:formType=QUICK_FORM 才是快捷表单,formId 即快捷表单id,走快捷表单保存逻辑
if (onlineFormDto.getFormType() != null && onlineFormDto.getFormType() == FormType.QUICK_FORM) {
this.quickFormSave(onlineFormDto, onlineFormDto.getFormId());
@ -302,7 +307,8 @@ public class OnlineFormController { @@ -302,7 +307,8 @@ public class OnlineFormController {
*/
@OperationLog(type = SysOperationLogType.DELETE)
@PostMapping("/delete")
public ResponseResult<Void> delete(@MyRequestBody Long formId) {
@McpToolClass(name = "online_form_delete", description = "删除在线表单")
public ResponseResult<Void> delete(@MyRequestBody @McpToolParam(fieldName = "formId", type = McpToolParamType.INTEGER, description = "表单主键Id", required = true) Long formId) {
String errorMessage;
ResponseResult<OnlineForm> verifyResult = this.doVerifyAndGet(formId);
if (!verifyResult.isSuccess()) {
@ -339,7 +345,8 @@ public class OnlineFormController { @@ -339,7 +345,8 @@ public class OnlineFormController {
*/
@OperationLog(type = SysOperationLogType.ADD)
@PostMapping("/clone")
public ResponseResult<Long> clone(@MyRequestBody Long formId) {
@McpToolClass(name = "online_form_clone", description = "复制在线表单")
public ResponseResult<Long> clone(@MyRequestBody @McpToolParam(fieldName = "formId", type = McpToolParamType.INTEGER, description = "源表单主键Id", required = true) Long formId) {
ResponseResult<OnlineForm> verifyResult = this.doVerifyAndGet(formId);
if (!verifyResult.isSuccess()) {
// formId 不是在线表单:可能是快捷表单,按快捷表单复制
@ -406,7 +413,8 @@ public class OnlineFormController { @@ -406,7 +413,8 @@ public class OnlineFormController {
* @return 应答结果对象,包含查询结果集。
*/
@PostMapping("/list")
public ResponseResult<MyPageData<OnlineFormVo>> list(@MyRequestBody OnlineFormDto onlineFormDtoFilter, @MyRequestBody MyOrderParam orderParam, @MyRequestBody MyPageParam pageParam) {
@McpToolClass(name = "online_form_list", description = "分页查询在线表单,pageParam 传 pageNum/pageSize")
public ResponseResult<MyPageData<OnlineFormVo>> list(@MyRequestBody @McpToolParam(fieldName = "onlineFormDtoFilter", type = McpToolParamType.OBJECT, description = "过滤条件", schemaClass = OnlineFormDto.class) OnlineFormDto onlineFormDtoFilter, @MyRequestBody MyOrderParam orderParam, @MyRequestBody @McpToolParam(fieldName = "pageParam", type = McpToolParamType.OBJECT, description = "分页参数(pageNum/pageSize)", schemaClass = MyPageParam.class) MyPageParam pageParam) {
// 不做分页:非快捷表单 + 快捷表单合并,再按前端排序字段排序
List<OnlineFormVo> voList = new ArrayList<>();
OnlineForm onlineFormFilter = MyModelUtil.copyTo(onlineFormDtoFilter, OnlineForm.class);
@ -432,7 +440,8 @@ public class OnlineFormController { @@ -432,7 +440,8 @@ public class OnlineFormController {
* @return 应答结果对象,包含对象详情。
*/
@GetMapping("/view")
public ResponseResult<OnlineFormVo> view(@RequestParam Long formId) {
@McpToolClass(name = "online_form_view", description = "查看在线表单详情")
public ResponseResult<OnlineFormVo> view(@RequestParam @McpToolParam(fieldName = "formId", type = McpToolParamType.INTEGER, description = "表单主键Id", required = true) Long formId) {
ResponseResult<OnlineForm> verifyResult = this.doVerifyAndGet(formId);
if (!verifyResult.isSuccess()) {
return ResponseResult.errorFrom(verifyResult);

32
common/common-online/src/main/java/apelet/common/online/controller/OnlinePageController.java

@ -2,6 +2,9 @@ package apelet.common.online.controller; @@ -2,6 +2,9 @@ package apelet.common.online.controller;
import apelet.common.core.annotation.MyRequestBody;
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.*;
import apelet.common.core.util.MyCommonUtil;
import apelet.common.core.util.MyModelUtil;
@ -95,7 +98,8 @@ public class OnlinePageController { @@ -95,7 +98,8 @@ public class OnlinePageController {
@OperationLog(type = SysOperationLogType.ADD)
@Transactional(rollbackFor = Exception.class)
@PostMapping("/add")
public ResponseResult<OnlinePage> add(@MyRequestBody OnlinePageDto onlinePageDto) {
@McpToolClass(name = "online_page_add", description = "新增在线表单页面数据,metaSource=new 时新建元数据,metaSource=existing 时使用已有表单元数据")
public ResponseResult<OnlinePage> add(@MyRequestBody @McpToolParam(fieldName = "onlinePageDto", type = McpToolParamType.OBJECT, description = "页面信息", schemaClass = OnlinePageDto.class, required = true) OnlinePageDto onlinePageDto) {
String errorMessage = MyCommonUtil.getModelValidationError(onlinePageDto);
if (errorMessage != null) {
return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
@ -404,7 +408,8 @@ public class OnlinePageController { @@ -404,7 +408,8 @@ public class OnlinePageController {
*/
@OperationLog(type = SysOperationLogType.UPDATE)
@PostMapping("/update")
public ResponseResult<Void> update(@MyRequestBody OnlinePageDto onlinePageDto) {
@McpToolClass(name = "online_page_update", description = "修改在线表单页面")
public ResponseResult<Void> update(@MyRequestBody @McpToolParam(fieldName = "onlinePageDto", type = McpToolParamType.OBJECT, description = "页面信息", schemaClass = OnlinePageDto.class, required = true) OnlinePageDto onlinePageDto) {
String errorMessage = MyCommonUtil.getModelValidationError(onlinePageDto, Default.class, UpdateGroup.class);
try {
if (errorMessage != null) {
@ -481,9 +486,10 @@ public class OnlinePageController { @@ -481,9 +486,10 @@ public class OnlinePageController {
*/
@OperationLog(type = SysOperationLogType.UPDATE)
@PostMapping("/updatePublished")
@McpToolClass(name = "online_page_update_status", description = "发布/取消发布在线表单页面")
public ResponseResult<Void> updateStatus(
@MyRequestBody(required = true) Long pageId,
@MyRequestBody(required = true) Boolean published) {
@MyRequestBody(required = true) @McpToolParam(fieldName = "pageId", type = McpToolParamType.INTEGER, description = "页面主键Id", required = true) Long pageId,
@MyRequestBody(required = true) @McpToolParam(fieldName = "published", type = McpToolParamType.BOOLEAN, description = "发布状态:true=发布,false=取消发布", required = true) Boolean published) {
String errorMessage;
ResponseResult<OnlinePage> verifyResult = this.doVerifyAndGet(pageId);
if (!verifyResult.isSuccess()) {
@ -513,7 +519,8 @@ public class OnlinePageController { @@ -513,7 +519,8 @@ public class OnlinePageController {
*/
@OperationLog(type = SysOperationLogType.DELETE)
@PostMapping("/delete")
public ResponseResult<Void> delete(@MyRequestBody Long pageId) {
@McpToolClass(name = "online_page_delete", description = "删除在线表单页面")
public ResponseResult<Void> delete(@MyRequestBody @McpToolParam(fieldName = "pageId", type = McpToolParamType.INTEGER, description = "页面主键Id", required = true) Long pageId) {
String errorMessage;
ResponseResult<OnlinePage> verifyResult = this.doVerifyAndGet(pageId);
if (!verifyResult.isSuccess()) {
@ -538,10 +545,11 @@ public class OnlinePageController { @@ -538,10 +545,11 @@ public class OnlinePageController {
* @return 应答结果对象,包含查询结果集。
*/
@PostMapping("/list")
@McpToolClass(name = "online_page_list", description = "分页查询在线表单页面,pageParam 传 pageNum/pageSize")
public ResponseResult<MyPageData<OnlinePageVo>> list(
@MyRequestBody OnlinePageDto onlinePageDtoFilter,
@MyRequestBody @McpToolParam(fieldName = "onlinePageDtoFilter", type = McpToolParamType.OBJECT, description = "过滤条件", schemaClass = OnlinePageDto.class) OnlinePageDto onlinePageDtoFilter,
@MyRequestBody MyOrderParam orderParam,
@MyRequestBody MyPageParam pageParam) {
@MyRequestBody @McpToolParam(fieldName = "pageParam", type = McpToolParamType.OBJECT, description = "分页参数(pageNum/pageSize)", schemaClass = MyPageParam.class) MyPageParam pageParam) {
if (pageParam != null) {
PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
}
@ -574,7 +582,8 @@ public class OnlinePageController { @@ -574,7 +582,8 @@ public class OnlinePageController {
* @return 应答结果对象,包含对象详情。
*/
@GetMapping("/view")
public ResponseResult<OnlinePageVo> view(@RequestParam Long pageId) {
@McpToolClass(name = "online_page_view", description = "查看在线表单页面详情")
public ResponseResult<OnlinePageVo> view(@RequestParam @McpToolParam(fieldName = "pageId", type = McpToolParamType.INTEGER, description = "页面主键Id", required = true) Long pageId) {
ResponseResult<OnlinePage> verifyResult = this.doVerifyAndGet(pageId);
if (!verifyResult.isSuccess()) {
return ResponseResult.errorFrom(verifyResult);
@ -597,11 +606,12 @@ public class OnlinePageController { @@ -597,11 +606,12 @@ public class OnlinePageController {
* @return 应答结果对象,返回符合条件的数据列表。
*/
@PostMapping("/listOnlinePageDatasource")
@McpToolClass(name = "online_page_list_datasource", description = "分页查询在线表单页面关联的数据源")
public ResponseResult<MyPageData<OnlineDatasourceVo>> listOnlinePageDatasource(
@MyRequestBody Long pageId,
@MyRequestBody OnlineDatasourceDto onlineDatasourceDtoFilter,
@MyRequestBody @McpToolParam(fieldName = "pageId", type = McpToolParamType.INTEGER, description = "页面主键Id", required = true) Long pageId,
@MyRequestBody @McpToolParam(fieldName = "onlineDatasourceDtoFilter", type = McpToolParamType.OBJECT, description = "数据源过滤条件", schemaClass = OnlineDatasourceDto.class) OnlineDatasourceDto onlineDatasourceDtoFilter,
@MyRequestBody MyOrderParam orderParam,
@MyRequestBody MyPageParam pageParam) {
@MyRequestBody @McpToolParam(fieldName = "pageParam", type = McpToolParamType.OBJECT, description = "分页参数(pageNum/pageSize)", schemaClass = MyPageParam.class) MyPageParam pageParam) {
ResponseResult<OnlinePage> verifyResult = this.doVerifyAndGet(pageId);
if (!verifyResult.isSuccess()) {
return ResponseResult.errorFrom(verifyResult);

27
common/common-online/src/main/java/apelet/common/online/dto/OnlineColumnDto.java

@ -1,5 +1,7 @@ @@ -1,5 +1,7 @@
package apelet.common.online.dto;
import apelet.common.core.mcp.annotation.McpToolParam;
import apelet.common.core.mcp.model.McpToolParamType;
import apelet.common.core.validator.ConstDictRef;
import apelet.common.core.validator.UpdateGroup;
import apelet.common.online.model.constant.FieldFilterType;
@ -25,6 +27,7 @@ public class OnlineColumnDto { @@ -25,6 +27,7 @@ public class OnlineColumnDto {
*/
@Schema(description = "主键Id")
@NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class})
@McpToolParam(fieldName = "columnId", type = McpToolParamType.INTEGER, description = "字段主键Id")
private Long columnId;
/**
@ -32,6 +35,7 @@ public class OnlineColumnDto { @@ -32,6 +35,7 @@ public class OnlineColumnDto {
*/
@Schema(description = "字段名")
@NotBlank(message = "数据验证失败,字段名不能为空!")
@McpToolParam(fieldName = "columnName", type = McpToolParamType.STRING, description = "字段名")
private String columnName;
/**
@ -39,6 +43,7 @@ public class OnlineColumnDto { @@ -39,6 +43,7 @@ public class OnlineColumnDto {
*/
@Schema(description = "数据表Id")
@NotNull(message = "数据验证失败,数据表Id不能为空!")
@McpToolParam(fieldName = "tableId", type = McpToolParamType.INTEGER, description = "数据表Id")
private Long tableId;
/**
@ -46,6 +51,7 @@ public class OnlineColumnDto { @@ -46,6 +51,7 @@ public class OnlineColumnDto {
*/
@Schema(description = "数据表中的字段类型")
@NotBlank(message = "数据验证失败,数据表中的字段类型不能为空!")
@McpToolParam(fieldName = "columnType", type = McpToolParamType.STRING, description = "数据表中的字段类型")
private String columnType;
/**
@ -53,6 +59,7 @@ public class OnlineColumnDto { @@ -53,6 +59,7 @@ public class OnlineColumnDto {
*/
@Schema(description = "数据表中的完整字段类型")
@NotBlank(message = "数据验证失败,数据表中的完整字段类型(包括了精度和刻度)不能为空!")
@McpToolParam(fieldName = "fullColumnType", type = McpToolParamType.STRING, description = "完整字段类型(含精度刻度)")
private String fullColumnType;
/**
@ -60,6 +67,7 @@ public class OnlineColumnDto { @@ -60,6 +67,7 @@ public class OnlineColumnDto {
*/
@Schema(description = "是否为主键")
@NotNull(message = "数据验证失败,是否为主键不能为空!")
@McpToolParam(fieldName = "primaryKey", type = McpToolParamType.BOOLEAN, description = "是否为主键")
private Boolean primaryKey;
/**
@ -67,6 +75,7 @@ public class OnlineColumnDto { @@ -67,6 +75,7 @@ public class OnlineColumnDto {
*/
@Schema(description = "是否是自增主键")
@NotNull(message = "数据验证失败,是否是自增主键(0: 不是 1: 是)不能为空!")
@McpToolParam(fieldName = "autoIncrement", type = McpToolParamType.BOOLEAN, description = "是否自增主键")
private Boolean autoIncrement;
/**
@ -74,12 +83,14 @@ public class OnlineColumnDto { @@ -74,12 +83,14 @@ public class OnlineColumnDto {
*/
@Schema(description = "是否可以为空")
@NotNull(message = "数据验证失败,是否可以为空 (0: 不可以为空 1: 可以为空)不能为空!")
@McpToolParam(fieldName = "nullable", type = McpToolParamType.BOOLEAN, description = "是否可以为空")
private Boolean nullable;
/**
* 缺省值。
*/
@Schema(description = "缺省值")
@McpToolParam(fieldName = "columnDefault", type = McpToolParamType.STRING, description = "缺省值")
private String columnDefault;
/**
@ -87,12 +98,14 @@ public class OnlineColumnDto { @@ -87,12 +98,14 @@ public class OnlineColumnDto {
*/
@Schema(description = "字段在数据表中的显示位置")
@NotNull(message = "数据验证失败,字段在数据表中的显示位置不能为空!")
@McpToolParam(fieldName = "columnShowOrder", type = McpToolParamType.INTEGER, description = "字段显示位置")
private Integer columnShowOrder;
/**
* 数据表中的字段注释。
*/
@Schema(description = "数据表中的字段注释")
@McpToolParam(fieldName = "columnComment", type = McpToolParamType.STRING, description = "字段注释")
private String columnComment;
/**
@ -100,6 +113,7 @@ public class OnlineColumnDto { @@ -100,6 +113,7 @@ public class OnlineColumnDto {
*/
@Schema(description = "对象映射字段名称")
@NotBlank(message = "数据验证失败,对象映射字段名称不能为空!")
@McpToolParam(fieldName = "objectFieldName", type = McpToolParamType.STRING, description = "对象映射字段名称")
private String objectFieldName;
/**
@ -107,18 +121,21 @@ public class OnlineColumnDto { @@ -107,18 +121,21 @@ public class OnlineColumnDto {
*/
@Schema(description = "对象映射字段类型")
@NotBlank(message = "数据验证失败,对象映射字段类型不能为空!")
@McpToolParam(fieldName = "objectFieldType", type = McpToolParamType.STRING, description = "对象映射字段类型")
private String objectFieldType;
/**
* 数值型字段的精度(目前仅Oracle使用)。
*/
@Schema(description = "数值型字段的精度")
@McpToolParam(fieldName = "numericPrecision", type = McpToolParamType.INTEGER, description = "数值精度")
private Integer numericPrecision;
/**
* 数值型字段的刻度(小数点后位数,目前仅Oracle使用)。
*/
@Schema(description = "数值型字段的刻度")
@McpToolParam(fieldName = "numericScale", type = McpToolParamType.INTEGER, description = "数值刻度")
private Integer numericScale;
/**
@ -127,6 +144,7 @@ public class OnlineColumnDto { @@ -127,6 +144,7 @@ public class OnlineColumnDto {
@Schema(description = "过滤类型字段")
@NotNull(message = "数据验证失败,过滤类型字段不能为空!", groups = {UpdateGroup.class})
@ConstDictRef(constDictClass = FieldFilterType.class, message = "数据验证失败,过滤类型字段为无效值!")
@McpToolParam(fieldName = "filterType", type = McpToolParamType.INTEGER, description = "过滤类型")
private Integer filterType;
/**
@ -134,6 +152,7 @@ public class OnlineColumnDto { @@ -134,6 +152,7 @@ public class OnlineColumnDto {
*/
@Schema(description = "是否是主键的父Id")
@NotNull(message = "数据验证失败,是否是主键的父Id不能为空!")
@McpToolParam(fieldName = "parentKey", type = McpToolParamType.BOOLEAN, description = "是否主键的父Id")
private Boolean parentKey;
/**
@ -141,6 +160,7 @@ public class OnlineColumnDto { @@ -141,6 +160,7 @@ public class OnlineColumnDto {
*/
@Schema(description = "是否部门过滤字段")
@NotNull(message = "数据验证失败,是否部门过滤字段标记不能为空!")
@McpToolParam(fieldName = "deptFilter", type = McpToolParamType.BOOLEAN, description = "是否部门过滤字段")
private Boolean deptFilter;
/**
@ -148,6 +168,7 @@ public class OnlineColumnDto { @@ -148,6 +168,7 @@ public class OnlineColumnDto {
*/
@Schema(description = "是否用户过滤字段")
@NotNull(message = "数据验证失败,是否用户过滤字段标记不能为空!")
@McpToolParam(fieldName = "userFilter", type = McpToolParamType.BOOLEAN, description = "是否用户过滤字段")
private Boolean userFilter;
/**
@ -155,35 +176,41 @@ public class OnlineColumnDto { @@ -155,35 +176,41 @@ public class OnlineColumnDto {
*/
@Schema(description = "字段类别")
@ConstDictRef(constDictClass = FieldKind.class, message = "数据验证失败,字段类别为无效值!")
@McpToolParam(fieldName = "fieldKind", type = McpToolParamType.INTEGER, description = "字段类别")
private Integer fieldKind;
/**
* 包含的文件文件数量,0表示无限制。
*/
@Schema(description = "包含的文件文件数量,0表示无限制")
@McpToolParam(fieldName = "maxFileCount", type = McpToolParamType.INTEGER, description = "包含的文件数量,0表示无限制")
private Integer maxFileCount;
/**
* 上传文件系统类型。
*/
@Schema(description = "上传文件系统类型")
@McpToolParam(fieldName = "uploadFileSystemType", type = McpToolParamType.INTEGER, description = "上传文件系统类型")
private Integer uploadFileSystemType;
/**
* 脱敏字段类型,具体值可参考MaskFieldTypeEnum枚举。
*/
@Schema(description = "脱敏字段类型")
@McpToolParam(fieldName = "maskFieldType", type = McpToolParamType.STRING, description = "脱敏字段类型")
private String maskFieldType;
/**
* 编码规则的JSON格式数据。
*/
@Schema(description = "编码规则的JSON格式数据")
@McpToolParam(fieldName = "encodedRule", type = McpToolParamType.STRING, description = "编码规则JSON")
private String encodedRule;
/**
* 字典Id。
*/
@Schema(description = "字典Id")
@McpToolParam(fieldName = "dictId", type = McpToolParamType.INTEGER, description = "字典Id")
private Long dictId;
}

8
common/common-online/src/main/java/apelet/common/online/dto/OnlineDatasourceDto.java

@ -1,5 +1,7 @@ @@ -1,5 +1,7 @@
package apelet.common.online.dto;
import apelet.common.core.mcp.annotation.McpToolParam;
import apelet.common.core.mcp.model.McpToolParamType;
import apelet.common.core.validator.AddGroup;
import apelet.common.core.validator.UpdateGroup;
import io.swagger.v3.oas.annotations.media.Schema;
@ -23,6 +25,7 @@ public class OnlineDatasourceDto { @@ -23,6 +25,7 @@ public class OnlineDatasourceDto {
*/
@Schema(description = "主键Id")
@NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class})
@McpToolParam(fieldName = "datasourceId", type = McpToolParamType.INTEGER, description = "数据源主键Id")
private Long datasourceId;
/**
@ -30,6 +33,7 @@ public class OnlineDatasourceDto { @@ -30,6 +33,7 @@ public class OnlineDatasourceDto {
*/
@Schema(description = "数据源名称")
@NotBlank(message = "数据验证失败,数据源名称不能为空!")
@McpToolParam(fieldName = "datasourceName", type = McpToolParamType.STRING, description = "数据源名称")
private String datasourceName;
/**
@ -37,6 +41,7 @@ public class OnlineDatasourceDto { @@ -37,6 +41,7 @@ public class OnlineDatasourceDto {
*/
@Schema(description = "数据源变量名,会成为数据访问url的一部分")
@NotBlank(message = "数据验证失败,数据源变量名不能为空!")
@McpToolParam(fieldName = "variableName", type = McpToolParamType.STRING, description = "数据源变量名")
private String variableName;
/**
@ -44,6 +49,7 @@ public class OnlineDatasourceDto { @@ -44,6 +49,7 @@ public class OnlineDatasourceDto {
*/
@Schema(description = "主表所在的数据库链接Id")
@NotNull(message = "数据验证失败,数据库链接Id不能为空!")
@McpToolParam(fieldName = "dblinkId", type = McpToolParamType.INTEGER, description = "数据库链接Id")
private Long dblinkId;
/**
@ -51,6 +57,7 @@ public class OnlineDatasourceDto { @@ -51,6 +57,7 @@ public class OnlineDatasourceDto {
*/
@Schema(description = "主表Id")
@NotNull(message = "数据验证失败,主表Id不能为空!", groups = {UpdateGroup.class})
@McpToolParam(fieldName = "masterTableId", type = McpToolParamType.INTEGER, description = "主表Id")
private Long masterTableId;
/**
@ -58,5 +65,6 @@ public class OnlineDatasourceDto { @@ -58,5 +65,6 @@ public class OnlineDatasourceDto {
*/
@Schema(description = "主表表名")
@NotBlank(message = "数据验证失败,主表名不能为空!", groups = {AddGroup.class})
@McpToolParam(fieldName = "masterTableName", type = McpToolParamType.STRING, description = "主表表名")
private String masterTableName;
}

14
common/common-online/src/main/java/apelet/common/online/dto/OnlineDatasourceRelationDto.java

@ -1,5 +1,7 @@ @@ -1,5 +1,7 @@
package apelet.common.online.dto;
import apelet.common.core.mcp.annotation.McpToolParam;
import apelet.common.core.mcp.model.McpToolParamType;
import apelet.common.core.validator.AddGroup;
import apelet.common.core.validator.ConstDictRef;
import apelet.common.core.validator.UpdateGroup;
@ -25,6 +27,7 @@ public class OnlineDatasourceRelationDto { @@ -25,6 +27,7 @@ public class OnlineDatasourceRelationDto {
*/
@Schema(description = "主键Id")
@NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class})
@McpToolParam(fieldName = "relationId", type = McpToolParamType.INTEGER, description = "关联主键Id")
private Long relationId;
/**
@ -32,6 +35,7 @@ public class OnlineDatasourceRelationDto { @@ -32,6 +35,7 @@ public class OnlineDatasourceRelationDto {
*/
@Schema(description = "关联名称")
@NotBlank(message = "数据验证失败,关联名称不能为空!")
@McpToolParam(fieldName = "relationName", type = McpToolParamType.STRING, description = "关联名称")
private String relationName;
/**
@ -39,6 +43,7 @@ public class OnlineDatasourceRelationDto { @@ -39,6 +43,7 @@ public class OnlineDatasourceRelationDto {
*/
@Schema(description = "变量名")
@NotBlank(message = "数据验证失败,变量名不能为空!")
@McpToolParam(fieldName = "variableName", type = McpToolParamType.STRING, description = "变量名")
private String variableName;
/**
@ -46,6 +51,7 @@ public class OnlineDatasourceRelationDto { @@ -46,6 +51,7 @@ public class OnlineDatasourceRelationDto {
*/
@Schema(description = "主数据源Id")
@NotNull(message = "数据验证失败,主数据源Id不能为空!")
@McpToolParam(fieldName = "datasourceId", type = McpToolParamType.INTEGER, description = "主数据源Id")
private Long datasourceId;
/**
@ -54,6 +60,7 @@ public class OnlineDatasourceRelationDto { @@ -54,6 +60,7 @@ public class OnlineDatasourceRelationDto {
@Schema(description = "关联类型")
@NotNull(message = "数据验证失败,关联类型不能为空!")
@ConstDictRef(constDictClass = RelationType.class, message = "数据验证失败,关联类型为无效值!")
@McpToolParam(fieldName = "relationType", type = McpToolParamType.INTEGER, description = "关联类型(0=一对一,1=一对多)")
private Integer relationType;
/**
@ -61,6 +68,7 @@ public class OnlineDatasourceRelationDto { @@ -61,6 +68,7 @@ public class OnlineDatasourceRelationDto {
*/
@Schema(description = "主表关联字段Id")
@NotNull(message = "数据验证失败,主表关联字段Id不能为空!")
@McpToolParam(fieldName = "masterColumnId", type = McpToolParamType.INTEGER, description = "主表关联字段Id")
private Long masterColumnId;
/**
@ -68,6 +76,7 @@ public class OnlineDatasourceRelationDto { @@ -68,6 +76,7 @@ public class OnlineDatasourceRelationDto {
*/
@Schema(description = "从表Id")
@NotNull(message = "数据验证失败,从表Id不能为空!", groups = {UpdateGroup.class})
@McpToolParam(fieldName = "slaveTableId", type = McpToolParamType.INTEGER, description = "从表Id")
private Long slaveTableId;
/**
@ -75,6 +84,7 @@ public class OnlineDatasourceRelationDto { @@ -75,6 +84,7 @@ public class OnlineDatasourceRelationDto {
*/
@Schema(description = "从表名")
@NotBlank(message = "数据验证失败,从表名不能为空!", groups = {AddGroup.class})
@McpToolParam(fieldName = "slaveTableName", type = McpToolParamType.STRING, description = "从表名")
private String slaveTableName;
/**
@ -82,6 +92,7 @@ public class OnlineDatasourceRelationDto { @@ -82,6 +92,7 @@ public class OnlineDatasourceRelationDto {
*/
@Schema(description = "从表关联字段Id")
@NotNull(message = "数据验证失败,从表关联字段Id不能为空!", groups = {UpdateGroup.class})
@McpToolParam(fieldName = "slaveColumnId", type = McpToolParamType.INTEGER, description = "从表关联字段Id")
private Long slaveColumnId;
/**
@ -89,6 +100,7 @@ public class OnlineDatasourceRelationDto { @@ -89,6 +100,7 @@ public class OnlineDatasourceRelationDto {
*/
@Schema(description = "从表字段名")
@NotBlank(message = "数据验证失败,从表字段名不能为空!", groups = {AddGroup.class})
@McpToolParam(fieldName = "slaveColumnName", type = McpToolParamType.STRING, description = "从表字段名")
private String slaveColumnName;
/**
@ -96,6 +108,7 @@ public class OnlineDatasourceRelationDto { @@ -96,6 +108,7 @@ public class OnlineDatasourceRelationDto {
*/
@Schema(description = "是否级联删除标记")
@NotNull(message = "数据验证失败,是否级联删除标记不能为空!")
@McpToolParam(fieldName = "cascadeDelete", type = McpToolParamType.BOOLEAN, description = "是否级联删除")
private Boolean cascadeDelete;
/**
@ -103,5 +116,6 @@ public class OnlineDatasourceRelationDto { @@ -103,5 +116,6 @@ public class OnlineDatasourceRelationDto {
*/
@Schema(description = "是否左连接标记")
@NotNull(message = "数据验证失败,是否左连接标记不能为空!")
@McpToolParam(fieldName = "leftJoin", type = McpToolParamType.BOOLEAN, description = "是否左连接")
private Boolean leftJoin;
}

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

@ -1,5 +1,7 @@ @@ -1,5 +1,7 @@
package apelet.common.online.dto;
import apelet.common.core.mcp.annotation.McpToolParam;
import apelet.common.core.mcp.model.McpToolParamType;
import apelet.common.core.validator.ConstDictRef;
import apelet.common.core.validator.UpdateGroup;
import apelet.common.online.model.OnlineQuickFormField;
@ -27,6 +29,7 @@ public class OnlineFormDto { @@ -27,6 +29,7 @@ public class OnlineFormDto {
*/
@Schema(description = "主键Id")
@NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class})
@McpToolParam(fieldName = "formId", type = McpToolParamType.INTEGER, description = "主键Id(编辑时使用)")
private Long formId;
/**
@ -34,12 +37,14 @@ public class OnlineFormDto { @@ -34,12 +37,14 @@ public class OnlineFormDto {
*/
@Schema(description = "页面id")
@NotNull(message = "数据验证失败,页面id不能为空!")
@McpToolParam(fieldName = "pageId", type = McpToolParamType.INTEGER, description = "页面id", required = true)
private Long pageId;
/**
* 表单编码。
*/
@Schema(description = "表单编码")
@McpToolParam(fieldName = "formCode", type = McpToolParamType.STRING, description = "表单编码")
private String formCode;
/**
@ -47,6 +52,7 @@ public class OnlineFormDto { @@ -47,6 +52,7 @@ public class OnlineFormDto {
*/
@Schema(description = "表单名称")
@NotBlank(message = "数据验证失败,表单名称不能为空!")
@McpToolParam(fieldName = "formName", type = McpToolParamType.STRING, description = "表单名称", required = true)
private String formName;
/**
@ -55,6 +61,7 @@ public class OnlineFormDto { @@ -55,6 +61,7 @@ public class OnlineFormDto {
@Schema(description = "表单类别")
@NotNull(message = "数据验证失败,表单类别不能为空!")
@ConstDictRef(constDictClass = FormKind.class, message = "数据验证失败,表单类别为无效值!")
@McpToolParam(fieldName = "formKind", type = McpToolParamType.INTEGER, description = "表单类别")
private Integer formKind;
/**
@ -63,6 +70,7 @@ public class OnlineFormDto { @@ -63,6 +70,7 @@ public class OnlineFormDto {
@Schema(description = "表单类型")
@NotNull(message = "数据验证失败,表单类型不能为空!")
@ConstDictRef(constDictClass = FormType.class, message = "数据验证失败,表单类型为无效值!")
@McpToolParam(fieldName = "formType", type = McpToolParamType.INTEGER, description = "表单类型", required = true)
private Integer formType;
/**
@ -70,36 +78,42 @@ public class OnlineFormDto { @@ -70,36 +78,42 @@ public class OnlineFormDto {
*/
@Schema(description = "表单主表id")
@NotNull(message = "数据验证失败,表单主表id不能为空!")
@McpToolParam(fieldName = "masterTableId", type = McpToolParamType.INTEGER, description = "表单主表id", required = true)
private Long masterTableId;
/**
* 当前表单关联的数据源Id集合。
*/
@Schema(description = "当前表单关联的数据源Id集合")
@McpToolParam(fieldName = "datasourceIdList", type = McpToolParamType.ARRAY, description = "当前表单关联的数据源Id集合")
private List<Long> datasourceIdList;
/**
* 表单组件JSON。
*/
@Schema(description = "表单组件JSON")
@McpToolParam(fieldName = "widgetJson", type = McpToolParamType.STRING, description = "表单组件JSON")
private String widgetJson;
/**
* 表单参数JSON。
*/
@Schema(description = "表单参数JSON")
@McpToolParam(fieldName = "paramsJson", type = McpToolParamType.STRING, description = "表单参数JSON")
private String paramsJson;
/**
* 快捷表单ID(编辑快捷表单配置时回传)。
*/
@Schema(description = "快捷表单ID")
@McpToolParam(fieldName = "quickFormId", type = McpToolParamType.INTEGER, description = "快捷表单ID")
private Long quickFormId;
/**
* 排版类型(快捷表单)。
*/
@Schema(description = "排版类型")
@McpToolParam(fieldName = "layoutType", type = McpToolParamType.INTEGER, description = "排版类型(快捷表单)")
private Integer layoutType;
/**

11
common/common-online/src/main/java/apelet/common/online/dto/OnlinePageDto.java

@ -1,5 +1,7 @@ @@ -1,5 +1,7 @@
package apelet.common.online.dto;
import apelet.common.core.mcp.annotation.McpToolParam;
import apelet.common.core.mcp.model.McpToolParamType;
import apelet.common.core.validator.ConstDictRef;
import apelet.common.core.validator.UpdateGroup;
import apelet.common.online.model.constant.PageStatus;
@ -25,12 +27,14 @@ public class OnlinePageDto { @@ -25,12 +27,14 @@ public class OnlinePageDto {
*/
@Schema(description = "主键Id")
@NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class})
@McpToolParam(fieldName = "pageId", type = McpToolParamType.INTEGER, description = "主键Id(编辑时使用)")
private Long pageId;
/**
* 页面编码。
*/
@Schema(description = "页面编码")
@McpToolParam(fieldName = "pageCode", type = McpToolParamType.STRING, description = "页面编码(metaSource=new 时必填)")
private String pageCode;
/**
@ -38,6 +42,7 @@ public class OnlinePageDto { @@ -38,6 +42,7 @@ public class OnlinePageDto {
*/
@Schema(description = "页面名称")
@NotBlank(message = "数据验证失败,页面名称不能为空!")
@McpToolParam(fieldName = "pageName", type = McpToolParamType.STRING, description = "页面名称", required = true)
private String pageName;
/**
@ -46,6 +51,7 @@ public class OnlinePageDto { @@ -46,6 +51,7 @@ public class OnlinePageDto {
@Schema(description = "页面类型")
@NotNull(message = "数据验证失败,页面类型不能为空!")
@ConstDictRef(constDictClass = PageType.class, message = "数据验证失败,页面类型为无效值!")
@McpToolParam(fieldName = "pageType", type = McpToolParamType.INTEGER, description = "页面类型", required = true)
private Integer pageType;
/**
@ -54,29 +60,34 @@ public class OnlinePageDto { @@ -54,29 +60,34 @@ public class OnlinePageDto {
@Schema(description = "页面编辑状态")
@NotNull(message = "数据验证失败,状态不能为空!")
@ConstDictRef(constDictClass = PageStatus.class, message = "数据验证失败,状态为无效值!")
@McpToolParam(fieldName = "status", type = McpToolParamType.INTEGER, description = "页面编辑状态", required = true)
private Integer status;
/**
* 表单元数据。
*/
@Schema(description = "表单元数据")
@McpToolParam(fieldName = "masterTableId", type = McpToolParamType.STRING, description = "表单元数据Id(metaSource=existing 时必填)")
private String masterTableId;
/**
* 表单元数据。
*/
@Schema(description = "表单元数据表名")
@McpToolParam(fieldName = "masterTableName", type = McpToolParamType.STRING, description = "表单元数据表名")
private String masterTableName;
/**
* 数据来源: "new"=新建元数据, "existing"=使用已有元数据。
*/
@Schema(description = "数据来源: new=新建元数据, existing=使用已有元数据")
@McpToolParam(fieldName = "metaSource", type = McpToolParamType.STRING, description = "数据来源:new=新建元数据,existing=使用已有元数据", required = true)
private String metaSource;
/**
* 表类型: "2"=基础资料(单表), "1"=业务单据(主表+附表)。仅在metaSource="new"时有效。
*/
@Schema(description = "表类型: 2=基础资料(单表), 1=业务单据(主表+附表)。仅在metaSource=new时有效")
@McpToolParam(fieldName = "metaType", type = McpToolParamType.STRING, description = "表类型:2=基础资料(单表),1=业务单据(主表+附表),仅在 metaSource=new 时有效")
private String metaType;
}

Loading…
Cancel
Save