first commit
This commit is contained in:
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
HELP.md
|
||||
target/
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
104
pom.xml
Normal file
104
pom.xml
Normal file
@@ -0,0 +1,104 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.5</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.ai</groupId>
|
||||
<artifactId>spring_ai_app</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>spring_ai_app</name>
|
||||
<description>spring_ai_app</description>
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<!-- spring ai 正式版-->
|
||||
<spring.ai.bom>1.0.1</spring.ai.bom>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- deepseek模型自动配置-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-deepseek</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- mybatis -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>3.5.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-generator</artifactId>
|
||||
<version>3.5.5</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 数据库驱动 -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>8.0.17</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<version>1.2.83</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.18.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- spring ai 版本管理-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>${spring.ai.bom}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
13
src/main/java/com/ai/app/SpringAiAppApplication.java
Normal file
13
src/main/java/com/ai/app/SpringAiAppApplication.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.ai.app;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SpringAiAppApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SpringAiAppApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
79
src/main/java/com/ai/app/config/AIConfig.java
Normal file
79
src/main/java/com/ai/app/config/AIConfig.java
Normal file
@@ -0,0 +1,79 @@
|
||||
package com.ai.app.config;
|
||||
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
|
||||
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
|
||||
import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository;
|
||||
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
|
||||
import org.springframework.ai.deepseek.DeepSeekChatModel;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @describe ai配置类
|
||||
* @Author kai.liu
|
||||
* @Date 2025/8/28 10:02
|
||||
*/
|
||||
@Configuration
|
||||
public class AIConfig {
|
||||
|
||||
|
||||
@Bean
|
||||
public InMemoryChatMemoryRepository inMemoryChatMemoryRepository() {
|
||||
return new InMemoryChatMemoryRepository();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MessageWindowChatMemory messageWindowChatMemory(InMemoryChatMemoryRepository inMemoryChatMemoryRepository) {
|
||||
return MessageWindowChatMemory.builder().chatMemoryRepository(inMemoryChatMemoryRepository).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置会话记忆环绕增强Advisor
|
||||
*
|
||||
* @param messageWindowChatMemory 消息窗口会话记忆
|
||||
* @return: org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor
|
||||
* @author kai.liu
|
||||
* @date: 2025/8/29 9:05
|
||||
*/
|
||||
@Bean
|
||||
public MessageChatMemoryAdvisor messageChatMemoryAdvisor(MessageWindowChatMemory messageWindowChatMemory) {
|
||||
return MessageChatMemoryAdvisor.builder(messageWindowChatMemory).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置日志环绕增强Advisor
|
||||
*
|
||||
* @return: org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor
|
||||
* @author kai.liu
|
||||
* @date: 2025/8/29 9:05
|
||||
*/
|
||||
@Bean
|
||||
public SimpleLoggerAdvisor simpleLoggerAdvisor() {
|
||||
return new SimpleLoggerAdvisor();
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置AI对话客户端
|
||||
*
|
||||
* @param deepSeekChatModel deepseek大模型
|
||||
* @param simpleLoggerAdvisor 日志Advisor
|
||||
* @param messageChatMemoryAdvisor 会话记忆Advisor
|
||||
* @return: org.springframework.ai.chat.client.ChatClient
|
||||
* @author kai.liu
|
||||
* @date: 2025/8/29 9:06
|
||||
*/
|
||||
@Bean
|
||||
public ChatClient chatClient(DeepSeekChatModel deepSeekChatModel,
|
||||
SimpleLoggerAdvisor simpleLoggerAdvisor,
|
||||
MessageChatMemoryAdvisor messageChatMemoryAdvisor) {
|
||||
return ChatClient
|
||||
//模型
|
||||
.builder(deepSeekChatModel)
|
||||
//系统角色
|
||||
.defaultSystem("你是一个小团团")
|
||||
//环绕增强
|
||||
.defaultAdvisors(simpleLoggerAdvisor, messageChatMemoryAdvisor)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
30
src/main/java/com/ai/app/config/CorsConfig.java
Normal file
30
src/main/java/com/ai/app/config/CorsConfig.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.ai.app.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* 跨域配置
|
||||
*
|
||||
* @author shiyong
|
||||
* 2022/10/24 16:23
|
||||
*/
|
||||
@Configuration
|
||||
public class CorsConfig {
|
||||
|
||||
@Bean
|
||||
public WebMvcConfigurer corsConfigurer() {
|
||||
return new WebMvcConfigurer() {
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOrigins("*")
|
||||
.allowedHeaders("*")
|
||||
.allowedMethods("*")
|
||||
.maxAge(3600);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.ai.app.config;
|
||||
|
||||
import com.ai.app.service.AIChatMessageService;
|
||||
import org.springframework.ai.chat.memory.ChatMemoryRepository;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @describe
|
||||
* @Author kai.liu
|
||||
* @Date 2025/8/29 9:01
|
||||
*/
|
||||
@Component
|
||||
public class RedisChatMemoryRepositor implements ChatMemoryRepository {
|
||||
|
||||
@Autowired
|
||||
private AIChatMessageService aiChatMessageService;
|
||||
|
||||
@Override
|
||||
public List<String> findConversationIds() {
|
||||
return aiChatMessageService.getConversationIds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> findByConversationId(String conversationId) {
|
||||
return aiChatMessageService.getMessage(conversationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveAll(String conversationId, List<Message> messages) {
|
||||
aiChatMessageService.saveAll(conversationId, messages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByConversationId(String conversationId) {
|
||||
aiChatMessageService.delete(conversationId);
|
||||
}
|
||||
}
|
||||
40
src/main/java/com/ai/app/config/RedisConfig.java
Normal file
40
src/main/java/com/ai/app/config/RedisConfig.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package com.ai.app.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
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.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* @describe
|
||||
* @Author kai.liu
|
||||
* @Date 2025/9/1 9:33
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "redisTemplate")
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(redisConnectionFactory);
|
||||
|
||||
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
|
||||
RedisObjectSerializer redisObjectSerializer = new RedisObjectSerializer();
|
||||
|
||||
// key采用String的序列化方式
|
||||
template.setKeySerializer(stringRedisSerializer);
|
||||
|
||||
// hash的key也采用String的序列化方式
|
||||
template.setHashKeySerializer(stringRedisSerializer);
|
||||
|
||||
// value序列化方式采用字节数组
|
||||
template.setValueSerializer(redisObjectSerializer);
|
||||
|
||||
// hash的value序列化方式采用字节数组
|
||||
template.setHashValueSerializer(redisObjectSerializer);
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
49
src/main/java/com/ai/app/config/RedisObjectSerializer.java
Normal file
49
src/main/java/com/ai/app/config/RedisObjectSerializer.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package com.ai.app.config;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.serializer.support.DeserializingConverter;
|
||||
import org.springframework.core.serializer.support.SerializingConverter;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.SerializationException;
|
||||
|
||||
/**
|
||||
* 自定义Redis序列化类
|
||||
*
|
||||
* @author shiyong
|
||||
* 2020-01-03 18:01
|
||||
*/
|
||||
public class RedisObjectSerializer implements RedisSerializer<Object> {
|
||||
private Converter<Object, byte[]> serializer = new SerializingConverter();
|
||||
private Converter<byte[], Object> deserializer = new DeserializingConverter();
|
||||
static final byte[] EMPTY_ARRAY = new byte[0];
|
||||
|
||||
/**
|
||||
* Serialize the given object to binary data.
|
||||
*
|
||||
* @param o object to serialize. Can be {@literal null}.
|
||||
* @return the equivalent binary data. Can be {@literal null}.
|
||||
*/
|
||||
@Override
|
||||
public byte[] serialize(Object o) throws SerializationException {
|
||||
if (o == null) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
return serializer.convert(o);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize an object from the given binary data.
|
||||
*
|
||||
* @param bytes object binary representation. Can be {@literal null}.
|
||||
* @return the equivalent object instance. Can be {@literal null}.
|
||||
*/
|
||||
@Override
|
||||
public Object deserialize(byte[] bytes) throws SerializationException {
|
||||
if (bytes == null || bytes.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return deserializer.convert(bytes);
|
||||
}
|
||||
}
|
||||
55
src/main/java/com/ai/app/controller/ChatController.java
Normal file
55
src/main/java/com/ai/app/controller/ChatController.java
Normal file
@@ -0,0 +1,55 @@
|
||||
package com.ai.app.controller;
|
||||
|
||||
import com.ai.app.dto.MessageDTO;
|
||||
import com.ai.app.service.AIChatMessageService;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.memory.ChatMemory;
|
||||
import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @describe AI对话控制器
|
||||
* @Author kai.liu
|
||||
* @Date 2025/8/28 10:06
|
||||
*/
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/ai")
|
||||
public class ChatController {
|
||||
|
||||
@Autowired
|
||||
private ChatClient chatClient;
|
||||
|
||||
@Autowired
|
||||
private InMemoryChatMemoryRepository inMemoryChatMemoryRepository;
|
||||
|
||||
@RequestMapping(value = "/chat", produces = "text/stream;charset=UTF-8")
|
||||
public Flux<String> chat(String prompt, String chatId) {
|
||||
return chatClient
|
||||
.prompt()
|
||||
.user(prompt)
|
||||
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId))
|
||||
.stream().content();
|
||||
}
|
||||
|
||||
@RequestMapping("/history/chat")
|
||||
public List<String> history() {
|
||||
return inMemoryChatMemoryRepository.findConversationIds();
|
||||
}
|
||||
|
||||
@RequestMapping("/history/chat/{chatId}")
|
||||
public List<MessageDTO> history(@PathVariable("chatId") String chatId) {
|
||||
List<Message> messages = inMemoryChatMemoryRepository.findByConversationId(chatId);
|
||||
return messages.stream().map(e -> {
|
||||
return new MessageDTO(e.getMessageType().getValue(), e.getText());
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
23
src/main/java/com/ai/app/dto/MessageDTO.java
Normal file
23
src/main/java/com/ai/app/dto/MessageDTO.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.ai.app.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @describe
|
||||
* @Author kai.liu
|
||||
* @Date 2025/8/29 15:09
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class MessageDTO implements Serializable {
|
||||
|
||||
private String role;
|
||||
|
||||
private String content;
|
||||
}
|
||||
30
src/main/java/com/ai/app/entity/AIChatMessage.java
Normal file
30
src/main/java/com/ai/app/entity/AIChatMessage.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.ai.app.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* @describe
|
||||
* @Author kai.liu
|
||||
* @Date 2025/8/29 9:20
|
||||
*/
|
||||
@Data
|
||||
@TableName("ai_chat_message")
|
||||
public class AIChatMessage {
|
||||
|
||||
@TableId
|
||||
private String id;
|
||||
|
||||
private String conversationId;
|
||||
|
||||
private String conversationType;
|
||||
|
||||
private String messageType;
|
||||
|
||||
private String message;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
29
src/main/java/com/ai/app/mapper/AIChatMessageMapper.java
Normal file
29
src/main/java/com/ai/app/mapper/AIChatMessageMapper.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.ai.app.mapper;
|
||||
|
||||
import com.ai.app.entity.AIChatMessage;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @describe
|
||||
* @Author kai.liu
|
||||
* @Date 2025/8/29 9:18
|
||||
*/
|
||||
@Mapper
|
||||
public interface AIChatMessageMapper extends BaseMapper<AIChatMessage> {
|
||||
|
||||
@Select("SELECT DISTINCT conversation_id FROM ai_chat_message ")
|
||||
List<String> selectConversationIds();
|
||||
|
||||
@Select("SELECT * FROM ai_chat_message WHERE conversation_id = #{conversationId}")
|
||||
List<AIChatMessage> selectByConversationId(String conversationId);
|
||||
|
||||
void batchInsert(List<AIChatMessage> messageList);
|
||||
|
||||
@Select("DELETE FROM ai_chat_message WHERE conversation_id = #{conversationId}")
|
||||
void deleteByConversationId(String conversationId);
|
||||
}
|
||||
23
src/main/java/com/ai/app/service/AIChatMessageService.java
Normal file
23
src/main/java/com/ai/app/service/AIChatMessageService.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.ai.app.service;
|
||||
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @describe
|
||||
* @Author kai.liu
|
||||
* @Date 2025/8/29 9:27
|
||||
*/
|
||||
public interface AIChatMessageService {
|
||||
|
||||
List<String> getConversationIds(String type);
|
||||
|
||||
List<String> getConversationIds();
|
||||
|
||||
List<Message> getMessage(String conversationId);
|
||||
|
||||
void saveAll(String conversationId, List<Message> messages);
|
||||
|
||||
void delete(String conversationId);
|
||||
}
|
||||
192
src/main/java/com/ai/app/service/RedisTemplateService.java
Normal file
192
src/main/java/com/ai/app/service/RedisTemplateService.java
Normal file
@@ -0,0 +1,192 @@
|
||||
package com.ai.app.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @describe
|
||||
* @Author kai.liu
|
||||
* @Date 2025/9/1 9:28
|
||||
*/
|
||||
public interface RedisTemplateService {
|
||||
|
||||
/**
|
||||
* 缓存对象信息
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @author 施勇
|
||||
* 2018年12月11日 上午10:54:49
|
||||
*/
|
||||
void set(String key, Object value);
|
||||
|
||||
/**
|
||||
* 缓存信息,并设置缓存时间
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param time 时间
|
||||
* @param timeUnit 时间单位
|
||||
* @author 施勇
|
||||
* 2018年12月11日 上午10:54:27
|
||||
*/
|
||||
void set(String key, Object value, long time, TimeUnit timeUnit);
|
||||
|
||||
/**
|
||||
* 原子递增1
|
||||
*
|
||||
* @param key 键
|
||||
* @return java.lang.Long
|
||||
*/
|
||||
Long increment(String key);
|
||||
|
||||
/**
|
||||
* 原子递增
|
||||
*
|
||||
* @param key 键
|
||||
* @param delta 增量
|
||||
* @return java.lang.Long
|
||||
*/
|
||||
Long increment(String key, Long delta);
|
||||
|
||||
/**
|
||||
* 递增
|
||||
*
|
||||
* @param key 键
|
||||
* @param delta 递增因子
|
||||
* @param time 时间
|
||||
* @param timeUnit 时间单位
|
||||
* @return java.lang.Long
|
||||
* @author shiyong
|
||||
* 2022/10/18 14:58
|
||||
*/
|
||||
Long increment(String key, Long delta, long time, TimeUnit timeUnit);
|
||||
|
||||
/**
|
||||
* 从缓存中获取信息
|
||||
*
|
||||
* @param key 键
|
||||
* @return Object
|
||||
* @author 施勇
|
||||
* 2018年12月11日 上午10:54:10
|
||||
*/
|
||||
Object get(Object key);
|
||||
|
||||
/**
|
||||
* 从redis中获取key对应的过期时间;
|
||||
* 如果该值有过期时间,就返回相应的过期时间;
|
||||
* 如果该值没有设置过期时间,就返回-1;
|
||||
* 如果没有该值,就返回-2;
|
||||
*/
|
||||
Long getExpire(String key);
|
||||
|
||||
/**
|
||||
* 设置key的失效时间
|
||||
*
|
||||
* @param key 键
|
||||
* @param time 时长
|
||||
* @param timeUnit 时间单位
|
||||
* @return java.lang.Boolean
|
||||
*/
|
||||
Boolean expire(String key, long time, TimeUnit timeUnit);
|
||||
|
||||
/**
|
||||
* 是否存在key
|
||||
*
|
||||
* @param key 键
|
||||
* @return boolean
|
||||
* @author 施勇
|
||||
* 2018年12月11日 上午10:53:54
|
||||
*/
|
||||
boolean hasKey(String key);
|
||||
|
||||
/**
|
||||
* 删除KEY对应的对象
|
||||
*
|
||||
* @param key 键
|
||||
* @author 施勇
|
||||
* 2018年12月24日 下午4:14:26
|
||||
*/
|
||||
void delete(String key);
|
||||
|
||||
/**
|
||||
* 查找匹配的key
|
||||
*
|
||||
* @param pattern 匹配字符串
|
||||
* @return java.util.Set<java.lang.String>
|
||||
* @author shiyong
|
||||
* 2020/1/3 13:42
|
||||
*/
|
||||
Set<String> getKeys(String pattern);
|
||||
|
||||
/**
|
||||
* 在队尾追加元素
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @return 添加后队列的长度
|
||||
*/
|
||||
Long rightPush(String key, Object value);
|
||||
|
||||
/**
|
||||
* 从队首移除元素
|
||||
*
|
||||
* @param key 键
|
||||
* @return 移除的元素
|
||||
*/
|
||||
Object leftPop(String key);
|
||||
|
||||
/**
|
||||
* 从队首开始取值,获取指定区间
|
||||
*
|
||||
* @param key 键
|
||||
* @param start 开始下标
|
||||
* @param stop 结束下标
|
||||
* @return 区间内的全部值
|
||||
*/
|
||||
List<?> leftRange(String key, long start, long stop);
|
||||
|
||||
/**
|
||||
* 将值加到集合中
|
||||
*
|
||||
* @param key 键
|
||||
* @param values 值
|
||||
* @return 添加是否成功(1:成功,0:失败)
|
||||
*/
|
||||
Long addToSet(String key, Object... values);
|
||||
|
||||
/**
|
||||
* 集合的数量
|
||||
*
|
||||
* @param key 键
|
||||
* @return 集合数量
|
||||
*/
|
||||
Long sizeOfSet(String key);
|
||||
|
||||
/**
|
||||
* 集合列表
|
||||
*
|
||||
* @param key 键
|
||||
* @return 集合列表
|
||||
*/
|
||||
Set<Object> getOfSet(String key);
|
||||
|
||||
/**
|
||||
* 集合中是否有该值
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @return Boolean
|
||||
*/
|
||||
Boolean hasOfSet(String key, Object value);
|
||||
|
||||
/**
|
||||
* 删除集合中值
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @return 删除是否成功(1:成功,0:失败)
|
||||
*/
|
||||
Long removeOfSet(String key, Object value);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.ai.app.service.impl;
|
||||
|
||||
import com.ai.app.service.AIChatMessageService;
|
||||
import com.ai.app.service.RedisTemplateService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @describe
|
||||
* @Author kai.liu
|
||||
* @Date 2025/8/29 9:27
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AIChatMessageServiceImpl implements AIChatMessageService {
|
||||
|
||||
@Resource
|
||||
private RedisTemplateService redisTemplateService;
|
||||
|
||||
public static final String AI_MESSAGE_CONVERSATION_ID = "ai:message:conversationId:";
|
||||
public static final String AI_MESSAGE_CONVERSATION_TYPE = "ai:message:conversationType:";
|
||||
|
||||
@Override
|
||||
public List<String> getConversationIds(String type) {
|
||||
String conversationTypeKey = getConversationTypeKey(type);
|
||||
if (redisTemplateService.hasKey(conversationTypeKey)) {
|
||||
return (List<String>) redisTemplateService.get(conversationTypeKey);
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getConversationIds() {
|
||||
Set<String> keys = redisTemplateService.getKeys(AI_MESSAGE_CONVERSATION_ID);
|
||||
return new ArrayList<>(keys);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> getMessage(String conversationId) {
|
||||
String conversationIdKey = getConversationIdKey(conversationId);
|
||||
if (redisTemplateService.hasKey(conversationIdKey)) {
|
||||
return (List<Message>) redisTemplateService.get(conversationIdKey);
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveAll(String conversationId, List<Message> messages) {
|
||||
String conversationIdKey = getConversationIdKey(conversationId);
|
||||
redisTemplateService.set(conversationIdKey, messages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String conversationId) {
|
||||
String conversationIdKey = getConversationIdKey(conversationId);
|
||||
redisTemplateService.delete(conversationIdKey);
|
||||
}
|
||||
|
||||
private String getConversationIdKey(String conversationId) {
|
||||
return AI_MESSAGE_CONVERSATION_ID + conversationId;
|
||||
}
|
||||
|
||||
private String getConversationTypeKey(String type) {
|
||||
return AI_MESSAGE_CONVERSATION_TYPE + type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package com.ai.app.service.impl;
|
||||
|
||||
import com.ai.app.service.RedisTemplateService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @describe
|
||||
* @Author kai.liu
|
||||
* @Date 2025/9/1 9:28
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class RedisTemplateServiceImpl implements RedisTemplateService {
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String,Object> redisTemplate;
|
||||
|
||||
/**
|
||||
* 缓存对象信息
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @author 施勇
|
||||
* 2018年12月11日 上午10:54:49
|
||||
*/
|
||||
public void set(String key, Object value) {
|
||||
redisTemplate.opsForValue().set(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存信息,并设置缓存时间
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param time 时间
|
||||
* @param timeUnit 时间单位
|
||||
* @author 施勇
|
||||
* 2018年12月11日 上午10:54:27
|
||||
*/
|
||||
public void set(String key, Object value, long time, TimeUnit timeUnit){
|
||||
if(time>0){
|
||||
redisTemplate.opsForValue().set(key, value, time, timeUnit);
|
||||
}else{
|
||||
set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递增1
|
||||
*
|
||||
* @param key 键
|
||||
* @return java.lang.Long
|
||||
*/
|
||||
public Long increment(String key) {
|
||||
return redisTemplate.opsForValue().increment(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递增
|
||||
*
|
||||
* @param key 键
|
||||
* @param delta 增量
|
||||
* @return java.lang.Long
|
||||
*/
|
||||
public Long increment(String key, Long delta) {
|
||||
if (delta == null || delta < 1) {
|
||||
delta = 1L;
|
||||
}
|
||||
return redisTemplate.opsForValue().increment(key, delta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递增
|
||||
*
|
||||
* @param key 键
|
||||
* @param delta 递增因子
|
||||
* @param time 时间
|
||||
* @param timeUnit 时间单位
|
||||
* @return java.lang.Long
|
||||
* @author shiyong
|
||||
* 2022/10/18 14:58
|
||||
*/
|
||||
public Long increment(String key, Long delta, long time, TimeUnit timeUnit) {
|
||||
if (null == delta || delta < 1) {
|
||||
delta = 1L;
|
||||
}
|
||||
Long increment = redisTemplate.opsForValue().increment(key, delta);
|
||||
redisTemplate.expire(key, time, timeUnit);
|
||||
return increment;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存中获取信息
|
||||
* @param key 键
|
||||
* @return Object
|
||||
* @author 施勇
|
||||
* 2018年12月11日 上午10:54:10
|
||||
*/
|
||||
public Object get(Object key) {
|
||||
return key==null?"":redisTemplate.opsForValue().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从redis中获取key对应的过期时间;
|
||||
* 如果该值有过期时间,就返回相应的过期时间;
|
||||
* 如果该值没有设置过期时间,就返回-1;
|
||||
* 如果没有该值,就返回-2;
|
||||
*/
|
||||
public Long getExpire(String key) {
|
||||
return redisTemplate.opsForValue().getOperations().getExpire(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置key的失效时间
|
||||
*
|
||||
* @param key 键
|
||||
* @param time 时长
|
||||
* @param timeUnit 时间单位
|
||||
* @return java.lang.Boolean
|
||||
*/
|
||||
public Boolean expire(String key, long time, TimeUnit timeUnit) {
|
||||
return redisTemplate.expire(key, time, timeUnit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否存在key
|
||||
* @param key 键
|
||||
* @return boolean
|
||||
* @author 施勇
|
||||
* 2018年12月11日 上午10:53:54
|
||||
*/
|
||||
public boolean hasKey(String key) {
|
||||
if (null == key || "".equals(key)) {
|
||||
return false;
|
||||
}
|
||||
Boolean result = redisTemplate.hasKey(key);
|
||||
if (null == result) {
|
||||
return false;
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除KEY对应的对象
|
||||
* @param key 键
|
||||
* @author 施勇
|
||||
* 2018年12月24日 下午4:14:26
|
||||
*/
|
||||
public void delete(String key){
|
||||
redisTemplate.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找匹配的key
|
||||
* @param pattern 匹配字符串
|
||||
* @return java.util.Set<java.lang.String>
|
||||
* @author shiyong
|
||||
* 2020/1/3 13:42
|
||||
*/
|
||||
public Set<String> getKeys(String pattern) {
|
||||
return redisTemplate.keys(pattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在队尾追加元素
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @return 添加后队列的长度
|
||||
*/
|
||||
public Long rightPush(String key, Object value) {
|
||||
return redisTemplate.opsForList().rightPush(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从队首移除元素
|
||||
*
|
||||
* @param key 键
|
||||
* @return 移除的元素
|
||||
*/
|
||||
public Object leftPop(String key) {
|
||||
return redisTemplate.opsForList().leftPop(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从队首开始取值,获取指定区间
|
||||
*
|
||||
* @param key 键
|
||||
* @param start 开始下标
|
||||
* @param stop 结束下标
|
||||
* @return 区间内的全部值
|
||||
*/
|
||||
public List<?> leftRange(String key, long start, long stop) {
|
||||
return redisTemplate.opsForList().range(key, start, stop);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将值加到集合中
|
||||
*
|
||||
* @param key 键
|
||||
* @param values 值
|
||||
* @return 添加是否成功(1:成功,0:失败)
|
||||
*/
|
||||
public Long addToSet(String key, Object... values) {
|
||||
return redisTemplate.opsForSet().add(key, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 集合的数量
|
||||
*
|
||||
* @param key 键
|
||||
* @return 集合数量
|
||||
*/
|
||||
public Long sizeOfSet(String key) {
|
||||
return redisTemplate.opsForSet().size(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 集合列表
|
||||
*
|
||||
* @param key 键
|
||||
* @return 集合列表
|
||||
*/
|
||||
public Set<Object> getOfSet(String key) {
|
||||
return redisTemplate.opsForSet().members(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 集合中是否有该值
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @return Boolean
|
||||
*/
|
||||
public Boolean hasOfSet(String key,Object value) {
|
||||
return redisTemplate.opsForSet().isMember(key,value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除集合中值
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @return 删除是否成功(1:成功,0:失败)
|
||||
*/
|
||||
public Long removeOfSet(String key,Object value) {
|
||||
return redisTemplate.opsForSet().remove(key,value);
|
||||
}
|
||||
|
||||
}
|
||||
0
src/main/resources/application-dev.yml
Normal file
0
src/main/resources/application-dev.yml
Normal file
95
src/main/resources/application.yml
Normal file
95
src/main/resources/application.yml
Normal file
@@ -0,0 +1,95 @@
|
||||
server:
|
||||
port: 8080
|
||||
# servlet:
|
||||
# context-path: /ai
|
||||
tomcat:
|
||||
# 等待队列长度 (所有线程忙碌时,排队等待的连接数, 默认: 100)
|
||||
accept-count: 1000
|
||||
# 最大连接数 (默认: 8192)
|
||||
max-connections: 20000
|
||||
# 连接超时 (接受连接后等待请求数据的最大时间, 默认值通常与系统有关)
|
||||
connection-timeout: 30000
|
||||
threads:
|
||||
# 最大工作线程数 (默认: 200)
|
||||
max: 2000
|
||||
# 最小工作线程数(初始启动的线程数, 默认: 10)
|
||||
min-spare: 200
|
||||
# 限制表单提交(application/x-www-form-urlencoded)的POST请求最大大小 (默认: 2MB)
|
||||
# 这是 Boot 2 中 'server.tomcat.max-http-post-size' 的替代品
|
||||
max-http-form-post-size: 100MB
|
||||
# 限制整个请求体(包括文件上传)的最大大小。超过此限制的请求将被拒绝 (默认: 2MB)
|
||||
max-swallow-size: 100MB
|
||||
# 定义响应的字符编码 (默认: UTF-8)
|
||||
uri-encoding: UTF-8
|
||||
compression:
|
||||
enabled: true
|
||||
http2:
|
||||
enabled: true
|
||||
spring:
|
||||
application:
|
||||
name: spring-ai-app
|
||||
# 指定运行环境
|
||||
profiles:
|
||||
# 指定默认的运行环境为dev,运行时通过--spring.profiles.active参数切换运行环境
|
||||
active: dev
|
||||
main:
|
||||
allow-bean-definition-overriding: true
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 100MB
|
||||
max-request-size: 100MB
|
||||
ai:
|
||||
deepseek:
|
||||
api-key: sk-f970111b34c84a8c812919fb4fad8b65
|
||||
chat:
|
||||
options:
|
||||
model: deepseek-reasoner
|
||||
data:
|
||||
redis:
|
||||
host: 180.76.119.46
|
||||
port: 6379
|
||||
password: qwer@1234
|
||||
database: 0
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
max-wait: 3000
|
||||
logging:
|
||||
level:
|
||||
org.springframework.ai.chat.client.advisor: debug
|
||||
com.ai.app: debug
|
||||
|
||||
spring.datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://180.76.119.46:3306/spring_ai_app?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&failOverReadOnly=false&useSSL=false&allowMultiQueries=true
|
||||
username: lk
|
||||
password: Qwer@1234
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
# Hikari 连接池配置
|
||||
hikari:
|
||||
# 最小空闲连接数量
|
||||
minimum-idle: 10
|
||||
# 空闲连接存活最大时间,默认600000(10分钟)
|
||||
idle-timeout: 180000
|
||||
# 连接池最大连接数,默认是10
|
||||
maximum-pool-size: 100
|
||||
# 此属性控制从池返回的连接的默认自动提交行为,默认值:true
|
||||
auto-commit: true
|
||||
# 连接池名称
|
||||
pool-name: HikariCP
|
||||
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
|
||||
max-lifetime: 1800000
|
||||
# 数据库连接超时时间,默认30秒,即30000
|
||||
connection-timeout: 30000
|
||||
connection-test-query: SELECT 1
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath:mapper/**/*.xml
|
||||
type-aliases-package: com.ai.app.entity
|
||||
configuration:
|
||||
# 日志打印
|
||||
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
|
||||
# 开启驼峰命名转换,如:Table(create_time) -> Entity(createTime)。
|
||||
# 不需要我们关心怎么进行字段匹配,mybatis会自动识别`大写字母与下划线`
|
||||
map-underscore-to-camel-case: true
|
||||
13
src/main/resources/mapper/AIChatMessageMapper.xml
Normal file
13
src/main/resources/mapper/AIChatMessageMapper.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?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.ai.app.mapper.AIChatMessageMapper">
|
||||
<insert id="batchInsert" parameterType="com.ai.app.entity.AIChatMessage">
|
||||
INSERT INTO ai_chat_message (id,conversation_id,conversation_type,message_type,message,create_time)
|
||||
VALUES
|
||||
<foreach collection="messageList" item="message" separator=",">
|
||||
(#{message.id}, #{message.conversationId}, #{message.conversationType}, #{message.messageType},#{message.message}, #{message.createTime})
|
||||
</foreach>
|
||||
</insert>
|
||||
</mapper>
|
||||
19
src/test/java/com/ai/app/SpringAiAppApplicationTests.java
Normal file
19
src/test/java/com/ai/app/SpringAiAppApplicationTests.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.ai.app;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.ParseException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@SpringBootTest
|
||||
class SpringAiAppApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads(){
|
||||
System.out.println(new BigDecimal("-0.25"));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user