SpringAI实现智能客服-java17+springboot3+阿里千问大模型
功能:
1.智能对话
2.预设角色
3.对话记忆
4.日志
5.function-call
<?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.3.4</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo</name> <description>Demo project for Spring Boot 3 with MyBatis Plus</description> <properties> <java.version>17</java.version> <spring-ai.version>1.0.0-M2</spring-ai.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Boot Validation Starter (包含Jakarta Validation) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- MyBatis Plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.7</version> </dependency> <!-- 通义千问SDK --> <dependency> <groupId>com.alibaba</groupId> <artifactId>dashscope-sdk-java</artifactId> <version>2.16.7</version> </dependency> <!-- Spring AI Core --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-core</artifactId> <version>${spring-ai.version}</version> </dependency> <!-- Reactor Core (for Flux support) --> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-core</artifactId> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> </dependencies> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> <releases> <enabled>false</enabled> </releases> </repository> </repositories> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.example.demo.SpringAiDemoApplication</mainClass> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
server: port: 8083 spring: application: name: demo # MySQL数据库配置 datasource: url: jdbc:mysql://172.38.40.146:3306/ehl_security?characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: Ehl@123~ driver-class-name: com.mysql.cj.jdbc.Driver # 数据库初始化配置 sql: init: mode: always schema-locations: classpath:data.sql data-locations: classpath:data.sql encoding: utf-8 # 通义千问配置 qianwen: api-key: sk-xxxxxxxx model: qwen-plus # MyBatis Plus配置 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath*:/mapper/**/*.xml # 日志配置 logging: level: com.example.demo: INFO # 聊天日志拦截器的详细日志 com.example.demo.config.ChatLoggingAdvisor: INFO org.springframework: INFO
关于通义千问模型注册:大模型服务平台百炼控制台
-- 创建用户表 CREATE TABLE IF NOT EXISTS users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(100) NOT NULL, nickname VARCHAR(50), phone VARCHAR(11), status VARCHAR(20) DEFAULT 'ACTIVE', create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- 插入测试数据 INSERT IGNORE INTO users (username, email, nickname, phone, status, create_time, update_time) VALUES ('admin', 'admin@example.com', '管理员', '13800138000', 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('user1', 'user1@example.com', '张三', '13900139001', 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('user2', 'user2@example.com', '李四', '13700137002', 'DORMANT', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('user3', 'user3@example.com', '王五', '13600136003', 'CANCELLED', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
package com.example.demo; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.InMemoryChatMemory; /** * @author hz * @version 1.0.0 * @description SpringBoot + MybatisPlus + 通义千问 Demo Application */ @SpringBootApplication @MapperScan("com.example.demo.mapper") public class SpringAiDemoApplication { public static void main(String[] args) { SpringApplication.run(SpringAiDemoApplication.class, args); } @Bean public ChatMemory chatMemory() { return new InMemoryChatMemory(); } }
package com.example.demo.config; import com.example.demo.dto.ChangeUserStatusRequest; import com.example.demo.dto.QueryUserRequest; import org.springframework.ai.chat.client.ChatClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.function.Function; /** * ChatClient配置类 */ @Configuration public class ChatClientConfig { /** * 创建ChatClient,集成日志拦截器和Function调用 */ @Bean public ChatClient chatClient(QianWenChatModel qianWenChatModel, ChatLoggingAdvisor chatLoggingAdvisor, Function<ChangeUserStatusRequest, String> changeUserStatus, Function<QueryUserRequest, String> queryUserInfo) { return ChatClient.builder(qianWenChatModel) .defaultAdvisors(chatLoggingAdvisor) .defaultFunctions("queryUserInfo", "changeUserStatus") .build(); } }
package com.example.demo.config; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.AdvisedRequest; import org.springframework.ai.chat.client.RequestResponseAdvisor; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Map; /** * 聊天日志拦截器 * 用于记录聊天请求和响应的详细信息 */ @Slf4j @Component public class ChatLoggingAdvisor implements RequestResponseAdvisor { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @Override public AdvisedRequest adviseRequest(AdvisedRequest request, Map<String, Object> context) { String timestamp = LocalDateTime.now().format(FORMATTER); String sessionId = extractSessionId(context); log.info("=== 聊天请求开始 [{}] ===", timestamp); log.info("会话ID: {}", sessionId); log.info("请求上下文: {}", context); // 记录消息内容 if (request.messages() != null && !request.messages().isEmpty()) { log.info("消息数量: {}", request.messages().size()); for (int i = 0; i < request.messages().size(); i++) { Message message = request.messages().get(i); log.info("消息[{}] - 类型: {}, 内容: {}", i + 1, message.getMessageType(), truncateMessage(message.getContent()) ); } } // 记录系统提示词 if (request.systemText() != null && !request.systemText().trim().isEmpty()) { log.info("系统提示词: {}", truncateMessage(request.systemText())); } // 记录函数配置信息 log.info("函数配置检查: 请查看ChatClient配置是否正确"); log.info("=== 聊天请求结束 ==="); return RequestResponseAdvisor.super.adviseRequest(request, context); } @Override public ChatResponse adviseResponse(ChatResponse response, Map<String, Object> context) { String timestamp = LocalDateTime.now().format(FORMATTER); String sessionId = extractSessionId(context); log.info("=== 聊天响应开始 [{}] ===", timestamp); log.info("会话ID: {}", sessionId); if (response != null) { log.info("响应结果数量: {}", response.getResults().size()); // 记录每个响应结果 for (int i = 0; i < response.getResults().size(); i++) { var result = response.getResults().get(i); if (result.getOutput() != null) { log.info("响应[{}] - 内容: {}", i + 1, truncateMessage(result.getOutput().getContent()) ); } } // 记录元数据 if (response.getMetadata() != null) { log.info("响应元数据: {}", response.getMetadata()); } } else { log.warn("响应为空"); } log.info("=== 聊天响应结束 ==="); return RequestResponseAdvisor.super.adviseResponse(response, context); } /** * 从上下文中提取会话ID */ private String extractSessionId(Map<String, Object> context) { if (context == null) { return "unknown"; } // 尝试从不同的key中获取sessionId Object sessionId = context.get("sessionId"); if (sessionId == null) { sessionId = context.get("chat_memory_conversation_id"); } if (sessionId == null) { sessionId = context.get("conversationId"); } return sessionId != null ? sessionId.toString() : "unknown"; } /** * 截断长消息以避免日志过长 */ private String truncateMessage(String message) { if (message == null) { return "null"; } int maxLength = 200; // 最大显示长度 if (message.length() <= maxLength) { return message; } return message.substring(0, maxLength) + "... (总长度: " + message.length() + " 字符)"; } @Override public String getName() { return "ChatLoggingAdvisor"; } }
package com.example.demo.config; import com.example.demo.dto.ChangeUserStatusRequest; import com.example.demo.dto.QueryUserRequest; import com.example.demo.entity.User; import com.example.demo.service.UserService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Description; import java.util.function.Function; /** * Function配置类 * 为AI提供可调用的函数 */ @Slf4j @Configuration @RequiredArgsConstructor public class FunctionConfig { private final UserService userService; /** * 修改用户状态的Function * AI可以通过聊天调用此函数来修改用户状态 */ @Bean @Description("修改用户状态 - 用户确认修改状态时调用") public Function<ChangeUserStatusRequest, String> changeUserStatus() { return request -> { try { log.info("AI正在调用修改用户状态功能: 姓名={}, 手机号={}, 新状态={}", request.getNickname(), request.getPhone(), request.getNewStatus()); // 调用用户服务修改状态 boolean success = userService.changeUserStatus(request); if (success) { String result = String.format("✅ 用户状态修改成功!\n" + "👤 用户姓名: %s\n" + "📱 手机号码: %s\n" + "🔄 新状态: %s\n" + "💬 修改原因: %s", request.getNickname(), request.getPhone(), getStatusDescription(request.getNewStatus().name()), request.getReason() != null ? request.getReason() : "无"); log.info("用户状态修改成功: {}", result); return result; } else { String result = "❌ 用户状态修改失败,请检查用户信息是否正确"; log.error("用户状态修改失败"); return result; } } catch (IllegalArgumentException e) { String result = "❌ 修改失败: " + e.getMessage(); log.warn("用户状态修改失败: {}", e.getMessage()); return result; } catch (Exception e) { String result = "❌ 系统异常,用户状态修改失败: " + e.getMessage(); log.error("用户状态修改异常: {}", e.getMessage(), e); return result; } }; } /** * 查询用户信息的Function * AI可以通过聊天调用此函数来查询用户当前信息 */ @Bean @Description("查询用户信息 - 用户提供姓名和手机号时调用") public Function<QueryUserRequest, String> queryUserInfo() { return request -> { try { log.info("AI正在调用查询用户信息功能: 姓名={}, 手机号={}", request.getNickname(), request.getPhone()); // 调用用户服务验证用户 User user = userService.validateUser(request.getNickname(), request.getPhone()); if (user != null) { String result = String.format("📋 用户信息确认\n\n" + "👤 用户姓名: %s\n" + "📱 手机号码: %s\n" + "📧 邮箱地址: %s\n" + "👨💼 用户名: %s\n" + "🔄 当前状态: %s\n" + "📅 创建时间: %s\n\n" + "✅ 用户信息验证成功!现在您可以告诉我要修改的状态:\n" + "• 输入\"启用\"或\"激活\"来启用账户\n" + "• 输入\"休眠\"或\"暂停\"来休眠账户\n" + "• 输入\"注销\"或\"关闭\"来注销账户", user.getNickname(), user.getPhone(), user.getEmail(), user.getUsername(), getStatusDescription(user.getStatus().name()), user.getCreateTime() != null ? user.getCreateTime().toString().substring(0, 19).replace("T", " ") : "未知"); log.info("用户信息查询成功: 姓名={}, 状态={}", user.getNickname(), user.getStatus()); return result; } else { String result = "❌ 用户信息验证失败\n\n" + "请检查以下信息是否正确:\n" + "👤 姓名: " + request.getNickname() + "\n" + "📱 手机号: " + request.getPhone() + "\n\n" + "如果信息有误,请重新提供正确的姓名和手机号。"; log.warn("用户信息验证失败: 姓名={}, 手机号={}", request.getNickname(), request.getPhone()); return result; } } catch (Exception e) { String result = "❌ 系统异常,查询用户信息失败: " + e.getMessage(); log.error("查询用户信息异常: {}", e.getMessage(), e); return result; } }; } /** * 获取状态描述 */ private String getStatusDescription(String statusCode) { return switch (statusCode) { case "ACTIVE" -> "启用"; case "DORMANT" -> "休眠"; case "CANCELLED" -> "注销"; default -> statusCode; }; } }
package com.example.demo.config; import com.alibaba.dashscope.aigc.generation.GenerationParam; import com.alibaba.dashscope.aigc.generation.GenerationResult; import com.alibaba.dashscope.common.Message; import com.alibaba.dashscope.common.Role; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; /** * 通义千问ChatModel实现 * * <p>这个类实现了Spring AI的ChatModel接口,用于集成阿里巴巴的通义千问大语言模型。 * 主要功能包括: * <ul> * <li>同步聊天调用 - 一次性返回完整响应</li> * <li>流式聊天调用 - 逐字符流式返回响应</li> * <li>消息格式转换 - Spring AI格式与通义千问SDK格式之间的转换</li> * </ul> * * <p>支持的消息类型: * <ul> * <li>SystemMessage - 系统提示消息</li> * <li>UserMessage - 用户输入消息</li> * <li>AssistantMessage - AI回复消息</li> * </ul> * * @author SpringAI Demo * @version 1.0.0 * @see ChatModel Spring AI聊天模型接口 * @see GenerationParam 通义千问生成参数 */ @Slf4j @Component public class QianWenChatModel implements ChatModel { /** * 通义千问API密钥 * 优先从配置文件 qianwen.api-key 读取,如果没有则从环境变量 DASHSCOPE_API_KEY 读取 */ @Value("${qianwen.api-key:${DASHSCOPE_API_KEY:}}") private String apiKey; /** * 通义千问模型名称 * 默认使用 qwen-plus 模型,可通过配置文件 qianwen.model 自定义 * * 可选模型: * - qwen-plus: 通用模型,平衡性能和成本 * - qwen-turbo: 快速响应模型 * - qwen-max: 最强性能模型 */ @Value("${qianwen.model:qwen-plus}") private String model; /** * 通义千问SDK的生成器实例 * 用于调用通义千问API进行文本生成 */ private final com.alibaba.dashscope.aigc.generation.Generation generation = new com.alibaba.dashscope.aigc.generation.Generation(); /** * 同步调用通义千问API * * <p>该方法会一次性返回完整的AI响应,适用于需要等待完整回答的场景。 * * @param prompt Spring AI的提示对象,包含用户消息、系统消息等 * @return ChatResponse 包含AI回复的响应对象 * @throws RuntimeException 当API调用失败或返回空响应时抛出 */ @Override public ChatResponse call(Prompt prompt) { try { log.info("QianWenChatModel - 调用同步接口"); // 转换Spring AI消息格式为通义千问SDK格式 List<Message> messages = convertToQianWenMessages(prompt.getInstructions()); // 构建通义千问API请求参数 GenerationParam param = GenerationParam.builder() .apiKey(apiKey) // API密钥 .model(model) // 模型名称 .messages(messages) // 消息列表 .resultFormat(GenerationParam.ResultFormat.MESSAGE) // 返回格式:消息格式 .incrementalOutput(false) // 非增量输出(一次性返回完整结果) .build(); // 调用通义千问API GenerationResult result = generation.call(param); // 验证响应结果并提取内容 if (result != null && result.getOutput() != null && result.getOutput().getChoices() != null && !result.getOutput().getChoices().isEmpty()) { // 获取第一个选择的消息内容 String content = result.getOutput().getChoices().get(0).getMessage().getContent(); // 转换为Spring AI格式的响应 Generation springGeneration = new Generation(new AssistantMessage(content)); return new ChatResponse(List.of(springGeneration)); } throw new RuntimeException("通义千问API返回空响应"); } catch (Exception e) { log.error("QianWenChatModel调用失败: {}", e.getMessage(), e); throw new RuntimeException("AI服务调用失败: " + e.getMessage(), e); } } /** * 流式调用通义千问API * * <p>该方法返回一个响应式流,AI的回答会逐步返回,适用于需要实时显示回答过程的场景。 * 每个流元素包含一个增量的文本片段,前端可以逐字符显示,提供更好的用户体验。 * * @param prompt Spring AI的提示对象,包含用户消息、系统消息等 * @return Flux<ChatResponse> 响应式流,包含逐步返回的AI回复片段 * @throws RuntimeException 当API调用失败时通过Flux.error返回 */ @Override public Flux<ChatResponse> stream(Prompt prompt) { try { log.info("QianWenChatModel - 调用流式接口"); // 转换Spring AI消息格式为通义千问SDK格式 List<Message> messages = convertToQianWenMessages(prompt.getInstructions()); // 构建通义千问API请求参数(流式模式) GenerationParam param = GenerationParam.builder() .apiKey(apiKey) // API密钥 .model(model) // 模型名称 .messages(messages) // 消息列表 .resultFormat(GenerationParam.ResultFormat.MESSAGE) // 返回格式:消息格式 .incrementalOutput(true) // 增量输出(流式返回) .build(); // 调用通义千问流式API并转换为Spring AI格式 return Flux.from(generation.streamCall(param)) .map(result -> { // 验证流式响应结果并提取内容片段 if (result != null && result.getOutput() != null && result.getOutput().getChoices() != null && !result.getOutput().getChoices().isEmpty()) { // 获取当前流式片段的内容 String content = result.getOutput().getChoices().get(0).getMessage().getContent(); Generation springGeneration = new Generation(new AssistantMessage(content)); return new ChatResponse(List.of(springGeneration)); } // 返回空响应(会被下面的filter过滤掉) return new ChatResponse(List.of()); }) // 过滤掉空响应,只保留有内容的响应 .filter(response -> !response.getResults().isEmpty()); } catch (Exception e) { log.error("QianWenChatModel流式调用失败: {}", e.getMessage(), e); return Flux.error(new RuntimeException("AI流式服务调用失败: " + e.getMessage(), e)); } } /** * 获取默认聊天选项 * * <p>该方法返回默认的聊天配置选项。当前实现返回null,表示使用Spring AI的默认配置。 * 如果需要自定义配置(如温度、最大token数等),可以在此方法中返回相应的ChatOptions对象。 * * @return ChatOptions 默认聊天选项,当前返回null使用系统默认配置 */ @Override public org.springframework.ai.chat.prompt.ChatOptions getDefaultOptions() { // 返回null表示使用Spring AI的默认配置 // 如果需要自定义配置,可以返回具体的ChatOptions实现 return null; } /** * 转换Spring AI消息格式为通义千问SDK格式 * * <p>该方法负责将Spring AI标准的消息格式转换为通义千问SDK所需的格式。 * 支持的消息类型映射关系: * <ul> * <li>SystemMessage → SYSTEM角色 - 用于设置AI的行为和上下文</li> * <li>UserMessage → USER角色 - 用户的输入消息</li> * <li>AssistantMessage → ASSISTANT角色 - AI的历史回复</li> * </ul> * * <p>不支持的消息类型会被自动跳过,不会影响转换过程。 * * @param springAiMessages Spring AI格式的消息列表 * @return List<Message> 转换后的通义千问SDK格式消息列表 */ private List<Message> convertToQianWenMessages(List<org.springframework.ai.chat.messages.Message> springAiMessages) { List<Message> qianwenMessages = new ArrayList<>(); for (org.springframework.ai.chat.messages.Message springAiMessage : springAiMessages) { String role; // 根据Spring AI消息类型确定通义千问角色 if (springAiMessage instanceof SystemMessage) { role = Role.SYSTEM.getValue(); // 系统消息:设置AI行为 } else if (springAiMessage instanceof UserMessage) { role = Role.USER.getValue(); // 用户消息:用户输入 } else if (springAiMessage instanceof AssistantMessage) { role = Role.ASSISTANT.getValue(); // 助手消息:AI历史回复 } else { // 跳过不支持的消息类型(如FunctionMessage等) log.debug("跳过不支持的消息类型: {}", springAiMessage.getClass().getSimpleName()); continue; } // 构建通义千问格式的消息对象 qianwenMessages.add(Message.builder() .role(role) // 设置角色 .content(springAiMessage.getContent()) // 设置消息内容 .build()); } log.debug("消息转换完成,原始消息数: {}, 转换后消息数: {}", springAiMessages.size(), qianwenMessages.size()); return qianwenMessages; } }
package com.example.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; /** * 跨域配置 */ @Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); // 允许的源 config.addAllowedOriginPattern("*"); // 允许的请求头 config.addAllowedHeader("*"); // 允许的请求方法 config.addAllowedMethod("*"); // 允许发送Cookie config.setAllowCredentials(true); // 预检请求的缓存时间(秒) config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }
package com.example.demo.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * MyBatis Plus 配置类 */ @Configuration public class MybatisPlusConfig { /** * 分页插件配置 */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }
package com.example.demo.controller; import com.example.demo.service.impl.ChatClientServiceImpl; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import org.springframework.http.MediaType; import reactor.core.publisher.Flux; import java.util.Map; /** * AI聊天控制器 */ @Slf4j @RestController @RequestMapping("/api/chat") @RequiredArgsConstructor public class ChatController { private final ChatClientServiceImpl chatClientService; /** * 发送消息给AI - 流式响应(带记忆) */ @PostMapping(value = "/send", produces = MediaType.TEXT_PLAIN_VALUE) public Flux<String> sendMessage(@RequestBody Map<String, String> request) { String message = request.get("message"); String sessionId = request.get("sessionId"); // 如果没有sessionId,生成一个默认的 if (sessionId == null || sessionId.trim().isEmpty()) { sessionId = "default-session"; } log.info("收到用户消息(流式): {}, 会话: {}", message, sessionId); try { return chatClientService.chatStreamWithMemory(sessionId, message); } catch (Exception e) { log.error("AI流式调用失败: {}", e.getMessage()); return Flux.error(e); } } /** * 清空对话记忆 */ @PostMapping("/clear-memory") public Map<String, Object> clearMemory(@RequestBody Map<String, String> request) { String sessionId = request.get("sessionId"); if (sessionId == null || sessionId.trim().isEmpty()) { sessionId = "default-session"; } log.info("清空会话记忆: {}", sessionId); try { chatClientService.clearMemory(sessionId); return Map.of( "success", true, "message", "对话记忆已清空,我们可以开始全新的对话!", "timestamp", System.currentTimeMillis(), "sessionId", sessionId ); } catch (Exception e) { log.error("清空记忆失败: {}", e.getMessage(), e); return Map.of( "success", false, "message", "清空记忆失败: " + e.getMessage(), "timestamp", System.currentTimeMillis(), "sessionId", sessionId ); } } }
package com.example.demo.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.demo.dto.ChangeUserStatusRequest; import com.example.demo.entity.User; import com.example.demo.enums.UserStatus; import com.example.demo.service.UserService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import jakarta.validation.Valid; import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 用户控制器 */ @Slf4j @RestController @RequestMapping("/api/users") @RequiredArgsConstructor public class UserController { private final UserService userService; /** * 获取所有用户 */ @GetMapping public List<User> getAllUsers() { log.info("请求获取所有用户"); List<User> users = userService.list(); log.info("获取到 {} 个用户", users.size()); return users; } /** * 根据ID获取用户 */ @GetMapping("/{id}") public User getUserById(@PathVariable Long id) { return userService.getById(id); } /** * 创建用户 */ @PostMapping public boolean createUser(@RequestBody User user) { user.setCreateTime(LocalDateTime.now()); user.setUpdateTime(LocalDateTime.now()); return userService.save(user); } /** * 更新用户 */ @PutMapping("/{id}") public boolean updateUser(@PathVariable Long id, @RequestBody User user) { user.setId(id); user.setUpdateTime(LocalDateTime.now()); return userService.updateById(user); } /** * 删除用户 */ @DeleteMapping("/{id}") public boolean deleteUser(@PathVariable Long id) { return userService.removeById(id); } /** * 分页查询用户 */ @GetMapping("/page") public Page<User> getUsersWithPage( @RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "10") Integer size) { log.info("分页查询用户,当前页: {}, 每页数量: {}", current, size); Page<User> page = userService.page(new Page<>(current, size)); log.info("查询结果,总记录数: {}, 当前页数据数: {}", page.getTotal(), page.getRecords().size()); return page; } /** * 根据用户名查询用户 */ @GetMapping("/search") public List<User> searchUsersByUsername(@RequestParam String username) { log.info("搜索用户: {}", username); LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.like(User::getUsername, username); return userService.list(queryWrapper); } /** * 通过用户姓名和手机号验证,修改用户状态 * * @param request 修改状态请求对象 * @return ResponseEntity 包含操作结果的响应 */ @PostMapping("/change-status") public ResponseEntity<Map<String, Object>> changeUserStatus(@Valid @RequestBody ChangeUserStatusRequest request) { log.info("收到修改用户状态请求: 姓名={}, 手机号={}, 新状态={}, 原因={}", request.getNickname(), request.getPhone(), request.getNewStatus(), request.getReason()); Map<String, Object> response = new HashMap<>(); try { // 调用服务层方法修改用户状态 boolean success = userService.changeUserStatus(request); if (success) { response.put("success", true); response.put("message", "用户状态修改成功"); response.put("data", Map.of( "nickname", request.getNickname(), "phone", request.getPhone(), "newStatus", request.getNewStatus(), "reason", request.getReason() != null ? request.getReason() : "" )); log.info("用户状态修改成功: 姓名={}, 手机号={}", request.getNickname(), request.getPhone()); return ResponseEntity.ok(response); } else { response.put("success", false); response.put("message", "用户状态修改失败,请稍后重试"); log.error("用户状态修改失败: 姓名={}, 手机号={}", request.getNickname(), request.getPhone()); return ResponseEntity.badRequest().body(response); } } catch (IllegalArgumentException e) { // 用户姓名或手机号错误 response.put("success", false); response.put("message", e.getMessage()); log.warn("用户状态修改失败: {}", e.getMessage()); return ResponseEntity.badRequest().body(response); } catch (Exception e) { // 其他系统异常 response.put("success", false); response.put("message", "系统异常,请稍后重试"); log.error("用户状态修改异常: {}", e.getMessage(), e); return ResponseEntity.internalServerError().body(response); } } /** * 获取所有用户状态枚举值 * * @return List<Map<String, String>> 用户状态列表 */ @GetMapping("/status-options") public List<Map<String, String>> getUserStatusOptions() { log.info("获取用户状态选项"); return List.of( Map.of("code", UserStatus.ACTIVE.getCode(), "description", UserStatus.ACTIVE.getDescription()), Map.of("code", UserStatus.DORMANT.getCode(), "description", UserStatus.DORMANT.getDescription()), Map.of("code", UserStatus.CANCELLED.getCode(), "description", UserStatus.CANCELLED.getDescription()) ); } /** * 验证用户姓名和手机号(用于测试) * * @param nickname 用户姓名 * @param phone 手机号 * @return ResponseEntity 验证结果 */ @PostMapping("/validate") public ResponseEntity<Map<String, Object>> validateUser( @RequestParam String nickname, @RequestParam String phone) { log.info("验证用户: 姓名={}, 手机号={}", nickname, phone); Map<String, Object> response = new HashMap<>(); try { User user = userService.validateUser(nickname, phone); if (user != null) { response.put("success", true); response.put("message", "用户验证成功"); response.put("data", Map.of( "id", user.getId(), "username", user.getUsername(), "email", user.getEmail(), "nickname", user.getNickname(), "phone", user.getPhone(), "status", user.getStatus() )); return ResponseEntity.ok(response); } else { response.put("success", false); response.put("message", "用户姓名或手机号错误"); return ResponseEntity.badRequest().body(response); } } catch (Exception e) { response.put("success", false); response.put("message", "验证异常: " + e.getMessage()); log.error("用户验证异常: {}", e.getMessage(), e); return ResponseEntity.internalServerError().body(response); } } }
package com.example.demo.dto; import com.example.demo.enums.UserStatus; import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; /** * 修改用户状态请求DTO * * @author SpringAI Demo * @version 1.0.0 */ @Data @NoArgsConstructor @AllArgsConstructor public class ChangeUserStatusRequest { /** * 用户姓名(用于身份验证) */ @NotBlank(message = "用户姓名不能为空") private String nickname; /** * 手机号(用于身份验证) */ @NotBlank(message = "手机号不能为空") @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") private String phone; /** * 新的用户状态 */ @NotNull(message = "用户状态不能为空") private UserStatus newStatus; /** * 状态变更原因(可选) */ private String reason; }
package com.example.demo.dto; import lombok.Data; /** * 查询用户信息请求 */ @Data public class QueryUserRequest { /** * 用户姓名 */ private String nickname; /** * 用户手机号 */ private String phone; }
package com.example.demo.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.example.demo.enums.UserStatus; import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import java.time.LocalDateTime; /** * 用户实体类 */ @Data @NoArgsConstructor @AllArgsConstructor @TableName("users") public class User { @TableId(type = IdType.AUTO) private Long id; private String username; private String email; private String nickname; /** * 用户手机号 */ private String phone; /** * 用户状态 * ACTIVE - 启用,DORMANT - 休眠,CANCELLED - 注销 */ private UserStatus status; private LocalDateTime createTime; private LocalDateTime updateTime; /** * 构造函数(不包含ID和时间字段) */ public User(String username, String email, String nickname, String phone) { this.username = username; this.email = email; this.nickname = nickname; this.phone = phone; this.status = UserStatus.ACTIVE; // 默认为启用状态 this.createTime = LocalDateTime.now(); this.updateTime = LocalDateTime.now(); } /** * 构造函数(包含所有主要字段) */ public User(String username, String email, String nickname, String phone, UserStatus status) { this.username = username; this.email = email; this.nickname = nickname; this.phone = phone; this.status = status; this.createTime = LocalDateTime.now(); this.updateTime = LocalDateTime.now(); } }
package com.example.demo.enums; import com.baomidou.mybatisplus.annotation.EnumValue; import com.fasterxml.jackson.annotation.JsonValue; import lombok.Getter; /** * 用户状态枚举 * * @author SpringAI Demo * @version 1.0.0 */ @Getter public enum UserStatus { /** * 启用状态 - 用户可以正常使用系统 */ ACTIVE("ACTIVE", "启用"), /** * 休眠状态 - 用户暂时无法使用系统,但保留账号信息 */ DORMANT("DORMANT", "休眠"), /** * 注销状态 - 用户账号已注销,无法使用系统 */ CANCELLED("CANCELLED", "注销"); /** * 状态码(存储在数据库中的值) */ @EnumValue @JsonValue private final String code; /** * 状态描述(用于显示) */ private final String description; UserStatus(String code, String description) { this.code = code; this.description = description; } /** * 根据状态码获取枚举 * * @param code 状态码 * @return UserStatus 对应的枚举值,如果找不到则返回null */ public static UserStatus fromCode(String code) { if (code == null) { return null; } for (UserStatus status : UserStatus.values()) { if (status.code.equals(code)) { return status; } } return null; } /** * 判断用户是否为活跃状态 * * @return boolean true表示用户可以正常使用系统 */ public boolean isActive() { return this == ACTIVE; } /** * 判断用户是否为休眠状态 * * @return boolean true表示用户处于休眠状态 */ public boolean isDormant() { return this == DORMANT; } /** * 判断用户是否已注销 * * @return boolean true表示用户已注销 */ public boolean isCancelled() { return this == CANCELLED; } }
package com.example.demo.service.impl; import com.example.demo.dto.QueryUserRequest; import com.example.demo.dto.ChangeUserStatusRequest; import com.example.demo.enums.UserStatus; import lombok.RequiredArgsConstructor; 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.Message; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import java.util.List; import java.util.ArrayList; import java.util.function.Function; import java.util.regex.Pattern; import java.util.regex.Matcher; /** * 使用Spring AI ChatClient的服务实现 * 按照截图中的方式实现对话记忆功能 */ @Slf4j @Service @Primary @RequiredArgsConstructor public class ChatClientServiceImpl { private final ChatClient chatClient; private final ChatMemory chatMemory; private final Function<QueryUserRequest, String> queryUserInfo; private final Function<ChangeUserStatusRequest, String> changeUserStatus; /** * 构建系统提示词 */ private String buildSystemPrompt() { return """ 你是用户管理助手。当用户提供姓名和手机号时,你需要查询用户信息;当用户确认修改状态时,你需要执行修改。 ## 重要:函数调用格式 **查询用户信息时,请按此格式回复:** QUERY_USER:{"nickname":"姓名","phone":"手机号"} **修改用户状态时,请按此格式回复:** CHANGE_STATUS:{"nickname":"姓名","phone":"手机号","newStatus":"ACTIVE","reason":"修改原因"} 状态类型:ACTIVE(启用)、DORMANT(休眠)、CANCELLED(注销) ## 工作流程 1. 用户提供姓名+手机号 → 立即使用QUERY_USER格式查询 2. 显示用户信息后,用户确认修改 → 使用CHANGE_STATUS格式修改 请严格按照上述JSON格式调用函数,不要有其他多余的文字说明。 """; } public void clearMemory(String sessionId) { try { chatMemory.clear(sessionId); log.info("清空会话 {} 的ChatClient记忆", sessionId); } catch (Exception e) { log.error("清空ChatClient记忆失败: {}", e.getMessage(), e); throw new RuntimeException("清空记忆失败: " + e.getMessage(), e); } } /** * 流式聊天(使用ChatMemory方式 + 日志拦截器) */ public Flux<String> chatStreamWithMemory(String sessionId, String message) { try { log.info("正在调用ChatClient流式API(带记忆),会话: {}, 消息: {}", sessionId, message); // 构建消息列表,包含系统消息、历史消息和当前用户消息 List<Message> messages = new ArrayList<>(); // 添加系统消息 messages.add(new SystemMessage(buildSystemPrompt())); // 获取历史消息 List<Message> historyMessages = chatMemory.get(sessionId, 10); // 获取最近10条消息 messages.addAll(historyMessages); // 添加当前用户消息 UserMessage userMessage = new UserMessage(message); messages.add(userMessage); // 调用ChatClient流式接口 Flux<String> content = chatClient .prompt() .messages(messages) .advisors(advisorSpec -> advisorSpec.param("sessionId", sessionId)) .stream() .content(); // 简单的记忆管理和函数调用检测 StringBuilder fullResponse = new StringBuilder(); return content .doOnNext(fullResponse::append) .concatWith(Flux.defer(() -> { String aiResponse = fullResponse.toString(); String functionResult = checkAndExecuteFunction(aiResponse); return functionResult != null ? Flux.just(functionResult) : Flux.empty(); })) .doOnComplete(() -> { try { // 保存用户消息到记忆 chatMemory.add(sessionId, userMessage); // 保存AI回复到记忆 String responseText = fullResponse.toString(); chatMemory.add(sessionId, new AssistantMessage(responseText)); log.debug("已保存流式回复到记忆,长度: {}", responseText.length()); } catch (Exception e) { log.error("保存对话记忆失败: {}", e.getMessage(), e); } }); } catch (Exception e) { log.error("调用ChatClient流式API时发生错误: {}", e.getMessage(), e); return Flux.error(new RuntimeException("AI服务暂时不可用,请稍后重试: " + e.getMessage(), e)); } } /** * 检测并执行函数调用(最简化逻辑) */ private String checkAndExecuteFunction(String aiResponse) { try { // 检测查询用户信息 if (aiResponse.contains("QUERY_USER:")) { Pattern pattern = Pattern.compile("QUERY_USER:\\{\"nickname\":\"([^\"]+)\",\"phone\":\"([^\"]+)\""); Matcher matcher = pattern.matcher(aiResponse); if (matcher.find()) { String nickname = matcher.group(1); String phone = matcher.group(2); QueryUserRequest request = new QueryUserRequest(); request.setNickname(nickname); request.setPhone(phone); return "\n\n" + queryUserInfo.apply(request); } } // 检测修改用户状态 if (aiResponse.contains("CHANGE_STATUS:")) { Pattern pattern = Pattern.compile("CHANGE_STATUS:\\{\"nickname\":\"([^\"]+)\",\"phone\":\"([^\"]+)\",\"newStatus\":\"([^\"]+)\",\"reason\":\"([^\"]*)\""); Matcher matcher = pattern.matcher(aiResponse); if (matcher.find()) { String nickname = matcher.group(1); String phone = matcher.group(2); String status = matcher.group(3); String reason = matcher.group(4); ChangeUserStatusRequest request = new ChangeUserStatusRequest(); request.setNickname(nickname); request.setPhone(phone); request.setNewStatus(UserStatus.valueOf(status)); request.setReason(reason.isEmpty() ? null : reason); return "\n\n" + changeUserStatus.apply(request); } } } catch (Exception e) { log.error("函数调用执行失败: {}", e.getMessage(), e); } return null; } }
package com.example.demo.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.demo.dto.ChangeUserStatusRequest; import com.example.demo.entity.User; import com.example.demo.enums.UserStatus; import com.example.demo.mapper.UserMapper; import com.example.demo.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.time.LocalDateTime; /** * 用户服务实现类 */ @Slf4j @Service @Transactional(rollbackFor = Exception.class) public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Override public boolean changeUserStatus(ChangeUserStatusRequest request) { log.info("开始修改用户状态: 姓名={}, 手机号={}, 新状态={}", request.getNickname(), request.getPhone(), request.getNewStatus()); // 1. 验证用户姓名和手机号 User user = validateUser(request.getNickname(), request.getPhone()); if (user == null) { log.warn("用户验证失败: 姓名或手机号错误 - 姓名={}, 手机号={}", request.getNickname(), request.getPhone()); throw new IllegalArgumentException("用户姓名或手机号错误"); } // 2. 检查新状态是否与当前状态相同 if (user.getStatus() == request.getNewStatus()) { log.info("用户状态无需修改: 当前状态已是 {}", request.getNewStatus()); return true; } // 3. 更新用户状态 user.setStatus(request.getNewStatus()); user.setUpdateTime(LocalDateTime.now()); boolean success = updateById(user); if (success) { log.info("用户状态修改成功: 姓名={}, 手机号={}, 旧状态={}, 新状态={}, 原因={}", request.getNickname(), request.getPhone(), user.getStatus(), request.getNewStatus(), request.getReason()); } else { log.error("用户状态修改失败: 姓名={}, 手机号={}, 新状态={}", request.getNickname(), request.getPhone(), request.getNewStatus()); } return success; } @Override public User validateUser(String nickname, String phone) { if (!StringUtils.hasText(nickname) || !StringUtils.hasText(phone)) { log.warn("用户姓名或手机号为空"); return null; } // 查找用户 User user = findByNicknameAndPhone(nickname, phone); if (user == null) { log.warn("用户不存在: 姓名={}, 手机号={}", nickname, phone); return null; } log.debug("用户验证成功: 姓名={}, 手机号={}", nickname, phone); return user; } @Override public User findByNicknameAndPhone(String nickname, String phone) { if (!StringUtils.hasText(nickname) || !StringUtils.hasText(phone)) { return null; } LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getNickname, nickname) .eq(User::getPhone, phone); return getOne(queryWrapper); } @Override public boolean save(User entity) { // 设置默认状态 if (entity.getStatus() == null) { entity.setStatus(UserStatus.ACTIVE); } log.info("保存用户: {}, 姓名: {}, 手机号: {}, 状态: {}", entity.getUsername(), entity.getNickname(), entity.getPhone(), entity.getStatus()); return super.save(entity); } @Override public boolean updateById(User entity) { entity.setUpdateTime(LocalDateTime.now()); log.info("更新用户: ID={}, Username={}, 姓名={}, 手机号={}, Status={}", entity.getId(), entity.getUsername(), entity.getNickname(), entity.getPhone(), entity.getStatus()); return super.updateById(entity); } @Override public boolean removeById(java.io.Serializable id) { log.info("删除用户: ID={}", id); return super.removeById(id); } }
package com.example.demo.service; import com.baomidou.mybatisplus.extension.service.IService; import com.example.demo.dto.ChangeUserStatusRequest; import com.example.demo.entity.User; /** * 用户服务接口 */ public interface UserService extends IService<User> { /** * 通过用户姓名和手机号验证,修改用户状态 * * @param request 修改状态请求对象 * @return boolean true表示修改成功,false表示修改失败 * @throws IllegalArgumentException 当姓名或手机号错误时抛出 * @throws RuntimeException 当用户不存在或其他业务异常时抛出 */ boolean changeUserStatus(ChangeUserStatusRequest request); /** * 验证用户姓名和手机号 * * @param nickname 用户姓名 * @param phone 手机号 * @return User 验证成功返回用户对象,验证失败返回null */ User validateUser(String nickname, String phone); /** * 根据姓名和手机号查找用户 * * @param nickname 用户姓名 * @param phone 手机号 * @return User 用户对象,如果不存在则返回null */ User findByNicknameAndPhone(String nickname, String phone); }
{
"name": "springai-frontend",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0 --port 3000",
"build": "vite build",
"preview": "vite preview",
"serve": "vite preview"
},
"dependencies": {
"vue": "^3.4.15",
"vue-router": "^4.2.5",
"element-plus": "^2.4.4",
"axios": "^1.6.2",
"@element-plus/icons-vue": "^2.3.1",
"pinia": "^2.1.7"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.2",
"vite": "^5.0.10"
}
}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
host: '0.0.0.0',
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8083',
changeOrigin: true,
secure: false
}
}
}
})
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
import router from './router'
import './style.css'
const app = createApp(App)
// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.mount('#app')
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
background-color: #f5f5f5;
}
#app {
height: 100vh;
overflow: hidden;
}
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* Element Plus 样式覆盖 */
.el-table {
font-size: 14px;
}
.el-button {
font-size: 14px;
}
.el-input {
font-size: 14px;
}
import request from './request'
// 聊天API接口
export const chatApi = {
// 发送消息给AI助手(流式聊天接口,带会话记忆)
async sendAiMessageStream(message, sessionId, onChunk, onComplete, onError) {
try {
const response = await fetch('/api/chat/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message,
sessionId: sessionId || 'default-session'
})
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
let fullMessage = ''
while (true) {
const { done, value } = await reader.read()
if (done) {
if (onComplete) {
onComplete({
success: true,
message: fullMessage,
timestamp: new Date().toISOString(),
sessionId: sessionId || 'default-session'
})
}
break
}
const chunk = decoder.decode(value, { stream: true })
fullMessage += chunk
if (onChunk) {
onChunk(chunk)
}
}
} catch (error) {
console.error('调用流式AI接口失败:', error)
if (onError) {
onError({
success: false,
message: 'AI服务暂时不可用,请稍后重试',
timestamp: new Date().toISOString(),
sessionId: sessionId || 'default-session'
})
}
}
},
// 发送消息给大模型(基础聊天接口)
async sendMessage(message) {
try {
const response = await request.post('/chat/send', { message })
return response
} catch (error) {
console.error('调用AI接口失败:', error)
// 如果API调用失败,返回错误信息
return {
success: false,
error: error.response?.data?.error || 'AI服务暂时不可用,请稍后重试'
}
}
},
// 清空对话记忆
async clearMemory(sessionId) {
try {
const response = await request.post('/chat/clear-memory', {
sessionId: sessionId || 'default-session'
})
return response
} catch (error) {
console.error('清空记忆失败:', error)
throw error
}
},
// 获取聊天历史
getChatHistory() {
return new Promise((resolve) => {
resolve({
success: true,
data: []
})
})
}
}
import axios from 'axios'
import { ElMessage } from 'element-plus'
// 创建axios实例
const request = axios.create({
baseURL: '/api',
timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
request.interceptors.request.use(
config => {
return config
},
error => {
console.error('请求错误:', error)
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
response => {
return response.data
},
error => {
console.error('响应错误:', error)
// 处理不同的错误状态码
const { response } = error
if (response) {
switch (response.status) {
case 400:
ElMessage.error('请求参数错误')
break
case 401:
ElMessage.error('未授权,请重新登录')
break
case 403:
ElMessage.error('拒绝访问')
break
case 404:
ElMessage.error('请求的资源不存在')
break
case 500:
ElMessage.error('服务器内部错误')
break
default:
ElMessage.error('网络错误')
}
} else {
ElMessage.error('网络连接失败')
}
return Promise.reject(error)
}
)
export default request
import request from './request'
// 用户API接口
export const userApi = {
// 获取所有用户
getAllUsers() {
return request.get('/users')
},
// 分页查询用户
getUsersWithPage(current = 1, size = 10) {
return request.get('/users/page', {
params: { current, size }
})
},
// 根据ID获取用户
getUserById(id) {
return request.get(`/users/${id}`)
},
// 创建用户
createUser(user) {
return request.post('/users', user)
},
// 更新用户
updateUser(id, user) {
return request.put(`/users/${id}`, user)
},
// 删除用户
deleteUser(id) {
return request.delete(`/users/${id}`)
},
// 根据用户名搜索用户
searchUsersByUsername(username) {
return request.get('/users/search', {
params: { username }
})
},
// 获取用户状态选项
getStatusOptions() {
return request.get('/users/status-options')
},
// 修改用户状态
changeUserStatus(statusData) {
return request.post('/users/change-status', statusData)
},
// 验证用户
validateUser(nickname, phone) {
return request.post('/users/validate', null, {
params: { nickname, phone }
})
}
}
<template> <div class="chat-box"> <div class="chat-header"> <h3>AI用户管理助手</h3> <el-button size="small" type="primary" @click="clearChat"> <el-icon><Delete /></el-icon> 清空对话 </el-button> </div> <div class="chat-messages" ref="messagesContainer"> <div v-for="(message, index) in messages" :key="index" :class="['message', message.type]" > <div class="message-content"> <div class="message-text" v-html="formatMessage(message.text)"></div> <div class="message-time">{{ formatTime(message.timestamp) }}</div> </div> </div> <div v-if="isLoading" class="message ai"> <div class="message-content"> <div class="typing-indicator"> <span></span> <span></span> <span></span> </div> </div> </div> </div> <div class="chat-input"> <el-input v-model="inputMessage" type="textarea" :rows="2" placeholder="请输入您的消息..." @keyup.ctrl.enter="sendMessage" :disabled="isLoading" /> <el-button type="primary" @click="sendMessage" :loading="isLoading" :disabled="!inputMessage.trim()" > <el-icon><Promotion /></el-icon> 发送 </el-button> </div> </div> </template> <script setup> import { ref, reactive, nextTick, onMounted } from 'vue' import { ElMessage } from 'element-plus' // 图标已在main.js中全局注册,无需导入 import { chatApi } from '../api/chat' // 定义事件 const emit = defineEmits(['user-operation']) // 响应式数据 const messages = ref([]) const inputMessage = ref('') const isLoading = ref(false) const messagesContainer = ref() const sessionId = ref('user-management-session-' + Date.now()) // 初始化欢迎消息 onMounted(() => { messages.value.push({ type: 'ai', text: '您好!我是您的专属用户管理助手。为了更好地为您服务,请先提供以下信息:\n\n📝 请输入您的姓名:\n📱 请输入您的电话号码:\n\n提供信息后,我将为您提供专业的用户管理服务。', timestamp: new Date() }) }) // 发送消息 const sendMessage = async () => { if (!inputMessage.value.trim() || isLoading.value) return const userMessage = inputMessage.value.trim() // 添加用户消息到聊天记录 messages.value.push({ type: 'user', text: userMessage, timestamp: new Date() }) // 清空输入框并设置加载状态 inputMessage.value = '' isLoading.value = true // 滚动到底部 await nextTick() scrollToBottom() // 调用AI接口 let aiResponse = '' try { await chatApi.sendAiMessageStream( userMessage, sessionId.value, // onChunk - 处理流式响应的每个片段 (chunk) => { aiResponse += chunk // 更新最后一条AI消息或创建新的AI消息 const lastMessage = messages.value[messages.value.length - 1] if (lastMessage && lastMessage.type === 'ai' && lastMessage.isStreaming) { lastMessage.text = aiResponse } else { messages.value.push({ type: 'ai', text: aiResponse, timestamp: new Date(), isStreaming: true }) } // 滚动到底部 nextTick(() => scrollToBottom()) }, // onComplete - 流式响应完成 (result) => { isLoading.value = false // 标记流式消息完成 const lastMessage = messages.value[messages.value.length - 1] if (lastMessage && lastMessage.isStreaming) { lastMessage.isStreaming = false } // 检查响应是否包含用户状态修改成功的标志 if (checkForUserStatusChange(aiResponse)) { console.log('检测到用户状态修改成功,触发页面刷新') // 触发用户操作事件,通知父组件刷新用户列表 emit('user-operation', { type: 'status-change', success: true, message: aiResponse, timestamp: new Date() }) } scrollToBottom() }, // onError - 处理错误 (error) => { isLoading.value = false messages.value.push({ type: 'ai', text: '抱歉,AI服务暂时不可用,请稍后重试。', timestamp: new Date(), isError: true }) ElMessage.error('AI服务暂时不可用') scrollToBottom() } ) } catch (error) { isLoading.value = false console.error('发送消息失败:', error) ElMessage.error('发送消息失败') } } // 检查AI响应是否包含用户状态修改成功的标志 const checkForUserStatusChange = (response) => { // 检查是否包含成功修改状态的标志 return response.includes('✅ 用户状态修改成功') || response.includes('状态修改成功') || (response.includes('用户状态') && response.includes('成功')) } // 清空对话 const clearChat = async () => { try { await chatApi.clearMemory(sessionId.value) messages.value = [] // 重新添加欢迎消息 messages.value.push({ type: 'ai', text: '您好!我是您的专属用户管理助手。为了更好地为您服务,请先提供以下信息:\n\n📝 请输入您的姓名:\n📱 请输入您的电话号码:\n\n提供信息后,我将为您提供专业的用户管理服务。', timestamp: new Date() }) ElMessage.success('对话已清空') } catch (error) { console.error('清空对话失败:', error) ElMessage.error('清空对话失败') } } // 格式化消息文本 const formatMessage = (text) => { if (!text) return '' // 将换行符转换为HTML换行 let formatted = text.replace(/\n/g, '<br>') // 高亮显示成功/失败标志 formatted = formatted.replace(/✅/g, '<span class="success-icon">✅</span>') formatted = formatted.replace(/❌/g, '<span class="error-icon">❌</span>') // 高亮显示重要信息 formatted = formatted.replace(/👤 用户姓名:/g, '<strong>👤 用户姓名:</strong>') formatted = formatted.replace(/📱 手机号码:/g, '<strong>📱 手机号码:</strong>') formatted = formatted.replace(/🔄 新状态:/g, '<strong>🔄 新状态:</strong>') formatted = formatted.replace(/💬 修改原因:/g, '<strong>💬 修改原因:</strong>') return formatted } // 格式化时间 const formatTime = (timestamp) => { return new Date(timestamp).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) } // 滚动到底部 const scrollToBottom = () => { if (messagesContainer.value) { messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight } } </script> <style scoped> .chat-box { display: flex; flex-direction: column; height: 100%; background: #fff; } .chat-header { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; border-bottom: 1px solid #e4e7ed; background: #f8f9fa; } .chat-header h3 { margin: 0; color: #303133; font-size: 16px; } .chat-messages { flex: 1; padding: 20px; overflow-y: auto; background: #f5f5f5; } .message { margin-bottom: 15px; } .message.user { display: flex; justify-content: flex-end; } .message.ai { display: flex; justify-content: flex-start; } .message-content { max-width: 80%; padding: 10px 15px; border-radius: 10px; position: relative; } .message.user .message-content { background: #409eff; color: white; border-bottom-right-radius: 3px; } .message.ai .message-content { background: white; color: #303133; border: 1px solid #e4e7ed; border-bottom-left-radius: 3px; } .message-text { line-height: 1.6; word-break: break-word; } .message-time { font-size: 12px; margin-top: 5px; opacity: 0.7; } .message.user .message-time { text-align: right; } .message.ai .message-time { text-align: left; } .typing-indicator { display: flex; align-items: center; gap: 3px; } .typing-indicator span { width: 6px; height: 6px; border-radius: 50%; background: #409eff; animation: typing 1.4s infinite; } .typing-indicator span:nth-child(2) { animation-delay: 0.2s; } .typing-indicator span:nth-child(3) { animation-delay: 0.4s; } @keyframes typing { 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; } 30% { transform: translateY(-10px); opacity: 1; } } .chat-input { display: flex; gap: 10px; padding: 20px; border-top: 1px solid #e4e7ed; background: white; } .chat-input .el-textarea { flex: 1; } /* 消息内容样式 */ .success-icon { color: #67c23a; font-weight: bold; } .error-icon { color: #f56c6c; font-weight: bold; } /* 滚动条样式 */ .chat-messages::-webkit-scrollbar { width: 6px; } .chat-messages::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 3px; } .chat-messages::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 3px; } .chat-messages::-webkit-scrollbar-thumb:hover { background: #a8a8a8; } /* 响应式设计 */ @media (max-width: 768px) { .chat-header { padding: 10px 15px; } .chat-messages { padding: 15px; } .chat-input { padding: 15px; } .message-content { max-width: 90%; } } </style>
<template> <div class="user-management"> <div class="header-actions"> <h2>用户管理</h2> <div class="actions"> <el-input v-model="searchKeyword" placeholder="搜索用户名" style="width: 200px; margin-right: 10px" @input="handleSearch" clearable > <template #prefix> <el-icon><Search /></el-icon> </template> </el-input> <el-button type="primary" @click="showAddDialog" :icon="Plus"> 新增用户 </el-button> </div> </div> <!-- 用户表格 --> <el-table :data="users" style="width: 100%" v-loading="loading" stripe border > <el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="username" label="用户名" width="120" /> <el-table-column prop="email" label="邮箱" width="180" /> <el-table-column prop="nickname" label="姓名" width="100" /> <el-table-column prop="phone" label="手机号" width="130" /> <el-table-column prop="status" label="状态" width="100"> <template #default="{ row }"> <el-tag :type="getStatusTagType(row.status)" size="small" > {{ getStatusText(row.status) }} </el-tag> </template> </el-table-column> <el-table-column prop="createTime" label="创建时间" width="160"> <template #default="{ row }"> {{ formatDateTime(row.createTime) }} </template> </el-table-column> <el-table-column label="操作" width="180"> <template #default="{ row }"> <el-button size="small" @click="showEditDialog(row)" :icon="Edit"> 编辑 </el-button> <el-button size="small" type="danger" @click="handleDelete(row)" :icon="Delete" > 删除 </el-button> </template> </el-table-column> </el-table> <!-- 分页 --> <div class="pagination"> <el-pagination v-model:current-page="pagination.current" v-model:page-size="pagination.size" :page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> </div> <!-- 新增/编辑对话框 --> <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑用户' : '新增用户'" width="500px" > <el-form ref="formRef" :model="form" :rules="rules" label-width="80px" > <el-form-item label="用户名" prop="username"> <el-input v-model="form.username" placeholder="请输入用户名" /> </el-form-item> <el-form-item label="邮箱" prop="email"> <el-input v-model="form.email" placeholder="请输入邮箱" /> </el-form-item> <el-form-item label="姓名" prop="nickname"> <el-input v-model="form.nickname" placeholder="请输入姓名" /> </el-form-item> <el-form-item label="手机号" prop="phone"> <el-input v-model="form.phone" placeholder="请输入手机号" /> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary" @click="handleSubmit" :loading="submitting"> 确定 </el-button> </div> </template> </el-dialog> </div> </template> <script setup> import { ref, reactive, onMounted } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { Plus, Edit, Delete, Search } from '@element-plus/icons-vue' import { userApi } from '../api/user' // 响应式数据 const users = ref([]) const loading = ref(false) const submitting = ref(false) const dialogVisible = ref(false) const isEdit = ref(false) const searchKeyword = ref('') const formRef = ref() // 分页数据 const pagination = reactive({ current: 1, size: 10, total: 0 }) // 表单数据 const form = reactive({ id: null, username: '', email: '', nickname: '', phone: '' }) // 表单验证规则 const rules = { username: [ { required: true, message: '请输入用户名', trigger: 'blur' }, { min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' } ], email: [ { required: true, message: '请输入邮箱', trigger: 'blur' }, { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' } ], nickname: [ { required: true, message: '请输入姓名', trigger: 'blur' }, { min: 2, max: 10, message: '姓名长度在 2 到 10 个字符', trigger: 'blur' } ], phone: [ { required: true, message: '请输入手机号', trigger: 'blur' }, { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' } ] } // 格式化日期时间 const formatDateTime = (dateTime) => { if (!dateTime) return '' return new Date(dateTime).toLocaleString('zh-CN') } // 获取状态标签类型 const getStatusTagType = (status) => { const statusMap = { 'ACTIVE': 'success', 'DORMANT': 'warning', 'CANCELLED': 'danger' } return statusMap[status] || 'info' } // 获取状态文本 const getStatusText = (status) => { const statusMap = { 'ACTIVE': '启用', 'DORMANT': '休眠', 'CANCELLED': '注销' } return statusMap[status] || '未知' } // 获取用户列表 const fetchUsers = async () => { loading.value = true try { const response = await userApi.getUsersWithPage(pagination.current, pagination.size) users.value = response.records || [] pagination.total = response.total || 0 } catch (error) { ElMessage.error('获取用户列表失败') console.error('获取用户列表失败:', error) } finally { loading.value = false } } // 搜索用户 const handleSearch = async () => { if (!searchKeyword.value.trim()) { fetchUsers() return } loading.value = true try { const response = await userApi.searchUsersByUsername(searchKeyword.value) users.value = response || [] pagination.total = response.length || 0 } catch (error) { ElMessage.error('搜索用户失败') console.error('搜索用户失败:', error) } finally { loading.value = false } } // 显示新增对话框 const showAddDialog = () => { isEdit.value = false resetForm() dialogVisible.value = true } // 显示编辑对话框 const showEditDialog = (row) => { isEdit.value = true form.id = row.id form.username = row.username form.email = row.email form.nickname = row.nickname form.phone = row.phone || '' dialogVisible.value = true } // 重置表单 const resetForm = () => { form.id = null form.username = '' form.email = '' form.nickname = '' form.phone = '' if (formRef.value) { formRef.value.clearValidate() } } // 提交表单 const handleSubmit = async () => { if (!formRef.value) return const valid = await formRef.value.validate().catch(() => false) if (!valid) return submitting.value = true try { if (isEdit.value) { await userApi.updateUser(form.id, { username: form.username, email: form.email, nickname: form.nickname, phone: form.phone }) ElMessage.success('更新用户成功') } else { await userApi.createUser({ username: form.username, email: form.email, nickname: form.nickname, phone: form.phone }) ElMessage.success('创建用户成功') } dialogVisible.value = false fetchUsers() } catch (error) { ElMessage.error(isEdit.value ? '更新用户失败' : '创建用户失败') console.error('提交表单失败:', error) } finally { submitting.value = false } } // 删除用户 const handleDelete = async (row) => { try { await ElMessageBox.confirm( `确定要删除用户 "${row.username}" 吗?`, '删除确认', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' } ) await userApi.deleteUser(row.id) ElMessage.success('删除用户成功') fetchUsers() } catch (error) { if (error !== 'cancel') { ElMessage.error('删除用户失败') console.error('删除用户失败:', error) } } } // 分页大小改变 const handleSizeChange = (size) => { pagination.size = size pagination.current = 1 fetchUsers() } // 当前页改变 const handleCurrentChange = (current) => { pagination.current = current fetchUsers() } // 组件挂载时获取数据 onMounted(() => { fetchUsers() }) // 对外暴露刷新方法 defineExpose({ refreshData: fetchUsers, fetchUsers }) </script> <style scoped> .user-management { height: 100%; display: flex; flex-direction: column; } .header-actions { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #e4e7ed; } .header-actions h2 { margin: 0; color: #303133; } .actions { display: flex; align-items: center; } .el-table { flex: 1; margin-bottom: 20px; } .pagination { display: flex; justify-content: center; padding: 20px 0; } .dialog-footer { text-align: right; } </style>
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'Home',
redirect: '/users'
},
{
path: '/users',
name: 'Users',
component: () => import('../views/UserPage.vue')
}
]
})
export default router
<template> <div class="user-page"> <div class="user-management-section"> <UserManagement ref="userManagementRef" @user-updated="handleUserUpdated" /> </div> <div class="chat-section"> <ChatBox @user-operation="handleUserOperation" /> </div> </div> </template> <script setup> import { ref } from 'vue' import UserManagement from '../components/UserManagement.vue' import ChatBox from '../components/ChatBox.vue' // 用户管理组件的引用 const userManagementRef = ref() // 处理用户操作(来自ChatBox) const handleUserOperation = (operation) => { // 通知用户管理组件刷新数据 if (userManagementRef.value && userManagementRef.value.refreshData) { userManagementRef.value.refreshData() } } // 处理用户更新(来自UserManagement) const handleUserUpdated = () => { // 可以在这里添加其他需要的逻辑 console.log('用户数据已更新') } </script> <style scoped> .user-page { display: flex; height: 100vh; background: #f5f5f5; } .user-management-section { flex: 1; background: #fff; border-right: 1px solid #e4e7ed; overflow: hidden; } .chat-section { width: 400px; background: #fff; overflow: hidden; } /* 响应式设计 */ @media (max-width: 1024px) { .chat-section { width: 350px; } } @media (max-width: 768px) { .user-page { flex-direction: column; } .user-management-section { flex: 1; border-right: none; border-bottom: 1px solid #e4e7ed; } .chat-section { width: 100%; height: 300px; } } </style>
<template> <div id="app"> <el-container> <el-header class="header"> <h1>Vue3 管理系统</h1> </el-header> <el-container> <!-- 左侧用户管理区域 --> <el-main class="left-panel"> <UserManagement /> </el-main> <!-- 右侧聊天区域 --> <el-aside width="400px" class="right-panel"> <ChatBox /> </el-aside> </el-container> </el-container> </div> </template> <script setup> import UserManagement from './components/UserManagement.vue' import ChatBox from './components/ChatBox.vue' </script> <style scoped> #app { height: 100vh; font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; } .header { background-color: #545c64; color: white; display: flex; align-items: center; padding: 0 20px; } .header h1 { margin: 0; font-size: 24px; } .left-panel { background-color: #f5f5f5; padding: 20px; } .right-panel { background-color: #fff; border-left: 1px solid #e4e7ed; } .el-container { height: 100%; } </style>