refactor(project):重构项目结构并迁移至snails-chat模块- 将项目主模块更名为snails-chat,调整包结构

- 移除JPA相关依赖,替换为MyBatis-Plus- 数据库从MySQL迁移至PostgreSQL- 移除QueryTool工具类及相关依赖- 更新Redis配置,使用JSON序列化- 移除DashScopeController及AIResponse类
- 添加User实体类及Mapper接口
- 调整ChatClientConfiguration配置类- 更新pom.xml依赖管理及模块配置
This commit is contained in:
2025-10-25 10:06:37 +08:00
parent 40c05838f7
commit 177dfff3c7
25 changed files with 203 additions and 338 deletions

45
pom.xml
View File

@@ -6,23 +6,33 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.6</version> <version>3.5.6</version>
<relativePath/> <!-- lookup parent from repository --> <relativePath/>
</parent> </parent>
<groupId>com.hanserwei</groupId> <groupId>com.hanserwei</groupId>
<artifactId>snails-ai</artifactId> <artifactId>snails-ai</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>snails-ai</name> <name>snails-ai</name>
<description>snails-ai</description> <description>snails-ai</description>
<modules>
<module>snails-chat</module>
</modules>
<properties> <properties>
<java.version>21</java.version> <java.version>21</java.version>
<spring-ai.version>1.0.3</spring-ai.version> <spring-ai.version>1.0.3</spring-ai.version>
<jasypt-starter-version>3.0.5</jasypt-starter-version>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
@@ -33,19 +43,10 @@
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId> <artifactId>spring-boot-starter-webflux</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- spring-ai-alibaba-starter --> <!-- spring-ai-alibaba-starter -->
<dependency> <dependency>
<groupId>com.alibaba.cloud.ai</groupId> <groupId>com.alibaba.cloud.ai</groupId>
@@ -55,7 +56,7 @@
<dependency> <dependency>
<groupId>com.github.ulisesbocchio</groupId> <groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId> <artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.5</version> <version>${jasypt-starter-version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.alibaba.cloud.ai</groupId> <groupId>com.alibaba.cloud.ai</groupId>
@@ -65,6 +66,19 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId> <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
</dependency>
</dependencies> </dependencies>
<dependencyManagement> <dependencyManagement>
@@ -83,6 +97,13 @@
<type>pom</type> <type>pom</type>
<scope>import</scope> <scope>import</scope>
</dependency> </dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-bom</artifactId>
<version>3.5.14</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>

28
snails-chat/pom.xml Normal file
View File

@@ -0,0 +1,28 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.hanserwei</groupId>
<artifactId>snails-ai</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.hanserwei.chat</groupId>
<artifactId>snails-chat</artifactId>
<packaging>jar</packaging>
<name>snails-chat</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,13 @@
package com.hanserwei.chat;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.hanserwei.chat.domain.mapper")
public class SnailsChatApplication {
public static void main(String[] args) {
SpringApplication.run(SnailsChatApplication.class, args);
}
}

View File

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

View File

@@ -1,4 +1,4 @@
package com.hanserwei.snailsai.config; package com.hanserwei.chat.config;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.CorsRegistry;

View File

@@ -0,0 +1,30 @@
package com.hanserwei.chat.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置 RedisTemplate 的连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值,确保 key 是可读的字符串
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值, 确保存储的是 JSON 格式
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}

View File

@@ -0,0 +1,37 @@
package com.hanserwei.chat.domain.dataobject;
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 lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "t_user")
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@TableField(value = "\"name\"")
private String name;
@TableField(value = "email")
private String email;
@TableField(value = "age")
private Integer age;
@TableField(value = "created_at")
private LocalDateTime createdAt;
@TableField(value = "is_active")
private Boolean isActive;
}

View File

@@ -0,0 +1,9 @@
package com.hanserwei.chat.domain.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hanserwei.chat.domain.dataobject.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

View File

@@ -0,0 +1,9 @@
package com.hanserwei.chat.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.hanserwei.chat.domain.dataobject.User;
public interface UserService extends IService<User> {
}

View File

@@ -0,0 +1,12 @@
package com.hanserwei.chat.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hanserwei.chat.domain.dataobject.User;
import com.hanserwei.chat.domain.mapper.UserMapper;
import com.hanserwei.chat.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

View File

@@ -1,4 +1,4 @@
package com.hanserwei.snailsai.utils; package com.hanserwei.chat.utils;
import org.jasypt.util.text.AES256TextEncryptor; import org.jasypt.util.text.AES256TextEncryptor;

View File

@@ -9,14 +9,6 @@ spring:
name: snails-ai name: snails-ai
banner: banner:
location: config/banner.txt location: config/banner.txt
jpa:
hibernate:
ddl-auto: update
properties:
# 开启 SQL 语句格式化 (重点:让 SQL 易读)
hibernate.format_sql: true
# 开启 SQL 语法高亮 (重点:让 SQL 醒目)
hibernate.highlight_sql: true
data: data:
redis: redis:
host: localhost host: localhost
@@ -32,10 +24,10 @@ spring:
min-idle: 10 min-idle: 10
time-between-eviction-runs: 10000 time-between-eviction-runs: 10000
datasource: datasource:
driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: org.postgresql.Driver
url: jdbc:mysql://127.0.0.1:3306/snails_ai?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&allowMultiQueries=true&useSSL=false&allowPublicKeyRetrieval=true url: jdbc:postgresql://localhost:5432/postgres
username: root username: postgres
password: mysql password: postgressql
# HikariCP 连接池配置 # HikariCP 连接池配置
hikari: hikari:
maximum-pool-size: 20 # 最大连接数设置为 20 maximum-pool-size: 20 # 最大连接数设置为 20
@@ -55,9 +47,15 @@ spring:
options: options:
model: qwen-plus model: qwen-plus
temperature: 0.5 temperature: 0.5
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
global-config:
banner: false
mapper-locations: classpath*:/mapperxml/*.xml
logging: logging:
level: level:
org.hibernate.SQL: debug
# 隐藏掉 Hibernate 冗长的连接池 INFO 信息 # 隐藏掉 Hibernate 冗长的连接池 INFO 信息
org.hibernate.orm.connections.pooling: WARN org.hibernate.orm.connections.pooling: WARN
org.springframework.ai.chat.client.advisor: DEBUG org.springframework.ai.chat.client.advisor: DEBUG

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hanserwei.chat.domain.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.hanserwei.chat.domain.dataobject.User">
<!--@mbg.generated-->
<!--@Table t_user-->
<id column="id" jdbcType="INTEGER" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="email" jdbcType="VARCHAR" property="email" />
<result column="age" jdbcType="INTEGER" property="age" />
<result column="created_at" jdbcType="TIMESTAMP" property="createdAt" />
<result column="is_active" jdbcType="BOOLEAN" property="isActive" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, "name", email, age, created_at, is_active
</sql>
</mapper>

View File

@@ -1,13 +0,0 @@
package com.hanserwei.snailsai;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SnailsAiApplication {
public static void main(String[] args) {
SpringApplication.run(SnailsAiApplication.class, args);
}
}

View File

@@ -1,7 +0,0 @@
package com.hanserwei.snailsai.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedisConfig {
}

View File

@@ -1,41 +0,0 @@
package com.hanserwei.snailsai.controller;
import com.hanserwei.snailsai.model.AIResponse;
import com.hanserwei.snailsai.dto.ChatMessageDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
@Slf4j
@RequestMapping("/dashscope")
@RestController
@CrossOrigin
public class DashScopeController {
@Resource
private ChatClient dashScopeChatClient;
@PostMapping(value = "/generateStream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<AIResponse> generateStream(@RequestBody ChatMessageDTO chatMessageDTO) {
// 构建提示词
Prompt prompt = new Prompt(new UserMessage(chatMessageDTO.getMessage()));
// 流式输出
return dashScopeChatClient.prompt(prompt)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatMessageDTO.getConversionId()))
.stream() // 流式输出
.chatResponse()
.mapNotNull(chatResponse -> {
Generation generation = chatResponse.getResult();
String text = generation.getOutput().getText();
return AIResponse.builder().v(text).build();
});
}
}

View File

@@ -1,29 +0,0 @@
package com.hanserwei.snailsai.controller;
import com.hanserwei.snailsai.entity.UserEntity;
import com.hanserwei.snailsai.service.UserService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class TestDataController {
private final UserService userService;
public TestDataController(UserService userService) {
this.userService = userService;
}
/**
* POST /api/test/generate-users?count=100
* 插入假数据用于分页测试
*/
@PostMapping("/api/test/generate-users")
public String generateTestData(@RequestParam(defaultValue = "100") int count) {
List<UserEntity> insertedUsers = userService.insertDummyUsers(count);
return String.format("成功插入了 %d 条假数据!", insertedUsers.size());
}
}

View File

@@ -1,17 +0,0 @@
package com.hanserwei.snailsai.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

@@ -1,54 +0,0 @@
package com.hanserwei.snailsai.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
@Entity
@Table(
name = "users",
uniqueConstraints = @UniqueConstraint(
name = "UQ_USER_NAME",
columnNames = {"user_name"} // 指定应用约束的列
)
)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserEntity implements Serializable {
@Serial
private static final long serialVersionUID = 812305521669146765L;
// 1. 主键自增ID
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
// 2. 用户名:不能为空,长度限制
@Column(name = "user_name", nullable = false, unique = true, length = 50)
private String username;
// 3. 密码:加密存储,不能为空,长度需足够长
@Column(name = "password", nullable = false, length = 100)
private String password;
// 4. 逻辑外键
@Column(name = "hobby_id")
private Long hobbyId;
// 5. 补充审计字段 (推荐)
@Column(name = "create_time", nullable = false)
private LocalDateTime createTime;
@Column(name = "update_time")
private LocalDateTime updateTime;
}

View File

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

View File

@@ -1,21 +0,0 @@
package com.hanserwei.snailsai.repository;
import com.hanserwei.snailsai.entity.UserEntity;
import org.jetbrains.annotations.NotNull;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
@NotNull List<UserEntity> findAll();
@NotNull List<UserEntity> findAllByIdIn(List<Long> ids);
@NotNull List<UserEntity> findAllByUsernameContaining(String name);
}

View File

@@ -1,69 +0,0 @@
package com.hanserwei.snailsai.service;
import com.hanserwei.snailsai.entity.UserEntity;
import com.hanserwei.snailsai.repository.UserRepository;
import jakarta.transaction.Transactional;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserService {
private final UserRepository userRepository;
// 构造器注入
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
/**
* 批量生成并插入指定数量的假用户数据
* @param count 要插入的用户数量
* @return 插入成功的用户列表
*/
@Transactional // 确保整个批量操作在一个事务中完成
public List<UserEntity> insertDummyUsers(int count) {
if (count <= 0) {
return List.of(); // 返回空列表
}
List<UserEntity> dummyUsers = new ArrayList<>(count);
LocalDateTime now = LocalDateTime.now();
// 确保用户名是唯一的
// 我们可以先获取当前数据库中用户数量,作为生成唯一用户名的起始点
long startId = userRepository.count();
for (int i = 1; i <= count; i++) {
// 使用 startId + i 来保证生成的用户名在多次运行时尽可能不重复
long userIndex = startId + i;
// 注意:密码通常应该是加密后的,这里为了演示使用明文
UserEntity user = new UserEntity(
null, // ID 设为 null让 JPA 自动生成
"testUser_" + userIndex, // 确保用户名唯一
"123456", // 模拟一个加密后的密码,或者使用一个测试用的明文,例如 "password"
(long) (i % 3) + 1, // 随机分配一个 hobbyId (例如 1, 2, 3)
now.plusSeconds(i), // 模拟创建时间略微递增
null // updateTime 初始为 null
);
dummyUsers.add(user);
}
// 使用 JpaRepository 的 saveAll 方法进行批量插入,效率比单个 save 要高
return userRepository.saveAll(dummyUsers);
}
public List<UserEntity> findAll() {
return userRepository.findAll();
}
public List<UserEntity> findAllByIdIn(List<Long> ids) {
return userRepository.findAllByIdIn(ids);
}
}

View File

@@ -1,27 +0,0 @@
package com.hanserwei.snailsai.tools;
import com.hanserwei.snailsai.entity.UserEntity;
import com.hanserwei.snailsai.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class QueryTool {
@Resource
private UserService userService;
@Tool(name = "findAll", description = "查询所有用户")
public List<UserEntity> findAll() {
return userService.findAll();
}
@Tool(name = "findAllByIdIn", description = "根据id列表查询用户")
public List<UserEntity> findAllByIdIn(List<Long> ids) {
return userService.findAllByIdIn(ids);
}
}

View File

@@ -1,13 +0,0 @@
package com.hanserwei.snailsai;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SnailsAiApplicationTests {
@Test
void contextLoads() {
}
}