Spring Boot + Spring AI 从入门到实战:7天转型计划 Day 6 —— 业务完善 + 会话消息预览

写在前面

作为一名技术人,我深知学习新框架时的迷茫与焦虑。尤其是当你已经熟悉了一套技术栈,想要转向另一个生态时,那种"无从下手"的感觉尤为强烈。

最近,我开始系统学习Spring Boot生态,并尝试将Spring AI集成到实际项目中。经过一段时间的摸索,我整理出了一份7天学习路线图,目标是让有编程基础的开发者能快速上手Spring Boot + Spring AI,独立完成一个完整的AI对话应用。

这份计划不是零基础教程,而是为有一定后端开发经验(不限于Java)的朋友量身定制的"快速转型指南"。如果你对Python、Node.js或其他后端语言已有了解,这份计划将帮助你在最短时间内建立Spring Boot的核心认知,并跟上AI应用开发的浪潮。

下面,我将分享这7天的学习中的第六天内容,业务完善 + 会话消息预览。

一、创建 DTO 类

1.1 会话预览 DTO(含最后消息)

创建 dto/SessionWithPreviewDTO.java:

java 复制代码
package com.example.chatapi.dto;


import lombok.Data;
import java.time.LocalDateTime;


@Data
public class SessionWithPreviewDTO {
    private Long id;
    private String name;
    private LocalDateTime createdAt;
    private String lastMessage;           // 最后一条消息内容
    private LocalDateTime lastMessageTime; // 最后一条消息时间
}

1.2 会话详情 DTO

创建 dto/SessionDetailDTO.java:

java 复制代码
package com.example.chatapi.dto;


import com.example.chatapi.entity.Message;
import com.example.chatapi.entity.Session;
import lombok.Data;
import java.util.List;


@Data
public class SessionDetailDTO {
    private Session session;
    private List<Message> messages;


    public SessionDetailDTO(Session session, List<Message> messages) {
        this.session = session;
        this.messages = messages;
    }
}

二、创建自定义业务异常

创建 exception/BusinessException.java:

scala 复制代码
package com.example.chatapi.exception;


import lombok.Getter;


@Getter
public class BusinessException extends RuntimeException {
    private final Integer code;


    public BusinessException(String message) {
        super(message);
        this.code = 400;
    }


    public BusinessException(Integer code, String message) {
        super(message);
        this.code = code;
    }
}

三、创建全局异常处理器

创建 handler/GlobalExceptionHandler.java:

typescript 复制代码
package com.example.chatapi.handler;


import com.example.chatapi.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;


@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {


    @ExceptionHandler(BusinessException.class)
    public Map<String, Object> handleBusinessException(BusinessException e) {
        log.warn("业务异常: {}", e.getMessage());
        Map<String, Object> result = new HashMap<>();
        result.put("code", e.getCode());
        result.put("message", e.getMessage());
        result.put("data", null);
        return result;
    }


    @ExceptionHandler(Exception.class)
    public Map<String, Object> handleException(Exception e) {
        log.error("系统异常", e);
        Map<String, Object> result = new HashMap<>();
        result.put("code", 500);
        result.put("message", "服务器内部错误");
        result.put("data", null);
        return result;
    }
}

四、增强 Mapper 层

4.1 更新 SessionMapper

java 复制代码
package com.example.chatapi.mapper;


import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.chatapi.dto.SessionWithPreviewDTO;
import com.example.chatapi.entity.Session;
import org.apache.ibatis.annotations.Select;
import java.util.List;


@Mapper
public interface SessionMapper extends BaseMapper<Session> {


    // 查询所有会话并带上最后一条消息预览
    @Select("""
        SELECT 
            s.id,
            s.name,
            s.created_at,
            (SELECT content FROM message 
             WHERE session_id = s.id 
             ORDER BY created_at DESC LIMIT 1) as last_message,
            (SELECT created_at FROM message 
             WHERE session_id = s.id 
             ORDER BY created_at DESC LIMIT 1) as last_message_time
        FROM session s
        ORDER BY s.created_at DESC
        """)
    List<SessionWithPreviewDTO> selectAllWithPreview();


    // 检查会话是否存在
    @Select("SELECT COUNT(*) FROM session WHERE id = #{id}")
    int countById(Long id);
}

4.2 更新 MessageMapper

kotlin 复制代码
@Mapper
public interface MessageMapper extends BaseMapper<Message> {


    @Select("SELECT * FROM message WHERE session_id = #{sessionId} ORDER BY created_at ASC")
    List<Message> selectBySessionId(Long sessionId);


    @Delete("DELETE FROM message WHERE session_id = #{sessionId}")
    int deleteBySessionId(Long sessionId);
}

五、完善 Service 层

5.1 完善 SessionService

typescript 复制代码
@Service
@RequiredArgsConstructor
public class SessionService {




    private final SessionMapper sessionMapper;
    private final MessageMapper messageMapper;




    // 查询会话列表(带最后消息预览)
    public List<SessionWithPreviewDTO> listWithPreview() {
        return sessionMapper.selectAllWithPreview();
    }




    // 查询会话详情(包含所有消息)
    public SessionDetailDTO getDetail(Long id) {
        Session session = sessionMapper.selectById(id);
        if (session == null) {
            throw new BusinessException("会话不存在");
        }
        List<Message> messages = messageMapper.selectBySessionId(id);
        return new SessionDetailDTO(session, messages);
    }




    // 创建会话
    @Transactional
    public Session create(String name) {
        Session session = new Session();
        session.setName(name);
        session.setCreatedAt(LocalDateTime.now());
        sessionMapper.insert(session);
        return session;
    }




    // 更新会话
    @Transactional
    public Session update(Long id, String name) {
        Session session = sessionMapper.selectById(id);
        if (session == null) {
            throw new BusinessException("会话不存在");
        }
        session.setName(name);
        sessionMapper.updateById(session);
        return session;
    }




    // 删除会话(级联删除消息)
    @Transactional
    public void delete(Long id) {
        Session session = sessionMapper.selectById(id);
        if (session == null) {
            throw new BusinessException("会话不存在");
        }
        messageMapper.deleteBySessionId(id);  // 先删消息
        sessionMapper.deleteById(id);         // 再删会话
    }




    // 检查会话是否存在
    public boolean exists(Long id) {
        return sessionMapper.countById(id) > 0;
    }
}

5.2 完善 MessageService

typescript 复制代码
@Service
@RequiredArgsConstructor
public class MessageService {


    private final MessageMapper messageMapper;
    private final SessionService sessionService;


    public List<Message> getMessagesBySession(Long sessionId) {
        if (!sessionService.exists(sessionId)) {
            throw new BusinessException("会话不存在");
        }
        return messageMapper.selectBySessionId(sessionId);
    }


    @Transactional
    public Message saveMessage(Long sessionId, String role, String content) {
        if (!sessionService.exists(sessionId)) {
            throw new BusinessException("会话不存在");
        }
        Message message = new Message();
        message.setSessionId(sessionId);
        message.setRole(role);
        message.setContent(content);
        message.setCreatedAt(LocalDateTime.now());
        messageMapper.insert(message);
        return message;
    }
}

六、完善 Controller 层

6.1 SessionController

less 复制代码
@Slf4j
@RestController
@RequestMapping("/api/sessions")
@RequiredArgsConstructor
public class SessionController {


    private final SessionService sessionService;


    // GET /api/sessions → 会话列表(带最后消息预览)
    @GetMapping
    public Result<List<SessionWithPreviewDTO>> list() {
        return Result.success(sessionService.listWithPreview());
    }


    // GET /api/sessions/{id}/detail → 会话详情
    @GetMapping("/{id}/detail")
    public Result<SessionDetailDTO> detail(@PathVariable Long id) {
        return Result.success(sessionService.getDetail(id));
    }


    // POST /api/sessions → 创建会话
    @PostMapping
    public Result<Session> create(@RequestBody Map<String, String> body) {
        String name = body.get("name");
        if (name == null || name.trim().isEmpty()) {
            return Result.error("会话名称不能为空");
        }
        return Result.success(sessionService.create(name.trim()));
    }


    // PUT /api/sessions/{id} → 更新会话
    @PutMapping("/{id}")
    public Result<Session> update(@PathVariable Long id, @RequestBody Map<String, String> body) {
        String name = body.get("name");
        if (name == null || name.trim().isEmpty()) {
            return Result.error("会话名称不能为空");
        }
        return Result.success(sessionService.update(id, name.trim()));
    }


    // DELETE /api/sessions/{id} → 删除会话
    @DeleteMapping("/{id}")
    public Result<Void> delete(@PathVariable Long id) {
        sessionService.delete(id);
        return Result.success();
    }
}

6.2 MessageController

less 复制代码
@RestController
@RequestMapping("/api/messages")
@RequiredArgsConstructor
public class MessageController {


    private final MessageService messageService;


    @GetMapping("/{sessionId}")
    public Result<List<Message>> getBySession(@PathVariable Long sessionId) {
        return Result.success(messageService.getMessagesBySession(sessionId));
    }
}

七、完整测试流程

测试1:创建会话

bash 复制代码
POST http://localhost:8080/api/sessions
{"name": "AI助手"}

测试2:发送消息

bash 复制代码
POST http://localhost:8080/api/chat/send
{"sessionId": 1, "message": "你好"}

测试3:获取会话列表(带预览)

bash 复制代码
GET http://localhost:8080/api/sessions

响应示例:

css 复制代码
{
    "code": 200,
    "message": "success",
    "data": [
        {
            "id": 1,
            "name": "AI助手",
            "createdAt": "2026-05-22T10:00:00",
            "lastMessage": "你好!我是通义千问...",
            "lastMessageTime": "2026-05-22T10:01:00"
        }
    ]
}

学习建议: 今天的重点是理解 DTO 的作用和子查询的写法。不要在 Controller 中直接返回 Entity,通过 DTO 可以更灵活地控制返回内容。

欢迎关注我的公众号(onething365),最新的技术与你分享

相关推荐
BingoGo1 小时前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
JaguarJack1 小时前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
IT_陈寒2 小时前
SpringBoot自动配置的坑,我爬了三天才出来
前端·人工智能·后端
甲维斯3 小时前
笑抽了!DeepSeek识图,豆包完胜了!
人工智能·deepseek
Lei活在当下11 小时前
【AI手记系列-2026/6/18】iSparto & Harness,Caveman 以及AI时代的生存指南
人工智能·llm·openai
冬奇Lab13 小时前
每日一个开源项目(第134篇):Zvec - 阿里开源的嵌入式向量数据库,向量搜索界的 SQLite
数据库·人工智能·llm
冬奇Lab13 小时前
Agent 系列(22):Context Engineering 深度——三种上下文管理策略的量化对比
人工智能·agent
hboot13 小时前
AI工程师第二课 - 数据处理
人工智能·python·数据分析
ServBay13 小时前
打通 AI 编程本地运维边界,利用 MCP 协议简化环境与服务管理
后端·ai编程·mcp