feat(ai): 实现AI聊天功能并集成数据库工具

- 新增 AiChatController 支持流式聊天响应
- 创建 AIResponse 和 ChatMessageDTO 用于数据传输
- 开发 AiDBTools 提供用户相关的增删改查及封禁功能- 配置 ChatClient 支持默认工具调用
- 调整 User 实体类时间字段为 OffsetDateTime 并格式化- 添加 jackson-datatype-jsr310 依赖以支持 JSR310 时间序列化
- 修改 PostgreSQL 连接字符串时区配置
- 启用 Jackson 日期写入为字符串而非时间戳
This commit is contained in:
2025-10-25 17:27:53 +08:00
parent 177dfff3c7
commit 5c0feab211
8 changed files with 181 additions and 3 deletions

View File

@@ -3,6 +3,7 @@ package com.hanserwei.chat.config;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.memory.redis.BaseRedisChatMemoryRepository;
import com.alibaba.cloud.ai.memory.redis.LettuceRedisChatMemoryRepository;
import com.hanserwei.chat.tools.AiDBTools;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor;
@@ -28,6 +29,8 @@ public class ChatClientConfiguration {
@Resource
private DashScopeChatModel dashScopeChatModel;
@Resource
private AiDBTools aiDBTools;
@Bean
public BaseRedisChatMemoryRepository redisChatMemoryRepository() {
@@ -51,6 +54,7 @@ public class ChatClientConfiguration {
@Bean
public ChatClient dashScopeChatClient(ChatMemory chatMemory) {
return ChatClient.builder(dashScopeChatModel)
.defaultTools(aiDBTools)
.defaultAdvisors(PromptChatMemoryAdvisor.builder(chatMemory).build(), new SimpleLoggerAdvisor())
.build();
}

View File

@@ -0,0 +1,36 @@
package com.hanserwei.chat.controller;
import com.hanserwei.chat.model.dto.ChatMessageDTO;
import com.hanserwei.chat.model.vo.AIResponse;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/ai")
public class AiChatController {
@Resource
private ChatClient dashScopeChatClient;
@PostMapping(path = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<AIResponse> chatWithAi(@RequestBody ChatMessageDTO chatMessageDTO) {
return dashScopeChatClient.prompt()
.user(chatMessageDTO.getMessage())
.advisors(p -> p.param(ChatMemory.CONVERSATION_ID, chatMessageDTO.getConversionId()))
.stream()
.chatResponse()
.mapNotNull(chatResponse -> AIResponse.builder()
.v(chatResponse.getResult().getOutput().getText())
.build());
}
}

View File

@@ -4,12 +4,13 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
@Data
@Builder
@@ -30,7 +31,8 @@ public class User {
private Integer age;
@TableField(value = "created_at")
private LocalDateTime createdAt;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssXXX")
private OffsetDateTime createdAt;
@TableField(value = "is_active")
private Boolean isActive;

View File

@@ -0,0 +1,17 @@
package com.hanserwei.chat.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ChatMessageDTO {
private String message;
private Long conversionId;
}

View File

@@ -0,0 +1,15 @@
package com.hanserwei.chat.model.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AIResponse {
// 流式响应内容
private String v;
}

View File

@@ -0,0 +1,97 @@
package com.hanserwei.chat.tools;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hanserwei.chat.domain.dataobject.User;
import com.hanserwei.chat.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class AiDBTools {
@Resource
private UserService userService;
@Tool(name = "findAll", description = "查询所有用户")
public List<User> findAll() {
return userService.list();
}
@Tool(name = "findAllByIdIn", description = "根据id列表查询用户")
public List<User> findAllByIdIn(@ToolParam(description = "用户id列表") List<Long> ids) {
return userService.listByIds(ids);
}
@Tool(name = "findById", description = "根据id查询用户")
public User findById(Long id) {
return userService.getById(id);
}
@Tool(name = "findByName", description = "根据名称查询用户")
public User findByName(String name) {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(User.class)
.eq(User::getName, name);
return userService.getOne(queryWrapper);
}
@Tool(name = "findByNameLike", description = "根据名称模糊查询用户")
public List<User> findByNameLike(String name) {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(User.class)
.like(User::getName, name);
return userService.list(queryWrapper);
}
@Tool(name = "findByAge", description = "根据年龄查询用户")
public List<User> findByAge(Integer age) {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(User.class)
.eq(User::getAge, age);
return userService.list(queryWrapper);
}
@Tool(name = "findByAgeBetween", description = "根据年龄范围查询用户")
public List<User> findByAgeBetween(Integer start, Integer end) {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(User.class)
.between(User::getAge, start, end);
return userService.list(queryWrapper);
}
// 插入 数据
@Tool(name = "insert",
description = """
插入一个新的用户。
需要一个用户对象作为参数,
该对象必须包含以下字段:
name (String), email (String), 和 age (Integer)。
""")
public void insert(@ToolParam(description = "用户对象") User user) {
userService.save(user);
}
@Tool(name = "update",
description = """
更新现有用户。需要一个用户对象作为参数,该对象 **必须包含用户 ID**
并携带要修改的字段,例如 name (String), email (String), 或 age (Integer)。
""")
public void update(@ToolParam(description = "用户对象") User user) {
userService.updateById(user);
}
@Tool(name = "delete", description = "删除用户")
public void delete(Long id) {
userService.removeById(id);
}
//封禁
@Tool(name = "ban", description = "根据用户ID封禁用户。")
public void ban(@ToolParam(description = "用户id") Long id) {
userService.update(User.builder()
.isActive(false)
.build(),
new LambdaQueryWrapper<>(User.class)
.eq(User::getId, id));
}
}

View File

@@ -9,6 +9,9 @@ spring:
name: snails-ai
banner:
location: config/banner.txt
jackson:
serialization:
write-dates-as-timestamps: false
data:
redis:
host: localhost
@@ -25,7 +28,7 @@ spring:
time-between-eviction-runs: 10000
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/postgres
url: jdbc:postgresql://localhost:5432/postgres?serverTimezone=Asia/Shanghai
username: postgres
password: postgressql
# HikariCP 连接池配置