From ab7a7623b4965499a27226f82a0e8b58884802ea Mon Sep 17 00:00:00 2001 From: ouhaolan Date: Fri, 27 Feb 2026 09:43:11 +0800 Subject: [PATCH] info --- README.md | 95 +++++++ pom.xml | 105 +++++++ .../app/mcp/YdoyunReportMcpApplication.java | 29 ++ .../junchi/app/mcp/config/McpToolConfig.java | 35 +++ .../app/mcp/config/RestTemplateConfig.java | 29 ++ .../mcp/dal/dataobject/ReportDatabaseDO.java | 91 ++++++ .../mcp/dal/mapper/ReportDatabaseMapper.java | 25 ++ .../mcp/mcptool/ReportMcpToolsService.java | 107 ++++++++ .../junchi/app/mcp/report/ReportService.java | 258 ++++++++++++++++++ .../app/mcp/report/vo/ProcedureRequestVO.java | 25 ++ .../mcp/report/vo/ProcedureResponseVO.java | 24 ++ .../mcp/report/vo/ReportDatabaseRespVO.java | 46 ++++ src/main/resources/application.yml | 78 ++++++ .../resources/mapper/ReportDatabaseMapper.xml | 36 +++ 14 files changed, 983 insertions(+) create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/com/junchi/app/mcp/YdoyunReportMcpApplication.java create mode 100644 src/main/java/com/junchi/app/mcp/config/McpToolConfig.java create mode 100644 src/main/java/com/junchi/app/mcp/config/RestTemplateConfig.java create mode 100644 src/main/java/com/junchi/app/mcp/dal/dataobject/ReportDatabaseDO.java create mode 100644 src/main/java/com/junchi/app/mcp/dal/mapper/ReportDatabaseMapper.java create mode 100644 src/main/java/com/junchi/app/mcp/mcptool/ReportMcpToolsService.java create mode 100644 src/main/java/com/junchi/app/mcp/report/ReportService.java create mode 100644 src/main/java/com/junchi/app/mcp/report/vo/ProcedureRequestVO.java create mode 100644 src/main/java/com/junchi/app/mcp/report/vo/ProcedureResponseVO.java create mode 100644 src/main/java/com/junchi/app/mcp/report/vo/ReportDatabaseRespVO.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/mapper/ReportDatabaseMapper.xml diff --git a/README.md b/README.md new file mode 100644 index 0000000..3330388 --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# ydoyun-report-mcp + +一个基于 Spring Boot 和 MCP (Model Context Protocol) 的报表服务项目,用于将报表存储过程调用能力暴露给 AI 模型。 + +## 技术栈 + +- Java 17 +- Spring Boot 3.2.6 +- Spring AI MCP Server WebMVC 1.0.0 +- Maven + +## 项目结构 + +``` +ydoyun-report-mcp/ +├── pom.xml +├── src/ +│ ├── main/ +│ │ ├── java/ +│ │ │ └── com/junchi/app/mcp/ +│ │ │ ├── YdoyunReportMcpApplication.java # 主应用类 +│ │ │ ├── config/ # 配置类 +│ │ │ │ ├── McpToolConfig.java # MCP 工具配置 +│ │ │ │ └── RestTemplateConfig.java # RestTemplate 配置 +│ │ │ ├── mcptool/ # MCP 工具服务 +│ │ │ │ └── ReportMcpToolsService.java # 报表 MCP 工具服务 +│ │ │ └── report/ # 报表服务 +│ │ │ ├── ReportService.java # 报表服务实现 +│ │ │ └── vo/ # 值对象 +│ │ │ ├── ProcedureRequestVO.java # 存储过程请求 VO +│ │ │ └── ProcedureResponseVO.java # 存储过程响应 VO +│ │ └── resources/ +│ │ └── application.yml # 应用配置文件 +│ └── test/ # 测试代码目录 +└── README.md +``` + +## 功能说明 + +### 核心功能 + +1. **MCP 工具服务**: 提供 `executeReportProcedure` 工具方法,供 AI 模型调用 +2. **存储过程调用**: 通过 HTTP 调用远程存储过程 API +3. **日期处理**: 支持 `today`、`yesterday`、`tomorrow` 关键字或具体日期格式(yyyy-MM-dd) + +### 配置说明 + +在 `application.yml` 中配置: + +- **服务端口**: 48090 +- **MCP 服务器**: 异步类型,仅启用 tools 能力 +- **远程 API**: 配置存储过程执行接口地址和 API 密钥 + +## 构建和运行 + +### 构建项目 + +```bash +mvn clean package +``` + +### 运行项目 + +```bash +mvn spring-boot:run +``` + +或者运行打包后的 jar: + +```bash +java -jar target/ydoyun-report-mcp-1.0.0.jar +``` + +## API 说明 + +### MCP 工具 + +**executeReportProcedure** + +根据存储过程名称、日期和仓库代码调用后端存储过程接口。 + +参数: +- `name` (String): 存储过程名称 +- `rq` (String): 日期,支持 `today`/`yesterday`/`tomorrow` 或 `2025-12-01` 格式 +- `ckdm` (String): 仓库代码 +- `p` (String): 秘钥 + +返回:执行结果的字符串表示 + +## 注意事项 + +1. 确保远程 API 服务可访问 +2. 配置正确的 API 密钥 +3. 根据实际需求调整日志级别 + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..dbde297 --- /dev/null +++ b/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + com.junchi.app + ydoyun-report-mcp + 1.0.0 + jar + ydoyun-report-mcp + + + 17 + 3.2.6 + UTF-8 + + + + org.springframework.boot + spring-boot-starter-parent + 3.2.6 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.projectlombok + lombok + true + + + + + org.springframework.ai + spring-ai-starter-mcp-server-webmvc + 1.0.0 + + + + + net.logstash.logback + logstash-logback-encoder + 7.4 + + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 3.0.3 + + + + + com.mysql + mysql-connector-j + runtime + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + ${encoding} + + + + + + diff --git a/src/main/java/com/junchi/app/mcp/YdoyunReportMcpApplication.java b/src/main/java/com/junchi/app/mcp/YdoyunReportMcpApplication.java new file mode 100644 index 0000000..8f0409e --- /dev/null +++ b/src/main/java/com/junchi/app/mcp/YdoyunReportMcpApplication.java @@ -0,0 +1,29 @@ +package com.junchi.app.mcp; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 云朵云报表 MCP 服务主应用类 + * + *

这是一个基于 Spring Boot 和 MCP (Model Context Protocol) 的报表服务应用, + * 用于将报表存储过程调用能力暴露给 AI 模型。

+ * + * @author 欧浩蓝 + * @version 1.0.0 + * @since 2025-01-27 + */ +@SpringBootApplication +@MapperScan("com.junchi.app.mcp.dal.mapper") +public class YdoyunReportMcpApplication { + /** + * 应用程序入口方法 + * + * @param args 命令行参数 + */ + public static void main(String[] args) { + SpringApplication.run(YdoyunReportMcpApplication.class, args); + } +} + diff --git a/src/main/java/com/junchi/app/mcp/config/McpToolConfig.java b/src/main/java/com/junchi/app/mcp/config/McpToolConfig.java new file mode 100644 index 0000000..b136c7c --- /dev/null +++ b/src/main/java/com/junchi/app/mcp/config/McpToolConfig.java @@ -0,0 +1,35 @@ +package com.junchi.app.mcp.config; + +import com.junchi.app.mcp.mcptool.ReportMcpToolsService; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.ai.tool.method.MethodToolCallbackProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * MCP 工具配置类 + * + *

用于注册 MCP 工具回调提供者,将报表工具服务暴露给 AI 模型使用。

+ * + * @author 欧浩蓝 + * @since 2025-01-27 + */ +@Configuration +public class McpToolConfig { + /** + * 创建工具回调提供者 + * + *

将 ReportMcpToolsService 中定义的工具方法注册到 MCP 服务器, + * 使得 AI 模型可以通过 MCP 协议调用这些工具。

+ * + * @param reportMcpToolsService 报表 MCP 工具服务实例 + * @return 工具回调提供者 + */ + @Bean + public ToolCallbackProvider userTools(ReportMcpToolsService reportMcpToolsService) { + return MethodToolCallbackProvider.builder() + .toolObjects(new Object[]{reportMcpToolsService}) + .build(); + } +} + diff --git a/src/main/java/com/junchi/app/mcp/config/RestTemplateConfig.java b/src/main/java/com/junchi/app/mcp/config/RestTemplateConfig.java new file mode 100644 index 0000000..0ba39e9 --- /dev/null +++ b/src/main/java/com/junchi/app/mcp/config/RestTemplateConfig.java @@ -0,0 +1,29 @@ +package com.junchi.app.mcp.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +/** + * RestTemplate 配置类 + * + *

用于配置 HTTP 客户端 RestTemplate,用于调用远程存储过程 API。

+ * + * @author 欧浩蓝 + * @since 2025-01-27 + */ +@Configuration +public class RestTemplateConfig { + /** + * 创建 RestTemplate Bean + * + *

用于执行 HTTP 请求,调用远程存储过程接口。

+ * + * @return RestTemplate 实例 + */ + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} + diff --git a/src/main/java/com/junchi/app/mcp/dal/dataobject/ReportDatabaseDO.java b/src/main/java/com/junchi/app/mcp/dal/dataobject/ReportDatabaseDO.java new file mode 100644 index 0000000..898818f --- /dev/null +++ b/src/main/java/com/junchi/app/mcp/dal/dataobject/ReportDatabaseDO.java @@ -0,0 +1,91 @@ +package com.junchi.app.mcp.dal.dataobject; + +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 报表数据源 DO + * + *

用于封装报表数据源信息,对应数据库表 ydoyun_report_database。

+ * + * @author 欧浩蓝 + * @since 2025-01-27 + */ +@Data +public class ReportDatabaseDO { + /** + * 主键ID + */ + private Long id; + + /** + * 数据源名称 + */ + private String dbName; + + /** + * 数据库类型(mysql、pgsql、oracle、sqlserver) + */ + private String dbType; + + /** + * 数据库地址 + */ + private String host; + + /** + * 数据库端口 + */ + private Integer port; + + /** + * 数据库账号 + */ + private String username; + + /** + * 数据库密码(建议加密存储) + */ + private String password; + + /** + * 数据库名称 + */ + private String databaseName; + + /** + * 备注说明 + */ + private String remark; + + /** + * 创建者 + */ + private String creator; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 更新者 + */ + private String updater; + + /** + * 更新时间 + */ + private LocalDateTime updateTime; + + /** + * 是否删除 + */ + private Boolean deleted; + + /** + * 租户编号 + */ + private Long tenantId; +} + diff --git a/src/main/java/com/junchi/app/mcp/dal/mapper/ReportDatabaseMapper.java b/src/main/java/com/junchi/app/mcp/dal/mapper/ReportDatabaseMapper.java new file mode 100644 index 0000000..c4cbe8b --- /dev/null +++ b/src/main/java/com/junchi/app/mcp/dal/mapper/ReportDatabaseMapper.java @@ -0,0 +1,25 @@ +package com.junchi.app.mcp.dal.mapper; + +import com.junchi.app.mcp.dal.dataobject.ReportDatabaseDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * 报表数据源 Mapper 接口 + * + *

用于查询报表数据源信息。

+ * + * @author 欧浩蓝 + * @since 2025-01-27 + */ +@Mapper +public interface ReportDatabaseMapper { + /** + * 根据 report_id 查询绑定的数据源信息 + * + * @param reportId 报表ID + * @return 数据源信息 + */ + ReportDatabaseDO selectReportDatabaseByReportId(@Param("reportId") Long reportId); +} + diff --git a/src/main/java/com/junchi/app/mcp/mcptool/ReportMcpToolsService.java b/src/main/java/com/junchi/app/mcp/mcptool/ReportMcpToolsService.java new file mode 100644 index 0000000..8c1d98f --- /dev/null +++ b/src/main/java/com/junchi/app/mcp/mcptool/ReportMcpToolsService.java @@ -0,0 +1,107 @@ +package com.junchi.app.mcp.mcptool; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.junchi.app.mcp.report.ReportService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.LinkedHashMap; + +/** + * 报表 MCP 工具服务类 + * + *

提供 MCP 工具方法,供 AI 模型调用执行报表存储过程。

+ * + * @author 欧浩蓝 + * @since 2025-01-27 + */ +@Service +public class ReportMcpToolsService { + + private static final Logger log = LoggerFactory.getLogger(ReportMcpToolsService.class); + + @Autowired + private ReportService reportService; + @Autowired + private ObjectMapper objectMapper; + + + /** + * 执行报表存储过程 + */ + @Tool( + description = "根据【报表ID(reportId) + 存储过程名称(name)】执行对应的数据库存储过程," + + "并按 params 中提供的动态参数查询报表数据。" + + "该工具用于获取指定报表在指定条件下的原始数据结果," + + "不做统计、不做分析,只负责数据查询。" + ) + public String executeReportProcedure( + @ToolParam(description = "报表ID,用于确定使用哪一个报表配置及其数据库连接") + Long reportId, + @ToolParam(description = "存储过程名称,对应数据库中的具体存储过程名") + String name, + @ToolParam( + description = "存储过程参数 JSON 字符串," + + "示例:{\"rq\":\"2025-12-15\",\"ckdm\":\"001\"}" + ) + String params + ) { + LinkedHashMap paramMap; + try { + if (params == null || params.isBlank()) { + paramMap = new LinkedHashMap<>(); + } else { + paramMap = objectMapper.readValue( + params, + new TypeReference>() {} + ); + } + } catch (Exception e) { + throw new IllegalArgumentException("存储过程参数 params JSON 解析失败:" + params, e); + } + long startTime = System.currentTimeMillis(); + + // ===== MCP 调用开始 ===== + log.info("[MCP] BEGIN executeReportProcedure, reportId={}, name={}", reportId, name); + log.debug("[MCP] params={}", paramMap); + + try { + // 参数校验 + if (name == null || name.isBlank()) { + log.warn("[MCP] 参数校验失败:name 为空"); + throw new IllegalArgumentException("参数 name(存储过程名称) 不能为空"); + } + if (reportId == null) { + log.warn("[MCP] 参数校验失败:reportId 为空"); + throw new IllegalArgumentException("参数 reportId(报表ID) 不能为空"); + } + + // 执行存储过程 + Object result = reportService.executeProcedure(reportId, name, paramMap); + String resultStr = result == null ? "" : result.toString(); + + long cost = System.currentTimeMillis() - startTime; + + log.info("[MCP] END executeReportProcedure success, cost={}ms, resultLength={}", + cost, resultStr.length()); + + log.debug("[MCP] result={}", resultStr); + + return resultStr; + + } catch (IllegalArgumentException e) { + long cost = System.currentTimeMillis() - startTime; + log.warn("[MCP] PARAM ERROR, cost={}ms, msg={}", cost, e.getMessage()); + throw e; + } catch (Exception e) { + long cost = System.currentTimeMillis() - startTime; + log.error("[MCP] ERROR executeReportProcedure, cost={}ms", cost, e); + throw e; + } + } +} diff --git a/src/main/java/com/junchi/app/mcp/report/ReportService.java b/src/main/java/com/junchi/app/mcp/report/ReportService.java new file mode 100644 index 0000000..ab57552 --- /dev/null +++ b/src/main/java/com/junchi/app/mcp/report/ReportService.java @@ -0,0 +1,258 @@ +package com.junchi.app.mcp.report; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.junchi.app.mcp.dal.dataobject.ReportDatabaseDO; +import com.junchi.app.mcp.dal.mapper.ReportDatabaseMapper; +import com.junchi.app.mcp.report.vo.ProcedureRequestVO; +import com.junchi.app.mcp.report.vo.ReportDatabaseRespVO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 报表服务类 + * + *

负责调用远程存储过程 API,处理日期参数解析和响应数据格式化。

+ * + * @author 欧浩蓝 + * @since 2025-01-27 + */ +@Service +public class ReportService { + /** 日志记录器 */ + private static final Logger log = LoggerFactory.getLogger(ReportService.class); + + /** 日期格式化器,用于将日期格式化为 yyyy-MM-dd 格式 */ + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + /** HTTP 客户端,用于调用远程 API */ + private final RestTemplate restTemplate; + + /** 报表数据源 Mapper */ + @Autowired + private ReportDatabaseMapper reportDatabaseMapper; + + /** 远程存储过程执行接口地址,从配置文件读取 */ + @Value("${ydoyun.mssqljdbc.report.api.execute-url}") + private String executeUrl; + + /** API 密钥,从配置文件读取 */ + @Value("${ydoyun.mssqljdbc.report.api.key}") + private String apiKey; + + /** + * 构造函数 + * + * @param restTemplate HTTP 客户端实例 + */ + public ReportService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + /** + * 解析日期关键字或日期字符串 + * + *

将日期关键字(today/yesterday/tomorrow)转换为具体的日期格式(yyyy-MM-dd), + * 如果输入已经是日期格式,则直接返回。

+ * + * @param dateKeyword 日期关键字(today/yesterday/tomorrow)或日期字符串(yyyy-MM-dd) + * @return 格式化后的日期字符串(yyyy-MM-dd 格式) + */ + private String resolveDate(String dateKeyword) { + LocalDate date = LocalDate.now(); + switch (dateKeyword.toLowerCase()) { + case "yesterday": + // 昨天:当前日期减一天 + date = date.minusDays(1); + break; + case "today": + // 今天:使用当前日期 + break; + case "tomorrow": + // 明天:当前日期加一天 + date = date.plusDays(1); + break; + default: + // 如果不是关键字,直接返回原值(假设是日期格式) + return dateKeyword; + } + return date.format(FORMATTER); + } + + /** + * 执行存储过程 + * + *

调用远程存储过程 API,执行指定的存储过程并返回结果。 + * 该方法会处理日期参数解析、构建请求、发送 HTTP 请求、解析响应等步骤。

+ * + * @param name 存储过程名称 + * @param rq 日期(支持 today/yesterday/tomorrow 或 yyyy-MM-dd 格式) + * @param ckdm 仓库代码 + * @param reportId 报表ID + * @param p 秘钥 + * @return 执行结果,可能是 List、Map、String 或其他对象类型 + * @throws RuntimeException 当远程接口返回为空、调用失败或解析失败时抛出 + */ + public Object executeProcedure(Long reportId, String name,LinkedHashMap params) { + log.info("【报表服务】开始执行存储过程: {}", name); + + // 根据 reportId 查询数据源信息 + ReportDatabaseRespVO reportDatabase = null; + try { + log.info("【报表服务】开始查询报表数据源信息,reportId: {}", reportId); + ReportDatabaseDO databaseDO = reportDatabaseMapper.selectReportDatabaseByReportId(reportId); + + if (databaseDO != null) { + // 将 DO 转换为 VO + reportDatabase = new ReportDatabaseRespVO(); + BeanUtils.copyProperties(databaseDO, reportDatabase); + log.info("【报表服务】数据源信息查询成功: id={}, dbName={}, dbType={}, host={}, port={}", + reportDatabase.getId(), reportDatabase.getDbName(), reportDatabase.getDbType(), + reportDatabase.getHost(), reportDatabase.getPort()); + } else { + log.warn("【报表服务】未找到报表ID对应的数据源信息: reportId={}", reportId); + } + } catch (Exception e) { + log.error("【报表服务】查询数据源信息失败: reportId={}", reportId, e); + // 查询失败不影响主流程,继续执行 + } + + // 构建请求对象 + ProcedureRequestVO req = new ProcedureRequestVO(); + req.setProcedureName(name); + req.setParams(params); + // 设置数据源信息 + req.setReportDatabase(reportDatabase); + log.debug("【报表服务】请求对象构建完成: procedureName={}, reportDatabase={}", + name, reportDatabase != null ? "已设置" : "未设置"); + + // 设置 HTTP 请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("X-API-KEY", apiKey); // 设置 API 密钥 + log.debug("【报表服务】HTTP 请求头设置完成"); + + // 创建 HTTP 实体 + HttpEntity entity = new HttpEntity<>(req, headers); + + // 发送 POST 请求 + log.info("【报表服务】开始调用远程接口: {}", executeUrl); + long requestStartTime = System.currentTimeMillis(); + + String rawResponse = restTemplate.postForObject(executeUrl, entity, String.class); + + long requestEndTime = System.currentTimeMillis(); + long requestDuration = requestEndTime - requestStartTime; + log.info("【报表服务】远程接口调用完成,耗时: {} 毫秒", requestDuration); + + // 检查响应是否为空 + if (rawResponse == null || rawResponse.trim().isEmpty()) { + log.error("【报表服务】远程接口返回为空"); + throw new RuntimeException("远程接口返回为空"); + } + + // 记录原始响应(调试级别) + log.info("【报表服务】收到远程接口响应,响应长度: {} 字符", rawResponse.length()); + log.debug("========================================"); + log.debug("【报表服务】原始响应 JSON: {}", rawResponse); + log.debug("========================================"); + + try { + // 解析 JSON 响应 + log.debug("【报表服务】开始解析 JSON 响应"); + ObjectMapper objectMapper = new ObjectMapper(); + TypeReference> typeRef = new TypeReference>() {}; + Map responseMap = objectMapper.readValue(rawResponse, typeRef); + log.info("【报表服务】JSON 响应解析成功"); + + // 记录解析后的信息(调试级别) + log.debug("【报表服务】解析后的 responseMap: {}", responseMap); + log.debug("【报表服务】data 字段值: {}", responseMap.get("data")); + log.debug("【报表服务】data 字段类型: {}", + responseMap.get("data") != null ? responseMap.get("data").getClass().getName() : "null"); + + if (responseMap.get("data") instanceof List) { + int listSize = ((List) responseMap.get("data")).size(); + log.info("【报表服务】data 是 List 类型,包含 {} 条记录", listSize); + log.debug("【报表服务】data 是 List,大小: {}", listSize); + } + log.debug("========================================"); + + // 检查响应码 + Object codeObj = responseMap.get("code"); + if (codeObj != null) { + int code = (codeObj instanceof Integer) ? (Integer) codeObj : Integer.parseInt(String.valueOf(codeObj)); + log.info("【报表服务】响应码: {}", code); + if (code != 0) { + // 响应码不为 0,表示调用失败 + Object msgObj = responseMap.get("msg"); + String msg = (msgObj != null) ? String.valueOf(msgObj) : "未知错误"; + log.error("【报表服务】远程接口调用失败: code={}, msg={}", code, msg); + throw new RuntimeException("远程接口调用失败: code=" + code + ", msg=" + msg); + } + log.info("【报表服务】响应码检查通过(code=0)"); + } + + // 处理响应数据 + Object data = responseMap.get("data"); + + // 如果 data 为 null,返回空列表 + if (data == null) { + log.warn("【报表服务】响应数据为 null,返回空列表"); + return new ArrayList<>(); + } + + // 如果 data 是字符串,尝试解析为 JSON + if (data instanceof String) { + String dataStr = (String) data; + log.debug("【报表服务】响应数据是字符串类型,长度: {}", dataStr.length()); + // 空字符串返回空列表 + if (dataStr.isEmpty() || dataStr.trim().isEmpty()) { + log.warn("【报表服务】响应数据为空字符串,返回空列表"); + return new ArrayList<>(); + } + + // 如果字符串看起来像 JSON(以 [ 或 { 开头),尝试解析 + String trimmed = dataStr.trim(); + if (trimmed.startsWith("[") || trimmed.startsWith("{")) { + log.debug("【报表服务】检测到 JSON 格式字符串,尝试解析"); + try { + Object parsedData = objectMapper.readValue(dataStr, Object.class); + log.info("【报表服务】JSON 字符串解析成功"); + return parsedData; + } catch (Exception e) { + log.error("【报表服务】解析 data JSON 字符串失败: {}", e.getMessage()); + return dataStr; // 解析失败,返回原始字符串 + } + } + log.debug("【报表服务】响应数据不是 JSON 格式,返回原始字符串"); + return dataStr; // 不是 JSON 格式,返回原始字符串 + } + + // 其他类型直接返回 + log.info("【报表服务】响应数据处理完成,类型: {}", data.getClass().getSimpleName()); + return data; + } catch (JsonProcessingException e) { + log.error("【报表服务】解析响应 JSON 失败: {}", e.getMessage()); + log.error("【报表服务】原始响应: {}", rawResponse); + throw new RuntimeException("解析响应 JSON 失败: " + e.getMessage() + ", 原始响应: " + rawResponse, e); + } + } +} + diff --git a/src/main/java/com/junchi/app/mcp/report/vo/ProcedureRequestVO.java b/src/main/java/com/junchi/app/mcp/report/vo/ProcedureRequestVO.java new file mode 100644 index 0000000..c81d274 --- /dev/null +++ b/src/main/java/com/junchi/app/mcp/report/vo/ProcedureRequestVO.java @@ -0,0 +1,25 @@ +package com.junchi.app.mcp.report.vo; + +import lombok.Data; +import java.util.Map; + +/** + * 存储过程请求值对象 + * + *

用于封装调用远程存储过程 API 的请求参数。

+ * + * @author 欧浩蓝 + * @since 2025-01-27 + */ +@Data +public class ProcedureRequestVO { + /** 存储过程名称 */ + private String procedureName; + + /** 存储过程参数映射,key 为参数名,value 为参数值 */ + private Map params; + + /** 报表数据源信息 */ + private ReportDatabaseRespVO reportDatabase; +} + diff --git a/src/main/java/com/junchi/app/mcp/report/vo/ProcedureResponseVO.java b/src/main/java/com/junchi/app/mcp/report/vo/ProcedureResponseVO.java new file mode 100644 index 0000000..1267239 --- /dev/null +++ b/src/main/java/com/junchi/app/mcp/report/vo/ProcedureResponseVO.java @@ -0,0 +1,24 @@ +package com.junchi.app.mcp.report.vo; + +import lombok.Data; + +/** + * 存储过程响应值对象 + * + *

用于封装远程存储过程 API 返回的响应数据。

+ * + * @author 欧浩蓝 + * @since 2025-01-27 + */ +@Data +public class ProcedureResponseVO { + /** 响应码,0 表示成功,非 0 表示失败 */ + private Integer code; + + /** 响应消息,通常用于描述错误信息 */ + private String msg; + + /** 响应数据,可能是 List、Map、String 或其他类型 */ + private Object data; +} + diff --git a/src/main/java/com/junchi/app/mcp/report/vo/ReportDatabaseRespVO.java b/src/main/java/com/junchi/app/mcp/report/vo/ReportDatabaseRespVO.java new file mode 100644 index 0000000..51046b8 --- /dev/null +++ b/src/main/java/com/junchi/app/mcp/report/vo/ReportDatabaseRespVO.java @@ -0,0 +1,46 @@ +package com.junchi.app.mcp.report.vo; + +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 报表数据源响应 VO + * + *

用于封装报表数据源信息,用于传递给存储过程接口。

+ * + * @author 欧浩蓝 + * @since 2025-01-27 + */ +@Data +public class ReportDatabaseRespVO { + /** 主键ID */ + private Long id; + + /** 数据源名称 */ + private String dbName; + + /** 数据库类型(mysql、pgsql、oracle、sqlserver) */ + private String dbType; + + /** 数据库地址 */ + private String host; + + /** 数据库端口 */ + private Integer port; + + /** 数据库账号 */ + private String username; + + /** 数据库密码(建议加密存储) */ + private String password; + + /** 数据库名称 */ + private String databaseName; + + /** 备注说明 */ + private String remark; + + /** 创建时间 */ + private LocalDateTime createTime; +} + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..8136eb0 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,78 @@ +# 服务器配置 +server: + # 服务端口号 + port: 48090 + +# Spring 应用配置 +spring: + application: + # 应用名称 + name: ydoyun-report-mcp + # 禁用条件评估报告(避免启动时输出大量自动配置信息) + main: + log-startup-info: true # 只显示启动信息,不显示条件评估报告 + # 数据源配置 + datasource: + url: jdbc:mysql://118.253.178.8:23306/ydoyun-report-ai?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true + username: root + password: mysql_Fa4wpQ + driver-class-name: com.mysql.cj.jdbc.Driver + ai: + mcp: + server: + # MCP 服务基础信息 + name: ydoyun-report-mcp + version: 1.0.0 + # 服务类型:ASYNC 表示异步模式 + type: ASYNC + # 服务说明:描述 MCP 服务器的功能 + instructions: "MCP server for Ydoyun report tools" + # SSE 消息推送接口地址:用于推送消息给客户端 + sse-message-endpoint: /mcp/messages + # SSE 订阅端点:客户端订阅服务器端事件的端点 + sse-endpoint: /mcp/sse + # MCP 能力声明:定义服务器支持的能力 + capabilities: + tool: true # 支持工具调用 + resource: false # 不支持资源访问 + prompt: false # 不支持提示模板 + completion: false # 不支持补全功能 + +# 云朵云报表服务配置 +ydoyun: + mssqljdbc: + report: + api: + # API 密钥:用于调用远程存储过程接口的认证密钥 + key: your_secret_api_key_123456 + # 存储过程执行接口地址 + execute-url: http://118.253.178.8:49090/api/procedure/execute + +# MyBatis 配置 +mybatis: + # Mapper XML 文件位置 + mapper-locations: classpath*:mapper/**/*.xml + # 实体类包路径 + type-aliases-package: com.junchi.app.mcp.dal.dataobject + configuration: + # 开启驼峰命名转换 + map-underscore-to-camel-case: true + # 日志实现 + log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl + +# 日志配置 +logging: + level: + root: INFO # 根日志级别 + # MCP 相关包的日志级别设置为 DEBUG,便于调试 + com.junchi.app.mcp: DEBUG + org: + # Spring 框架日志级别:INFO 用于正常运行时 + # 注意:如果设置为 DEBUG,会输出条件评估报告(CONDITIONS EVALUATION REPORT) + springframework: INFO + # 特别控制自动配置包的日志级别,避免输出条件评估报告 + springframework.boot.autoconfigure: WARN + # 日志输出格式配置(可选,使用默认格式) + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" + diff --git a/src/main/resources/mapper/ReportDatabaseMapper.xml b/src/main/resources/mapper/ReportDatabaseMapper.xml new file mode 100644 index 0000000..4a1170b --- /dev/null +++ b/src/main/resources/mapper/ReportDatabaseMapper.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +