SpringAI实现智能客服-java17+springboot3+阿里千问大模型

image

 功能:

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&lt;ChatResponse&gt; 响应式流,包含逐步返回的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&lt;Message&gt; 转换后的通义千问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>

image

 

 

 

 

posted @ 2025-09-26 11:27  蔡徐坤1987  阅读(61)  评论(0)    收藏  举报