springboot整合deepseek V3+MCP Server实现查询班表信息(数据库)

223 阅读6分钟

写在前面的话

  • 使用springboot3.4.5(最新版本),JDK版本>=17,本文档为17。
  • Mybatis-Plus 为最新版本,使用了Hutool工具类。
  • 如使用的Springboot版本与本文档相近,最好以本文档为主。
  • 因为在整理代码的这段时间,发现很多文章引入的jar包有问题,甚至导致@Tool注解无法正常使用。
  • 自己娱乐的项目,主要用来通过关键指令查询某位员工的班表以及同个班次的其他同事等信息。
  • 如有疑问请评论区友好交流指点。

实现的功能

之前自娱自乐的微信机器人小程序,功能就两个。

  • 发送员工名字简写+今天/明天/后天/X号/yyyy-MM-dd来查询班表情况。
  • 发送今天/明天/后天/X号/yyyy-MM-dd来查询对应的整体班表情况。

使用的一些文档

引入相关依赖

<properties>
    <java.version>17</java.version>
    <hutool.version>5.8.26</hutool.version>
    <mybatis-plus.version>3.5.12</mybatis-plus.version>
    <lombok.version>1.18.38</lombok.version>
    <freemarker.version>2.3.34</freemarker.version>
    <spring-ai.version>1.0.0-M7</spring-ai.version>
    <jsonschema.version>4.38.0</jsonschema.version>
</properties>

<dependencies>
    <!--需要用到web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--hutool-->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>${hutool.version}</version>
    </dependency>
    <!--mybatis-plus-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        <version>${mybatis-plus.version}</version>
    </dependency>
    <!--mybatis-plus代码生成器-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-generator</artifactId>
        <version>${mybatis-plus.version}</version>
    </dependency>
    <!--生成器模板-->
    <dependency>
        <groupId>org.freemarker</groupId>
        <artifactId>freemarker</artifactId>
        <version>${freemarker.version}</version>
    </dependency>
    <!--lombok-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${lombok.version}</version>
    </dependency>

    <!-- Spring AI 核心依赖 -->
    <!-- 注意:下面引入的依赖是关键。
    截止本文档发布之前的很多文章jar依赖现在使用都是无法点出spring.ai.mcp的,甚至会发生依赖冲突,导致一些项目无法运行/@Tool不起作用。
    spring-ai-starter-model-openai     ->使用这个作为最新版本链接deepSeek
    spring-ai-starter-mcp-server-webflux   mcp-server-webflux服务端,后续细说
    -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
    </dependency>

</dependencies>

<!-- 来自于官网摘录,对于使用SpringAI的最好是引用一下哈。-->
<!-- Spring AI 物料清单 (BOM) 声明了 Spring AI 特定版本所使用的所有依赖项的推荐版本。-->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

关于jar包的提醒

我在查阅多篇文章的时候,发现即使是一个月前或是两三个月前,使用的jar有spring-ai-mcp-server-webmvc-spring-boot-starter的,但是我测试是不起作用的。
后面在官网上查询到是spring-ai-starter-mcp-server-webflux,用webflux/webmvc这个要根据你的项目来。 简单的说,webflux异步非阻塞适合高并发的场景下使用。

注解描述

着重说一下两个注解,也是贯穿整篇文章的重点之一。

@Tool

注解加上后,表明这是个工具。
可能听到这里第一次接触ai的会有些迷糊,不着急,下面慢慢解释。

举个例子:
用户message:"查一下zs今天的班表信息呢?"。 经过ai处理之后的关键词,就会以分词的形式呈现['zs''今天''班表信息']。
那么它该怎么去调用数据库查询呢?
没错,就是通过@Tool的参数description定义好的文字来进行匹配,所以@Tool的description绝不是简简单单的描述句,而是越贴切业务越好

name就很好理解了,代表这个Tool的名字是什么。

如果还是比较迷糊,没关系,继续往下面看。

@ToolParam

有个上面的基础,也就比较好理解ToolParam的description是什么作用了吧,这里就不再多赘述。

下面正是进入主题。

application.yml

spring:
  datasource:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://127.0.0.1:3306/bot?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF8
      username: username
      password: password
  ai:
    openai:
      api-key: 填写DeepSeek的密钥
      chat:
        base-url: https://api.deepseek.com
        options:
          # 使用的V3版本所以用下面的命令,具体可在deepseek官网查阅
          model: deepseek-chat

# mcp
    mcp:
      server:
        # 下面自行填写
        name: mcp-server的名字
        version: 1.0.0
        type: sync
        sse-endpoint: /mcp/messages

config类

McpServerConfig

@Configuration
public class McpServerConfig {

    @Bean
    public ToolCallbackProvider toolCallbackProvider(BanbiaoService banbiaoService) {
        // service注册
        MethodToolCallbackProvider.Builder builder = MethodToolCallbackProvider.builder()
                .toolObjects(banbiaoService);
        return builder.build();
    }

}

ChatClientConfig客户端构建

用来设定ai的画像,比如"你是个xxx的助手"类似的,这里我还设定了获取数据后根据指定模板输出。
如果也想根据指定的模板输出,就可以像下面例子一样,设定参数变量,比如{pinyin},{dateStr}。

/**
 * 配置ChatClient,注册系统指令和工具函数
 */
@Bean
public ChatClient chatClient(ChatClient.Builder builder, ToolCallbackProvider toolCallbackProvider) {
    return builder
            .defaultSystem(
                    """
                            你是一个智能班表助手,必须严格遵守以下规则:
                            **规则1 - 单人员查询**:
                            当用户输入格式为"[拼音][日期]"(如"zs今天"、"ls明天")时:
                            1.使用工具链接数据库进行查询。
                            2. 输出格式:
                               {pinyin},{dateStr}班表:
                               你的班次为:{banci}
                               和你一起的同事有:{otherList}/没有和你同个班次的同事
                            **强制要求**:
                            - 空列表显示为"没有和你同个班次的同事"
                            - 绝对禁止虚构数据
                        """
            )
            // 注册工具方法
            .defaultTools(toolCallbackProvider)
            .build();
}

entity

/**
 * <p>
 * 
 * </p>
 *
 * @author yueranzs
 * @since 2025-05-08
 */
@Getter
@Setter
@ToString
public class Banbiao implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 姓名
     */
    private String name;

    /**
     * 拼音首字母
     */
    private String pinyin;

    /**
     * 班次:早中晚休事假年假病假/
     */
    private String banci;

    /**
     * 日期
     */
    private String riqi;

    /**
     * 日期,年月
     */
    private String ny;

    /**
     * 星期
     */
    private String xq;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 创建人编号
     */
    private Integer createUser;

    /**
     * 修改时间
     */
    private LocalDateTime updateTime;

    /**
     * 修改人编号
     */
    private Integer updateUser;

    /**
     * 是否删除:默认:1 不删除;0 删除
     */
    private Integer delFlag;
}

dto

@Getter
@Setter
@ToString
public class ChatRequest {

    @NotNull(message = "用户请求的信息不能为空!")
    private String message;

}

resp响应

/**
 * <p>
 * 响应体
 * </p>
 *
 * @author yueranzs
 * @since 2025-05-08
 */
public class Response<T> {

    /**
     * 响应码
     */
    public Integer code;
    /**
     * 响应备注
     */
    public String msg;
    /**
     * 响应数据
     */
    public T data;

    private static <T> Response<T> ok(){
        Response<T> response = new Response<>();
        response.code = 200;
        response.msg = "成功";
        return response;
    }
    private static <T> Response<T> failing(){
        Response<T> response = new Response<>();
        response.code = 500;
        return response;
    }

    /**
     * 成功
     * @param data 成功数据
     * @return
     * @param <T>
     */
    public static <T> Response<T> success(T data){
        Response<T> response = ok();
        response.data = data;
        return response;
    }

    /**
     * 失败
     * @param msg 失败提示语
     * @return
     * @param <T>
     */
    public static <T> Response<T> error(String msg){
        Response<T> response = failing();
        response.msg = msg;
        return response;
    }

}

utils

/**
 * 日期工具类
 */
public class DateParseUtil {

    /**
     * 今天/明天/后天/X号转为yyyy-MM-dd
     * @param dateStr 今天/明天/后天/X号
     * @return
     */
    public static String parseDate(String dateStr) {
        LocalDate today = LocalDate.now();
        LocalDate now = today;
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        switch (dateStr) {
            case "今天" -> now = today.plusDays(0);
            case "明天" -> now = today.plusDays(1);
            case "后天" -> now =today.plusDays(2);
            default -> {
                if (dateStr.matches("\d+号")) { // 处理"15号"格式
                    int day = Integer.parseInt(dateStr.replace("号", ""));
                    now = today.withDayOfMonth(day);
                } else { // 处理标准日期格式
                    now = LocalDate.parse(dateStr, DateTimeFormatter.ISO_DATE);
                }
            }
        };
        return now.format(formatter);
    }

}

service

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author yueranzs
 * @since 2025-05-08
 */
@Slf4j
@Service
public class BanbiaoServiceImpl extends ServiceImpl<BanbiaoMapper, Banbiao> implements BanbiaoService {

    @Resource
    private BanbiaoMapper banbiaoMapper;

    public BanbiaoServiceImpl(){
        log.info("banbiaoServiceImpl已创建");
    }



    @Override
    @Tool(name = "getBanBiaoByUserPinYin",description = "根据员工名字简写查询员工班表信息")
    public JSONObject getBanBiaoByUserPinYin(
            @ToolParam(description = "员工名字首字母简写(如zs)") String pinyin,
            @ToolParam(description = "日期(今天/明天/后天/X号/yyyy-MM-dd)") String date
    ){
        //1.转换日期
        String dateStr = getDateStr(date);

        //2.查询当前员工的班表信息
        LambdaQueryWrapper<Banbiao> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(Banbiao::getPinyin, pinyin.toUpperCase());
        queryWrapper.eq(Banbiao::getRiqi,dateStr);
        queryWrapper.eq(Banbiao::getDelFlag,1);
        queryWrapper.select(Banbiao::getName,Banbiao::getBanci,Banbiao::getPinyin,Banbiao::getRiqi,Banbiao::getXq);
        Banbiao meBanBiao = getOne(queryWrapper);

        //3.查询同个时间同个班次的其他同事
        LambdaQueryWrapper<Banbiao> queryOtherWrapper = new LambdaQueryWrapper<>();
        queryOtherWrapper.eq(Banbiao::getBanci,meBanBiao.getBanci());
        queryOtherWrapper.eq(Banbiao::getRiqi,meBanBiao.getRiqi());
        queryOtherWrapper.notIn(Banbiao::getPinyin,meBanBiao.getPinyin());
        queryOtherWrapper.eq(Banbiao::getDelFlag,1);
        queryWrapper.select(Banbiao::getName,Banbiao::getBanci,Banbiao::getPinyin,Banbiao::getRiqi,Banbiao::getXq);
        List<Banbiao> otherList = list(queryOtherWrapper);

        //4.返回整合数据
        JSONObject result = new JSONObject();
        result.set("pinyin",meBanBiao.getPinyin().toLowerCase());
        result.set("dateStr",dateStr);
        result.set("banci",meBanBiao.getBanci());
        result.set("otherList",otherList.stream().map(Banbiao::getName).collect(Collectors.toList()));
        return result;
    }


    private static String getDateStr(String date){
        //1.转换日期
        String dateStr = "";
        try {
            dateStr = DateParseUtil.parseDate(date);
        }catch (IllegalArgumentException e){
            log.error("日期格式错误:{}",date);
        }
        return dateStr;
    }


}

上面的service看完之后,想必就对@Tool和@Toolparam有一些认知了,具体的话可以去官网上看看Tool的文档。
docs.spring.io/spring-ai/r…

controller

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author yueranzs
 * @since 2025-05-08
 */
@Slf4j
@RestController
@RequestMapping("/banbiao")
public class BanbiaoController {

    @Resource
    private ChatClient chatClient;

    @PostMapping("chat")
    public Response<String> chat(@RequestBody ChatRequest request) {
        try {
            // 创建用户消息
            String userMessage = request.getMessage();
            log.info("用户问题:{}", userMessage);
            // 使用流式API调用聊天
            String content = chatClient.prompt()
                    .user(userMessage)
                    .call()
                    .content();

            return Response.success(content);
        } catch (Exception e) {
            log.error("处理请求时出错: {}", e.getMessage());
            return Response.error("处理请求时出错: " + e.getMessage());
        }
    }

}

效果图

image.png

image.png

image.png

可以看到ai的自然语言处理很强大,另外,经测试,使用汉字的员工名称也可以直接查询,为什么呢?因为在@ToolParam里面已经定义好了,参数就是员工名字首字母简写

结尾

前几年的ChatGPT,到如今的deepseek横空崛起,不光提高性能的同时还降低了大量成本,甚至一度拉动股市红火了大半年。ai——这一股强劲的势头真的很猛。
几年过去,虽然已经不在这个行业奋进,但依然心有触动。
时代的浪潮席卷之下,愿大家一切都好。