写在前面
作为一名技术人,我深知学习新框架时的迷茫与焦虑。尤其是当你已经熟悉了一套技术栈,想要转向另一个生态时,那种"无从下手"的感觉尤为强烈。
最近,我开始系统学习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),最新的技术与你分享