1、导语

        在第1章我们系统学习了人工智能相关的基础知识,接下来将使用这些基础知识进行项目实战。第1章博客地址:https://blog.csdn.net/BiandanLoveyou/article/details/160115844

        本章节将以一个简单的电商客服系统为原型,尽可能的将 SpringAI 各项技术都沿用进来,让读者对大模型实际落地项目有一个基本认识。

本套系统代码公开免费,可以参考学习和商用,纯原创没有任何第三方约束!让天下没有难写的代码!代码已经放入 Gitee 仓库, master 分支为最新分支。本篇博客是详细的技术拆解文章https://gitee.com/biandanLoveyou/customer-service/tree/04-system-chat/

先展示系统功能:

1、让大模型自我介绍

2、询问商品

3、下单购物

4、查询订单详情

5、给订单好评

2、技术选型

2.1 技术选型

序号 技术栈 描述
1 SpringBoot 3.0X SpringBoot 框架,用于开发Java系统
2 MySql 关系型数据库
3 Mybatis-Plus 操作数据库的持久层框架
4 Redis 基于内存的数据结构存储系统
5 SpringAI Spring 生态中专门用于人工智能应用开发的官方项目
6 validation 参数校验
7 JWT 数字身份证或加密的通行证
8 hutool 封装好的日常开发工具包
9 fastjson 阿里巴巴开发的一个 Java 语言 JSON 处理库
10 Ollama Ollama 是一个让你能在自己电脑上,免费、离线运行大语言模型(LLM)的工具。
11 VectorStore 向量存储
12 RedisStack Redis Stack 是 Redis 官方推出的一款增强版“全家桶”
13 RAG 检索增强生成
14 ChatClient Spring AI 框架的核心客户端接口,你可以把它理解为一个封装好的"对话助手"
15 Tool 大模型工具
16 EmbeddingModel 文本嵌入模型
17 ToolContext 获取大模型上下文数据
18 @RestControllerAdvice+@ExceptionHandler Spring 提供的"全局 AOP 增强器",专门用来统一处理应用中所有 @RestController 控制器抛出的异常
19 ThreadLocal ThreadLocal 让每个线程都有自己的变量副本,线程之间相互隔离。
20 @SpringBootTest+@AutoConfigureMockMvc 单元测试

 2.2 系统脚手架搭建

系统脚手架搭建可以参考博客:https://gitee.com/biandanLoveyou/customer-service/tree/01-project-init/

在此脚手架的基础上,我们会不断扩展,直至把系统完成,每次有迭代都会递增一个分支。如:01-project-init、02-xxx 等。

3、大模型实现多人对话

3.1 多人对话效果展示

多人对话效果演示:

演示说明:

1、系统需要用户登录后才能正常使用,用户登录后,系统就存入了用户的相关信息,如:用户ID、用户姓名等。

2、与大模型的对话聊天存入 Redis,每个用户与大模型对话都会被记住身份,每个用户之间与大模型的对话互不干扰。

3、使用 SSE 流式输出,前端对话有思考过渡效果和呈现打印机输出效果。

3.2 多人对话代码实现

大型语言模型(LLM)是无状态的,这意味着它们不会保留之前交互的信息。当您希望在多次交互中保持上下文或状态时,这可能是一个限制。为了解决这个问题,Spring AI提供了聊天记忆功能,允许您在与大型语言模型的多次交互中存储和检索信息

”大模型的对话记忆”这一概念,根植于人工智能与自然语言处理领域,特别是针对具有深度学习能力的大型语言模型而言,它指的是模型在与用户进行交互式对话过程中,能够追踪、理解并利用先前对话上下文的能力。 此机制使得大模型不仅能够响应即时的输入请求,还能基于之前的交流内容能够在对话中记住先前的对话内容,并根据这些信息进行后续的响应。这种记忆机制使得模型能够在对话中持续跟踪和理解用户的意图和上下文,从而实现更自然和连贯的对话

为什么需要持久化?因为大模型本身是不存储数据的,需要将历史对话的信息一次性提供给它,以实现连续对话,不然服务一旦重启数据就全部丢失了,所以需要持久化。

本次系统开发,我们编写代码,将会话存储于 Redis 中。

本节代码 Gitee 仓库:https://gitee.com/biandanLoveyou/customer-service/tree/02-chatMemory/

3.2.1 系统用户初始化

为节约时间,本系统不演示用户的 CRUD 功能。用户初始化直接通过 SQL 目录里的 SQL 代码进行初始化,如图:

-- 初始化数据
INSERT INTO `shopping_center`.`user_account`(`user_id`, `user_account`, `password`, `user_name`, `state`, `remark`, `create_time`, `update_time`) VALUES ("10001", 'admin', '202cb962ac59075b964b07152d234b70', 'CSDN流放深圳', 1, '管理员账号,密码123的MD5加密值', '2026-05-13 18:09:50', '2026-05-18 14:57:12');
INSERT INTO `shopping_center`.`user_account`(`user_id`, `user_account`, `password`, `user_name`, `state`, `remark`, `create_time`, `update_time`) VALUES ("10002", 'Mary', '202cb962ac59075b964b07152d234b70', 'Mary客服', 1, 'Mary客服账号,密码123的MD5加密值', '2026-05-15 15:48:44', '2026-05-18 14:57:13');
INSERT INTO `shopping_center`.`user_account`(`user_id`, `user_account`, `password`, `user_name`, `state`, `remark`, `create_time`, `update_time`) VALUES ("10003", 'Tom', '202cb962ac59075b964b07152d234b70', 'Tom客服', 1, 'Tom客服账号,密码123的MD5加密值', '2026-05-15 15:50:12', '2026-05-18 14:57:15');
INSERT INTO `shopping_center`.`user_account`(`user_id`, `user_account`, `password`, `user_name`, `state`, `remark`, `create_time`, `update_time`) VALUES ("20001", '13666666666', '202cb962ac59075b964b07152d234b70', '靓仔', 1, '靓仔的购物账号,密码123的MD5加密值', '2026-05-15 15:51:26', '2026-05-18 14:57:16');
INSERT INTO `shopping_center`.`user_account`(`user_id`, `user_account`, `password`, `user_name`, `state`, `remark`, `create_time`, `update_time`) VALUES ("20002", '13888888888', '202cb962ac59075b964b07152d234b70', '美女', 1, '美女的购物账号,密码123的MD5加密值', '2026-05-15 15:52:31', '2026-05-18 14:57:18');

3.2.2 引入 Spring AI 相关依赖

修改 pom 文件,引入 SpringAI、Ollama 等依赖。注意这里使用的是 1.1.5 版本。最新版 2.0 需要 SpringBoot 4.X 版本,而且还没正式 Release 版,这里仅学习 1.1.5 版本。

        <!-- ******************  以下是 Spring AI 依赖  ********************** -->
        <!-- 引入 spring-ai-starter-model-ollama 依赖-->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-ollama</artifactId>
            <version>1.1.5</version>
            <scope>compile</scope>
        </dependency>
        <!-- 向量存储:SpringAI Redis Vector Store https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-starter-vector-store-redis -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-vector-store-redis</artifactId>
            <version>1.1.5</version>
            <scope>compile</scope>
        </dependency>
        <!-- RAG 检索增强:SpringAI RAG https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-rag -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-rag</artifactId>
            <version>1.1.5</version>
            <scope>compile</scope>
        </dependency>
        <!-- mcp Client 异步模式必须使用这个依赖:https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-starter-mcp-client-webflux -->
        <!-- ❌ 如果用的是这个同步版,异步模式不会生效 spring-ai-starter-mcp-client -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
            <version>1.1.5</version>
            <scope>compile</scope>
        </dependency>

3.2.3 修改 yml 配置文件

在 spring 节点下增加 Ollama 的路径配置:

  ai:
    ollama:
      # Ollama API URL,默认端口号 11434
      base-url: http://192.168.3.79:11434

3.2.4 初始化 JedisRedisChatMemoryRepository

SpringAI 1.X 版本并不支持用 Redis 来做对话记忆,2.X 版本才支持使用 Redis 做对话记忆,因此我们只能使用阿里巴巴的 JedisRedisChatMemoryRepository 来做技术替换。

RedisStackConfig 完整代码如下:

package com.customer.config;

import com.alibaba.cloud.ai.memory.redis.JedisRedisChatMemoryRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPooled;

@Configuration
public class RedisStackConfig {

    @Value("${spring.data.redis.host:127.0.0.1}")
    private String host;

    @Value("${spring.data.redis.port:6379}")
    private int port;

    @Value("${spring.data.redis.password:}")
    private String password;

    @Value("${spring.data.redis.timeout:5000}")
    private int timeout;

    /**
     * 初始化 JedisPooled
     * @return
     */
    @Bean
    public JedisPooled initJedisPooled() {
        //配置JedisPooled(根据你实际 RedisStack 信息来配置)
        JedisPooled jedisClient = new JedisPooled(host, port, null, password);
        return jedisClient;
    }

    /**
     * 初始化 RedisMemoryRepository
     * @return
     */
    @Bean
    public JedisRedisChatMemoryRepository initRepository() {
        // 使用阿里云的 JedisRedisChatMemoryRepository
        return JedisRedisChatMemoryRepository.builder()
                .host(host)
                .port(port)
                .password(password)
                .timeout(timeout)
                .build();
    }

}

3.2.5 ChatClient 配置

ChatClient 无法自动注入,需要手动注入。ChatClient 内部依赖并持有 ChatModel 来完成最终的模型调用。在日常开发中,建议优先使用 ChatClient,它能极大提升你的开发效率。只有当需要绕过 ChatClient 的封装,对请求进行最底层的精细控制时,才直接使用ChatModel。

大模型我们选用的是千问(qwen2.5:7b),如果你个人电脑性能强大,可以选 qwen3.5、qwen3.6 版本的。

ChatClientConfig 完整代码如下:

package com.customer.config;

import com.alibaba.cloud.ai.memory.redis.JedisRedisChatMemoryRepository;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaChatOptions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

/**
 * @author CSDN流放深圳
 * @description 模型配置类
 * @create 2026-05-15 18:17
 * @since 1.0.0
 */
@Configuration
public class ChatClientConfig {

    /**
     * qwen 模型名称
     */
    private final String QWEN_MODEL_NAME = "qwen2.5:7b";

    @Value("${spring.ai.ollama.base-url}")
    private String BASE_URL;

    /**
     * 创建一个 qwen 的 ChatModel
     * @return
     */
    @Bean
    @Primary
    public ChatModel qwen() {
        // 1. 先创建 OllamaApi,配置 base-url
        OllamaApi ollamaApi = OllamaApi.builder()
                .baseUrl(BASE_URL)
                .build();

        //2、通过 OllamaApi 创建 OllamaChatModel
        return OllamaChatModel.builder()
                .ollamaApi(ollamaApi)
                .defaultOptions(OllamaChatOptions.builder()
                        .model(QWEN_MODEL_NAME)
                        .build())
                .build();
    }

    /**
     * 创建 qwenChatClient
     * @param chatModel
     * @return
     */
    @Bean
    @Primary
    public ChatClient qwenChatClient(ChatModel chatModel,
                                      JedisRedisChatMemoryRepository repository) {
        MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
                .chatMemoryRepository(repository)
                .maxMessages(20) // 最多保存20条消息
                .build();

        return ChatClient.builder(chatModel)
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
                .build();
    }

}

3.2.6 配置跨域、异步执行器

  1. 配置跨域的目的是为了让前端 html 页面在调用时避免出现跨域问题。
  2. 因为我们的项目还是 SpringMVC 框架,项目实际运行的是 Spring MVC,不是 WebFlux,所以这里配置的线程池是 Spring MVC 的线程池。不配置也不会报错,但是ChatClient 在使用 SSE 流式编程时会出现警告。

在自定义拦截器类里我们配置跨域处理和异步执行器、异步线程池,InterceptorWebConfig 完整代码如下:

package com.customer.interceptor;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.servlet.config.annotation.*;

/**
 * @author CSDN流放深圳
 * @description web拦截器
 * @create 2026-05-12 17:17
 * @since 1.0.0
 */
@Configuration
@EnableWebMvc
public class InterceptorWebConfig implements WebMvcConfigurer {

    @Autowired
    private CustomerInterceptor customerInterceptor;

    /**
     * 配置拦截器
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(customerInterceptor)
                .addPathPatterns("/**");//拦截所有请求
    }

    /**
     * 配置跨域
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")  // 允许所有来源
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")// 允许所有方法
                .allowedHeaders("*")// 允许所有请求头
                .allowCredentials(false);// 不允许携带 cookie
    }

    /**
     * 配置异步执行器
     * 项目实际运行的是 Spring MVC,不是 WebFlux,所以这里配置的线程池是 Spring MVC 的线程池。
     * @param configurer
     */
    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        configurer.setTaskExecutor(mvcTaskExecutor());
        configurer.setDefaultTimeout(30000);
    }

    /**
     * 创建异步线程池
     * @return
     */
    @Bean
    public AsyncTaskExecutor mvcTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("customer-async-");
        executor.initialize();
        return executor;
    }
}

  3.2.7 与大模型聊天的 Controller、Service实现

3.2.7.1、ChatController
package com.customer.controller;

import com.customer.entity.dto.ChatDTO;
import com.customer.service.ChatService;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

/**
 * @author CSDN流放深圳
 * @description 聊天控制层
 * @create 2026-05-15 14:02
 * @since 1.0.0
 */
@RestController
public class ChatController {

    @Autowired
    private ChatService chatsService;

    /**
     * 与大模型AI聊天
     * @param dto
     * @return 流式输出
     */
    @PostMapping(value = "/chat" , produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> chat(@RequestBody ChatDTO dto, HttpServletResponse response) {
        // 关键:手动设置响应头,避免中文乱码
        response.setContentType("text/event-stream;charset=UTF-8");
        return chatsService.chat(dto);
    }

}

说明:

这里需要手动设置请求路径的 produces = MediaType.TEXT_EVENT_STREAM_VALUE

和响应的字符集为 UTF-8,否则会出现中文乱码的情况,非常难排查问题。

3.2.7.2、ChatService
package com.customer.service;

import com.customer.entity.dto.ChatDTO;
import reactor.core.publisher.Flux;

/**
 * @author CSDN流放深圳
 * @description 聊天服务接口
 * @create 2026-05-16 11:32
 * @since 1.0.0
 */
public interface ChatService {

    /**
     * 与大模型AI聊天
     * @param dto
     * @return 流式输出
     */
    Flux<String> chat(ChatDTO dto);
}
3.2.7.3、ChatServiceImpl
package com.customer.service.impl;

import com.customer.entity.dto.ChatDTO;
import com.customer.holder.UserContextHolder;
import com.customer.service.ChatService;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

/**
 * @author CSDN流放深圳
 * @description 聊天服务实现类
 * @create 2026-05-16 12:05
 * @since 1.0.0
 */
@Service
public class ChatServiceImpl implements ChatService {


    @Autowired
    private ChatClient chatClient;


    /**
     * 与大模型AI聊天
     *
     * @param dto
     * @return 流式输出
     */
    @Override
    public Flux<String> chat(ChatDTO dto) {
        //从上下文获取用户ID
        String userId = UserContextHolder.customerUserInfo().getUserId().toString();
        //从上下文获取用户姓名
        String userName = "我名字是:" + UserContextHolder.customerUserInfo().getUserName() + "。";

        return chatClient.prompt()
                .user(userName + dto.getUserMessage())
                // 增加“顾问”,允许通过注入检索数据(Retrieval Context)和对话历史(Chat Memory)来修改传入的 Prompt
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, userId)) //固定值 chat_memory_conversation_id
                .stream().content();
    }
}

说明:

1、我们可以通过 UserContextHolder.customerUserInfo() 来获取拦截器中设置的用户上下文信息。

2、每次与大模型聊天把用户姓名作为 user 角色参数传递,大模型就知道当前用户的名字叫做什么。因为本节演示的是多人对话中记住用户名,实际生产项目并不会这么做,而只是设置 ChatMemory.CONVERSATION_ID 为当前用户ID。

3.2.7.4 ChatDTO
package com.customer.entity.dto;

import com.customer.validator.OtherGroup;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;

/**
 * @author CSDN流放深圳
 * @description 聊天参数
 * @create 2026-05-16 11:35
 * @since 1.0.0
 */
@Data
public class ChatDTO {

    /**
     * 聊天内容
     */
    @NotBlank(message = "【聊天内容】不允许为空[userMessage]", groups = {OtherGroup.class})
    private String userMessage;

}
3.2.7.5 前端 html 页面
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>智能AI客服 · 小淘</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            font-family: 'Segoe UI', 'Poppins', 'Roboto', -apple-system, BlinkMacSystemFont, sans-serif;
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }

        /* 通用卡片容器 */
        .login-card, .chat-container {
            width: 100%;
            max-width: 500px;
            background: #ffffff;
            border-radius: 32px;
            box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
            overflow: hidden;
            transition: all 0.25s ease;
        }

        /* ========= 登录页面样式 ========= */
        .login-card {
            padding: 32px 28px 40px;
        }

        .logo-area {
            text-align: center;
            margin-bottom: 32px;
        }

        .logo-icon {
            width: 70px;
            height: 70px;
            background: linear-gradient(135deg, #4f46e5, #7c3aed);
            border-radius: 30px;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            font-size: 40px;
            box-shadow: 0 10px 20px rgba(79, 70, 229, 0.3);
            margin-bottom: 16px;
        }

        .login-card h2 {
            font-size: 26px;
            color: #1f2937;
            margin-bottom: 8px;
        }

        .login-sub {
            color: #6b7280;
            font-size: 14px;
        }

        .input-group {
            margin-bottom: 22px;
        }

        .input-group label {
            display: block;
            font-size: 13px;
            font-weight: 500;
            color: #374151;
            margin-bottom: 8px;
        }

        .input-group input {
            width: 100%;
            padding: 14px 16px;
            border: 1.5px solid #e5e7eb;
            border-radius: 24px;
            font-size: 15px;
            outline: none;
            transition: all 0.2s;
            background: #f9fafb;
        }

        .input-group input:focus {
            border-color: #4f46e5;
            background: white;
            box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
        }

        .login-btn {
            width: 100%;
            background: linear-gradient(135deg, #4f46e5, #7c3aed);
            border: none;
            padding: 14px;
            border-radius: 40px;
            color: white;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s;
            margin-top: 12px;
            box-shadow: 0 5px 12px rgba(79, 70, 229, 0.3);
        }

        .login-btn:hover {
            transform: translateY(-1px);
            filter: brightness(1.02);
        }

        .login-btn:active {
            transform: translateY(1px);
        }

        .error-message {
            background: #fee2e2;
            color: #dc2626;
            padding: 10px 14px;
            border-radius: 30px;
            font-size: 13px;
            margin-top: 16px;
            text-align: center;
            display: none;
        }

        /* ========= 聊天主界面样式 ========= */
        .chat-container {
            display: flex;
            flex-direction: column;
            height: 700px;
        }

        .chat-header {
            background: linear-gradient(135deg, #4f46e5, #7c3aed);
            padding: 18px 24px;
            color: white;
            display: flex;
            align-items: center;
            gap: 12px;
            border-bottom: 1px solid rgba(255, 255, 255, 0.2);
        }

        .avatar {
            width: 44px;
            height: 44px;
            background: rgba(255, 255, 255, 0.2);
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 24px;
        }

        .header-info h2 {
            font-size: 18px;
            font-weight: 600;
        }

        .header-info p {
            font-size: 11px;
            opacity: 0.85;
            margin-top: 4px;
        }

        .logout-btn {
            margin-left: auto;
            background: rgba(255,255,255,0.15);
            border: none;
            padding: 6px 14px;
            border-radius: 40px;
            color: white;
            font-size: 12px;
            cursor: pointer;
            transition: 0.2s;
        }

        .logout-btn:hover {
            background: rgba(255,255,255,0.3);
        }

        .chat-messages {
            flex: 1;
            overflow-y: auto;
            padding: 20px 16px;
            display: flex;
            flex-direction: column;
            gap: 16px;
            background: #f9fafb;
            scroll-behavior: smooth;
        }

        .message {
            display: flex;
            gap: 12px;
            animation: fadeInUp 0.25s ease;
        }

        @keyframes fadeInUp {
            from { opacity: 0; transform: translateY(10px);}
            to { opacity: 1; transform: translateY(0);}
        }

        .user-message {
            justify-content: flex-end;
        }

        .assistant-message .message-bubble {
            background: white;
            color: #1f2937;
            border: 1px solid #e5e7eb;
            border-radius: 20px 20px 20px 4px;
            box-shadow: 0 1px 2px rgba(0,0,0,0.05);
        }

        .user-message .message-bubble {
            background: #4f46e5;
            color: white;
            border-radius: 20px 20px 4px 20px;
        }

        .message-bubble {
            max-width: 75%;
            padding: 12px 16px;
            font-size: 14px;
            line-height: 1.45;
            word-wrap: break-word;
        }

        .message-avatar {
            width: 34px;
            height: 34px;
            background: #eef2ff;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 18px;
            flex-shrink: 0;
        }

        .typing-indicator {
            display: flex;
            gap: 5px;
            padding: 12px 16px;
            background: white;
            border-radius: 20px;
            width: fit-content;
            border: 1px solid #e5e7eb;
        }
        .typing-indicator span {
            width: 8px;
            height: 8px;
            background: #9ca3af;
            border-radius: 50%;
            display: inline-block;
            animation: blink 1.2s infinite;
        }
        .typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
        .typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
        @keyframes blink {
            0%,60%,100% { transform: scale(0.8); opacity: 0.5; }
            30% { transform: scale(1.2); opacity: 1; }
        }

        .chat-input-area {
            background: white;
            border-top: 1px solid #e9eef3;
            padding: 16px 20px 20px;
            display: flex;
            gap: 12px;
            align-items: center;
        }
        .chat-input-area input {
            flex: 1;
            border: 1px solid #e2e8f0;
            border-radius: 60px;
            padding: 12px 18px;
            font-size: 14px;
            outline: none;
        }
        .chat-input-area input:focus {
            border-color: #4f46e5;
            box-shadow: 0 0 0 3px rgba(79,70,229,0.2);
        }
        .chat-input-area button {
            background: #4f46e5;
            border: none;
            width: 46px;
            height: 46px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            transition: 0.2s;
        }
        .chat-input-area button:hover { background: #4338ca; transform: scale(1.02);}
        .footer-hint {
            background: #f9fafb;
            text-align: center;
            padding: 8px;
            font-size: 10px;
            color: #9ca3af;
        }
		
		/* 用户信息样式 */
		.user-info {
			display: flex;
			align-items: center;
			gap: 12px;
			margin-left: auto;
		}

		.user-name {
			font-size: 13px;
			font-weight: 500;
			background: rgba(255, 255, 255, 0.15);
			padding: 6px 12px;
			border-radius: 20px;
			display: flex;
			align-items: center;
			gap: 6px;
		}

		.user-name::before {
			content: "👤";
			font-size: 12px;
		}

		.logout-btn {
			background: rgba(255, 255, 255, 0.15);
			border: none;
			padding: 6px 14px;
			border-radius: 40px;
			color: white;
			font-size: 12px;
			cursor: pointer;
			transition: 0.2s;
		}

		.logout-btn:hover {
			background: rgba(255, 255, 255, 0.3);
		}
    </style>
</head>
<body>

<!-- 登录卡片 -->
<div id="loginView" class="login-card">
    <div class="logo-area">
        <div class="logo-icon">🤖</div>
        <h2>AI客服 · 小淘</h2>
        <div class="login-sub">登录后即可体验智能对话</div>
		<div class="login-sub"><a href="https://blog.csdn.net/BiandanLoveyou" target="_blank" rel="noopener noreferrer">power by【CSDN流放深圳】</a></div>
    </div>
    <div class="input-group">
        <label>账号</label>
        <input type="text" id="username" placeholder="请输入账号" autocomplete="off">
    </div>
    <div class="input-group">
        <label>密码</label>
        <input type="password" id="password" placeholder="请输入密码">
    </div>
    <button class="login-btn" id="doLoginBtn">登 录</button>
    <div id="loginErrorMsg" class="error-message"></div>
</div>

<!-- 聊天主界面 (默认隐藏) -->
<div id="chatView" class="chat-container" style="display: none;">
    <div class="chat-header">
        <div class="avatar">🤖</div>
        <div class="header-info">
            <h2>智能客服 · 小淘</h2>
            <p>✨ 随时在线,为您解答</p>
        </div>
		<!-- 添加用户信息显示 -->
        <div class="user-info" id="userInfo">
            <span class="user-name" id="userNameDisplay"></span>
            <button class="logout-btn" id="logoutBtn">退出登录</button>
        </div>
    </div>
    <div class="chat-messages" id="chatMessages"></div>
    <div class="chat-input-area">
        <input type="text" id="messageInput" placeholder="输入您的问题,例如:商品咨询 / 订单查询 / 售后服务 ...">
        <button id="sendBtn">📤</button>
    </div>
    <div class="footer-hint">⚡ 小淘AI | <a href="https://blog.csdn.net/BiandanLoveyou" target="_blank" rel="noopener noreferrer">power by【CSDN流放深圳】</a></div>
</div>

<script>
    // ======================= 后端接口配置 =======================
    // 🔧 重要:请替换成您的真实后端地址
    const LOGIN_API_URL = "http://127.0.0.1/login";     // 登录接口
	const LOGOUT_API_URL = "http://127.0.0.1/logout";   // 退出登录接口
    const CHAT_API_URL  = "http://127.0.0.1/chat";      // AI对话接口

    // 存储登录后的token
    let authToken = localStorage.getItem("ai_chat_token") || "";

    // DOM 元素
    const loginView = document.getElementById('loginView');
    const chatView = document.getElementById('chatView');
    const usernameInput = document.getElementById('username');
    const passwordInput = document.getElementById('password');
    const loginBtn = document.getElementById('doLoginBtn');
    const loginErrorMsg = document.getElementById('loginErrorMsg');
    const logoutBtn = document.getElementById('logoutBtn');
    const chatMessages = document.getElementById('chatMessages');
    const messageInput = document.getElementById('messageInput');
    const sendBtn = document.getElementById('sendBtn');

    let isWaitingResponse = false;   // 聊天锁

    // ========== 辅助函数 ==========
    function showError(msg) {
        loginErrorMsg.innerText = msg;
        loginErrorMsg.style.display = 'block';
        setTimeout(() => {
            loginErrorMsg.style.display = 'none';
        }, 3000);
    }

    // 清除登录错误
    function hideError() {
        loginErrorMsg.style.display = 'none';
    }

    // 存储token
    function saveToken(token) {
        authToken = token;
        localStorage.setItem("ai_chat_token", token);
    }

    // 清除token并退出登录
	
	async function logout() {
		console.group("🚪 退出登录");
		console.log("当前token:", authToken ? authToken.substring(0, 30) + "..." : "无");
		
		const currentToken = authToken || localStorage.getItem("ai_chat_token");
		
		try {
			// 如果后端需要 token 做清理(比如加入黑名单),可以传递
			// 但根据您的 @NoNeedLogin,可以不传
			const response = await fetch(LOGOUT_API_URL, {
				method: 'POST',
				headers: {
					'Content-Type': 'application/json'
				}
				// 如果需要传递 token,取消下面的注释
				// body: JSON.stringify({ token: currentToken })
			});
			
			console.log("响应状态:", response.status);
			
			if (response.ok) {
				const data = await response.json();
				console.log("后端返回:", data);
				if (data.code === 0) {
					console.log("✅ 后端退出成功:", data.message || "goodbye!");
				} else {
					console.warn("⚠️ 后端返回异常:", data.message);
				}
			} else {
				console.warn("⚠️ HTTP请求失败:", response.status);
			}
			
		} catch (error) {
			console.error("❌ 调用退出接口失败:", error.message);
			// 即使后端调用失败,也继续清除前端状态
		}
		
		// 无论后端成功与否,都清除前端数据
		console.log("清除前端数据...");
		clearLocalData();
		
		console.log("✅ 已退出登录");
		console.groupEnd();
	}

	// 清除前端所有登录相关数据
	function clearLocalData() {
		// 清除存储
		authToken = "";
		localStorage.removeItem("ai_chat_token");
		localStorage.removeItem("userId");
		localStorage.removeItem("userAccount");
		localStorage.removeItem("userName");
		
		// 清空聊天界面
		if (chatMessages) {
			chatMessages.innerHTML = '';
		}
		
		// 切换视图
		loginView.style.display = "block";
		chatView.style.display = "none";
		
		// 清空输入框
		if (messageInput) {
			messageInput.value = '';
		}
		
		// 重置状态锁
		isWaitingResponse = false;
		
		// 清空登录表单
		if (usernameInput) {
			usernameInput.value = '';
		}
		if (passwordInput) {
			passwordInput.value = '';
		}
		
		console.log("前端数据已清除");
	}

    // 检查token是否存在并尝试自动登录(仅页面加载时验证token有效性建议调一个校验接口,此处简单判断)
    async function tryAutoLogin() {
        if (authToken) {
            // 可选: 调用后端校验 token 接口,如果有效则直接进入聊天界面
            // 为了健壮,可以请求一个校验端点,我们在这里简单模拟:如果token存在,默认展示聊天界面(用户无需重新登录)
            // 如果您需要严格校验,可增加一个 /verify 请求。为安全,简单演示直接切换到聊天
            loginView.style.display = "none";
            chatView.style.display = "flex";
            initChatMessages();
            return true;
        }
        return false;
    }

    // 登录请求
    async function handleLogin() {
		const username = usernameInput.value.trim();
		const password = passwordInput.value.trim();
		
		if (!username || !password) {
			showError("请填写账号和密码");
			return;
		}
		
		hideError();
		loginBtn.disabled = true;
		loginBtn.innerText = "登录中...";
		
		try {
			const response = await fetch(LOGIN_API_URL, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ userAccount: username, password })
			});
			
			const data = await response.json();
			
			if (data.code === 0 && data.data && data.data.token) {
				// 保存用户信息
				localStorage.setItem("ai_chat_token", data.data.token);
				localStorage.setItem("userId", data.data.userId);
				localStorage.setItem("userAccount", data.data.userAccount);
				localStorage.setItem("userName", data.data.userName);
				
				// 更新显示用户名
				updateUserDisplay(data.data.userName);
				
				console.log(`欢迎回来,${data.data.userName}!`);
				
				// 切换到聊天界面
				loginView.style.display = "none";
				chatView.style.display = "flex";
				initChatMessages();
				bindChatEvents();
				
			} else {
				throw new Error(data.message || "登录失败");
			}
			
		} catch (error) {
			console.error("登录失败:", error);
			showError(error.message);
		} finally {
			loginBtn.disabled = false;
			loginBtn.innerText = "登 录";
		}
	}

	// 更新用户显示
	function updateUserDisplay(userName) {
		const userNameElement = document.getElementById('userNameDisplay');
		if (userNameElement) {
			userNameElement.textContent = userName || "用户";
		}
	}

    // 初始化聊天消息 (添加欢迎语)
    function initChatMessages() {
        if (!chatMessages) return;
        chatMessages.innerHTML = '';
        addSystemMessage("嗨~ 我是小淘 🤗,你的智能客服助手!有任何问题尽管吩咐~");
        scrollToBottom();
        if (messageInput) messageInput.focus();
    }

    // 添加AI消息(小淘)
    function addAssistantMessage(content) {
        const messageDiv = document.createElement('div');
        messageDiv.className = 'message assistant-message';
        messageDiv.innerHTML = `
            <div class="message-avatar">🤖</div>
            <div class="message-bubble">${escapeHtml(content)}</div>
        `;
        chatMessages.appendChild(messageDiv);
        scrollToBottom();
    }

    // 添加用户消息
    function addUserMessage(content) {
        const messageDiv = document.createElement('div');
        messageDiv.className = 'message user-message';
        messageDiv.innerHTML = `
            <div class="message-bubble">${escapeHtml(content)}</div>
            <div class="message-avatar" style="background:#4f46e5; color:white;">👤</div>
        `;
        chatMessages.appendChild(messageDiv);
        scrollToBottom();
    }

    function addSystemMessage(content) {
        const div = document.createElement('div');
        div.className = 'message assistant-message';
        div.innerHTML = `<div class="message-avatar">🤖</div><div class="message-bubble" style="background:#f3f4f6;">${escapeHtml(content)}</div>`;
        chatMessages.appendChild(div);
        scrollToBottom();
    }

    let typingIndicator = null;
    function showTyping() {
        removeTyping();
        const div = document.createElement('div');
        div.className = 'message assistant-message';
        div.id = 'typingIndicator';
        div.innerHTML = `<div class="message-avatar">🤖</div><div class="typing-indicator"><span></span><span></span><span></span></div>`;
        chatMessages.appendChild(div);
        scrollToBottom();
        typingIndicator = div;
    }
    function removeTyping() {
        if (typingIndicator) {
            typingIndicator.remove();
            typingIndicator = null;
        }
        const exist = document.getElementById('typingIndicator');
        if (exist) exist.remove();
    }

    function scrollToBottom() {
        if (chatMessages) chatMessages.scrollTop = chatMessages.scrollHeight;
    }

    function escapeHtml(str) {
        if (!str) return '';
        return str.replace(/[&<>]/g, function(m) {
            if (m === '&') return '&amp;';
            if (m === '<') return '&lt;';
            if (m === '>') return '&gt;';
            return m;
        });
    }

    // ========== 核心:调用AI对话接口(自动携带Authorization) ==========
    // 全局变量:当前正在回复的消息元素
	let currentAssistantMessageElement = null;
	async function sendMessageToAI(userMessage) {
		console.group("🔵 聊天请求详情");
		console.log("用户消息:", userMessage);
		
		const token = localStorage.getItem("ai_chat_token");
		
		if (!token) {
			console.error("未获取到 token");
			console.groupEnd();
			throw new Error("未登录,请先登录");
		}
		
		const requestBody = {
			userMessage: userMessage
		};
		
		try {
			const response = await fetch(CHAT_API_URL, {
				method: 'POST',
				headers: {
					'Content-Type': 'application/json',
					'Accept': 'text/event-stream; charset=utf-8',
					'Authorization': token
				},
				body: JSON.stringify(requestBody)
			});
			
			if (response.status === 401) {
				localStorage.removeItem("ai_chat_token");
				throw new Error("登录已过期,请重新登录");
			}
			
			let isFirstChunk = true;  // 标记第一条消息
			let fullReply = "";
			let buffer = "";
			
			const reader = response.body.getReader();
			const decoder = new TextDecoder('utf-8');
			
			while (true) {
				const { done, value } = await reader.read();
				if (done) break;
				
				const chunk = decoder.decode(value, { stream: true });
				buffer += chunk;
				const lines = buffer.split('\n');
				buffer = lines.pop() || "";
				
				for (const line of lines) {
					if (!line.trim()) continue;
					
					if (line.startsWith('data:')) {
						let content = line.substring(5);
						if (content.startsWith(' ')) content = content.substring(1);
						
						if (content && content !== '[DONE]') {
							// 第一条消息时,移除思考动画,创建真实消息气泡
							if (isFirstChunk) {
								isFirstChunk = false;
								removeTyping();  // 移除思考动画
								currentAssistantMessageElement = createAssistantMessageElement();
							}
							
							fullReply += content;
							updateAssistantMessageContent(fullReply);
						}
					}
				}
			}
			
			// 如果没有收到任何有效消息(比如后端返回空),移除思考动画并显示空消息
			if (isFirstChunk) {
				removeTyping();
				addAssistantMessage("抱歉,没有收到回复");
			}
			
			console.log("完整回复:", fullReply);
			console.groupEnd();
			
			currentAssistantMessageElement = null;
			return fullReply;
			
		} catch (error) {
			console.error("请求失败:", error);
			console.groupEnd();
			// 出错时移除思考动画
			removeTyping();
			throw error;
		}
	}

	// 解码 unicode 转义字符
	function decodeUnicode(str) {
		return str.replace(/\\u[\dA-F]{4}/gi, function(match) {
			return String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16));
		});
	}

	// 创建 AI 消息元素
	function createAssistantMessageElement() {
		const messagesContainer = document.getElementById('chatMessages');
		const messageDiv = document.createElement('div');
		messageDiv.className = 'message assistant-message';
		messageDiv.innerHTML = `
			<div class="message-avatar">🤖</div>
			<div class="message-bubble"></div>
		`;
		messagesContainer.appendChild(messageDiv);
		scrollToBottom();
		return messageDiv;
	}

	// 更新 AI 消息内容
	function updateAssistantMessageContent(content) {
		if (currentAssistantMessageElement) {
			const bubble = currentAssistantMessageElement.querySelector('.message-bubble');
			if (bubble) {
				bubble.textContent = content;
				scrollToBottom();
			}
		}
	}

    // 发送消息主要流程
    async function handleSendMessage() {
        if (isWaitingResponse) return;
        const text = messageInput.value.trim();
        if (!text) return;
        messageInput.value = '';
        isWaitingResponse = true;
        sendBtn.disabled = true;
        sendBtn.style.opacity = '0.6';
        addUserMessage(text);
        showTyping();
        let reply = "";
        try {
            reply = await sendMessageToAI(text);
        } catch (err) {
            reply = `系统错误: ${err.message}`;
        } finally {
            removeTyping();
            isWaitingResponse = false;
            sendBtn.disabled = false;
            sendBtn.style.opacity = '1';
            messageInput.focus();
        }
    }

    // 键盘回车发送
    function onInputKeyPress(e) {
        if (e.key === 'Enter' && !e.shiftKey && !isWaitingResponse && messageInput.value.trim()) {
            e.preventDefault();
            handleSendMessage();
        }
    }

    // 绑定聊天事件(在切换界面后重新绑定)
    function bindChatEvents() {
        if (sendBtn) sendBtn.onclick = handleSendMessage;
        if (messageInput) messageInput.onkeypress = onInputKeyPress;
        if (logoutBtn) logoutBtn.onclick = () => logout();
    }

    // 登出重置且展示登录
    window.logout = logout;

    // 页面初始化自动尝试登录
    (async function() {
        bindChatEvents();  // 防止点击无事件
        const loggedIn = await tryAutoLogin();
        if (!loggedIn) {
            loginView.style.display = "block";
            chatView.style.display = "none";
        } else {
            loginView.style.display = "none";
            chatView.style.display = "flex";
            initChatMessages();
            bindChatEvents();
        }
        // 绑定登录按钮
        if (loginBtn) loginBtn.onclick = handleLogin;
        // 允许回车登录
        const handleLoginEnter = (e) => { if (e.key === 'Enter') handleLogin(); };
        usernameInput?.addEventListener('keypress', handleLoginEnter);
        passwordInput?.addEventListener('keypress', handleLoginEnter);
    })();


</script>
</body>
</html>

4、开发电商购物系统

4.1 电商购物系统介绍

        经常使用购物平台的朋友应该都知道购物的基本流程,本系统将开发一个非常小的 mini 版,旨在把购物的核心功能与 SpringAI 人工智能大模型整合。本章节只单纯开发电商购物系统,把购物流程基本跑通,为下一章节做准备。 

4.2 电商购物系统代码实现

 本节代码 Gitee 仓库:https://gitee.com/biandanLoveyou/customer-service/tree/03-shopping/

MySQL数据库建表语句和初始化数据语句:

-- 用户收件地址表
drop table if exists user_address;
CREATE TABLE `user_address` (
    `address_id` varchar(50) NOT NULL COMMENT '地址ID主键',
    `user_id` varchar(50) NOT NULL COMMENT '用户ID(参考表:user_account)',
    `contact_name` varchar(20) DEFAULT NULL COMMENT '收件人',
    `contact_phone` varchar(20) DEFAULT NULL COMMENT '收件电话',
    `address` varchar(255) DEFAULT NULL COMMENT '收件地址',
    PRIMARY KEY (`address_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户收件地址表';
-- 初始化收件地址数据
INSERT INTO `shopping_center`.`user_address`(`address_id`,`user_id`, `contact_name`, `contact_phone`, `address`) VALUES ("add1", "10001", 'CSDN流放深圳', '15888888888', '广东省深圳市南山区深圳湾1号108号楼1层前台');
INSERT INTO `shopping_center`.`user_address`(`address_id`,`user_id`, `contact_name`, `contact_phone`, `address`) VALUES ("add2", "10002", 'Mary客服', '15999999999', '广东省广州市番禺路66号1层门卫室');
INSERT INTO `shopping_center`.`user_address`(`address_id`,`user_id`, `contact_name`, `contact_phone`, `address`) VALUES ("add3", "10003", 'Tom客服', '15777777777', '广东省佛山市南海大道88号3层传达室');
INSERT INTO `shopping_center`.`user_address`(`address_id`,`user_id`, `contact_name`, `contact_phone`, `address`) VALUES ("add4", "20001", '靓仔', '13666666666', '上海市浦东新区99号5楼前台');
INSERT INTO `shopping_center`.`user_address`(`address_id`,`user_id`, `contact_name`, `contact_phone`, `address`) VALUES ("add5", "20002", '美女', '13888888888', '北京市朝阳区朝阳街幸福路305号');

-- 商品信息表
drop table if exists product_info;
CREATE TABLE `product_info` (
    `product_id` varchar(50) NOT NULL COMMENT '商品ID',
    `product_name` VARCHAR(200) NOT NULL COMMENT '商品名称',
    `price` decimal(10,2) NOT NULL COMMENT '商品价格',
    `description` text COMMENT '商品详细描述',
    PRIMARY KEY (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品信息表';
-- 初始化商品信息数据
INSERT INTO `shopping_center`.`product_info`(`product_id`, `product_name`, `price`, `description`) VALUES ("10001", '华为笔记本电脑', 8000.00, '\'{\"屏幕\":\"16.5英寸LCD\",\"处理器\":\"i7-1360P\",\"内存\":\"16GB\",\"存储\":\"1024GB\",\"显卡\":\"Iris Xe\"}\'');
INSERT INTO `shopping_center`.`product_info`(`product_id`, `product_name`, `price`, `description`) VALUES ("10002", '荣耀手机', 5000.00, '\'{\"屏幕\":\"6.8英寸OLED\",\"处理器\":\"骁龙8 Gen3\",\"内存\":\"12GB\",\"存储\":\"512GB\",\"摄像头\":\"50MP主摄\"}\'');
INSERT INTO `shopping_center`.`product_info`(`product_id`, `product_name`, `price`, `description`) VALUES ("10003", '美的电饭锅', 600.00, '\'{\"加热方式\":\"底盘加热\",\"额定容量\":\"5L\",\"内胆厚度\":\"1.7mm\",\"控制方式\":\"微电脑式\",\"内胆材质\":\"抗菌银钻圆灶釜\"}\'');
INSERT INTO `shopping_center`.`product_info`(`product_id`, `product_name`, `price`, `description`) VALUES ("10004", '格力空调', 5000.00, '\'{\"制冷量\":\"7330W\",\"送风方式\":\"小于180°送风\",\"变频技术类型\":\"智能变频\",\"适用面积\":\"31㎡-40㎡\",\"空调功率\":\"3匹\"}\'');
INSERT INTO `shopping_center`.`product_info`(`product_id`, `product_name`, `price`, `description`) VALUES ("10005", '七匹狼男装', 200.00, '\'{\"基础风格\":\"时尚都市\",\"功能\":\"凉感\",\"面料\":\"棉,聚酯纤维\",\"领型设计\":\"V领\",\"版型分类\":\"标准\"}\'');
INSERT INTO `shopping_center`.`product_info`(`product_id`, `product_name`, `price`, `description`) VALUES ("10006", '欧莱雅护肤品', 1000.00, '\'{\"包装种类\":\"基础包装\",\"功效\":\"控油,保湿,抗皱,修护,紧致\",\"净含量\":\"500ml\",\"规格类型\":\"正常规格\",\"适用人群\":\"普通人群\"}\'');
INSERT INTO `shopping_center`.`product_info`(`product_id`, `product_name`, `price`, `description`) VALUES ("10007", '旺旺零食大礼包', 200.00, '\'{\"是否独立包装\":\"独立包装\",\"包装规格\":\"1盒\",\"保质期\":\"270天\",\"成分健康\":\"极低钠\",\"包装方式\":\"礼盒装\"}\'');
INSERT INTO `shopping_center`.`product_info`(`product_id`, `product_name`, `price`, `description`) VALUES ("10008", '乔丹运动鞋', 500.00, '\'{\"中底功能\":\"缓震\",\"外底功能\":\"防滑\",\"鞋面功能\":\"包裹\",\"适用场地\":\"通用\",\"鞋面材质\":\"工程网布\"}\'');


-- 订单表
drop table if exists orders;
CREATE TABLE `orders` (
    `order_id` varchar(50) NOT NULL COMMENT '订单ID',
    `user_id` varchar(50) NOT NULL COMMENT '用户ID(参考表:user_account)',
    `order_time` varchar(50) NOT NULL COMMENT '下单时间',
    `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额',
    `order_state` int(11) NOT NULL DEFAULT 0 COMMENT '订单状态:0-待付款,1-已付款,2-已发货,3-已完成,4-已取消',
    `contact_name` varchar(20) DEFAULT NULL COMMENT '收件人',
    `contact_phone` varchar(20) DEFAULT NULL COMMENT '收件电话',
    `address` varchar(255) DEFAULT NULL COMMENT '收件地址',
    PRIMARY KEY (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';


-- 订单详情表
drop table if exists order_details;
CREATE TABLE `order_details` (
     `detail_id` varchar(50) NOT NULL COMMENT '详情ID',
     `order_id` varchar(50) NOT NULL COMMENT '订单ID(参考表:orders)',
     `product_id` varchar(50) NOT NULL COMMENT '商品ID(参考表:product_info)',
     `unit_price` decimal(10,2) NOT NULL COMMENT '单价',
     `quantity` int(11) NOT NULL COMMENT '购买数量',
     `total_price` decimal(10,2) NOT NULL COMMENT '总价',
     PRIMARY KEY (`detail_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单详情表';

-- 订单评价表
drop table if exists order_evaluation;
CREATE TABLE `order_evaluation` (
    `evaluation_id` varchar(50) NOT NULL COMMENT '评价ID',
    `user_id` varchar(50) NOT NULL COMMENT '用户ID(参考表:user_account)',
    `order_id` varchar(50) NOT NULL COMMENT '订单ID(参考表:orders)',
    `product_id` varchar(50) NOT NULL COMMENT '商品ID(参考表:product_info)',
    `evaluation_time` varchar(50) NOT NULL COMMENT '评价时间',
    `evaluation_level` varchar(20) DEFAULT '好评' COMMENT '评价等级:好评、中评、差评',
    `evaluation_content` varchar(255) DEFAULT NULL COMMENT '评价内容',
    PRIMARY KEY (`evaluation_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单评价表';

4.2.1 开发用户收件模块

1、数据库实体映射类

package com.customer.entity.po;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

/**
 * @author CSDN流放深圳
 * @description 用户收货地址表
 * @create 2026-05-19 17:12
 * @since 1.0.0
 */
@Data
@TableName("user_address")
public class UserAddressEntity {

    /**
     * 地址ID主键
     */
    @TableId("address_id")
    private String addressId;

    /**
     * 用户ID
     */
    private String userId;

    /**
     * 收件人
     */
    private String contactName;

    /**
     * 收件电话
     */
    private String contactPhone;

    /**
     * 收件地址
     */
    private String address;

}

2、dao 层

package com.customer.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.customer.entity.po.UserAddressEntity;
import org.apache.ibatis.annotations.Mapper;

/**
 * @author CSDN流放深圳
 * @description 用户收货地址表(user_address)数据库访问层
 * @create 2026-05-19 14:57
 * @since 1.0.0
 */
@Mapper
public interface UserAddressDao extends BaseMapper<UserAddressEntity> {

}

3、service 层

package com.customer.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.customer.entity.po.UserAddressEntity;

/**
 * @author CSDN流放深圳
 * @description 用户收件地址服务接口
 * @create 2026-05-19 14:42
 * @since 1.0.0
 */
public interface UserAddressService extends IService<UserAddressEntity> {

    /**
     * 根据用户ID查询收件地址
     * @param userId
     * @return
     */
    UserAddressEntity getAddressByUserId(String userId);

}

4、接口实现层

package com.customer.service.impl;

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.customer.dao.UserAddressDao;
import com.customer.entity.po.UserAddressEntity;
import com.customer.holder.UserContextHolder;
import com.customer.service.UserAddressService;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 用户收件地址服务实现类
 * @create 2026-05-19 14:42
 * @since 1.0.0
 */
@Service
public class UserAddressServiceImpl extends ServiceImpl<UserAddressDao, UserAddressEntity> implements UserAddressService {

    /**
     * 根据用户ID查询收件地址
     *
     * @param userId
     * @return
     */
    @Override
    public UserAddressEntity getAddressByUserId(String userId) {
        //如果用户ID为空,则从用户上下文获取用户ID
        if(StrUtil.isBlank(userId)){
            userId = UserContextHolder.customerUserInfo().getUserId();
        }
        //理论上一个用户有多个收件地址,这里只取1个,方便测试
        List<UserAddressEntity> dbList = this.baseMapper.selectList(Wrappers.<UserAddressEntity>lambdaQuery()
                .eq(UserAddressEntity::getUserId, userId));
        if(dbList.size() == 0) return null;
        return dbList.get(0);
    }

}

5、控制层

package com.customer.controller;

import com.customer.entity.po.ProductInfoEntity;
import com.customer.holder.UserContextHolder;
import com.customer.service.UserAddressService;
import com.customer.util.CallResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author CSDN流放深圳
 * @description 用户收件地址控制器
 * @create 2026-05-19 14:41
 * @since 1.0.0
 */
@RestController
@RequestMapping("/user-address")
public class UserAddressController {

    @Autowired
    private UserAddressService userAddressService;


    /**
     * 根据用户ID查询收件地址
     * @return
     */
    @GetMapping("/getAddressByUserId")
    public CallResult<ProductInfoEntity> getAddressByUserId() {
        // 获取上下文的用户ID
        String userId = UserContextHolder.customerUserInfo().getUserId();
        return CallResult.success(userAddressService.getAddressByUserId(userId));
    }

}

6、单元测试

import com.customer.CustomerServiceApp;
import com.customer.entity.constant.CommonKeys;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
 * @author CSDN流放深圳
 * @description 用户收件地址测试类
 * @create 2026-05-20 16:15
 * @since 1.0.0
 */
//指定主启动类
@SpringBootTest(classes = CustomerServiceApp.class, webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
public class UserAddressTest {

    @Resource
    private MockMvc mockMvc;

    private String token;

    /**
     * 测试之前,获取 token
     */
    @BeforeEach
    void setUp() {
        // 通过登录接口获取 token 后赋值
        this.token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJUT0tFTl9LRVkiOiJlYzE2ZThlZGZkZjI0MmU1OTMxZGRkMTQ1OWVhZGNjYyIsIlVzZXItSWQiOiIxMDAwMSIsImlhdCI6MTc3OTI2NTc2Nn0.fQVGofMnVZPcTVyderwzrMbF7ypfWxh2FNu4qbocOn0";
    }

    /**
     * 查询用户收件地址
     * @throws Exception
     */
    @Test
    public void getAddressByUserId() throws Exception{
        System.out.println("------- 单元测试 --------");
        ResultActions resultActions = mockMvc.perform(get("/user-address/getAddressByUserId")
                        .header(CommonKeys.AUTHORIZATION, token)
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk());
        System.out.println("------- 返回结果 --------");
        resultActions.andDo(result -> System.out.println(result.getResponse().getContentAsString()));
    }


}

7、测试结果

先通过登录测试类获取 token,手动赋值。

4.2.2 开发商品信息模块

1、数据库实体映射类

package com.customer.entity.po;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.math.BigDecimal;

/**
 * @author CSDN流放深圳
 * @description 商品信息
 * @create 2026-05-19 14:31
 * @since 1.0.0
 */
@Data
@TableName("product_info")
public class ProductInfoEntity {

    /**
     * 商品ID(自增主键)
     */
    @TableId("product_id")
    private String productId;

    /**
     * 商品名称
     */
    private String productName;

    /**
     * 商品价格
     */
    private BigDecimal price;

    /**
     * 商品详细描述
     */
    private String description;
}

2、dao 层

package com.customer.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.customer.entity.po.ProductInfoEntity;
import org.apache.ibatis.annotations.Mapper;

/**
 * @author CSDN流放深圳
 * @description 商品信息表(product_info)数据库访问层
 * @create 2026-05-19 14:57
 * @since 1.0.0
 */
@Mapper
public interface ProductInfoDao extends BaseMapper<ProductInfoEntity> {

}

3、service 层

package com.customer.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.customer.entity.po.ProductInfoEntity;

import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 商品信息服务接口
 * @create 2026-05-19 14:42
 * @since 1.0.0
 */
public interface ProductInfoService extends IService<ProductInfoEntity> {

    /**
     * 根据ID查询商品
     * @param productId
     * @return
     */
    ProductInfoEntity getProductById(String productId);

    /**
     * 查询所有商品(可根据商品名称模糊查询匹配)
     * @param productName
     * @return
     */
    List<ProductInfoEntity> getAllProduct(String productName);

}

4、接口实现层

package com.customer.service.impl;

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.customer.dao.ProductInfoDao;
import com.customer.entity.po.ProductInfoEntity;
import com.customer.service.ProductInfoService;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 商品信息服务实现类
 * @create 2026-05-19 14:42
 * @since 1.0.0
 */
@Service
public class ProductInfoServiceImpl extends ServiceImpl<ProductInfoDao, ProductInfoEntity> implements ProductInfoService {

    /**
     * 根据ID查询商品
     *
     * @param productId
     */
    @Override
    public ProductInfoEntity getProductById(String productId) {
        ProductInfoEntity productInfoEntity = this.baseMapper.selectById(productId);
        return productInfoEntity;
    }

    /**
     * 查询所有商品(可根据商品名称模糊查询匹配)
     *
     * @param productName
     */
    @Override
    public List<ProductInfoEntity> getAllProduct(String productName) {
        List<ProductInfoEntity> list = this.baseMapper.selectList(Wrappers.<ProductInfoEntity>lambdaQuery()
                .like(StrUtil.isNotBlank(productName), ProductInfoEntity::getProductName, productName));
        return list;
    }

}

5、控制层

package com.customer.controller;

import com.customer.entity.po.ProductInfoEntity;
import com.customer.service.ProductInfoService;
import com.customer.util.CallResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 商品信息控制器
 * @create 2026-05-19 14:41
 * @since 1.0.0
 */
@RestController
@RequestMapping("/product-info")
public class ProductInfoController {

    @Autowired
    private ProductInfoService productInfoService;


    /**
     * 根据ID查询商品
     */
    @GetMapping("/getProductById")
    public CallResult<ProductInfoEntity> getProductById(@RequestParam(name = "productId") String productId) {
        return CallResult.success(productInfoService.getProductById(productId));
    }

    /**
     * 查询所有商品(可根据商品名称模糊查询匹配)
     *
     * @param productName
     * @return
     */
    @GetMapping("/getAllProduct")
    public CallResult<List<ProductInfoEntity>> getAllProduct(@RequestParam(name = "productName") String productName) {
        return CallResult.success(productInfoService.getAllProduct(productName));
    }

}

6、单元测试

import com.customer.CustomerServiceApp;
import com.customer.entity.constant.CommonKeys;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
 * @author CSDN流放深圳
 * @description 商品信息测试类
 * @create 2026-05-20 16:15
 * @since 1.0.0
 */
//指定主启动类
@SpringBootTest(classes = CustomerServiceApp.class, webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
public class ProductInfoTest {

    @Resource
    private MockMvc mockMvc;

    private String token;

    /**
     * 测试之前,获取 token
     */
    @BeforeEach
    void setUp() {
        // 通过登录接口获取 token 后赋值
        this.token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJUT0tFTl9LRVkiOiJlYzE2ZThlZGZkZjI0MmU1OTMxZGRkMTQ1OWVhZGNjYyIsIlVzZXItSWQiOiIxMDAwMSIsImlhdCI6MTc3OTI2NTc2Nn0.fQVGofMnVZPcTVyderwzrMbF7ypfWxh2FNu4qbocOn0";
    }

    /**
     * 查询所有商品(可根据商品名称模糊查询匹配)
     * @throws Exception
     */
    @Test
    public void getAllProduct() throws Exception {
        System.out.println("------- 单元测试 --------");
        String productName = "电脑";
        ResultActions resultActions = mockMvc.perform(get("/product-info/getAllProduct")
                        .header(CommonKeys.AUTHORIZATION, token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .param("productName", productName)) // 使用param传递请求参数
                .andExpect(status().isOk());
        System.out.println("------- 返回结果 --------");
        resultActions.andDo(result -> System.out.println(result.getResponse().getContentAsString()));
    }

    /**
     * 根据ID查询商品
     * @throws Exception
     */
    @Test
    public void getProductById() throws Exception {
        System.out.println("------- 单元测试 --------");
        String productId = "10002";
        ResultActions resultActions = mockMvc.perform(get("/product-info/getProductById")
                        .header(CommonKeys.AUTHORIZATION, token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .param("productId", productId)) // 使用param传递请求参数
                .andExpect(status().isOk());
        System.out.println("------- 返回结果 --------");
        resultActions.andDo(result -> System.out.println(result.getResponse().getContentAsString()));
    }


}

7、测试结果

先通过登录测试类获取 token,手动赋值。

 4.2.3 开发订单详情模块

1、数据库实体映射类

package com.customer.entity.po;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.math.BigDecimal;

/**
 * @author CSDN流放深圳
 * @description 订单详情
 * @create 2026-05-19 14:39
 * @since 1.0.0
 */
@Data
@TableName("order_details")
public class OrderDetailsEntity {

    /**
     * 详情ID
     */
    @TableId("detail_id")
    private String detailId;

    /**
     * 订单ID(参考表:orders)
     */
    private String orderId;

    /**
     * 商品ID(参考表:product_info)
     */
    private String productId;

    /**
     * 单价
     */
    private BigDecimal unitPrice;

    /**
     * 购买数量
     */
    private Integer quantity;

    /**
     * 总价
     */
    private BigDecimal totalPrice;
}


2、dao 层

package com.customer.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.customer.entity.po.OrderDetailsEntity;
import org.apache.ibatis.annotations.Mapper;

/**
 * @author CSDN流放深圳
 * @description 订单详情表(order_details)数据库访问层
 * @create 2026-05-19 14:57
 * @since 1.0.0
 */
@Mapper
public interface OrderDetailsDao extends BaseMapper<OrderDetailsEntity> {

}


3、service 层

package com.customer.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.customer.entity.po.OrderDetailsEntity;
import com.customer.entity.vo.OrderDetailsVo;

import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 订单详情服务接口
 * @create 2026-05-19 14:42
 * @since 1.0.0
 */
public interface OrderDetailsService extends IService<OrderDetailsEntity> {

    /**
     * 根据订单ID查询订单详情,返回数据库实体
     * @param orderId
     * @return 返回订单详情集合(一个订单可能购买了多个商品)
     */
    List<OrderDetailsEntity> getOrderDetailsEntityById(String orderId);

    /**
     * 根据订单ID查询订单详情,返回Vo
     * @param orderId
     * @return 返回Vo
     */
    List<OrderDetailsVo> getOrderDetailVoByOrderId(String orderId);

}


4、接口实现层

package com.customer.service.impl;

import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.customer.dao.OrderDetailsDao;
import com.customer.entity.po.OrderDetailsEntity;
import com.customer.entity.vo.OrderDetailsVo;
import com.customer.service.OrderDetailsService;
import com.customer.service.ProductInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 订单详情服务实现类
 * @create 2026-05-19 14:42
 * @since 1.0.0
 */
@Service
public class OrderDetailsServiceImpl extends ServiceImpl<OrderDetailsDao, OrderDetailsEntity> implements OrderDetailsService {

    @Autowired
    private ProductInfoService productInfoService;

    /**
     * 根据订单ID查询订单详情
     *
     * @param orderId
     * @return 返回订单详情集合(一个订单可能购买了多个商品)
     */
    @Override
    public List<OrderDetailsEntity> getOrderDetailsEntityById(String orderId) {
        List<OrderDetailsEntity> list = this.baseMapper.selectList(Wrappers.<OrderDetailsEntity>lambdaQuery()
                .eq(OrderDetailsEntity::getOrderId, orderId));
        return list;
    }

    /**
     * 根据订单ID查询订单详情,返回Vo
     *
     * @param orderId
     * @return 返回Vo
     */
    @Override
    public List<OrderDetailsVo> getOrderDetailVoByOrderId(String orderId) {
        List<OrderDetailsEntity> list = this.baseMapper.selectList(Wrappers.<OrderDetailsEntity>lambdaQuery()
                .eq(OrderDetailsEntity::getOrderId, orderId));
        List<OrderDetailsVo> voList = new ArrayList<>();
        for (OrderDetailsEntity entity : list) {
            OrderDetailsVo vo = new OrderDetailsVo();
            BeanUtil.copyProperties(entity, vo);
            //根据商品ID查询商品名称
            vo.setProductName(productInfoService.getProductById(entity.getProductId()).getProductName());
        }
        return voList;
    }

}

5、OrderDetailsVo(比 Entity 增加了商品名称字段)

package com.customer.entity.vo;

import lombok.Data;

import java.math.BigDecimal;

/**
 * @author CSDN流放深圳
 * @description 订单详情Vo
 * @create 2026-05-19 14:39
 * @since 1.0.0
 */
@Data
public class OrderDetailsVo {

    /**
     * 详情ID
     */
    private String detailId;

    /**
     * 订单ID(参考表:orders)
     */
    private String orderId;

    /**
     * 商品ID(参考表:product_info)
     */
    private String productId;

    /**
     * 商品名称
     */
    private String productName;

    /**
     * 单价
     */
    private BigDecimal unitPrice;

    /**
     * 购买数量
     */
    private Integer quantity;

    /**
     * 总价
     */
    private BigDecimal totalPrice;
}

4.2.4 开发订单模块

1、数据库实体映射类

package com.customer.entity.po;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.math.BigDecimal;

/**
 * @author CSDN流放深圳
 * @description 订单表
 * @create 2026-05-19 14:33
 * @since 1.0.0
 */
@Data
@TableName("orders")
public class OrdersEntity {

    /**
     * 订单ID
     */
    @TableId("order_id")
    private String orderId;

    /**
     * 用户ID(参考表:user_account)
     */
    private String userId;

    /**
     * 下单时间
     */
    private String orderTime;

    /**
     * 订单总金额
     */
    private BigDecimal totalAmount;

    /**
     * 订单状态
     * @see com.customer.entity.enums.OrderStateEnum
     */
    private Integer orderState;

    /**
     * 收件人
     */
    private String contactName;

    /**
     * 收件电话
     */
    private String contactPhone;

    /**
     * 收件地址
     */
    private String address;

}


2、dao 层

package com.customer.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.customer.entity.po.OrdersEntity;
import org.apache.ibatis.annotations.Mapper;

/**
 * @author CSDN流放深圳
 * @description 订单表(orders)数据库访问层
 * @create 2026-05-19 14:57
 * @since 1.0.0
 */
@Mapper
public interface OrdersDao extends BaseMapper<OrdersEntity> {

}


3、service 层

package com.customer.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.customer.entity.dto.OrderProductInfoDto;
import com.customer.entity.po.OrdersEntity;
import com.customer.entity.vo.OrdersVo;

import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 订单服务接口
 * @create 2026-05-19 14:42
 * @since 1.0.0
 */
public interface OrdersService extends IService<OrdersEntity> {

    /**
     * 用户下订单
     * @param dto 用户下订单商品信息
     * @return
     */
    OrdersVo saveOrder(OrderProductInfoDto dto);

    /**
     * 更新订单状态为:已付款
     * @param orderId
     */
    Boolean updateOrderState_PAID(String orderId);

    /**
     * 更新订单状态为:已发货
     * @param orderId
     */
    Boolean updateOrderState_DELIVERED(String orderId);

    /**
     * 更新订单状态为:已完成
     * @param orderId
     */
    Boolean updateOrderState_FINISHED(String orderId);

    /**
     * 更新订单状态为:已取消
     * @param orderId
     */
    Boolean updateOrderState_CANCELED(String orderId);


    /**
     * 根据用户ID查询订单列表
     * @param userId 不传则默认查询当前用户
     * @param orderState 不传则查询所有
     * @see com.customer.entity.enums.OrderStateEnum
     * @return
     */
    List<OrdersEntity> getOrdersByUserId(String userId, Integer orderState);

    /**
     * 根据ID查询订单
     * @param orderId
     * @return
     */
    OrdersVo getOrdersById(String orderId);
}


4、接口实现层

package com.customer.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.customer.dao.OrdersDao;
import com.customer.entity.dto.OrderProductInfoDto;
import com.customer.entity.enums.OrderStateEnum;
import com.customer.entity.enums.SysExceptionEnum;
import com.customer.entity.po.OrderDetailsEntity;
import com.customer.entity.po.OrdersEntity;
import com.customer.entity.po.ProductInfoEntity;
import com.customer.entity.po.UserAddressEntity;
import com.customer.entity.vo.OrderDetailsVo;
import com.customer.entity.vo.OrdersVo;
import com.customer.exception.CustomerRuntimeException;
import com.customer.holder.UserContextHolder;
import com.customer.service.OrderDetailsService;
import com.customer.service.OrdersService;
import com.customer.service.ProductInfoService;
import com.customer.service.UserAddressService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 订单服务实现类
 * @create 2026-05-19 14:42
 * @since 1.0.0
 */
@Service
public class OrdersServiceImpl extends ServiceImpl<OrdersDao, OrdersEntity> implements OrdersService {

    @Autowired
    private ProductInfoService productInfoService;

    @Autowired
    private OrderDetailsService orderDetailsService;

    @Autowired
    private UserAddressService userAddressService;

    /**
     * 用户下订单
     *
     * @param dto 用户下订单商品信息
     * @param userId 用户Id,不传则从上下文获取当前用户Id。注意:使用大模型(异步线程)是无法获取到用户Id的,请自行传入。
     * @return 返回订单Vo
     */
    @Override
    @Transactional(rollbackFor = Exception.class)//添加事务
    public OrdersVo saveOrder(OrderProductInfoDto dto, String userId) {
        if (userId == null) {
            userId = UserContextHolder.customerUserInfo().getUserId();
        }
        // 订单总价
        BigDecimal orderTotalAmount = BigDecimal.ZERO;
        //生成订单ID
        String orderId = IdUtil.simpleUUID().replaceAll("-", "");

        List<OrderDetailsVo> orderDetailList = new ArrayList<>();
        // 1. 获取商品信息
        List<OrderProductInfoDto.ProductInfoDto> productInfoList = dto.getProductInfoList();
        for (OrderProductInfoDto.ProductInfoDto productInfoDto : productInfoList) {
            // 根据商品ID查询商品信息
            ProductInfoEntity productInfoEntity = productInfoService.getProductById(productInfoDto.getProductId());
            //商品价格
            BigDecimal price = productInfoEntity.getPrice();
            //计算购买的商品总价:单价×数量=总价
            BigDecimal productTotalAmount = price.multiply(new BigDecimal(productInfoDto.getCount()));
            //计算订单总价
            orderTotalAmount = orderTotalAmount.add(productTotalAmount);

            // 2. 创建订单详情
            OrderDetailsEntity orderDetailsEntity = new OrderDetailsEntity();
            orderDetailsEntity.setDetailId(IdUtil.simpleUUID().replaceAll("-", ""));//订单详情ID
            orderDetailsEntity.setOrderId(orderId);//订单ID
            orderDetailsEntity.setProductId(productInfoDto.getProductId());//商品ID
            orderDetailsEntity.setUnitPrice(price);//商品单价
            orderDetailsEntity.setQuantity(productInfoDto.getCount());//商品数量
            orderDetailsEntity.setTotalPrice(productTotalAmount);//商品总价
            //保存订单详情
            orderDetailsService.save(orderDetailsEntity);
            //封装订单详情
            OrderDetailsVo orderDetailsVo = new OrderDetailsVo();
            BeanUtil.copyProperties(orderDetailsEntity, orderDetailsVo);
            orderDetailsVo.setProductName(productInfoEntity.getProductName());//赋值商品名称
            orderDetailList.add(orderDetailsVo);
        }
        // 3. 创建订单
        OrdersEntity ordersEntity = new OrdersEntity();
        ordersEntity.setOrderId(orderId);
        ordersEntity.setOrderTime(DateTime.now().toString());//下订单时间
        ordersEntity.setTotalAmount(orderTotalAmount);//订单总价
        ordersEntity.setOrderState(OrderStateEnum.WAIT_PAY.getCode());//订单状态,默认待付款
        setOrdersFieldsByUser(ordersEntity, userId);//设置订单用户相关字段
        this.save(ordersEntity);
        System.out.println("订单创建成功,订单ID为:" + orderId);
        OrdersVo vo = new OrdersVo();
        BeanUtil.copyProperties(ordersEntity, vo);
        vo.setOrderDetails(orderDetailList);
        return vo;
    }

    /**
     * 更新订单状态为:已付款
     * @param orderId
     */
    @Override
    public Boolean updateOrderState_PAID(String orderId) {
        //判断订单是否存在
        OrdersEntity ordersEntity = this.getById(orderId);
        if(ordersEntity == null){
            throw new CustomerRuntimeException(SysExceptionEnum.ORDER_NOT_EXIST);
        }
        //判断订单是否为待付款状态
        if(ordersEntity.getOrderState() != OrderStateEnum.WAIT_PAY.getCode()){
            throw new CustomerRuntimeException(SysExceptionEnum.ORDER_ONLY_WAIT_PAY_CAN_PAID);
        }
        return updateOrderState(orderId, OrderStateEnum.PAID.getCode());
    }

    /**
     * 更新订单状态为:已发货
     * @param orderId
     */
    @Override
    public Boolean updateOrderState_DELIVERED(String orderId) {
        //查询订单是否为已付款状态
        OrdersEntity ordersEntity = this.getById(orderId);
        if(ordersEntity == null){
            throw new CustomerRuntimeException(SysExceptionEnum.ORDER_NOT_EXIST);
        }
        if(ordersEntity.getOrderState() != OrderStateEnum.PAID.getCode()){
            throw new CustomerRuntimeException(SysExceptionEnum.ORDER_ONLY_PAID_CAN_DELIVERED);
        }
        return updateOrderState(orderId, OrderStateEnum.DELIVERED.getCode());
    }

    /**
     * 更新订单状态为:已完成
     * @param orderId
     */
    @Override
    public Boolean updateOrderState_FINISHED(String orderId) {
        OrdersEntity ordersEntity = this.getById(orderId);
        if(ordersEntity == null){
            throw new CustomerRuntimeException(SysExceptionEnum.ORDER_NOT_EXIST);
        }
        //查询订单是否为已发货状态
        if(ordersEntity.getOrderState() != OrderStateEnum.DELIVERED.getCode()){
            throw new CustomerRuntimeException(SysExceptionEnum.ORDER_ONLY_DELIVERED_CAN_FINISH);
        }
        return updateOrderState(orderId, OrderStateEnum.FINISHED.getCode());
    }

    /**
     * 更新订单状态为:已取消
     * @param orderId
     */
    @Override
    public Boolean updateOrderState_CANCELED(String orderId) {
        OrdersEntity ordersEntity = this.getById(orderId);
        if(ordersEntity == null){
            throw new CustomerRuntimeException(SysExceptionEnum.ORDER_NOT_EXIST);
        }
        //【已发货、已完成、已取消】的订单不允许取消
        if(ordersEntity.getOrderState() == OrderStateEnum.DELIVERED.getCode()
                || ordersEntity.getOrderState() == OrderStateEnum.FINISHED.getCode()
                || ordersEntity.getOrderState() == OrderStateEnum.CANCELED.getCode()){
            throw new CustomerRuntimeException(SysExceptionEnum.ORDER_NOT_ALLOW_CANCEL);
        }
        return updateOrderState(orderId, OrderStateEnum.CANCELED.getCode());
    }


    /**
     * 更新订单状态
     * @param orderId
     * @param orderState
     */
    private Boolean updateOrderState(String orderId, Integer orderState){
        //实际项目中需要校验该订单是否属于该用户的。且发货是由商家来操作。这里简化过程,直接更新订单状态
        Boolean flag = this.update(Wrappers.<OrdersEntity>lambdaUpdate()
                .eq(OrdersEntity::getOrderId, orderId)
                .set(OrdersEntity::getOrderState, orderState));
        return flag;
    }

    /**
     * 设置订单用户相关字段
     * @param ordersEntity
     * @param userId
     */
    private void setOrdersFieldsByUser(OrdersEntity ordersEntity, String userId){
        //根据用户ID查询收件地址相关信息
        UserAddressEntity userAddressEntity = userAddressService.getAddressByUserId(userId);
        if(userAddressEntity == null){
            throw new CustomerRuntimeException(SysExceptionEnum.USER_NO_ADDRESS_ERROR_ERROR);
        }
        ordersEntity.setUserId(userId);//用户ID
        ordersEntity.setContactName(userAddressEntity.getContactName());//收件人
        ordersEntity.setContactPhone(userAddressEntity.getContactPhone());//收件电话
        ordersEntity.setAddress(userAddressEntity.getAddress());//收件地址
    }


    /**
     * 根据用户ID查询订单列表
     * @param userId 不传则默认查询当前用户
     * @param orderState 不传则查询所有
     * @see com.customer.entity.enums.OrderStateEnum
     * @return
     */
    @Override
    public List<OrdersEntity> getOrdersByUserId(String userId, Integer orderState) {
        // 如果 userId 为空,则从上下文获取用户ID
        if(null == userId){
            userId = UserContextHolder.customerUserInfo().getUserId();
        }
        List<OrdersEntity> dbList = this.baseMapper.selectList(Wrappers.<OrdersEntity>lambdaQuery()
                .eq(OrdersEntity::getUserId, userId)
                //订单状态,不传则查询所有
                .eq(null != orderState, OrdersEntity::getOrderState, orderState)
                .orderByDesc(OrdersEntity::getOrderId));
        return dbList;
    }

    /**
     * 根据ID查询订单
     *
     * @param orderId
     */
    @Override
    public OrdersVo getOrdersById(String orderId) {
        OrdersEntity ordersEntity = this.baseMapper.selectById(orderId);
        //获取订单详情
        List<OrderDetailsVo> orderDetailList = orderDetailsService.getOrderDetailVoByOrderId(orderId);
        OrdersVo vo = new OrdersVo();
        BeanUtil.copyProperties(ordersEntity, vo);//赋值相同属性
        vo.setOrderDetails(orderDetailList);
        return vo;
    }
}


5、控制层

package com.customer.controller;

import com.customer.entity.dto.OrderProductInfoDto;
import com.customer.entity.po.OrdersEntity;
import com.customer.entity.vo.OrdersVo;
import com.customer.holder.UserContextHolder;
import com.customer.service.OrdersService;
import com.customer.util.CallResult;
import com.customer.validator.AddGroup;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 订单控制器
 * @create 2026-05-19 14:41
 * @since 1.0.0
 */
@RestController
@RequestMapping("/orders")
public class OrdersController {

    @Autowired
    private OrdersService ordersService;

    /**
     * 用户下订单
     * @return 返回订单ID
     */
    @PostMapping("/saveOrder")
    public CallResult<OrdersVo> saveOrder(@RequestBody @Validated(AddGroup.class) OrderProductInfoDto dto) {
        return CallResult.success(ordersService.saveOrder(dto, null));
    }


    /**
     * 更新订单状态为:已付款
     * @param orderId
     * @return
     */
    @PostMapping("/updateOrderState/paid")
    public CallResult<Boolean> updateOrderState_PAID(@RequestParam(name = "orderId") String orderId) {
        return CallResult.success(ordersService.updateOrderState_PAID(orderId));
    }

    /**
     * 更新订单状态为:已发货
     * @param orderId
     * @return
     */
    @PostMapping("/updateOrderState/delivered")
    public CallResult<Boolean> updateOrderState_DELIVERED(@RequestParam(name = "orderId") String orderId) {
        return CallResult.success(ordersService.updateOrderState_DELIVERED(orderId));
    }

    /**
     * 更新订单状态为:已完成
     * @param orderId
     * @return
     */
    @PostMapping("/updateOrderState/finished")
    public CallResult<Boolean> updateOrderState_FINISHED(@RequestParam(name = "orderId") String orderId) {
        return CallResult.success(ordersService.updateOrderState_FINISHED(orderId));
    }

    /**
     * 更新订单状态为:已取消
     * @param orderId
     * @return
     */
    @PostMapping("/updateOrderState/canceled")
    public CallResult<Boolean> updateOrderState_CANCELED(@RequestParam(name = "orderId") String orderId) {
        return CallResult.success(ordersService.updateOrderState_CANCELED(orderId));
    }

    /**
     * 根据用户ID查询订单列表
     * @param orderState 不传则查询所有
     * @see com.customer.entity.enums.OrderStateEnum
     * @return
     */
    @GetMapping("/getOrdersByUserId")
    public CallResult<List<OrdersEntity>> getOrdersByUserId(@RequestParam(name = "orderState", required = false) Integer orderState) {
        String userId = UserContextHolder.customerUserInfo().getUserId();//当前用户ID
        return CallResult.success(ordersService.getOrdersByUserId(userId, orderState));
    }


    /**
     * 根据ID查询订单
     * @param orderId
     * @return
     */
    @GetMapping("/getOrdersById")
    public CallResult<OrdersVo> getOrdersById(@RequestParam(name = "orderId") String orderId) {
        return CallResult.success(ordersService.getOrdersById(orderId));
    }

}

6、接口入参 dto

package com.customer.entity.dto;

import com.customer.validator.AddGroup;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;

import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 用户下订单商品信息
 * @create 2026-05-19 14:31
 * @since 1.0.0
 */
@Data
public class OrderProductInfoDto {

    /**
     * 商品信息集合
     */
    @NotEmpty(message = "商品信息不能为空[productInfoList]", groups = {AddGroup.class})
    private List<ProductInfoDto> productInfoList;

    /**
     * 商品信息内部类
     */
    @Data
    public static class ProductInfoDto {

        /**
         * 商品id
         */
        private String productId;

        /**
         * 购买的商品数量
         */
        private Integer count;

    }

}

7、订单状态枚举

package com.customer.entity.enums;

import cn.hutool.core.util.StrUtil;

import java.util.HashMap;
import java.util.Map;

/**
 * @author CSDN流放深圳
 * @description 订单状态枚举
 * @create 2026-05-19 17:01
 * @since 1.0.0
 */
public enum OrderStateEnum {
    //订单状态:0-待付款,1-已付款,2-已发货,3-已完成,4-已取消
    WAIT_PAY(0, "待付款"),
    PAID(1, "已付款"),
    DELIVERED(2, "已发货"),
    FINISHED(3, "已完成"),
    CANCELED(4, "已取消"),

    ;

    private Integer code;

    private String message;

    OrderStateEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
    public Integer getCode() {
        return code;
    }
    public String getMessage() {
        return message;
    }

    /**
     * 根据code获取描述
     * @param code
     * @return
     */
    public static String getMessageByCode(Integer code) {
        for (OrderStateEnum value : OrderStateEnum.values()) {
            if (value.getCode().equals(code)) {
                return value.getMessage();
            }
        }
        return null;
    }

    /**
     * 根据描述获取code
     * @param message
     * @return
     */
    public static Integer getCodeByMessage(String message) {
        if (StrUtil.isBlank(message)) {
            return null;
        }

        // 映射表
        Map<String, Integer> stateMap = new HashMap<>();
        stateMap.put("待付款", 0); stateMap.put("未付款", 0); stateMap.put("未支付", 0);
        stateMap.put("已付款", 1); stateMap.put("已支付", 1); stateMap.put("支付成功", 1);
        stateMap.put("已发货", 2); stateMap.put("发货了", 2); stateMap.put("配送中", 2);
        stateMap.put("已完成", 3); stateMap.put("完成", 3); stateMap.put("收货了", 3);
        stateMap.put("已取消", 4); stateMap.put("取消", 4); stateMap.put("退款", 4);

        // 精确匹配
        Integer code = stateMap.get(message);
        if (code != null) return code;

        // 包含匹配
        for (Map.Entry<String, Integer> entry : stateMap.entrySet()) {
            if (message.contains(entry.getKey().substring(0, 1))) { // 简单包含逻辑
                return entry.getValue();
            }
        }

        return null; // 兜底:查全部
    }

}

8、系统异常枚举类 SysExceptionEnum,在原有的基础上新增:

    /**************** 用户下单类错误枚举 ******************/
    USER_NO_ADDRESS_ERROR_ERROR(20001, "用户没有添加收件地址!"),

    /**************** 订单错误枚举 ******************/
    ORDER_ONLY_WAIT_PAY_CAN_PAID(30001, "仅待支付的订单可以变成已付款!"),
    ORDER_ONLY_PAID_CAN_DELIVERED(30002, "仅已付款的订单可以变成已发货!"),
    ORDER_ONLY_DELIVERED_CAN_FINISH(30003, "仅已发货的订单可以变成已完成!"),
    ORDER_NOT_ALLOW_CANCEL(30004, "该订单不允许取消!"),
    ORDER_NOT_EXIST(30005, "该订单不存在!"),
    ORDER_STATE_ERROR(30006, "该订单状态异常!"),

9、订单信息展示层

package com.customer.entity.vo;

import lombok.Data;

import java.math.BigDecimal;
import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 订单信息展示层
 * @create 2026-05-19 14:33
 * @since 1.0.0
 */
@Data
public class OrdersVo {

    /**
     * 订单ID
     */
    private String orderId;

    /**
     * 用户ID(参考表:user_account)
     */
    private String userId;

    /**
     * 下单时间
     * 注意:如果使用 Date 类型,大模型会解析出错。比如:2026-05-27 11:46:51 大模型会解析成 2026-07-01 10:26:50
     */
    private String orderTime;

    /**
     * 订单总金额
     */
    private BigDecimal totalAmount;

    /**
     * 订单状态:0-待付款,1-已付款,2-已发货,3-已完成,4-已取消
     */
    private Integer orderState;

    /**
     * 收件人
     */
    private String contactName;

    /**
     * 收件电话
     */
    private String contactPhone;

    /**
     * 收件地址
     */
    private String address;

    /**
     * 订单商品详情
     */
    private List<OrderDetailsVo> orderDetails;

}


10、单元测试

import com.alibaba.fastjson2.JSON;
import com.customer.CustomerServiceApp;
import com.customer.entity.constant.CommonKeys;
import com.customer.entity.dto.OrderProductInfoDto;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;

import java.util.ArrayList;
import java.util.List;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
 * @author CSDN流放深圳
 * @description 订单测试类
 * @create 2026-05-20 16:15
 * @since 1.0.0
 */
//指定主启动类
@SpringBootTest(classes = CustomerServiceApp.class, webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
public class OrdersTest {

    @Resource
    private MockMvc mockMvc;

    private String token;

    private String orderId = null;

    /**
     * 测试之前,获取 token
     */
    @BeforeEach
    void setUp() {
        // 通过登录接口获取 token 后赋值
        this.token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJUT0tFTl9LRVkiOiIyZmUzMWIwZTUzMzU0Zjk1ODA5OWY0Zjg4NTQ1M2VmMiIsInVzZXJJZCI6IjEwMDAxIiwiaWF0IjoxNzc5NzU3OTQzfQ.LBT62Weci0yJGj0fl43izVT0CqoPOM1eECh6bmz377g";

        // 通过用户下单返回的订单ID进行赋值
        this.orderId = "fcb3e83d769d40fc920859285165f1f5";
    }

    /**
     * 用户下订单
     * @throws Exception
     */
    @Test
    public void saveOrder() throws Exception {
        System.out.println("------- 单元测试 --------");

        OrderProductInfoDto dto = new OrderProductInfoDto();
        List<OrderProductInfoDto.ProductInfoDto> productInfoList = new ArrayList<>();
        OrderProductInfoDto.ProductInfoDto productInfoDto_1 = new OrderProductInfoDto.ProductInfoDto();
        productInfoDto_1.setProductId("10003");// 商品ID
        productInfoDto_1.setCount(1);//购买数量
        productInfoList.add(productInfoDto_1);

        OrderProductInfoDto.ProductInfoDto productInfoDto_2 = new OrderProductInfoDto.ProductInfoDto();
        productInfoDto_2.setProductId("10004");// 商品ID
        productInfoDto_2.setCount(2);//购买数量
        productInfoList.add(productInfoDto_2);

        dto.setProductInfoList(productInfoList);

        ResultActions resultActions = mockMvc.perform(post("/orders/saveOrder")
                        .header(CommonKeys.AUTHORIZATION, token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(JSON.toJSONString(dto))) // 使用content传递请求参数
                .andExpect(status().isOk());
        System.out.println("------- 返回结果 --------");
        resultActions.andDo(result -> System.out.println(result.getResponse().getContentAsString()));
    }

    /**
     * 修改订单状态为:已付款
     * @throws Exception
     */
    @Test
    public void updateOrderState_PAID() throws Exception {
        System.out.println("------- 单元测试 --------");
        ResultActions resultActions = mockMvc.perform(post("/orders/updateOrderState/paid")
                        .header(CommonKeys.AUTHORIZATION, token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .param("orderId", orderId)) // 使用param传递请求参数
                .andExpect(status().isOk());
        System.out.println("------- 返回结果 --------");
        resultActions.andDo(result -> System.out.println(result.getResponse().getContentAsString()));
    }

    /**
     * 修改订单状态为:已发货
     * @throws Exception
     */
    @Test
    public void updateOrderState_DELIVERED() throws Exception {
        System.out.println("------- 单元测试 --------");
        ResultActions resultActions = mockMvc.perform(post("/orders/updateOrderState/delivered")
                        .header(CommonKeys.AUTHORIZATION, token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .param("orderId", orderId)) // 使用param传递请求参数
                .andExpect(status().isOk());
        System.out.println("------- 返回结果 --------");
        resultActions.andDo(result -> System.out.println(result.getResponse().getContentAsString()));
    }

    /**
     * 修改订单状态为:已完成
     * @throws Exception
     */
    @Test
    public void updateOrderState_FINISHED() throws Exception {
        System.out.println("------- 单元测试 --------");
        ResultActions resultActions = mockMvc.perform(post("/orders/updateOrderState/finished")
                        .header(CommonKeys.AUTHORIZATION, token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .param("orderId", orderId)) // 使用param传递请求参数
                .andExpect(status().isOk());
        System.out.println("------- 返回结果 --------");
        resultActions.andDo(result -> System.out.println(result.getResponse().getContentAsString()));
    }

    /**
     * 更新订单状态为:已取消
     * @throws Exception
     */
    @Test
    public void updateOrderState_CANCELED() throws Exception {
        System.out.println("------- 单元测试 --------");
        ResultActions resultActions = mockMvc.perform(post("/orders/updateOrderState/canceled")
                        .header(CommonKeys.AUTHORIZATION, token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .param("orderId", orderId)) // 使用param传递请求参数
                .andExpect(status().isOk());
        System.out.println("------- 返回结果 --------");
        resultActions.andDo(result -> System.out.println(result.getResponse().getContentAsString()));
    }

    /**
     * 根据用户ID查询订单列表
     * @see com.customer.entity.enums.OrderStateEnum
     * @throws Exception
     */
    @Test
    public void getOrdersByUserId() throws Exception {
        System.out.println("------- 单元测试 --------");
        // 要查询的订单状态列表
        String orderState = "";
        //Integer orderState = OrderStateEnum.WAIT_PAY.getCode();// 待付款
        //Integer orderState = OrderStateEnum.PAID.getCode();// 已付款
        //Integer orderState = OrderStateEnum.DELIVERED.getCode();// 已发货
        //Integer orderState = OrderStateEnum.FINISHED.getCode();// 已完成
        //Integer orderState = OrderStateEnum.CANCELED.getCode();// 已取消

        ResultActions resultActions = mockMvc.perform(get("/orders/getOrdersByUserId")
                        .header(CommonKeys.AUTHORIZATION, token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .param("orderState", String.valueOf(orderState))) // 使用param传递请求参数
                .andExpect(status().isOk());
        System.out.println("------- 返回结果 --------");
        resultActions.andDo(result -> System.out.println(result.getResponse().getContentAsString()));
    }

    /**
     * 根据订单ID查询订单详情
     * @throws Exception
     */
    @Test
    public void getOrdersById() throws Exception {
        System.out.println("------- 单元测试 --------");
        ResultActions resultActions = mockMvc.perform(get("/orders/getOrdersById")
                        .header(CommonKeys.AUTHORIZATION, token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .param("orderId", orderId)) // 使用param传递请求参数
                .andExpect(status().isOk());
        System.out.println("------- 返回结果 --------");
        resultActions.andDo(result -> System.out.println(result.getResponse().getContentAsString()));
    }

}

11、测试结果

先通过登录测试类获取 token,手动赋值。并且新增订单的ID也需要手动赋值。

4.2.5 开发订单评价模块

1、数据库实体映射类

package com.customer.entity.po;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

/**
 * @author CSDN流放深圳
 * @description 订单评价表
 * @create 2026-05-19 14:33
 * @since 1.0.0
 */
@Data
@TableName("order_evaluation")
public class OrderEvaluationEntity {

    /**
     * 评价ID
     */
    @TableId("evaluation_id")
    private String evaluationId;

    /**
     * 用户ID(参考表:user_account)
     */
    private String userId;

    /**
     * 订单ID(参考表:orders)
     */
    private String orderId;

    /**
     * 商品ID(参考表:product_info)
     */
    private String productId;

    /**
     * 评价时间
     */
    private String evaluationTime;

    /**
     * 评价等级:好评=good、中评=average、差评=bad
     */
    private String evaluationLevel;

    /**
     * 评价内容
     */
    private String evaluationContent;
}


2、dao 层

package com.customer.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.customer.entity.po.OrderEvaluationEntity;
import org.apache.ibatis.annotations.Mapper;

/**
 * @author CSDN流放深圳
 * @description 订单评价表(order_evaluation)数据库访问层
 * @create 2026-05-19 14:57
 * @since 1.0.0
 */
@Mapper
public interface OrderEvaluationDao extends BaseMapper<OrderEvaluationEntity> {

}


3、service 层

package com.customer.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.customer.entity.dto.AddOrderEvaluationDto;
import com.customer.entity.po.OrderEvaluationEntity;
import com.customer.entity.vo.OrderEvaluationVo;

import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 订单评价服务接口
 * @create 2026-05-19 14:42
 * @since 1.0.0
 */
public interface OrderEvaluationService extends IService<OrderEvaluationEntity> {

    /**
     * 新增订单评价
     * @param dto 订单评价信息
     * @param userId 用户ID
     * @return
     */
    Boolean addOrderEvaluation(AddOrderEvaluationDto dto, String userId);


    /**
     * 根据用户ID和订单ID查询订单评价详情
     * @param userId
     * @param orderId
     * @return
     */
    List<OrderEvaluationVo> getByOrderId(String userId, String orderId);


    /**
     * 根据商品ID查询订单评价详情
     * @param productId
     * @return
     */
    List<OrderEvaluationVo> getEvaluationByProductId(String productId);

}


4、接口实现层

package com.customer.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.customer.dao.OrderEvaluationDao;
import com.customer.entity.dto.AddOrderEvaluationDto;
import com.customer.entity.po.OrderEvaluationEntity;
import com.customer.entity.po.ProductInfoEntity;
import com.customer.entity.po.UserAccountEntity;
import com.customer.entity.vo.OrderEvaluationVo;
import com.customer.holder.UserContextHolder;
import com.customer.service.OrderEvaluationService;
import com.customer.service.ProductInfoService;
import com.customer.service.UserAccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 订单评价详情服务实现类
 * @create 2026-05-19 14:42
 * @since 1.0.0
 */
@Service
public class OrderEvaluationServiceImpl extends ServiceImpl<OrderEvaluationDao, OrderEvaluationEntity> implements OrderEvaluationService {

    @Autowired
    private UserAccountService userAccountService;

    @Autowired
    private ProductInfoService productInfoService;

    /**
     * 新增订单评价
     * @param dto 订单评价信息
     * @param userId 用户ID,为空则获取上下文用户ID
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class) //增加事务
    public Boolean addOrderEvaluation(AddOrderEvaluationDto dto, String userId) {
        String orderId = dto.getOrderId();
        List<AddOrderEvaluationDto.ProductEvaluationDto> productEvaluationList = dto.getProductEvaluationList();
        for (AddOrderEvaluationDto.ProductEvaluationDto eval : productEvaluationList) {
            addOrderEvaluationToDB(orderId, userId, eval.getProductId(), eval.getEvaluationLevel(), eval.getEvaluationContent());
        }
        return true;
    }

    /**
     * 新增订单评价到数据库
     *
     * @param orderId           订单ID
     * @param userId            用户ID
     * @param productId         商品ID
     * @param evaluationLevel   评价等级
     * @param evaluationContent 评价内容
     */
    private void addOrderEvaluationToDB(String orderId, String userId, String productId, String evaluationLevel, String evaluationContent) {
        if(StrUtil.isBlank(userId)){
            //获取用户上下文ID
            userId = UserContextHolder.customerUserInfo().getUserId();
        }

        OrderEvaluationEntity entity = new OrderEvaluationEntity();
        entity.setEvaluationId(IdUtil.simpleUUID().replaceAll("-", ""));//评价ID
        entity.setUserId(userId);//用户ID
        entity.setOrderId(orderId);//订单ID
        entity.setProductId(productId);//商品ID
        entity.setEvaluationTime(DateTime.now().toString());//评价时间
        entity.setEvaluationLevel(evaluationLevel);//评价等级
        entity.setEvaluationContent(evaluationContent);//评价内容
        this.baseMapper.insert(entity);
    }

    /**
     * 根据用户ID和订单ID查询订单评价详情
     *
     * @param userId
     * @param orderId
     * @return
     */
    @Override
    public List<OrderEvaluationVo> getByOrderId(String userId, String orderId) {
        List<OrderEvaluationEntity> list = this.baseMapper.selectList(Wrappers.<OrderEvaluationEntity>lambdaQuery()
                .eq(OrderEvaluationEntity::getUserId, userId)
                .eq(OrderEvaluationEntity::getOrderId, orderId));
        List<OrderEvaluationVo> voList = fieldsVo(list);
        return voList;
    }


    /**
     * 根据商品ID查询订单评价详情
     *
     * @param productId
     * @return
     */
    @Override
    public List<OrderEvaluationVo> getEvaluationByProductId(String productId) {
        List<OrderEvaluationEntity> list = this.baseMapper.selectList(Wrappers.<OrderEvaluationEntity>lambdaQuery()
                .eq(OrderEvaluationEntity::getProductId, productId));
        List<OrderEvaluationVo> voList = fieldsVo(list);
        return voList;
    }

    /**
     * 填充字段
     * @param list
     * @return
     */
    private List<OrderEvaluationVo> fieldsVo(List<OrderEvaluationEntity> list) {
        List<OrderEvaluationVo> voList = new ArrayList<>();
        for (OrderEvaluationEntity entity : list) {
            OrderEvaluationVo vo = new OrderEvaluationVo();
            BeanUtil.copyProperties(entity, vo);//赋值相同属性
            //根据用户ID查询用户名称
            UserAccountEntity accountEntity = userAccountService.getById(entity.getUserId());
            if(null != accountEntity){
                vo.setUserName(accountEntity.getUserName());
            }
            //根据商品ID查询商品名称
            ProductInfoEntity productInfoEntity = productInfoService.getProductById(entity.getProductId());
            if(null != productInfoEntity){
                vo.setProductName(productInfoEntity.getProductName());
            }
            voList.add(vo);
        }
        return voList;
    }
}


5、控制层

package com.customer.controller;

import com.customer.entity.dto.AddOrderEvaluationDto;
import com.customer.entity.vo.OrderEvaluationVo;
import com.customer.holder.UserContextHolder;
import com.customer.service.OrderEvaluationService;
import com.customer.util.CallResult;
import com.customer.validator.AddGroup;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 订单评价控制器
 * @create 2026-05-19 14:41
 * @since 1.0.0
 */
@RestController
@RequestMapping("/order-evaluation")
public class OrderEvaluationController {

    @Autowired
    private OrderEvaluationService orderEvaluationService;


    /**
     * 增加订单评价
     * @param dto
     * @return
     */
    @PostMapping("/addOrderEvaluation")
    public CallResult<Boolean> addOrderEvaluation(@RequestBody @Validated(AddGroup.class) AddOrderEvaluationDto dto) {
        return CallResult.success(orderEvaluationService.addOrderEvaluation(dto, null));
    }


    /**
     * 根据用户ID和订单ID查询订单评价详情
     * @param orderId
     * @return
     */
    @GetMapping("/getByOrderId")
    public CallResult<List<OrderEvaluationVo>> getByOrderId(@RequestParam(name = "orderId") String orderId) {
        // 获取上下文的用户ID
        String userId = UserContextHolder.customerUserInfo().getUserId();
        return CallResult.success(orderEvaluationService.getByOrderId(userId, orderId));
    }


    /**
     * 根据商品ID查询订单评价详情
     * @param productId
     * @return
     */
    @GetMapping("/getEvaluationByProductId")
    public CallResult<List<OrderEvaluationVo>> getEvaluationByProductId(@RequestParam(name = "productId") String productId) {
        return CallResult.success(orderEvaluationService.getEvaluationByProductId(productId));
    }


}

6、vo 展示层 OrderEvaluationVo

package com.customer.entity.vo;

import lombok.Data;

/**
 * @author CSDN流放深圳
 * @description 订单评价Vo层
 * @create 2026-05-19 14:33
 * @since 1.0.0
 */
@Data
public class OrderEvaluationVo {

    /**
     * 评价ID
     */
    private String evaluationId;

    /**
     * 用户ID
     */
    private String userId;

    /**
     * 用户名(原 Entity 没有的字段)
     */
    private String userName;

    /**
     * 订单ID(参考表:orders)
     */
    private String orderId;

    /**
     * 商品ID(参考表:product_info)
     */
    private String productId;

    /**
     * 商品名称(原 Entity 没有的字段)
     */
    private String productName;

    /**
     * 评价时间
     */
    private String evaluationTime;

    /**
     * 评价等级
     */
    private String evaluationLevel;

    /**
     * 评价内容
     */
    private String evaluationContent;

}


7、单元测试

import com.alibaba.fastjson2.JSON;
import com.customer.CustomerServiceApp;
import com.customer.entity.constant.CommonKeys;
import com.customer.entity.dto.AddOrderEvaluationDto;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;

import java.util.ArrayList;
import java.util.List;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
 * @author CSDN流放深圳
 * @description 订单评价测试类
 * @create 2026-05-20 16:15
 * @since 1.0.0
 */
//指定主启动类
@SpringBootTest(classes = CustomerServiceApp.class, webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
public class OrderEvaluationTest {

    @Resource
    private MockMvc mockMvc;

    private String token;

    private String orderId = null;

    /**
     * 测试之前,获取 token
     */
    @BeforeEach
    void setUp() {
        // 通过登录接口获取 token 后赋值
        this.token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJUT0tFTl9LRVkiOiIyZmUzMWIwZTUzMzU0Zjk1ODA5OWY0Zjg4NTQ1M2VmMiIsInVzZXJJZCI6IjEwMDAxIiwiaWF0IjoxNzc5NzU3OTQzfQ.LBT62Weci0yJGj0fl43izVT0CqoPOM1eECh6bmz377g";

        // 通过用户下单返回的订单ID进行赋值
        this.orderId = "实际订单ID";
    }

    /**
     * 增加订单评价
     * @throws Exception
     */
    @Test
    public void addOrderEvaluation() throws Exception {
        System.out.println("------- 单元测试 --------");

        AddOrderEvaluationDto dto = new AddOrderEvaluationDto();
        dto.setOrderId(orderId);
        List<AddOrderEvaluationDto.ProductEvaluationDto> productEvaluationList = new ArrayList<>();
        AddOrderEvaluationDto.ProductEvaluationDto productEvaluationDto_1 = new AddOrderEvaluationDto.ProductEvaluationDto();
        productEvaluationDto_1.setProductId("10001");// 商品ID
        productEvaluationDto_1.setEvaluationLevel("好评");// 评价等级
        productEvaluationDto_1.setEvaluationContent("这个华为笔记本电脑很棒,i7处理器飞一般的感觉!");
        productEvaluationList.add(productEvaluationDto_1);

        AddOrderEvaluationDto.ProductEvaluationDto productEvaluationDto_2 = new AddOrderEvaluationDto.ProductEvaluationDto();
        productEvaluationDto_2.setProductId("10002");// 商品ID
        productEvaluationDto_2.setEvaluationLevel("中评");// 评价等级
        productEvaluationDto_2.setEvaluationContent("这个荣耀手机一般般吧!电池用久了就不耐用了!");
        productEvaluationList.add(productEvaluationDto_2);

        dto.setProductEvaluationList(productEvaluationList);

        ResultActions resultActions = mockMvc.perform(post("/order-evaluation/addOrderEvaluation")
                        .header(CommonKeys.AUTHORIZATION, token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(JSON.toJSONString(dto))) // 使用content传递请求参数
                .andExpect(status().isOk());
        System.out.println("------- 返回结果 --------");
        resultActions.andDo(result -> System.out.println(result.getResponse().getContentAsString()));
    }


    /**
     * 根据用户ID和订单ID查询订单评价详情
     * @throws Exception
     */
    @Test
    public void getOrdersById() throws Exception {
        System.out.println("------- 单元测试 --------");
        ResultActions resultActions = mockMvc.perform(get("/order-evaluation/getByOrderId")
                        .header(CommonKeys.AUTHORIZATION, token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .param("orderId", orderId)) // 使用param传递请求参数
                .andExpect(status().isOk());
        System.out.println("------- 返回结果 --------");
        resultActions.andDo(result -> System.out.println(result.getResponse().getContentAsString()));
    }

    /**
     * 根据商品ID查询订单评价详情
     * @throws Exception
     */
    @Test
    public void getEvaluationByProductId() throws Exception {
        System.out.println("------- 单元测试 --------");
        String productId = "10001";// 商品ID
        ResultActions resultActions = mockMvc.perform(get("/order-evaluation/getEvaluationByProductId")
                        .header(CommonKeys.AUTHORIZATION, token)
                        .contentType(MediaType.APPLICATION_JSON)
                        .param("productId", productId)) // 使用param传递请求参数
                .andExpect(status().isOk());
        System.out.println("------- 返回结果 --------");
        resultActions.andDo(result -> System.out.println(result.getResponse().getContentAsString()));
    }

}


8、测试结果

先通过登录测试类获取 token,手动赋值。

5、SpringAI 整合电商系统

5.1 把电商系统服务条款存入向量数据库

        嵌入模型 (Embedding Model)的工作原理是将文本、图像和视频转换为称为向量(Vectors)的浮点数数组。实际项目中,我们经常需要上传一些文件“投喂”给大模型,比如:公司规章制度、行业政策文件、用户服务协议、系统说明文档、报表文件等。当要将用户查询发送到 AI 模型时,首先检索一组相似文档。然后,这些文档作为用户问题的上下文,并与用户的查询一起发送到 AI 模型。
        向量存储(VectorStore)是一种用于存储和检索高维向量数据的数据库或存储解决方案,它特别适用于处理那些经过嵌入模型转化后的数据。
        检索增强生成(Retrieval Augmented Generation,RAG)检索增强生成 (RAG,Retrieval Augmented Generation) 是一种使用来自私有或专有数据源的信息来辅助文本生成的技术。为什么需要 RAG 检索增强?因为大模型的训练,都是有时间节点的(大模型基本都是预训练模式)。比如某个大模型训练出来是在2025年10月,但是在2026年5月份再去问大模型某些特定领域的知识,大模型可能会不知道,就会产生“幻觉”。常见的大模型幻觉有:已读不回、已读乱回、似是而非。这就大大降低了大模型的准确率和知识覆盖率。

5.1.1 创建电商系统服务条款文件

在 resources 目录下创建服务条款文件 terms-of-service.txt ,简单来说,服务条款就是你与某个服务提供方(比如腾讯、阿里、抖音、银行、航空公司等)之间签订的一份具有法律效力的合同。它规定了双方在使用该服务时的权利、义务和责任。同时也是系统如何使用的“说明书”。

# 电商系统服务条款

**更新日期:[2026年5月20日]**
**生效日期:[2026年5月20日]**

感谢您使用本电商平台(以下简称“本平台”)。本服务条款(以下简称“本条款”)是您与本平台运营方之间关于您注册、使用本平台各项服务所订立的有效协议。  
**当您注册或使用本平台时,即表示您已阅读、理解并同意接受本条款的全部内容。**

## 一、用户注册与账户管理

1. **注册资格**  
   您确认,您具备完全民事行为能力,或是在监护人陪同下使用本服务。如您为未成年人,请在监护人指导下使用本平台。

2. **账户信息**  
   您需提供真实、准确、完整的注册信息(包括但不限于手机号、电子邮箱、用户名等)。如信息发生变更,请及时更新。因信息不实、不完整或未及时更新造成的损失,由您自行承担。

3. **账户安全**  
   您对账户及密码的安全性负全部责任。任何通过您的账户进行的操作(包括但不限于下单、支付、评价等)均视为您本人行为。如发现账户异常,请立即联系客服。

4. **账户注销**  
   您可申请注销账户。若账户存在未完成订单、未处理纠纷或其他违规行为,本平台有权暂不予注销。

## 二、购物流程

1. **下单方式**  
   您可通过本平台官方网站、移动应用程序或经授权的第三方合作平台进行下单。

2. **信息填写**  
   下单时请务必仔细填写以下个人信息:  
   - 收件人姓名  
   - 收件电话  
   - 收件地址(含详细门牌号)  
   因信息错误、不完整或无法联系导致的配送失败或损失,由您自行承担。

3. **订单生成**  
   提交订单后,系统将生成订单编号。订单的成立以本平台生成有效订单为准,而非仅加入购物车。

## 三、订单状态说明

| 状态码 | 状态名称 | 说明 |
|--------|----------|------|
| 0 | 待付款 | 订单已创建但未支付。系统将保留订单 **15–30 分钟**(具体以页面提示为准),超时未支付则自动取消。 |
| 1 | 已付款 | 买家已完成支付,商家应在承诺时间内发货。 |
| 2 | 已发货 | 商家已出库并提供物流单号。买家可自行追踪物流信息。 |
| 3 | 已完成 | 买家确认收货或系统自动确认收货(如发货后 **7–15 天** 无异议)。交易结束。 |
| 4 | 已取消 | 订单终止,包括买家主动取消、支付超时、商家或系统取消等。 |

## 四、订单状态流转限制条件

1. 系统先查询该订单是否存在。
2. 仅 **【待付款】** 状态的订单可流转到 **【已付款】**。
3. 仅 **【已付款】** 状态的订单可流转到 **【已发货】**。
4. 仅 **【已发货】** 状态的订单可流转到 **【已完成】**。
5. 仅 **【待付款、已付款】** 状态的订单可被取消;**【已发货、已完成、已取消】** 状态的订单不可取消。

## 五、支付与价格

1. **支付方式**  
   本平台支持多种支付方式(如微信支付、支付宝、银行卡等),具体以实际页面展示为准。

2. **价格**  
   商品价格以您下单时页面展示为准。若价格错误或明显不合理,本平台有权取消订单并全额退款。

3. **税费与运费**  
   订单中会明确展示商品金额、运费及适用税费(如有)。

## 六、物流与配送

1. **配送时间**  
   配送时间受物流方、天气、地址等因素影响,页面显示的配送时间为预估,不作为承诺。

2. **签收与验收**  
   - 请在签收时检查商品包装是否完好、商品是否齐全。  
   - 如发现破损、短少等问题,可拒收或拍照留存并及时联系客服。

## 七、退换货与售后

1. **七天无理由退货**  
   符合法律法规及本平台规则的,您可在签收后 **7 天内** 申请无理由退货(特定商品除外,如贴身衣物、生鲜食品等)。

2. **质量问题**  
   若商品存在质量问题,请联系客服,本平台将依据国家相关法律法规处理退换货或维修。

3. **退换货流程**  
   请通过订单页面申请售后,不得私自寄回商品,否则可能导致无法受理。

## 八、订单评价说明

1. **评价原则**  
   您可对购买的商品进行评价。评价内容应真实、合法,不得包含广告、辱骂、泄露他人信息等内容。

2. **平台权利**  
   本平台有权在不通知的情况下删除违规评价(如虚假、恶意、违法信息)。

3. **感谢您的认可**  
   如果您对商品满意,请给我们好评。您的认可是我们进步的最大动力。

## 九、隐私声明

**重要:本平台高度重视您的个人信息安全。**

1. **信息收集**  
   我们可能收集您在注册、下单、支付、评价等过程中主动提供的信息,包括:  
   - 姓名、电话、地址  
   - 订单与支付信息  
   - 设备信息与日志信息

2. **信息使用**  
   您的信息将用于:  
   - 完成订单与配送  
   - 处理售后与客服  
   - 优化服务体验与安全风控

3. **信息共享**  
   我们不会向第三方出售或非法共享您的个人信息,但以下情况除外:  
   - 为完成配送(与物流公司共享必要信息)  
   - 依法依司法机关或政府要求  
   - 经您明确同意

4. **信息保护**  
   我们采用行业标准的加密与安全措施保护您的信息,但请注意:任何网络传输方式均无法保证100%安全。

5. **Cookie 使用**  
   本平台使用 Cookie 以改善您的浏览体验。您可通过浏览器设置拒绝 Cookie,但可能影响部分功能。

6. **隐私政策**  
   详细隐私处理规则请参阅《隐私政策》,其为本条款不可分割的一部分。

## 十、用户行为规范

您承诺在使用本平台过程中:  
- 不发布违法、侵权、虚假或恶意信息  
- 不进行刷单、套利、恶意退款等不正当行为  
- 不攻击、破坏本平台系统或获取非授权数据  
- 不干扰其他用户的正常使用

如违反上述规范,本平台有权限制、暂停或终止您的账户,并保留追究法律责任的权利。

## 十一、免责声明

**在法律允许的最大范围内:**

1. **不可抗力**  
   因自然灾害、战争、罢工、政府行为、网络攻击、系统维护等不可抗力导致的服务中断或数据丢失,本平台不承担责任。

2. **第三方服务**  
   支付、物流、短信等由第三方提供服务,本平台对其服务质量和可靠性不做任何保证。

3. **价格与库存**  
   商品价格与库存信息会动态变化,本平台尽力保证准确,但可能存在延迟或误差。

4. **损失范围**  
   在任何情况下,本平台对您的直接损失赔偿上限不超过您就该订单实际支付的金额;不对间接损失(如预期收益、业务中断)承担责任。

5. **外部链接**  
   本平台可能包含第三方网站链接,本平台不对其内容或隐私行为负责。

## 十二、协议修改与终止

1. **条款修改**  
   本平台有权根据业务或法律法规的变化修改本条款。重大修改将提前通过站内信、公告等方式通知您。如您继续使用本平台,视为同意修改后的条款。

2. **协议终止**  
   您可停止使用本平台或注销账户。本平台在您存在严重违约或依法要求时,可终止本协议。

## 十三、法律适用与争议解决

1. **适用法律**  
   本条款的解释、效力及纠纷解决适用中华人民共和国法律(不含冲突法)。

2. **争议解决**  
   因本条款产生的争议,双方应友好协商解决;协商不成的,任何一方可向 **本平台运营方所在地有管辖权的人民法院** 提起诉讼。

## 十四、其他

1. **完整协议**  
   本条款构成您与本平台之间关于使用服务的完整协议,取代此前任何口头或书面的沟通。

2. **可分割性**  
   若本条款某一条款被认定为无效或不可执行,不影响其余条款的效力。

3. **联系方式**  
   如您对本条款或服务有任何疑问,请联系客服:  
   - 官网地址:https://blog.csdn.net/BiandanLoveyou
   - 客服邮箱:service@qq.com
   - 客服电话:400-800-6666
   - 版权所有© CSDN流放深圳

---

**再次感谢您的信任与支持。**

5.1.2 把服务条款通过文本嵌入模型存入向量数据库

服务条款相当于系统的使用说明和隐私说明,相当于“知识库”,我们要投喂知识给大模型,再利用检索增强生成 (RAG,Retrieval Augmented Generation) ,让大模型的回复更智能。

5.1.2.1 配置文本嵌入模型

文本嵌入模型是把文本转换成一个固定长度的向量。在这个向量空间里,语义相近的文本会彼此靠近,从而方便进行高效检索。这里我们使用的模型是 qwen3-embedding:4b

package com.customer.config;

import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.ollama.OllamaEmbeddingModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaEmbeddingOptions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

/**
 * @author CSDN流放深圳
 * @description 文本嵌入模型配置类
 * @create 2026-05-22 9:06
 * @since 1.0.0
 */
@Configuration
public class EmbeddingModelConfig {

    @Value("${spring.ai.ollama.base-url}")
    private String BASE_URL;

    /**
     * 模型名称
     */
    private final String EMBEDDING_MODEL_NAME = "qwen3-embedding:4b";

    /**
     * 初始化文本嵌入模型
     *
     * @return
     */
    @Bean
    @Primary //需要设置优先级
    public EmbeddingModel initEmbeddingModel() {
        // 构建 Ollama API 客户端
        OllamaApi ollamaApi = OllamaApi.builder()
                .baseUrl(BASE_URL)
                .build();

        // 构建配置选项
        OllamaEmbeddingOptions options = OllamaEmbeddingOptions.builder()
                .model(EMBEDDING_MODEL_NAME)//模型名称
                .keepAlive("12h")//模型驻留内存时间12小时,不用每次都重启[citation:1][citation:8]
                .truncate(true)//超长文本截断
                .numBatch(512) //批次大小
                .numThread(4) //CPU线程数
                .build();

        // 创建 OllamaEmbeddingModel
        return OllamaEmbeddingModel.builder()
                .ollamaApi(ollamaApi)
                .defaultOptions(options)
                .build();
    }
}
5.1.2.2 配置向量存储类

向量存储这里使用的是 RedisStack,而并非 Redis。有关 RedisStack 的安装查看博客:https://blog.csdn.net/BiandanLoveyou/article/details/160614418

package com.customer.config;

import com.customer.entity.constant.CommonRedisKeys;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.redis.RedisVectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPooled;

/**
 * @author CSDN流放深圳
 * @description 向量存储配置类
 * @create 2026-05-22 9:14
 * @since 1.0.0
 */
@Configuration
public class VectorStoreConfig {

    /**
     * 创建向量存储
     *
     * @param embeddingModel
     * @param jedisPooled
     * @return
     */
    @Bean
    public VectorStore vectorStore(EmbeddingModel embeddingModel, JedisPooled jedisPooled) {
        return RedisVectorStore.builder(jedisPooled, embeddingModel)
                .indexName(CommonRedisKeys.VECTOR_STORE_INDEX_NAME_KEY)           // 索引名称
                .prefix(CommonRedisKeys.VECTOR_STORE_PREFIX_KEY)         // Key 前缀
                .initializeSchema(true)          // 自动初始化索引
                .build();
    }

}
5.1.2.3 配置文件内容存入到向量数据库类

每次系统启动后,我们先把要存放到向量数据库的文件初始化到向量数据库。考虑到后期有文件的增删改,我们这里统一先删除(根据文档ID删除)再新增,这样就不会堆积旧文件。当然,如果需要保留版本的话,建议再增加一个文件映射表到数据库。这里仅作简单的业务,不需要保留旧数据。

package com.customer.config;

import cn.hutool.crypto.SecureUtil;
import com.alibaba.fastjson.JSON;
import com.customer.entity.constant.CommonRedisKeys;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.data.redis.core.RedisTemplate;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 文件内容存入到向量数据库配置类
 * @create 2026-05-22 10:20
 * @since 1.0.0
 */
@Configuration
@Slf4j
public class VectorDatabaseConfig {

    @Autowired
    private VectorStore vectorStore;

    /**
     * 读取resources目录下的文件【电商系统服务条款】
     */
    @Value("classpath:terms-of-service.txt")
    private Resource resource;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 初始化向量数据库。程序启动时就执行
     */
    @PostConstruct
    public void init() {
        //1。读取文件内容
        TextReader textReader = new TextReader(resource);
        //设置编码格式,防止乱码
        textReader.setCharset(Charset.defaultCharset());

        //获取文件路径等元数据
        //拿到文件路径后,常见用途是做去重校验——用文件路径生成哈希值,存入 Redis,避免同一文件被重复加载到向量数据库中。但需要注意,这种方式只能判断"是否同一个文件",无法感知文件内容的变化。如果文件内容更新但路径不变,你可以考虑用文件内容的哈希值来做判断。
        String fileName = (String) textReader.getCustomMetadata().get(TextReader.SOURCE_METADATA);
        //对文件路径进行md5加密
        String securityFileName = SecureUtil.md5(fileName);
        String redisKey = CommonRedisKeys.CUSTOMER_TERMS_OF_SERVICE + securityFileName;
        //判断文件是否已经存在
        Object obj = redisTemplate.opsForValue().get(redisKey);
        //如果文档存在,先删掉,再存储
        if (null != obj) {
            List<String> documentIds = JSON.parseArray((String) redisTemplate.opsForValue().get(redisKey), String.class);
            vectorStore.delete(documentIds);
            redisTemplate.delete(redisKey);
        }
        //写入向量数据库
        List<Document> documentList = new TokenTextSplitter().transform(textReader.read());
        vectorStore.add(documentList);

        //保存文档Id集合
        List<String> documentIds = new ArrayList<>();
        for (Document document : documentList) {
            documentIds.add(document.getId());//获取文档Id
        }
        //保存到 Redis 中
        redisTemplate.opsForValue().setIfAbsent(redisKey, JSON.toJSONString(documentIds));
        log.info("向量数据库初始化完成,本次共初始化 " + documentIds.size() + " 条数据");
    }
}
5.1.2.4 增加常量、RedisKey

修改 CommonKeys 类如下:

package com.customer.entity.constant;

/**
 * @author CSDN流放深圳
 * @description 常量类
 * @create 2026-05-13 11:29
 * @since 1.0.0
 */
public class CommonKeys {

    /**
     * Authorization 权限字段
     */
    public static final String AUTHORIZATION = "Authorization";

    /**
     * token_key 用于加解密 token 的字段
     */
    public static final String TOKEN_KEY = "TOKEN_KEY";

    /**
     * 用户id
     */
    public static final String USER_ID = "userId";

}

重要提示:

public static final String USER_ID = "userId"; 我们这里统一使用了常量 userId,这是之前版本有所不同的地方。如果登录失败或者无法获取到用户数据,请修改成最新代码版本。

修改 CommonRedisKeys 类如下:

package com.customer.entity.constant;

/**
 * @author CSDN流放深圳
 * @description 公共Redis key
 * @create 2026-05-13 11:36
 * @since 1.0.0
 */
public class CommonRedisKeys {

    /**
     * 用户登录 token
     */
    public static final String CUSTOMER_USER_LOGIN_TOKEN = "customer:user:login:token:";

    /**
     * 向量数据库的key前缀
     */
    public static final String VECTOR_STORE_PREFIX_KEY = "customer-embedding:";

    /**
     * 向量数据库的索引名称
     */
    public static final String VECTOR_STORE_INDEX_NAME_KEY = "customer-index";

    /**
     * 服务条款文件名的key
     */
    public static final String CUSTOMER_TERMS_OF_SERVICE = "customer:terms:of:service:";

}

5.2 开发电商系统工具 Tool(核心技术)

“工具(Tool)”或“功能调用(Function Calling)”允许大型语言模型(LLM)在必要时调用一个或多个可用的工具,这些工具通常由开发者定义。工具可以是任何东西:网页搜索、对外部 API 的调用,或特定代码的执行等。LLM 本身不能实际调用工具;相反,它们会在响应中表达调用特定工具的意图(而不是以纯文本回应)。然后,我们应用程序应该执行这个工具,并报告工具执行的结果给模型。

5.2.1 开发电商系统工具

电商系统对接工具类:SystemToolConfig 完整代码如下:

package com.customer.config;

import com.alibaba.fastjson2.JSON;
import com.customer.entity.constant.CommonKeys;
import com.customer.entity.dto.AddOrderEvaluationDto;
import com.customer.entity.dto.OrderProductInfoDto;
import com.customer.entity.enums.OrderStateEnum;
import com.customer.entity.po.OrdersEntity;
import com.customer.entity.po.ProductInfoEntity;
import com.customer.entity.po.UserAddressEntity;
import com.customer.entity.vo.OrderEvaluationVo;
import com.customer.entity.vo.OrdersVo;
import com.customer.service.OrderEvaluationService;
import com.customer.service.OrdersService;
import com.customer.service.ProductInfoService;
import com.customer.service.UserAddressService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 大模型调用系统的工具类。包含 @Tool 注解方法的 Bean。这些方法就是 AI 可以调用的能力。
 * @create 2026-05-23 12:00
 * @since 1.0.0
 */
@Component //将工具类注册为 Spring Bean
@Slf4j
public class SystemToolConfig {

    @Autowired
    private UserAddressService userAddressService;

    @Autowired
    private ProductInfoService productInfoService;

    @Autowired
    private OrdersService ordersService;

    @Autowired
    private OrderEvaluationService orderEvaluationService;


    @Tool(name = "getAddressByUserId", description = "查询用户的收件地址/收货地址")
    public UserAddressEntity getAddressByUserId(ToolContext toolContext){
        //获取大模型上下文中的用户ID
        String userId = (String)toolContext.getContext().get(CommonKeys.USER_ID);
        log.info("【查询用户的收件地址】-> userId:{}", userId);
        return userAddressService.getAddressByUserId(userId);
    }


    @Tool(name = "getAllProduct", description = "查询商品列表" +
            "每次只能查询一个商品关键词。如果用户提到了多个商品(例如'电脑和手机'),必须分别调用多次本工具,收集所有结果后统一回复用户。")
    public List<ProductInfoEntity> getAllProduct(
            @ToolParam(description = "商品名称关键词,从用户提问的关键词中获取。如果用户未提供关键词,则传null", required = false) String productName) {
        log.info("【查询商品列表】-> productName:{}", productName);
        return productInfoService.getAllProduct(productName);
    }


    @Tool(name = "saveOrder", description = "用户下订单" +
            "支持购买多个商品,参数 items 是商品列表,每个商品包含商品ID productId 和购买数量 count。" +
            "productId参数从工具getAllProduct的返回结果中获取productId字段。" +
            "当用户说出'买'、'购买'、'确认'、'确定'、'下单'、'确认下单'等购买意图时,立即调用本工具,传入商品ID和对应数量的列表。")
    public OrdersVo saveOrder(
            @ToolParam(description = "商品列表,格式:[{\\\"productId\\\":\\\"商品ID\\\",\\\"count\\\":数量}]")
                    List<OrderProductInfoDto.ProductInfoDto> items, ToolContext toolContext) {
        log.info("用户下单,商品列表:{}", JSON.toJSONString(items));
        OrderProductInfoDto dto = new OrderProductInfoDto();
        List<OrderProductInfoDto.ProductInfoDto> productInfoList = new ArrayList<>();
        for (OrderProductInfoDto.ProductInfoDto item : items) {
            OrderProductInfoDto.ProductInfoDto productInfoDto = new OrderProductInfoDto.ProductInfoDto();
            productInfoDto.setProductId(item.getProductId());// 商品ID
            productInfoDto.setCount(item.getCount());//购买数量
            productInfoList.add(productInfoDto);
        }
        dto.setProductInfoList(productInfoList);
        /*获取大模型上下文中的用户ID(注意:如果通过大模型对话,程序无法使用拦截器获取到上下文userId)
        String userId = UserContextHolder.customerUserInfo().getUserId();//也就是后续代码里出现这句话会抛异常,因为没有获取到用户ID,需要手动设置。
        */
        String userId = (String)toolContext.getContext().get(CommonKeys.USER_ID);
        return ordersService.saveOrder(dto, userId);
    }

    @Tool(name  = "getOrdersByUserId", description = "查询订单列表(查询我的订单)")
    public List<OrdersEntity> getOrdersByUserId(
            @ToolParam(description = "订单状态筛选,可选值:待付款/已付款/已发货/已完成/已取消,不传则查全部", required = false) String state,
            ToolContext toolContext) {
        //获取大模型上下文中的用户ID
        String userId = (String)toolContext.getContext().get(CommonKeys.USER_ID);
        //获取订单状态
        Integer orderState = OrderStateEnum.getCodeByMessage(state);
        log.info("【查询订单列表】-> state={},匹配的 orderState={}", state, orderState);
        return ordersService.getOrdersByUserId(userId, orderState);
    }

    @Tool(name  = "updateOrderState_PAID", description = "更新订单状态为已付款")
    public Boolean updateOrderState_PAID(
            @ToolParam(description = "订单ID,获取对话历史记录中最近一条,没有订单ID则调用工具 getOrdersByUserId 让用户确认哪一条订单ID") String orderId) {
        log.info("调用工具【已付款】-> orderId:{}", orderId);
        return ordersService.updateOrderState_PAID(orderId);
    }

    @Tool(name  = "updateOrderState_DELIVERED", description = "更新订单状态为已发货")
    public Boolean updateOrderState_DELIVERED(
            @ToolParam(description = "订单ID,获取对话历史记录中最近一条,没有订单ID则调用工具 getOrdersByUserId 让用户确认哪一条订单ID") String orderId) {
        log.info("调用工具【已发货】-> orderId:{}", orderId);
        return ordersService.updateOrderState_DELIVERED(orderId);
    }

    @Tool(name  = "updateOrderState_FINISHED", description = "更新订单状态为已完成" )
    public Boolean updateOrderState_FINISHED(
            @ToolParam(description = "订单ID,获取对话历史记录中最近一条,没有订单ID则调用工具 getOrdersByUserId 让用户确认哪一条订单ID") String orderId) {
        log.info("调用工具【已完成】-> orderId:{}", orderId);
        return ordersService.updateOrderState_FINISHED(orderId);
    }

    @Tool(name  = "updateOrderState_CANCELED", description = "更新订单状态为已取消")
    public Boolean updateOrderState_CANCELED(
            @ToolParam(description = "订单ID,获取对话历史记录中最近一条,没有订单ID则调用工具 getOrdersByUserId 让用户确认哪一条订单ID") String orderId) {
        log.info("调用工具【已取消】-> orderId:{}", orderId);
        return ordersService.updateOrderState_CANCELED(orderId);
    }

    @Tool(name = "getOrdersById", description = "查询订单详情")
    public OrdersVo getOrdersById(
            @ToolParam(description = "订单ID,获取对话历史记录中最近一条,没有订单ID则调用工具 getOrdersByUserId 让用户确认哪一条订单ID") String orderId) {
        log.info("【查询订单详情】-> orderId:{}", orderId);
        return ordersService.getOrdersById(orderId);
    }

    @Tool(name = "getEvaluationByProductId", description = "查询商品的评价详情" +
            "productId参数从工具getAllProduct的返回结果中获取。" +
            "如果用户没有明确指定商品ID或名称,请先调用getAllProduct展示商品列表让用户选择。" +
            "当用户说出'确认'、'确定'、'查看'等查询意图时,立即调用本工具,传入商品ID。")
    public List<OrderEvaluationVo> getEvaluationByProductId(
            @ToolParam(description = "商品ID") String productId) {
        log.info("【商品的评价详情】-> productId:{}", productId);
        return orderEvaluationService.getEvaluationByProductId(productId);
    }


    @Tool(name = "addOrderEvaluation", description = "新增订单评价" +
            "支持评价多个商品。productId参数从工具getAllProduct的返回结果中获取。" +
            "如果用户没有明确指定商品ID或名称,请先调用getAllProduct展示商品列表让用户选择。" +
            "当用户说出'确认'、'确定'、'评价'、'好评'、'中评'、'差评'等评价意图时,立即调用本工具,传入商品ID和对应评价等级和评价内容。" +
            "用户没有给评价等级和评价内容默认都是“好评”"+
            "注意:无需向用户询问'是否确认评价',直接调用即可。")
    public Boolean addOrderEvaluation(
            @ToolParam(description = "订单ID,从对话记录中获取。如果记录中没有,则先调用订单列表工具查询用户的订单,让用户确认要评价哪一条订单ID") String orderId,
            @ToolParam(description = "评价的商品列表,每个商品包含productId和evaluationLevel、evaluationContent。例如:[{\"productId\":\"商品ID1\",\"evaluationLevel\":\"好评\",\"evaluationContent\":\"商品很好,价格很优惠,快递送到家!\"}, {\"productId\":\"商品ID2\",\"evaluationLevel\":\"好评\",\"evaluationContent\":\"商品很好,使用体验感很好\"}]")
                    List<AddOrderEvaluationDto.ProductEvaluationDto> items, ToolContext toolContext) {
        log.info("用户新增订单评价,订单ID:{}", orderId);
        log.info("用户新增订单评价,items:{}", JSON.toJSONString(items));
        AddOrderEvaluationDto dto = new AddOrderEvaluationDto();
        dto.setOrderId(orderId);// 订单ID
        List<AddOrderEvaluationDto.ProductEvaluationDto> productEvaluationList = new ArrayList<>();
        for (AddOrderEvaluationDto.ProductEvaluationDto item : items) {
            AddOrderEvaluationDto.ProductEvaluationDto productEvaluationDto = new AddOrderEvaluationDto.ProductEvaluationDto();
            productEvaluationDto.setProductId(item.getProductId());// 商品ID
            productEvaluationDto.setEvaluationLevel(item.getEvaluationLevel());// 评价等级
            productEvaluationDto.setEvaluationContent(item.getEvaluationContent());// 评价内容
            productEvaluationList.add(productEvaluationDto);
        }
        dto.setProductEvaluationList(productEvaluationList);
        /*获取大模型上下文中的用户ID(注意:如果通过大模型对话,程序无法使用拦截器获取到上下文userId)
        String userId = UserContextHolder.customerUserInfo().getUserId();//也就是后续代码里出现这句话会抛异常,因为没有获取到用户ID,需要手动设置。
        */
        String userId = (String)toolContext.getContext().get(CommonKeys.USER_ID);
        return orderEvaluationService.addOrderEvaluation(dto, userId);
    }

}

关于 SystemToolConfig 的重要说明:

1、工具类是大模型调用我们电商系统的入口,是大模型与我们系统交互的桥梁,这个类包含 @Tool 注解方法的 Bean。这些方法就是 AI 可以调用的能力。

2、每个函数都需要标注 @Tool 注解才能注册成为工具,@Tool 注解有2个重要属性:name 和 description,重点完善这2个属性的描述,以便大模型能知道如何调用工具。

3、@ToolParam 是关于工具要求的传参字段说明,也是要重点掌握。如果某个属性非必填,需要标注 required = false。

4、在与大模型对话过程中,系统都是使用异步多线程的策略,因此无法通过当前线程获取到上下文信息,即:String userId = UserContextHolder.customerUserInfo().getUserId(); 会失效,因为程序里保存用户上下文是放在 ThreadLocal 单个线程里,只对单线程请求有效。

在大模型技术架构里,要获取当前用户信息,需要在发起 Chat 对话前就存入用户信息,如:

toolContext(Map.of(CommonKeys.USER_ID, userId))。

然后再使用:String userId = (String)toolContext.getContext().get(CommonKeys.USER_ID);

5.2.2 将工具暴露给外部调用者

通过 ToolCallbackProvider 将工具统一注册后再提供给外部调用者:

package com.customer.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author CSDN流放深圳
 * @description 工具类配置
 * @create 2026-05-23 12:09
 * @since 1.0.0
 */
@Configuration
@Slf4j
public class ToolCallBackConfig {


    /**
     * 将工具方法暴露给外部
     * @return
     */
    @Bean
    public ToolCallbackProvider toolCallbackProvider(SystemToolConfig systemToolConfig) {
        // 扫描 SystemTools Bean 中的所有 @Tool 方法,并创建为工具回调提供者
        MethodToolCallbackProvider provider = MethodToolCallbackProvider.builder()
                .toolObjects(systemToolConfig)
                .build();

        // 打印注册的 Tool 方法
        for (ToolCallback callback : provider.getToolCallbacks()) {
            log.info("注册 Tool: " + callback.getToolDefinition().name() + ", 描述: " + callback.getToolDefinition().description());
        }
        return provider;
    }

}

5.2.3 将工具回调设置到 ChatClient 中

package com.customer.config;

import com.alibaba.cloud.ai.memory.redis.JedisRedisChatMemoryRepository;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.api.Advisor;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaChatOptions;
import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor;
import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * @author CSDN流放深圳
 * @description 模型配置类
 * @create 2026-05-15 18:17
 * @since 1.0.0
 */
@Configuration
public class ChatClientConfig {

    /**
     * qwen 模型名称
     */
    private final String QWEN_MODEL_NAME = "qwen3.5:4b";

    @Value("${spring.ai.ollama.base-url}")
    private String BASE_URL;

    /**
     * 系统角色说明,从 classpath 加载资源文件,也可以通过 URL 加载
     */
    @Value("classpath:system-role-wording.txt")
    private Resource resource;

    /**
     * 创建一个 qwen 的 ChatModel
     * @return
     */
    @Bean
    @Primary
    public ChatModel qwen() {
        // 1. 先创建 OllamaApi,配置 base-url
        OllamaApi ollamaApi = OllamaApi.builder()
                .baseUrl(BASE_URL)
                .build();

        //2、通过 OllamaApi 创建 OllamaChatModel
        return OllamaChatModel.builder()
                .ollamaApi(ollamaApi)
                .defaultOptions(OllamaChatOptions.builder()
                        .model(QWEN_MODEL_NAME)
                        .build())
                .build();
    }

    /**
     * 创建 qwenChatClient
     * @param chatModel
     * @return
     */
    @Bean
    public ChatClient qwenChatClient(ChatModel chatModel,
                                     JedisRedisChatMemoryRepository repository,
                                     VectorStore vectorStore,
                                     ToolCallbackProvider toolCallbackProvider) {
        // 顾问集合
        List<Advisor> advisors = new ArrayList<>();
        // 创建消息窗口顾问
        MessageChatMemoryAdvisor messageChatMemoryAdvisor = MessageChatMemoryAdvisor.builder(
                MessageWindowChatMemory.builder().chatMemoryRepository(repository)
                        .maxMessages(20) // 最多保存20条消息(默认也是20条)
                        .build()).build();
        advisors.add(messageChatMemoryAdvisor);//添加消息窗口顾问

        // 创建向量存储顾问
        RetrievalAugmentationAdvisor vectorStoreAdvisor = RetrievalAugmentationAdvisor.builder()
                .documentRetriever(VectorStoreDocumentRetriever.builder().vectorStore(vectorStore).topK(4)//检索向量个数,默认也是4个
                .build()).build();
        advisors.add(vectorStoreAdvisor);//添加向量存储顾问

        // 其它选项(大模型调优参数,基本上设置温度即可)
        ChatOptions options = ChatOptions.builder()
                .temperature(0.0)//模型温度,设置0.0确定性输出,每次输出结果都一样,禁止创造性。工具调用 = 确定性 > 创造性。
                .build();

        // 创建 ChatClient
        return ChatClient.builder(chatModel)
                .defaultSystem(resource, StandardCharsets.UTF_8)//系统角色设定内容
                .defaultAdvisors(advisors)// 添加顾问集合
                .defaultToolCallbacks(toolCallbackProvider.getToolCallbacks())// 工具回调
                .defaultOptions(options)  // 其它选项
                .build();
    }

}

重要说明:

1、在创建 ChatClient Bean 我们增加了系统角色(System Role):指导 AI 的行为和响应方式,设置 AI 如何解释和回复输入的参数或规则。这类似于在发起对话之前向 AI 提供说明。

2、增加工具回调配置,把系统里有的工具全部告诉大模型。

3、参数调优。调整大模型温度 temperature 为 0.0,让大模型减少创造性,更注重工具调用的结果返回确定性。

4、本地模型这里改为使用 qwen3.5:4b,因为在开发过程中,发现模型版本越高且对算力要求越高,其深度思考的结果越准确。以下是各种模型对复杂 Tool 回复速度和准确率的表格对比(个人测试数据,仅作参考)。

序号 模型名称 回复速度 准确率
1

kamekichi128/qwen3-4b-instruct-2507:latest

1分钟左右 80%
2

qwen3:4b

2分钟左右 80%
3

qwen2.5:7b

30秒左右 50%
4

qwen3.5:2b

1分钟左右 80%
5

qwen3.5:4b

1分钟左右 95%

5.2.4 创建系统角色(system-role)话术文件

在 resources 目录下创建 system-role-wording.txt 文件,用于详细阐述大模型应该扮演什么角色,话术是什么。这个跟服务条款有区别,服务条款主要用于 RAG 也就是知识库,而系统角色是指导 AI 的行为和响应。当大模型无法返回你的预期效果的话,就要考虑调整工具 Tool 和调整系统角色的话术:

# 角色
你是电商平台的AI智能客服助手「小淘」,所有输出必须遵循以下格式规范。

# 规则1:必须使用可用的工具来完成用户请求
用户与你在对话过程中,匹配到用户关键词的,你必须使用提供的工具来完成,必须等工具调用成功后再回复用户,绝对不允许自己编造回复。


# 规则2:下单流程(必须遵守)
1. 用户说"购买XXX" → 先调用查询商品列表工具 getAllProduct 搜索商品。
2. 用户确认商品后 → 再调用 saveOrder 创建订单。
3. 禁止直接调用 saveOrder,必须先搜索确认商品ID。


# 规则3:自我介绍
用户提问"你是谁/自我介绍"时,回复:
我是电商平台的智能 AI 助手「小淘」。我们公司成立于2026年,总部位于深圳市,是国内领先的人工智能电子商务平台。我们的服务宗旨是"以用户为中心,坚持【好货不贵、服务到家、售后无忧】的核心价值"。平台官网:https://blog.csdn.net/BiandanLoveyou,版权所有:CSDN流放深圳


# 规则4:短回复
- 你好 / 在吗 / hi / hello → 您好!欢迎光临!我是智能客服助手「小淘」,请问有什么需要帮助?
- 人工 / 人工客服 → 人工客服工作时间 9:00-18:00,客服热线【400-800-6666】


# 规则5:可选参数的处理
当工具参数的 `required=false` 时,AI 可以直接调用该工具并传入 null 或不传该参数,无需追问用户。系统会使用默认值处理。


# 规则6:其他问题
回复:有关商品咨询、订单查询、售后服务的问题都可以直接提问我哦~

5.2.5 将用户信息放入大模型上下文中

刚才说过,在与大模型对话中,不能使用拦截器设置的上下文用户信息,因为大模型是使用异步多线程的,无法获取到单线程的用户信息,也就是无法使用:

String userId = UserContextHolder.customerUserInfo().getUserId();

来获取用户信息,而是通过:

String userId = (String)toolContext.getContext().get(CommonKeys.USER_ID);

来获取用户信息,因此需要在每一次发起与大模型的对话之前,把用户信息放入大模型的上下文中

package com.customer.service.impl;

import com.customer.entity.constant.CommonKeys;
import com.customer.entity.dto.ChatDTO;
import com.customer.holder.UserContextHolder;
import com.customer.service.ChatService;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

import java.util.Map;

/**
 * @author CSDN流放深圳
 * @description 聊天服务实现类
 * @create 2026-05-16 12:05
 * @since 1.0.0
 */
@Service
public class ChatServiceImpl implements ChatService {

    @Autowired
    private ChatClient chatClient;


    /**
     * 与大模型AI聊天
     *
     * @param dto
     * @return 流式输出
     */
    @Override
    public Flux<String> chat(ChatDTO dto) {
        //从上下文获取用户ID
        String userId = UserContextHolder.customerUserInfo().getUserId();

        return chatClient.prompt()
                .user(dto.getUserMessage())
                //设置用户ID到大模型的上下文中,还可以设置其他业务参数,放在 Map 集合中
                .toolContext(Map.of(CommonKeys.USER_ID, userId))
                // 增加“顾问”,允许通过注入检索数据(Retrieval Context)和对话历史(Chat Memory)来修改传入的 Prompt
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, userId)) //固定值 chat_memory_conversation_id
                .stream().content();
    }
}

6、系统演示

6.1 修改 yml 配置与你本地的环境匹配

需要修改以下内容项,以匹配你本地环境:

1、Redis 相关配置,如IP、端口号、连接密码等。

2、MySql 相关配置,如IP、端口号、数据库名等,并且需要提前初始化数据库脚本。

3、Ollama 相关配置,如IP地址。

4、大模型配置,需要提前在 Ollama 工具中下载好对应的大模型。本次用到2个大模型,一个是文本嵌入模型(qwen3-embedding:4b)、大语言模型(qwen3.5:4b)。

5、RedisStack 相关配置,需要提前安装 RedisStack。(如果不想安装RedisStack,则无法使用 RAG 相关功能,需要去掉那部分功能代码,只保留核心对话功能。)

6.2 系统启动后,初始化数据到向量数据库

在控制台可以看到初始化了 4 个文档到向量数据库,注册了多个 Tool 工具。

在 RedisStack 管理后台可以看到有 4 个向量数据,也就是服务条款文件被拆成4个文档存入向量数据库,同时有一个 String 类型的数据用于管理向量数据。

6.3 系统登录(支持多用户,数据间有隔离)

用户表 user_account 里初始化了几条数据,密码都是 123

6.4 用户问候:你好、在吗,使用 RAG 检索增强回复系统角色设定内容

让大模型自我介绍/你是谁,回复规定的内容:

6.5 用户询问平台在售商品

6.6 用户下单流程

后端控制台输出:

查询数据库记录:

6.7 更新订单状态

把订单改为已发货状态:

把订单改为已完成状态:

6.8 查询订单信息

6.9 给订单好评

查询数据库订单评价表:

没毛病,老铁!

其它功能可以自行测试和完善。

7、开发过程遇到的问题与解决办法(总结)

7.1 与大模型聊天遇到乱码

在开发过程中遇到了一个中文乱码的问题,把 IDEA 编辑器的编码设置了 UTF-8、把 server 编码也设置了 UTF8 也不行,另外也验证了大模型返回的数据其实不会乱码。最后查看资料,只需要在接收聊天的 Controller 层处理一下响应的编码即可。

    /**
     * 与大模型AI聊天
     * @param dto
     * @return 流式输出
     */
    @PostMapping(value = "/chat" , produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> chat(@RequestBody ChatDTO dto, HttpServletResponse response) {
        // 关键:手动设置响应头,避免中文乱码
        response.setContentType("text/event-stream;charset=UTF-8");
        return chatsService.chat(dto);
    }

7.2 获取不到上下文当前用户

在与大模型对话过程,使用拦截器获取上下文用户信息时,无法获取到,抛出异常。即:

String userId = UserContextHolder.customerUserInfo().getUserId();

不能使用该方法获取与大模型对话的上下文用户信息。因为该方案使用的是本地隔离线程 ThreadLocal,无法在多线程中共享用户信息。大模型使用的就是多线程机制。因此,要解决这个办法需要把用户信息放到大模型的上下文中,即:

在使用时,先在函数中增加入参:

ToolContext toolContext

再使用:

String userId = (String)toolContext.getContext().get(CommonKeys.USER_ID);

当然,一些业务属性(比如用户姓名、性别、所属部门)等也可以放入上下文中,可以直接获取到。

7.3 大模型已读乱回

因为本人电脑配置不算太高,使用以下几个大模型作对比后得出小结:

序号 模型名称 回复速度 准确率
1

kamekichi128/qwen3-4b-instruct-2507:latest

1分钟左右 80%
2

qwen3:4b

2分钟左右 80%
3

qwen2.5:7b

30秒左右 50%
4

qwen3.5:2b

1分钟左右 80%
5

qwen3.5:4b

1分钟左右 95%

越是深度思考(即算力要求越高)的大模型,回答准确率越高,当然,回答的速度也会对应的变慢。这就很好说明了当前世界科技企业都在搞算力!!!

比如我使用 qwen2.5:7b 这个模型,它有时候会偷懒,直接以文字的形式描述调用工具 Tool 的过程,而不是直接去调用。

除了大模型本身问题之外,Tool 的描述也至关重要。写得越明白,大模型执行起来就越准确!

8、结尾

        从确定要搞项目到写完技术博客,前后共花费3周多零星时间。纸上得来终觉浅,绝知此事要躬行。但有理论知识还不行,还得有实践经验。

        后续还会继续深究 AI 人工智能,比如使用阿里百练平台,搭建 MCP 等。敬请期待吧!

        另:本人出品的代码、博客,绝不设门槛,绝不收费!因为在 IT 界还有千千万万的新手想学习一门技术,不是不重视原著或者知识产权,而是:世界上最珍贵的东西都是免费的!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐