AI智能课堂推荐系统

目录:

一、项目架构分析

典型的 Spring Cloud 微服务架构 ,采用 前后端分离 + 微服务拆分 的设计模式。

二、网关服务实现详解

1、技术选型与依赖

网关服务基于 Spring Cloud Gateway 构建,这是一个非阻塞式的响应式网关。

2、项目结构

3、核心实现详解

3.1、启动类详解

java 复制代码
@Slf4j
@EnableScheduling
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) throws UnknownHostException {
        SpringApplication app = new SpringApplicationBuilder(GatewayApplication.class).build(args);
        Environment env = app.run(args).getEnvironment();
        // 打印启动信息(协议、端口、环境)
        log.info("--/\n---------------------------------------------------------------------------------------\n\t" +
                        "Application '{}' is running! Access URLs:\n\t" +
                        "Local: \t\t{}://localhost:{}\n\t" +
                        "External: \t{}://{}:{}\n\t" +
                        "Profile(s): \t{}" +
                        "\n---------------------------------------------------------------------------------------",
                env.getProperty("spring.application.name"),
                protocol, env.getProperty("server.port"),
                protocol, InetAddress.getLocalHost().getHostAddress(),
                env.getProperty("server.port"),
                env.getActiveProfiles());
    }
}

3.2、application.yml路由规则定义

yaml 复制代码
server:
  port: 10010  #端口
  tomcat:
    uri-encoding: UTF-8   #服务编码
spring:
  profiles:
    active: local
  application:
    name: gateway-service
  cloud:
    gateway:
      routes:
        - id: ms
          uri: lb://media-service
          predicates:
            - Path=/ms/**
        - id: as
          uri: lb://auth-service
          predicates:
            - Path=/as/**
          filters:
            - PreserveHostHeader
        - id: ds
          uri: lb://data-service
          predicates:
            - Path=/ds/**
        - id: sms
          uri: lb://message-service
          predicates:
            - Path=/sms/**
        - id: us
          uri: lb://user-service
          predicates:
            - Path=/us/**
        - id: cs
          uri: lb://course-service
          predicates:
            - Path=/cs/**
        - id: os
          uri: lb://order-service
          predicates:
            - Path=/os/**
        - id: ss
          uri: lb://search-service
          predicates:
            - Path=/ss/**
        - id: ls
          uri: lb://learning-service
          predicates:
            - Path=/ls/**
        - id: ps
          uri: lb://pay-service
          predicates:
            - Path=/ps/**
        - id: ts
          uri: lb://trade-service
          predicates:
            - Path=/ts/**
        - id: es
          uri: lb://exam-service
          predicates:
            - Path=/es/**
        - id: rs
          uri: lb://remark-service
          predicates:
            - Path=/rs/**
        - id: prs
          uri: lb://promotion-service
          predicates:
            - Path=/prs/**
        - id: ais
          uri: lb://aigc-service
          predicates:
            - Path=/ais/**
      default-filters:
        - StripPrefix=1
      globalcors: # 全局的跨域处理
        add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
        corsConfigurations:
          '[/**]':
            allowedOriginPatterns: # 允许哪些网站的跨域请求
              - "*"
            allowedMethods: # 允许的跨域ajax的请求方式
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*" # 允许在请求中携带的头信息
            allowCredentials: true # 是否允许携带cookie
            maxAge: 360000 # 这次跨域检测的有效期

3.3、配置属性 ( AuthProperties.java )

java 复制代码
@Data
@Component
@ConfigurationProperties(prefix = "tj.auth")
public class AuthProperties implements InitializingBean {
    private Set<String> excludePath;  // 无需登录的路径集合

    @Override
    public void afterPropertiesSet() throws Exception {
        // 添加默认不拦截的路径
        excludePath.add("/error/**");
        excludePath.add("/jwks");
        excludePath.add("/accounts/login");
        excludePath.add("/accounts/admin/login");
        excludePath.add("/accounts/refresh");
    }
}

3.4、请求ID追踪过滤器 ( RequestIdRelayFilter.java )

java 复制代码
@Component
public class RequestIdRelayFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1.生成RequestId(去掉横杠的UUID)
        String requestId = UUID.randomUUID().toString(true);
        
        // 2.保存到日志变量池(MDC),便于日志追踪
        MDC.put(REQUEST_ID_HEADER, requestId);
        
        // 3.更新请求头,传递给下游服务
        exchange = exchange.mutate().request(b -> {
            b.header(REQUEST_ID_HEADER, requestId);
            // 添加网关来源标识(支付回调除外)
            if (!path.startsWith("/ps/notify")) {
                b.header(REQUEST_FROM_HEADER, GATEWAY_ORIGIN_NAME);
            }
        }).build();

        return chain.filter(exchange);
    }
}

3.5、 账号认证过滤器 ( AccountAuthFilter.java )

java 复制代码
@Component
public class AccountAuthFilter implements GlobalFilter, Ordered {
    private final AuthUtil authUtil;           // 认证工具类(来自auth-gateway-sdk)
    private final AuthProperties authProperties;
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String method = request.getMethod().name();
        String path = request.getPath().toString();
        String antPath = method + ":" + path;  // 组合成 "GET:/users/me" 格式

        // 1.判断是否是无需登录的路径
        if(isExcludePath(antPath)){
            return chain.filter(exchange);
        }

        // 2.从请求头获取Token
        List<String> authHeaders = exchange.getRequest().getHeaders().get(AUTHORIZATION_HEADER);
        String token = authHeaders == null ? "" : authHeaders.get(0);

        // 3.解析Token(调用auth-gateway-sdk)
        R<LoginUserDTO> r = authUtil.parseToken(token);

        // 4.如果解析成功,将用户信息放入请求头传递给下游
        if (r.success()) {
            exchange.mutate()
                    .request(builder -> builder
                            .header(USER_HEADER, r.getData().getUserId().toString())
                            .header(TOKEN_HEADER, token))
                    .build();
        }

        // 5.校验权限(失败会抛出异常)
        authUtil.checkAuth(antPath, r);

        // 6.放行
        return chain.filter(exchange);
    }
}

认证流程 :

3.6、 全局异常处理器 ( GatewayExceptionHandler.java )

java 复制代码
@Component
public class GatewayExceptionHandler implements ErrorWebExceptionHandler, Ordered {
    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        ServerHttpResponse response = exchange.getResponse();
        
        // 1.如果响应已提交,直接返回
        if (response.isCommitted()) {
            return Mono.error(ex);
        }

        // 2.根据异常类型处理
        String message;
        int code = FAILED;
        if (ex instanceof UnauthorizedException) {
            // 登录异常:直接返回HTTP状态码
            return Mono.error(new ResponseStatusException(e.getStatus(), e.getMessage(), e));
        } else if (ex instanceof CommonException) {
            // 业务异常:使用自定义错误码和消息
            code = e.getCode();
            message = e.getMessage();
        } else if (ex instanceof NotFoundException) {
            // 服务不存在
            message = "服务不存在";
        } else {
            // 其他异常:记录日志,返回通用错误
            message = SERVER_INTER_ERROR;
            writeLog(exchange, ex);
        }

        // 3.封装统一响应格式
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        R<Object> r = R.error(code, message);
        // 添加RequestId到响应
        if (requestIds != null) {
            r.requestId(requestIds.get(0));
        }
        
        // 4.写出响应
        byte[] resp = JsonUtils.toJsonStr(r).getBytes(StandardCharsets.UTF_8);
        return response.writeWith(Mono.fromSupplier(() -> response.bufferFactory().wrap(resp)));
    }
}

3.7、 Nacos配置中心 ( application-local.yml )

yaml 复制代码
spring:
  cloud:
    nacos:
      server-addr: 192.168.150.101:8848
      username: nacos
      password: nacos
      discovery:
        namespace: f923fb34-cb0a-4c06-8fca-ad61ea61a3f0
        group: DEFAULT_GROUP
        ip: 192.168.150.1
      config:
        file-extension: yaml
  config:
    import:
      - nacos:${spring.application.name}.yaml    # 网关自身配置
      - nacos:shared-spring.yaml                 # 共享Spring配置
      - nacos:shared-redis.yaml                  # 共享Redis配置
      - nacos:shared-logs.yaml                   # 共享日志配置

请求流程:

三、AI问答助手服务实现详解

1、技术选型

2、项目结构

3、核心实现流程

3.1 对话请求入口 ( ChatController.java )

java 复制代码
@Slf4j
@RestController
@RequestMapping("/chat")
@RequiredArgsConstructor
public class ChatController {

    private final ChatService chatService;

    // @NoWrapper 标记结果不进行包装
    // produces = MediaType.TEXT_EVENT_STREAM_VALUE 声明返回SSE流
    @NoWrapper
    @PostMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ChatEventVO> chat(@RequestBody ChatDTO chatDTO) {
        return this.chatService.chat(chatDTO.getQuestion(), chatDTO.getSessionId());
    }
}

关键点 :

  • 使用 @NoWrapper 跳过统一响应包装,直接返回数据
  • 返回类型 Flux 是响应式流,实现 SSE (Server-Sent Events)
  • MediaType.TEXT_EVENT_STREAM_VALUE 声明这是流式响应

3.2 流式对话服务 ( ChatServiceImpl.java )

java 复制代码
@Slf4j
@Service
@RequiredArgsConstructor
public class ChatServiceImpl implements ChatService {

    private final ChatClient chatClient;              // Spring AI ChatClient
    private final SystemPromptConfig systemPromptConfig; // 系统提示词

    @Override
    public Flux<ChatEventVO> chat(String question, String sessionId) {
        return this.chatClient.prompt()
                .system(promptSystemSpec -> promptSystemSpec
                        .text(this.systemPromptConfig.getChatSystemMessage().get())
                        .param("now", DateUtil.now()))
                .user(question)
                .stream()                                   // 流式调用
                .chatResponse()
                .doFirst(() -> GENERATE_STATUS.put(sessionId, true))   // 开始生成
                .doOnComplete(() -> GENERATE_STATUS.remove(sessionId)) // 生成完成
                .doOnError(throwable -> GENERATE_STATUS.remove(sessionId)) // 生成失败
                .takeWhile(s -> Optional.ofNullable(GENERATE_STATUS.get(sessionId)).orElse(false))
                .map(chatResponse -> {
                    // 获取大模型输出的文本
                    String text = chatResponse.getResult().getOutput().getText();
                    return ChatEventVO.builder()
                            .eventData(text)                    // 文本内容
                            .eventType(ChatEventTypeEnum.DATA.getValue())  // 1001=数据事件
                            .build();
                })
                .concatWith(Flux.just(ChatEventVO.builder()  // 最后发送停止事件
                        .eventType(ChatEventTypeEnum.STOP.getValue())  // 1002=停止事件
                        .build()));
    }
}

3.3 系统提示词热加载 ( SystemPromptConfig.java )

java 复制代码
@Slf4j
@Getter
@Configuration
@RequiredArgsConstructor
public class SystemPromptConfig {
    private final NacosConfigManager nacosConfigManager;
    private final AIProperties aiProperties;
    
    // 原子引用,线程安全
    private final AtomicReference<String> chatSystemMessage = new AtomicReference<>();

    @PostConstruct
    public void init() {
        loadConfig(aiProperties.getSystem().getChat(), chatSystemMessage);
    }

    private void loadConfig(AIProperties.System.Chat chatConfig, AtomicReference<String> target) {
        String dataId = chatConfig.getDataId();      // system-chat-message.txt
        String group = chatConfig.getGroup();         // DEFAULT_GROUP
        long timeoutMs = chatConfig.getTimeoutMs();   // 20000ms

        // 1. 从Nacos读取配置
        String config = nacosConfigManager.getConfigService().getConfig(dataId, group, timeoutMs);
        target.set(config);

        // 2. 添加监听器,实现热更新
        nacosConfigManager.getConfigService().addListener(dataId, group, new Listener() {
            @Override
            public Executor getExecutor() { return null; }
            
            @Override
            public void receiveConfigInfo(String info) {
                target.set(info);  // 配置变更时自动更新
                log.info("更新系统提示词成功");
            }
        });
    }
}

配置来源 ( application.yml ):

yaml 复制代码
tj:
  ai:
    prompt:
      system:
        chat:
          data-id: system-chat-message.txt   # Nacos中的配置文件
          group: DEFAULT_GROUP
          timeout-ms: 20000

设计亮点 :

  • 系统提示词存储在 Nacos配置中心 ,无需重启即可更新
  • 使用 AtomicReference 保证并发安全
  • 配置变更时自动触发回调,实现热更新

3.4 Spring AI配置 ( SpringAIConfig.java )

java 复制代码
@Configuration
public class SpringAIConfig {

    @Bean
    public ChatClient chatClient(ChatClient.Builder chatClientBuilder, Advisor loggerAdvisor) {
        return chatClientBuilder
                .defaultAdvisors(loggerAdvisor)  // 添加日志记录增强器
                .build();
    }

    @Bean
    public Advisor loggerAdvisor() {
        return new SimpleLoggerAdvisor();  // 打印对话日志
    }
}

3.5、完整请求流程

java 复制代码
┌─────────────────────────────────────────────────────────────────────────┐
│                           用户请求流程                                   │
├─────────────────────────────────────────────────────────────────────────┤
│  1. POST /chat {"question": "什么是Java?", "sessionId": "xxx"}         │
│     ↓                                                                    │
│  2. ChatController.chat()                                               │
│     ↓                                                                    │
│  3. ChatServiceImpl.chat()                                               │
│     ↓                                                                    │
│  4. 构建 Prompt(系统提示词 + 用户问题)                                  │
│     ↓                                                                    │
│  5. ChatClient.stream() 发起流式请求                                      │
│     ↓                                                                    │
│  6. 阿里云通义千问 API 返回流式数据                                        │
│     ↓                                                                    │
│  7. 每收到一个文本片段,封装为 ChatEventVO 事件                           │
│     ↓                                                                    │
│  8. Flux<ChatEventVO> 通过 SSE 推送给客户端                              │
│     ↓                                                                    │
│  9. 客户端收到 "data: {...eventData: '部', eventType: 1001}"            │
│     客户端收到 "data: {...eventData: '分', eventType: 1001}"            │
│     ... 逐字显示(打字机效果)                                            │
│     客户端收到 "data: {...eventData: null, eventType: 1002}"            │
│     ↑ 停止事件,结束                                                     │
└─────────────────────────────────────────────────────────────────────────┘

四、会话管理在AI问答中的作用详解

1、会话管理的四大核心作用

java 复制代码
┌─────────────────────────────────────────────────────────────────────────────────┐
│                      会话管理在AI问答中的四大核心作用                              │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                  │
│   作用1: 唯一标识 ──────────────────────────────────────────────────────────────│
│   ┌────────────────────────────────────────────────────────────────────────────┐│
│   │  sessionId 是一次完整对话的唯一标识                                           ││
│   │  • 区分不同用户的对话                                                        ││
│   │  • 区分同一用户的不同话题                                                    ││
│   │  • 用于前端展示对话列表                                                      ││
│   └────────────────────────────────────────────────────────────────────────────┐│
│                                                                                  │
│   作用2: 多轮对话关联 ───────────────────────────────────────────────────────────│
│   ┌────────────────────────────────────────────────────────────────────────────┐│
│   │  同一个 sessionId 下的所有消息属于同一对话                                   ││
│   │  • 用户: "什么是Java多态?"                                                 ││
│   │  • AI:  "多态是指..."                                                       ││
│   │  • 用户: "能举个例子吗?"          ← 同一个sessionId                       ││
│   │  • AI:  "比如动物类..."           ← AI知道之前在讨论多态                    ││
│   └────────────────────────────────────────────────────────────────────────────┐│
│                                                                                  │
│   作用3: 生成状态控制 ───────────────────────────────────────────────────────────│
│   ┌────────────────────────────────────────────────────────────────────────────┐│
│   │  GENERATE_STATUS Map 记录每个会话的生成状态                                  ││
│   │  • 标记正在生成:GENERATE_STATUS.put(sessionId, true)                       ││
│   │  • 停止生成:GENERATE_STATUS.put(sessionId, false)                         ││
│   │  • 防止同一会话并发生成                                                      ││
│   └────────────────────────────────────────────────────────────────────────────┐│
│                                                                                  │
│   作用4: 会话持久化 ─────────────────────────────────────────────────────────────│
│   ┌────────────────────────────────────────────────────────────────────────────┐│
│   │  chat_session 表存储会话元数据                                              ││
│   │  • 用户ID:userId                                                          ││
│   │  • 会话标题:title(用于展示)                                              ││
│   │  • 创建时间:createTime                                                    ││
│   └────────────────────────────────────────────────────────────────────────────┐│
│                                                                                  │
└─────────────────────────────────────────────────────────────────────────────────┘

2、详细流程图(分步骤讲解)

步骤1:创建会话

java 复制代码
┌─────────────────────────────────────────────────────────────────────────────────┐
│  步骤1:创建会话                                                                 │
│  目的:为用户生成一个唯一对话标识,并返回示例问题供参考                            │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                  │
│   用户打开AI助手                                              前端               │
│       │                                                                          │
│       │  1.用户点击"新对话"按钮                                                 │
│       │                                                                          │
│       │──────────────────────────────────────────────────────────────────────▶ │
│       │                                                                          │
│       │  2.前端发送请求                                                          │
│       │  POST /session?n=3                                                      │
│       │                                                                          │
│       │              ┌──────────────────────────────────────────────────────┐   │
│       │              │              tj-gateway                               │   │
│       │              │  • 路由匹配:/ais/** → aigc-service                    │   │
│       │              │  • 认证校验:解析Token获取userId                        │   │
│       │              │  • StripPrefix:去除 /ais 前缀                         │   │
│       │              └──────────────────────────┬───────────────────────────┘   │
│       │                                         │                               │
│       │              ┌──────────────────────────▼───────────────────────────┐   │
│       │              │              SessionController                       │   │
│       │              │  @PostMapping                                         │   │
│       │              │  public SessionVO createSession(Integer num)         │   │
│       │              └──────────────────────────┬───────────────────────────┘   │
│       │                                         │                               │
│       │              ┌──────────────────────────▼───────────────────────────┐   │
│       │              │         ChatSessionServiceImpl                       │   │
│       │              │                                                       │   │
│       │              │  ┌─────────────────────────────────────────────────┐│   │
│       │              │  │ 步骤A: 生成唯一sessionId                          ││   │
│       │              │  │ sessionId = IdUtil.fastSimpleUUID()              ││   │
│       │              │  │ 结果: "a1b2c3d4e5f6g7h8i9j0"                      ││   │
│       │              │  └─────────────────────────────────────────────────┘│   │
│       │              │                                                       │   │
│       │              │  ┌─────────────────────────────────────────────────┐│   │
│       │              │  │ 步骤B: 从配置读取AI助手信息                        ││   │
│       │              │  │ SessionProperties (从Nacos配置中心读取)           ││   │
│       │              │  │ • title: "天机AI助手"                            ││   │
│       │              │  │ • describe: "我可以帮你解答..."                  ││   │
│       │              │  │ • examples: [多个示例问题]                       ││   │
│       │              │  └─────────────────────────────────────────────────┘│   │
│       │              │                                                       │   │
│       │              │  ┌─────────────────────────────────────────────────┐│   │
│       │              │  │ 步骤C: 随机选择n个示例问题                         ││   │
│       │              │  │ RandomUtil.randomEleList(examples, 3)            ││   │
│       │              │  │ 选择结果:                                        ││   │
│       │              │  │ • "Java基础" → "解释面向对象"                     ││   │
│       │              │  │ • "Spring Boot" → "如何搭建项目"                 ││   │
│       │              │  │ • "数据库" → "MySQL优化技巧"                     ││   │
│       │              │  └─────────────────────────────────────────────────┘│   │
│       │              │                                                       │   │
│       │              │  ┌─────────────────────────────────────────────────┐│   │
│       │              │  │ 步骤D: 保存会话到数据库                           ││   │
│       │              │  │ ChatSession chatSession = ChatSession.builder() ││   │
│       │              │  │     .sessionId("a1b2c3d4...")                    ││   │
│       │              │  │     .userId(UserContext.getUser()) ← 当前用户ID  ││   │
│       │              │  │     .build();                                     ││   │
│       │              │  │ super.save(chatSession);                         ││   │
│       │              │  └─────────────────────────────────────────────────┘│   │
│       │              │                                                       │   │
│       │              │                                                       │   │
│       │              │              ┌─────────────────┐                     │   │
│       │              │              │  MySQL          │                     │   │
│       │              │              │ chat_session 表 │                     │   │
│       │              │              ├─────────────────┤                     │   │
│       │              │              │ id | 123        │                     │   │
│       │              │              │ session_id | a1b2 │                     │   │
│       │              │              │ user_id | 1001  │                     │   │
│       │              │              │ title | NULL    │                     │   │
│       │              │              │ create_time | ..│                     │   │
│       │              │              └─────────────────┘                     │   │
│       │              │                                                       │   │
│       │              └──────────────────────────────────────────────────────┘   │
│       │                                                                         │
│       │  3.返回SessionVO给前端                                                 │
│       │◀───────────────────────────────────────────────────────────────────── │
│       │                                                                         │
│       │  ┌───────────────────────────────────────────────────────────────────┐│
│       │  │ SessionVO                                                          ││
│       │  │ {                                                                  ││
│       │  │   "sessionId": "a1b2c3d4e5f6g7h8i9j0",                            ││
│       │  │   "title": "天机AI助手",                                           ││
│       │  │   "describe": "我可以帮你解答学习问题...",                          ││
│       │  │   "examples": [                                                    ││
│       │  │     {"title": "Java基础", "describe": "解释面向对象"},             ││
│       │  │     {"title": "Spring Boot", "describe": "如何搭建项目"},          ││
│       │  │     {"title": "数据库", "describe": "MySQL优化技巧"}               ││
│       │  │   ]                                                                ││
│       │  │ }                                                                  ││
│       │  └───────────────────────────────────────────────────────────────────┘│
│       │                                                                         │
│       │  4.前端展示                                                            │
│       │  ┌───────────────────────────────────────────────────────────────────┐│
│       │  │ ┌───────────────────────────────────────────────────────────────┐ ││
│       │  │ │ 天机AI助手                                                    │ ││
│       │  │ │ 我可以帮你解答学习问题...                                     │ ││
│       │  │ ├───────────────────────────────────────────────────────────────┤ ││
│       │  │ │ 你可以问我:                                                  │ ││
│       │  │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐              │ ││
│       │  │ │ │ Java基础    │ │ Spring Boot │ │ 数据库      │              │ ││
│       │  │ │ │ 解释面向对象│ │ 如何搭建项目│ │ MySQL优化   │              │ ││
│       │  │ │ └─────────────┘ └─────────────┘ └─────────────┘              │ ││
│       │  │ ├───────────────────────────────────────────────────────────────┤ ││
│       │  │ │ [输入框: 请输入你的问题...]                                    │ ││
│       │  │ └───────────────────────────────────────────────────────────────┘ ││
│       │  └───────────────────────────────────────────────────────────────────┘│
│                                                                                  │
└─────────────────────────────────────────────────────────────────────────────────┘

步骤2:发送第一条消息

java 复制代码
┌─────────────────────────────────────────────────────────────────────────────────┐
│  步骤2:用户发送第一条消息                                                       │
│  目的:开始与AI对话,sessionId用于标识这次对话                                   │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                  │
│   用户输入问题                                               前端               │
│       │                                                                          │
│       │  1.用户输入:"什么是Java多态?"                                         │
│       │                                                                          │
│       │  2.前端发送请求(携带sessionId)                                        │
│       │  POST /chat                                                              │
│       │  {                                                                       │
│       │    "question": "什么是Java多态?",                                      │
│       │    "sessionId": "a1b2c3d4e5f6g7h8i9j0"  ← 步骤1返回的sessionId          │
│       │  }                                                                       │
│       │                                                                          │
│       │              ┌──────────────────────────────────────────────────────┐   │
│       │              │              ChatController                          │   │
│       │              │  @PostMapping(produces = TEXT_EVENT_STREAM_VALUE)    │   │
│       │              │  public Flux<ChatEventVO> chat(ChatDTO chatDTO)       │   │
│       │              │                                                       │   │
│       │              │  关键点:                                               │   │
│       │              │  • @NoWrapper: 不包装响应                              │   │
│       │              │  • TEXT_EVENT_STREAM_VALUE: SSE流式响应               │   │
│       │              │  • Flux<ChatEventVO>: 响应式流                        │   │
│       │              └──────────────────────────┬───────────────────────────┘   │
│       │                                         │                               │
│       │              ┌──────────────────────────▼───────────────────────────┐   │
│       │              │         ChatServiceImpl                              │   │
│       │              │                                                       │   │
│       │              │  ┌─────────────────────────────────────────────────┐│   │
│       │              │  │ 关键步骤1: 标记生成状态                            ││   │
│       │              │  │                                                  ││   │
│       │              │  │ GENERATE_STATUS.put(sessionId, true)             ││   │
│       │              │  │                                                  ││   │
│       │              │  │ GENERATE_STATUS 内存状态:                        ││   │
│       │              │  │ ┌─────────────────────────────────────────────┐ ││   │
│       │              │  │ │ "a1b2c3d4..." → true  ← 正在生成             │ ││   │
│       │              │  │ │ "其他sessionId" → false                       │ ││   │
│       │              │  │ └─────────────────────────────────────────────┘ ││   │
│       │              │  └─────────────────────────────────────────────────┘│   │
│       │              │                                                       │   │
│       │              │  ┌─────────────────────────────────────────────────┐│   │
│       │              │  │ 关键步骤2: 构建Prompt                             ││   │
│       │              │  │                                                  ││   │
│       │              │  │ this.chatClient.prompt()                        ││   │
│       │              │  │   .system(promptSystemSpec -> {                  ││   │
│       │              │  │       // 从Nacos读取系统提示词                    ││   │
│       │              │  │       // systemPromptConfig.getChatSystemMessage ││   │
│       │              │  │       // 内容示例:                                ││   │
│       │              │  │       // "你是天机学堂的AI助手,                  ││   │
│       │              │  │       //  当前时间: {now}"                        ││   │
│       │              │  │       .text(systemPrompt)                        ││   │
│       │              │  │       .param("now", "2026-06-27 10:30:00")       ││   │
│       │              │  │   })                                              ││   │
│       │              │  │   .user("什么是Java多态?")  ← 用户问题           ││   │
│       │              │  │                                                  ││   │
│       │              │  │ ⚠️ 注意: 没有历史消息,只有当前问题               ││   │
│       │              │  └─────────────────────────────────────────────────┘│   │
│       │              │                                                       │   │
│       │              │  ┌─────────────────────────────────────────────────┐│   │
│       │              │  │ 关键步骤3: 流式调用AI                             ││   │
│       │              │  │                                                  ││   │
│       │              │  │   .stream()                                      ││   │
│       │              │  │   .chatResponse()                                ││   │
│       │              │  │                                                  ││   │
│       │              │  │ ┌───────────────────────────────────────────┐   ││   │
│       │              │  │ │            阿里云通义千问API                │   ││   │
│       │              │  │ │                                            │   ││   │
│       │              │  │ │  输入:                                     │   ││   │
│       │              │  │ │  system: 你是天机学堂AI助手...             │   ││   │
│       │              │  │ │  user: 什么是Java多态?                     │   ││   │
│       │              │  │ │                                            │   ││   │
│       │              │  │ │  输出(流式):                               │   ││   │
│       │              │  │ │  "Java" → "多态" → "是" → "面向" → ...      │   ││   │
│       │              │  │ └───────────────────────────────────────────┘   ││   │
│       │              │  └─────────────────────────────────────────────────┘│   │
│       │              │                                                       │   │
│       │              └──────────────────────────────────────────────────────┘   │
│                                                                                  │
└─────────────────────────────────────────────────────────────────────────────────┘

步骤3:流式响应与状态控制

java 复制代码
┌─────────────────────────────────────────────────────────────────────────────────┐
│  步骤3:流式响应返回给前端                                                       │
│  目的:实现打字机效果,sessionId控制生成状态                                     │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                  │
│   AI模型返回流式数据                                          AI服务            │
│       │                                                                          │
│       │  每收到一个文本片段:                                                     │
│       │                                                                          │
│       │              ┌──────────────────────────────────────────────────────┐   │
│       │              │         ChatServiceImpl (响应式流处理)               │   │
│       │              │                                                       │   │
│       │              │  .map(chatResponse -> {                               │   │
│       │              │      String text = chatResponse                       │   │
│       │              │          .getResult()                                 │   │
│       │              │          .getOutput()                                 │   │
│       │              │          .getText();                                  │   │
│       │              │                                                       │   │
│       │              │      return ChatEventVO.builder()                     │   │
│       │              │          .eventData(text)                             │   │
│       │              │          .eventType(1001)  ← DATA事件                 │   │
│       │              │          .build();                                    │   │
│       │              │  })                                                    │   │
│       │              │                                                       │   │
│       │              │  .takeWhile(s ->                                       │   │
│       │              │      GENERATE_STATUS.get(sessionId) == true           │   │
│       │              │  )                                                     │   │
│       │              │                                                       │   │
│       │              │  ⚠️ 关键点:                                            │   │
│       │              │  takeWhile根据sessionId的状态决定是否继续              │   │
│       │              │  如果 GENERATE_STATUS[sessionId] = false              │   │
│       │              │  则停止接收数据                                         │   │
│       │              └──────────────────────────────────────────────────────┘   │
│       │                                                                         │
│       │  SSE流式推送:                                                          │
│       │                                                                         │
│       │◀── data: {"eventData":"Java","eventType":1001}                       │
│       │                                                                         │
│       │  前端显示: "Java"                                                      │
│       │                                                                         │
│       │◀── data: {"eventData":"多态","eventType":1001}                       │
│       │                                                                         │
│       │  前端显示: "Java多态"                                                  │
│       │                                                                         │
│       │◀── data: {"eventData":"是","eventType":1001}                         │
│       │                                                                         │
│       │  前端显示: "Java多态是"                                                │
│       │                                                                         │
│       │◀── data: {"eventData":"面向","eventType":1001}                       │
│       │                                                                         │
│       │  前端显示: "Java多态是面向"                                            │
│       │                                                                         │
│       │◀── ... (继续推送)                                                      │
│       │                                                                         │
│       │  前端效果:                                                              │
│       │  ┌───────────────────────────────────────────────────────────────────┐│
│       │  │ ┌───────────────────────────────────────────────────────────────┐ ││
│       │  │ │ 用户: 什么是Java多态?                                         │ ││
│       │  │ ├───────────────────────────────────────────────────────────────┤ ││
│       │  │ │ AI: Java多态是面向对象编程的核心概念...█  ← 打字机效果         │ ││
│       │  │ │                     ↑ 正在生成                               │ ││
│       │  │ └───────────────────────────────────────────────────────────────┘ ││
│       │  └───────────────────────────────────────────────────────────────────┘│
│                                                                                  │
└─────────────────────────────────────────────────────────────────────────────────┘

步骤4:生成完成与清理

java 复制代码
┌─────────────────────────────────────────────────────────────────────────────────┐
│  步骤4:生成完成,清理状态                                                       │
│  目的:标记对话结束,清理生成状态                                                │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                  │
│   AI模型生成完成                                              AI服务            │
│       │                                                                          │
│       │              ┌──────────────────────────────────────────────────────┐   │
│       │              │         ChatServiceImpl                              │   │
│       │              │                                                       │   │
│       │              │  .doOnComplete(() ->                                  │   │
│       │              │      GENERATE_STATUS.remove(sessionId)                │   │
│       │              │  )                                                     │   │
│       │              │                                                       │   │
│       │              │  GENERATE_STATUS 内存状态更新:                        │   │
│       │              │  ┌─────────────────────────────────────────────────┐ │   │
│       │              │  │ "a1b2c3d4..." → 已移除                            │ │   │
│       │              │  │ (该sessionId不再在Map中)                          │ │   │
│       │              │  └─────────────────────────────────────────────────┘ │   │
│       │              │                                                       │   │
│       │              │  .concatWith(Flux.just(                               │   │
│       │              │      ChatEventVO.builder()                            │   │
│       │              │          .eventType(1002)  ← STOP事件                 │   │
│       │              │          .build()                                     │   │
│       │              │  ))                                                    │   │
│       │              └──────────────────────────────────────────────────────┘   │
│       │                                                                         │
│       │◀── data: {"eventData":null,"eventType":1002}                         │
│       │                                                                         │
│       │  前端收到STOP事件:                                                      │
│       │  ┌───────────────────────────────────────────────────────────────────┐│
│       │  │ ┌───────────────────────────────────────────────────────────────┐ ││
│       │  │ │ 用户: 什么是Java多态?                                         │ ││
│       │  │ ├───────────────────────────────────────────────────────────────┤ ││
│       │  │ │ AI: Java多态是面向对象编程的核心概念,                          │ ││
│       │  │ │     指同一个方法在不同对象中有不同实现...                       │ ││
│       │  │ │                                                              │ ││
│       │  │ │ [生成完成] ← 停止按钮消失                                     │ ││
│       │  │ └───────────────────────────────────────────────────────────────┘ ││
│       │  └───────────────────────────────────────────────────────────────────┘│
│                                                                                  │
│  ⚠️ 注意: 当前代码没有保存聊天记录到数据库                                       │
│  ⚠️ 如果用户再次提问,AI不会记得之前说过什么                                    │
│                                                                                  │
└─────────────────────────────────────────────────────────────────────────────────┘

步骤5:停止生成(用户中断)

java 复制代码
┌─────────────────────────────────────────────────────────────────────────────────┐
│  步骤5:用户点击"停止生成"                                                       │
│  目的:中断AI生成,sessionId用于定位需要停止的会话                               │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                  │
│   用户点击停止                                                前端               │
│       │                                                                          │
│       │  用户觉得回答太长,想停止                                               │
│       │                                                                          │
│       │  ┌───────────────────────────────────────────────────────────────────┐│
│       │  │ ┌───────────────────────────────────────────────────────────────┐ ││
│       │  │ │ AI: Java多态是面向对象编程的核心概念,                          │ ││
│       │  │ │     指同一个方法在不同对象中有...█                              │ ││
│       │  │ │ [停止生成] ← 用户点击此按钮                                     │ ││
│       │  │ └───────────────────────────────────────────────────────────────┘ ││
│       │  └───────────────────────────────────────────────────────────────────┘│
│       │                                                                         │
│       │  POST /chat/stop?sessionId=a1b2c3d4e5f6...                            │
│       │─────────────────────────────────────────────────────────────────────▶ │
│       │                                                                         │
│       │              ┌──────────────────────────────────────────────────────┐   │
│       │              │         ChatServiceImpl                              │   │
│       │              │                                                       │   │
│       │              │  @Override                                            │   │
│       │              │  public void stop(String sessionId) {                 │   │
│       │              │      GENERATE_STATUS.put(sessionId, false);           │   │
│       │              │  }                                                     │   │
│       │              │                                                       │   │
│       │              │  GENERATE_STATUS 状态更新:                            │   │
│       │              │  ┌─────────────────────────────────────────────────┐ │   │
│       │              │  │ "a1b2c3d4..." → false                            │ │   │
│       │              │  └─────────────────────────────────────────────────┘ │   │
│       │              │                                                       │   │
│       │              │  同时在 chat() 方法中:                                │   │
│       │              │  .takeWhile(s ->                                      │   │
│       │              │      GENERATE_STATUS[sessionId] == true               │   │
│       │              │  )                                                     │   │
│       │              │                                                       │   │
│       │              │  因为状态变为false,takeWhile返回false                │   │
│       │              │  流停止接收数据                                        │   │
│       │              └──────────────────────────────────────────────────────┘   │
│       │                                                                         │
│       │◀── 流式响应立即停止                                                    │
│       │                                                                         │
│       │  前端显示:                                                              │
│       │  ┌───────────────────────────────────────────────────────────────────┐│
│       │  │ ┌───────────────────────────────────────────────────────────────┐ ││
│       │  │ │ 用户: 什么是Java多态?                                         │ ││
│       │  │ ├───────────────────────────────────────────────────────────────┤ ││
│       │  │ │ AI: Java多态是面向对象编程的核心概念,                          │ ││
│       │  │ │     指同一个方法在不同... [已停止]                              │ ││
│       │  │ └───────────────────────────────────────────────────────────────┘ ││
│       │  └───────────────────────────────────────────────────────────────────┘│
│                                                                                  │
└─────────────────────────────────────────────────────────────────────────────────┘

3、sessionId 在整个流程中的作用总结

java 复制代码
┌─────────────────────────────────────────────────────────────────────────────────┐
│                    sessionId 在AI问答中的作用总结                                 │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                  │
│   ┌────────────────────────────────────────────────────────────────────────────┐│
│   │                    sessionId 的四大核心作用                                  ││
│   ├────────────────────────────────────────────────────────────────────────────┤│
│   │                                                                              ││
│   │   1️⌢ 唯一标识对话                                                           ││
│   │   ┌──────────────────────────────────────────────────────────────────────┐ ││
│   │   │ • 前端: 用于展示对话列表,区分不同对话                                 │ ││
│   │   │ • 后端: 用于关联 chat_session 表中的会话记录                           │ ││
│   │   │ • 数据库: session_id 字段存储                                          │ ││
│   │   └──────────────────────────────────────────────────────────────────────┐ ││
│   │                                                                              ││
│   │   2️⌢ 生成状态控制                                                           ││
│   │   ┌──────────────────────────────────────────────────────────────────────┐ ││
│   │   │ • GENERATE_STATUS.put(sessionId, true)  → 标记正在生成               │ ││
│   │   │ • GENERATE_STATUS.put(sessionId, false) → 标记停止生成               │ ││
│   │   │ • takeWhile() 根据状态决定是否继续接收AI数据                           │ ││
│   │   │ • 支持用户主动停止生成                                                 │ ││
│   │   └──────────────────────────────────────────────────────────────────────┐ ││
│   │                                                                              ││
│   │   3️⌢ 多轮对话关联(理论上)                                                  ││
│   │   ┌──────────────────────────────────────────────────────────────────────┐ ││
│   │   │ • 同一 sessionId 下的消息应属于同一对话                                │ ││
│   │   │ • ⚠️ 当前实现缺失: 没有存储聊天记录                                   │ ││
│   │   │ • ⚠️ 没有历史消息查询                                                 │ ││
│   │   │ • ⚠️ AI 不记得之前对话内容                                            │ ││
│   │   └──────────────────────────────────────────────────────────────────────┐ ││
│   │                                                                              ││
│   │   4️⌢ 会话元数据存储                                                         ││
│   │   ┌──────────────────────────────────────────────────────────────────────┐ ││
│   │   │ • chat_session 表存储:                                                │ ││
│   │   │   - sessionId: 会话唯一标识                                           │ ││
│   │   │   - userId: 用户ID                                                    │ ││
│   │   │   - title: 会话标题(用于前端展示)                                    │ ││
│   │   │   - createTime: 创建时间                                              │ ││
│   │   └──────────────────────────────────────────────────────────────────────┐ ││
│   │                                                                              ││
│   └────────────────────────────────────────────────────────────────────────────┘│
│                                                                                  │
└─────────────────────────────────────────────────────────────────────────────────┘

4、会话总结

4.1、问题汇总

4.2、用户与会话的关联关系

4.3、sessionId 的传递机制

java 复制代码
┌─────────────────────────────────────────────────────────────────────────────────┐
│                        sessionId 的传递流程                                     │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                  │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │                              前端                                        │  │
│   │                                                                          │  │
│   │   创建会话请求:                                                          │  │
│   │   POST /ais/session                                                      │  │
│   │   ↓                                                                     │  │
│   │   响应: { sessionId: "xxx" }                                            │  │
│   │   ↓                                                                     │  │
│   │   存储: sessionStorage.setItem('currentSessionId', 'xxx')              │  │
│   │   ↓                                                                     │  │
│   │   发送消息请求:                                                          │  │
│   │   POST /ais/chat                                                        │  │
│   │   { question: "...", sessionId: "xxx" } ← 从存储中读取                   │  │
│   │                                                                          │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                        │                                        │
│                                        │ HTTP请求                               │
│                                        ▼                                        │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │                              后端                                        │  │
│   │                                                                          │  │
│   │   ChatController.chat(ChatDTO chatDTO)                                  │  │
│   │   ↓                                                                     │  │
│   │   ChatServiceImpl.chat(question, sessionId)                             │  │
│   │   ↓                                                                     │  │
│   │   GENERATE_STATUS.put(sessionId, true) ← 标记生成状态                    │  │
│   │   ↓                                                                     │  │
│   │   chatClient.prompt()... ← 调用AI                                       │  │
│   │                                                                          │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
└─────────────────────────────────────────────────────────────────────────────────┘

4.3、前端客户新开一个对话完整处理流程

java 复制代码
┌─────────────────────────────────────────────────────────────────────────────────┐
│                        前端新开对话的处理流程                                     │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                  │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │                     用户界面示意图                                        │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │                                                                          │  │
│   │   ┌─────────────────┐  ┌───────────────────────────────────────────────┐│  │
│   │   │   对话列表       │  │            对话内容区域                        ││  │
│   │   ├─────────────────┤  ├───────────────────────────────────────────────┤│  │
│   │   │ • 对话1 [会话A] │  │ 用户: 什么是Java多态?                         ││  │
│   │   │ • 对话2 [会话B] │  │ AI: Java多态是面向对象编程的...               ││  │
│   │   │ • 对话3 [会话C] │  │                                               ││  │
│   │   │                 │  │ [输入框]                                     ││  │
│   │   │  + 新建对话      │  │                                               ││  │
│   │   └─────────────────┘  └───────────────────────────────────────────────┘│  │
│   │                                                                          │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │                        新开对话流程                                       │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
│   阶段1: 用户点击"新建对话"                                                     │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │ 用户操作:                                                                 │  │
│   │ • 在对话列表中点击"+ 新建对话"按钮                                         │  │
│   │ • 或者在对话内容区域点击"新对话"按钮                                       │  │
│   │                                                                          │  │
│   │ 前端状态:                                                                 │  │
│   │ • 当前 sessionId: "a1b2c3d4..." (旧会话)                                │  │
│   │ • 需要创建新会话                                                         │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
│   阶段2: 发送创建会话请求                                                       │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │ 前端代码逻辑:                                                             │  │
│   │                                                                          │  │
│   │ async function createNewSession() {                                      │  │
│   │   try {                                                                  │  │
│   │     // 发送请求到后端                                                     │  │
│   │     const response = await fetch('/ais/session?n=3', {                   │  │
│   │       method: 'POST',                                                    │  │
│   │       headers: {                                                         │  │
│   │         'Authorization': 'Bearer ' + token,                             │  │
│   │         'Content-Type': 'application/json'                               │  │
│   │       }                                                                  │  │
│   │     });                                                                  │  │
│   │                                                                          │  │
│   │     const sessionVO = await response.json();                             │  │
│   │     // sessionVO = {                                                     │  │
│   │     //   sessionId: "e5f6g7h8...",                                      │  │
│   │     //   title: "天机AI助手",                                            │  │
│   │     //   describe: "...",                                               │  │
│   │     //   examples: [...]                                                │  │
│   │     // }                                                                │  │
│   │                                                                          │  │
│   │     // 更新当前会话ID                                                    │  │
│   │     sessionStorage.setItem('currentSessionId', sessionVO.sessionId);    │  │
│   │                                                                          │  │
│   │     // 更新UI显示                                                        │  │
│   │     renderSessionList([...oldSessions, sessionVO]);                     │  │
│   │     renderChatArea(sessionVO);                                          │  │
│   │   } catch (error) {                                                     │  │
│   │     console.error('创建会话失败:', error);                               │  │
│   │   }                                                                     │  │
│   │ }                                                                        │  │
│   │                                                                          │  │
│   │ 请求路径: /ais/session                                                   │  │
│   │ • /ais/ 是网关路由前缀                                                   │  │
│   │ • /session 是实际接口路径                                                │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
│   阶段3: 后端创建新会话                                                        │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │ 后端处理流程:                                                             │  │
│   │                                                                          │  │
│   │ 1. 网关解析Token → 获取 userId=1001                                     │  │
│   │ 2. 路由转发到 aigc-service                                               │  │
│   │ 3. SessionController.createSession(3)                                   │  │
│   │ 4. ChatSessionServiceImpl:                                               │  │
│   │    • 生成新UUID: "e5f6g7h8i9j0..."                                      │  │
│   │    • 随机选择3个示例                                                     │  │
│   │    • 保存到数据库:                                                       │  │
│   │      session_id: e5f6g7h8i9j0...                                         │  │
│   │      user_id: 1001                                                      │  │
│   │ 5. 返回 SessionVO                                                       │  │
│   │                                                                          │  │
│   │ 数据库状态:                                                               │  │
│   │ chat_session 表新增一条记录                                               │  │
│   │ 用户1001 现在有4个会话                                                    │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
│   阶段4: 前端更新UI                                                            │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │ 前端更新:                                                                 │  │
│   │                                                                          │  │
│   │ 1. 更新对话列表                                                          │  │
│   │   ┌─────────────────┐                                                   │  │
│   │   │ • 对话1 [会话A] │                                                   │  │
│   │   │ • 对话2 [会话B] │                                                   │  │
│   │   │ • 对话3 [会话C] │                                                   │  │
│   │   │ • 对话4 [会话D] │ ← 新增                                            │  │
│   │   │                 │                                                   │  │
│   │   │  + 新建对话      │                                                   │  │
│   │   └─────────────────┘                                                   │  │
│   │                                                                          │  │
│   │ 2. 更新对话内容区域                                                      │  │
│   │   ┌───────────────────────────────────────────────────────────────┐     │  │
│   │   │                    天机AI助手                                  │     │  │
│   │   │            我可以帮你解答学习问题...                           │     │  │
│   │   ├───────────────────────────────────────────────────────────────┤     │  │
│   │   │ 你可以问我:                                                  │     │  │
│   │   │ [Java基础] [数据库] [算法]                                    │     │  │
│   │   ├───────────────────────────────────────────────────────────────┤     │  │
│   │   │ [输入框: 请输入你的问题...]                                  │     │  │
│   │   └───────────────────────────────────────────────────────────────┘     │  │
│   │                                                                          │  │
│   │ 3. 隐藏旧对话内容,显示新对话的初始状态                                  │  │
│   │                                                                          │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
│   阶段5: 用户开始新对话                                                        │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │ 用户在新会话中输入问题:                                                   │  │
│   │                                                                          │  │
│   │ 前端发送:                                                                │  │
│   │ POST /ais/chat                                                          │  │
│   │ {                                                                        │  │
│   │   "question": "什么是快速排序?",                                        │  │
│   │   "sessionId": "e5f6g7h8i9j0..."  ← 新的会话ID                          │  │
│   │ }                                                                        │  │
│   │                                                                          │  │
│   │ 后端处理:                                                                │  │
│   │ • 使用新的 sessionId 进行生成状态控制                                    │  │
│   │ • 与旧会话完全独立                                                       │  │
│   │ • 不会影响其他会话                                                       │  │
│   │                                                                          │  │
│   │ UI效果:                                                                  │  │
│   │ • 用户可以在多个会话之间切换                                             │  │
│   │ • 每个会话有独立的聊天内容                                               │  │
│   │ • 前端通过 sessionId 区分不同会话                                        │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
└─────────────────────────────────────────────────────────────────────────────────┘

4.4、创建一个会话完整创建流程

java 复制代码
┌─────────────────────────────────────────────────────────────────────────────────┐
│                        创建会话的完整流程                                        │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                  │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │                        创建会话流程图                                     │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
│   步骤1: 用户请求                                                               │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │ 用户打开AI助手页面 → 点击"新对话"按钮                                      │  │
│   │                                                                          │  │
│   │ 前端发送请求:                                                             │  │
│   │ POST /session?n=3                                                        │  │
│   │                                                                          │  │
│   │ 请求头中携带:                                                             │  │
│   │ Authorization: Bearer xxx.yyy.zzz (JWT Token)                           │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
│   步骤2: 网关处理                                                               │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │ tj-gateway 收到请求                                                      │  │
│   │                                                                          │  │
│   │ 1. AccountAuthFilter 解析JWT Token                                        │  │
│   │    • 提取用户ID: userId = 1001                                           │  │
│   │    • 将用户ID放入请求头: X-TJ-USER-ID: 1001                              │  │
│   │                                                                          │  │
│   │ 2. 路由匹配: /ais/** → aigc-service                                      │  │
│   │                                                                          │  │
│   │ 3. StripPrefix: /ais/session → /session                                  │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
│   步骤3: 后端处理                                                               │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │ SessionController.createSession(num=3)                                   │  │
│   │         ↓                                                                │  │
│   │ ChatSessionServiceImpl.createSession(num=3)                              │  │
│   │         ↓                                                                │  │
│   │                                                                          │  │
│   │ ┌──────────────────────────────────────────────────────────────────────┐ │  │
│   │ │ 第一步: 生成UUID sessionId                                          │ │  │
│   │ │ sessionId = IdUtil.fastSimpleUUID()                                 │ │  │
│   │ │ 结果: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"                            │ │  │
│   │ └──────────────────────────────────────────────────────────────────────┘ │  │
│   │                                                                          │  │
│   │ ┌──────────────────────────────────────────────────────────────────────┐ │  │
│   │ │ 第二步: 读取配置                                                     │ │  │
│   │ │ SessionProperties (从Nacos读取)                                     │ │  │
│   │ │ • title: "天机AI助手"                                               │ │  │
│   │ │ • describe: "我可以帮你解答学习问题..."                              │ │  │
│   │ │ • examples: [                                                        │ │  │
│   │ │     {"title":"Java基础","describe":"解释面向对象"},                   │ │  │
│   │ │     {"title":"Spring Boot","describe":"如何搭建项目"},                │ │  │
│   │ │     {"title":"数据库","describe":"MySQL优化"},                        │ │  │
│   │ │     {"title":"算法","describe":"快速排序原理"}                        │ │  │
│   │ │   ]                                                                 │ │  │
│   │ └──────────────────────────────────────────────────────────────────────┘ │  │
│   │                                                                          │  │
│   │ ┌──────────────────────────────────────────────────────────────────────┐ │  │
│   │ │ 第三步: 随机选择3个示例                                              │ │  │
│   │ │ RandomUtil.randomEleList(examples, 3)                               │ │  │
│   │ │ 选中结果:                                                            │ │  │
│   │ │ • Java基础 → 解释面向对象                                            │ │  │
│   │ │ • 数据库 → MySQL优化                                                 │ │  │
│   │ │ • 算法 → 快速排序原理                                                 │ │  │
│   │ └──────────────────────────────────────────────────────────────────────┘ │  │
│   │                                                                          │  │
│   │ ┌──────────────────────────────────────────────────────────────────────┐ │  │
│   │ │ 第四步: 获取当前用户ID                                               │ │  │
│   │ │ userId = UserContext.getUser()                                      │ │  │
│   │ │ 结果: userId = 1001                                                 │ │  │
│   │ └──────────────────────────────────────────────────────────────────────┘ │  │
│   │                                                                          │  │
│   │ ┌──────────────────────────────────────────────────────────────────────┐ │  │
│   │ │ 第五步: 构建实体并保存                                               │ │  │
│   │ │ ChatSession.builder()                                               │ │  │
│   │ │     .sessionId("a1b2c3d4...")                                       │ │  │
│   │ │     .userId(1001)                                                   │ │  │
│   │ │     .build()                                                        │ │  │
│   │ │ super.save(chatSession)                                             │ │  │
│   │ │                                                                     │ │  │
│   │ │ 数据库 chat_session 表新增记录:                                      │ │  │
│   │ │ id: 123                                                             │ │  │
│   │ │ session_id: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6                        │ │  │
│   │ │ user_id: 1001                                                       │ │  │
│   │ │ title: NULL                                                         │ │  │
│   │ │ create_time: 2026-06-27 10:00:00                                    │ │  │
│   │ └──────────────────────────────────────────────────────────────────────┘ │  │
│   │                                                                          │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
│   步骤4: 返回结果                                                               │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │ 返回 SessionVO 给前端:                                                   │  │
│   │ {                                                                        │  │
│   │   "sessionId": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",                     │  │
│   │   "title": "天机AI助手",                                                 │  │
│   │   "describe": "我可以帮你解答学习问题...",                              │  │
│   │   "examples": [                                                         │  │
│   │     {"title":"Java基础","describe":"解释面向对象"},                      │  │
│   │     {"title":"数据库","describe":"MySQL优化"},                          │  │
│   │     {"title":"算法","describe":"快速排序原理"}                          │  │
│   │   ]                                                                     │  │
│   │ }                                                                        │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
│   步骤5: 前端存储 sessionId                                                    │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │ 前端接收到响应后,保存 sessionId                                          │  │
│   │ • 存储位置: localStorage / sessionStorage / 内存变量                     │  │
│   │ • 用途: 后续对话请求都要携带这个 sessionId                                │  │
│   │ • 示例:                                                                 │  │
│   │   sessionStorage.setItem('currentSessionId', 'a1b2c3d4...')            │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
└─────────────────────────────────────────────────────────────────────────────────┘

4.5、如何区分用户是哪个会话

核心机制:userId + sessionId 双层标识

java 复制代码
┌─────────────────────────────────────────────────────────────────────────────────┐
│                     用户与会话的区分机制                                         │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                  │
│   ┌────────────────────────────────────────────────────────────────────────────┐│
│   │                      双层标识体系                                           ││
│   ├────────────────────────────────────────────────────────────────────────────┤│
│   │                                                                              ││
│   │   第一层: 用户标识 (userId)                                                  ││
│   │   ┌──────────────────────────────────────────────────────────────────────┐ ││
│   │   │ • 来源: JWT Token中的用户ID                                           │ ││
│   │   │ • 获取: UserContext.getUser()                                        │ ││
│   │   │ • 用途: 区分不同用户                                                  │ ││
│   │   │ • 存储: chat_session.user_id 字段                                    │ ││
│   │   └──────────────────────────────────────────────────────────────────────┘ ││
│   │                                                                              ││
│   │   第二层: 会话标识 (sessionId)                                               ││
│   │   ┌──────────────────────────────────────────────────────────────────────┐ ││
│   │   │ • 来源: 创建会话时生成的UUID                                          │ ││
│   │   │ • 生成: IdUtil.fastSimpleUUID()                                      │ ││
│   │   │ • 用途: 区分同一用户的不同对话                                        │ ││
│   │   │ • 存储: chat_session.session_id 字段                                  │ ││
│   │   └──────────────────────────────────────────────────────────────────────┘ ││
│   │                                                                              ││
│   └────────────────────────────────────────────────────────────────────────────┘│
│                                                                                  │
│   ┌────────────────────────────────────────────────────────────────────────────┐│
│   │                         数据库存储示例                                       ││
│   ├────────────────────────────────────────────────────────────────────────────┤│
│   │                                                                              ││
│   │   chat_session 表:                                                         ││
│   │   ┌──────┬───────────────┬──────────┬──────────────────┐                   ││
│   │   │ id   │ session_id    │ user_id  │ create_time      │                   ││
│   │   ├──────┼───────────────┼──────────┼──────────────────┤                   ││
│   │   │ 1    │ a1b2c3d4...   │ 1001     │ 2026-06-27 10:00 │                   ││
│   │   │ 2    │ e5f6g7h8...   │ 1001     │ 2026-06-27 11:00 │                   ││
│   │   │ 3    │ i9j0k1l2...   │ 1002     │ 2026-06-27 10:30 │                   ││
│   │   └──────┴───────────────┴──────────┴──────────────────┘                   ││
│   │                                                                              ││
│   │   解读:                                                                      ││
│   │   • 用户1001 有两个会话: a1b2c3d4... 和 e5f6g7h8...                          ││
│   │   • 用户1002 有一个会话: i9j0k1l2...                                        ││
│   │   • 通过 userId + sessionId 唯一确定一个会话                                 ││
│   │                                                                              ││
│   └────────────────────────────────────────────────────────────────────────────┘│
│                                                                                  │
└─────────────────────────────────────────────────────────────────────────────────┘

4.6、代码示例

1、会话实体 ( ChatSession.java ):
java 复制代码
@Data
@TableName("chat_session")
public class ChatSession implements Serializable {
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;              // 主键

    private String sessionId;     // 会话ID(UUID)
    private Long userId;          // 用户ID
    private String title;         // 会话标题
    private LocalDateTime createTime;  // 创建时间
    private LocalDateTime updateTime; // 更新时间
    private Long creater;        // 创建人
    private Long updater;        // 更新人
}
2、创建会话 ( SessionController.java )
java 复制代码
@RestController
@RequestMapping("/session")
@RequiredArgsConstructor
public class SessionController {
    private final ChatSessionService sessionService;

    @PostMapping
    public SessionVO createSession(@RequestParam(value = "n", defaultValue = "3") Integer num) {
        return this.sessionService.createSession(num);
    }
}
3、创建会话服务 ( ChatSessionServiceImpl.java )
java 复制代码
@Service
@RequiredArgsConstructor
public class ChatSessionServiceImpl extends ServiceImpl<ChatSessionMapper, ChatSession> 
    implements ChatSessionService {
    
    private final SessionProperties sessionProperties;

    @Override
    public SessionVO createSession(Integer num) {
        // 1. 转换配置属性为 VO
        SessionVO sessionVO = BeanUtil.toBean(sessionProperties, SessionVO.class);
        
        // 2. 随机选择示例问题
        sessionVO.setExamples(RandomUtil.randomEleList(sessionProperties.getExamples(), num));
        
        // 3. 生成唯一会话ID(UUID)
        sessionVO.setSessionId(IdUtil.fastSimpleUUID());
        
        // 4. 构建会话实体
        ChatSession chatSession = ChatSession.builder()
                .sessionId(sessionVO.getSessionId())
                .userId(UserContext.getUser())  // 从上下文获取当前用户ID
                .build();
        
        // 5. 保存到数据库
        super.save(chatSession);
        
        return sessionVO;
    }
}
4、会话配置属性 ( SessionProperties.java )
java 复制代码
@Data
@Configuration
@ConfigurationProperties(prefix = "tj.ai.session")
public class SessionProperties {
    private String title;         // AI助手标题
    private String describe;      // AI助手描述
    private List<Example> examples;  // 示例问题列表
}

配置文件示例 (从 Nacos 读取;创建会话初始提示词):

java 复制代码
tj:
  ai:
    session:
      title: "天机AI助手"
      describe: "我可以帮你解答学习问题,代码问题等"
      examples:
        - title: "Java基础"
          describe: "解释面向对象编程的概念"
        - title: "Spring Boot"
          describe: "如何快速搭建一个Spring Boot项目"
5、返回数据模型 ( SessionVO.java )
java 复制代码
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SessionVO {
    private String sessionId;      // 会话ID
    private String title;         // AI助手标题
    private String describe;      // AI助手描述
    private List<Example> examples; // 示例问题列表
}

返回示例 :

java 复制代码
{
  "sessionId": "a1b2c3d4e5f6...",
  "title": "天机AI助手",
  "describe": "我可以帮你解答学习问题,代码问题等",
  "examples": [
    { "title": "Java基础", "describe": "解释面向对象编程的概念" },
    { "title": "Spring Boot", "describe": "如何快速搭建Spring Boot项目" }
  ]
}

完整流程图:

五、支付服务粗略讲解

5.1、完整支付时序图和项目结构

java 复制代码
交易服务          支付服务          支付宝/微信         用户
   │                │                  │                │
   │ 1.申请支付      │                  │                │
   │──POST /pay-orders→                │                │
   │                │                  │                │
   │                │ 2.创建预支付订单   │                │
   │                │──────────────────→│                │
   │                │                  │                │
   │                │ 3.返回支付链接     │                │
   │                │←──────────────────│                │
   │                │                  │                │
   │ 4.返回支付URL   │                  │                │
   │←───────────────│                  │                │
   │                │                  │                │
   │                │                  │  5.扫码支付     │
   │                │                  │←────────────────│
   │                │                  │                │
   │                │ 6.支付回调通知     │                │
   │                │←──────────────────│                │
   │                │                  │                │
   │                │ 7.发送MQ消息       │                │
   │←───────────────│(PAY_SUCCESS)     │                │
   │                │                  │                │
   │ 8.更新订单状态  │                  │                │
   │                │                  │                │

5.2、统一支付接口和策略模式实现

java 复制代码
public interface IPayService {
    // 创建预支付订单(生成支付链接)
    PrepayResponse createPrepayOrder(String title, String orderNo, Integer amount);
    
    // 查询支付订单状态
    PayStatusResponse queryPayOrderStatus(String payOrderNo);
    
    // 申请退款
    RefundResponse refundOrder(String payOrderNo, String refundOrderNo, 
                               Integer refundAmount, Integer totalAmount);
    
    // 查询退款状态
    RefundResponse queryRefundStatus(String orderNo, String refundOrderNo);
}
java 复制代码
// PayOrderServiceImpl.java
@Resource
private Map<String, IPayService> payServiceChannels;  // Spring自动注入所有实现

@Override
public String applyPayOrder(PayApplyDTO payApplyDTO) {
    // 根据支付渠道代码动态选择实现
    IPayService payService = payServiceChannels.get(payApplyDTO.getPayChannelCode());
    if (payService == null) {
        throw new BadRequestException(INVALID_PAY_CHANNEL);
    }
    // ... 调用对应支付渠道
}

5.3、支付申请流程详解

接口入口 ( PayOrderController.java ):

java 复制代码
@PostMapping
public String applyPayOrder(@RequestBody PayApplyDTO payApplyDTO){
    // 仅支持扫码支付(NATIVE)
    if(!PayType.NATIVE.equalsValue(payApplyDTO.getPayType())){
        throw new BadRequestException(PayErrorInfo.INVALID_PAY_TYPE);
    }
    return payOrderService.applyPayOrder(payApplyDTO);
}

支付申请核心流程 ( PayOrderServiceImpl.java ):

java 复制代码
@Override
@Lock(name = PayConstants.RedisKeyFormatter.PAY_APPLY, leaseTime = 3, autoUnlock = false)
public String applyPayOrder(PayApplyDTO payApplyDTO) {
    // 1.选择支付渠道(策略模式)
    IPayService payService = payServiceChannels.get(payApplyDTO.getPayChannelCode());
    
    // 2.幂等性校验(关键!防止重复创建支付单)
    PayOrder payOrder = checkIdempotent(payApplyDTO);
    if (StringUtils.isNotBlank(payOrder.getQrCodeUrl())) {
        return payOrder.getQrCodeUrl();  // 已有支付链接,直接返回
    }

    // 3.调用第三方创建预支付订单
    PrepayResponse prepayResponse = payService.createPrepayOrder(
            payApplyDTO.getOrderInfo(), 
            payOrder.getPayOrderNo().toString(), 
            payOrder.getAmount());

    // 4.更新支付结果到数据库
    updatePayResult2DB(prepayResponse, payOrder.getId());

    // 5.返回支付链接
    return prepayResponse.getPayUrl();
}

幂等性校验逻辑 ( checkIdempotent ):

java 复制代码
private PayOrder checkIdempotent(PayApplyDTO payApplyDTO) {
    // 1.查询是否已有支付单
    PayOrder oldOrder = queryByBizOrderNo(payApplyDTO.getBizOrderNo());
    
    if (oldOrder == null) {
        // 第一次请求:创建新支付单
        PayOrder payOrder = buildPayOrder(payApplyDTO);
        payOrder.setPayOrderNo(IdWorker.getId());
        save(payOrder);
        return payOrder;
    }
    
    // 2.已存在:判断状态
    if (PayStatus.TRADE_SUCCESS.equalsValue(oldOrder.getStatus())) {
        throw new BizIllegalException(PAY_ORDER_ALREADY_PAY_CODE, PAY_ORDER_ALREADY_PAY);
    }
    
    if (PayStatus.TRADE_CLOSED.equalsValue(oldOrder.getStatus())) {
        throw new BizIllegalException(PAY_ORDER_ALREADY_CLOSE_CODE, PAY_ORDER_ALREADY_CLOSE);
    }
    
    // 3.支付渠道不一致:重置数据重新申请
    if (!StringUtils.equals(oldOrder.getPayChannelCode(), payApplyDTO.getPayChannelCode())) {
        PayOrder payOrder = buildPayOrder(payApplyDTO);
        payOrder.setId(oldOrder.getId());
        payOrder.setQrCodeUrl("");
        updateById(payOrder);
        payOrder.setPayOrderNo(oldOrder.getPayOrderNo());
        return payOrder;
    }
    
    // 4.未支付且渠道一致:直接返回旧数据
    return oldOrder;
}

5.4、支付实现

支付宝实现:

java 复制代码
@Service(ALI_CHANNEL_CODE)
public class AliPayService implements IPayService {
    
    @Override
    public PrepayResponse createPrepayOrder(String title, String orderNo, Integer amount) {
        // 1.构建回调地址
        String notifyUrl = commonPayProperties.getNotifyHost() + "/notify/aliPay";
        
        // 2.调用支付宝API
        AlipayTradePrecreateResponse response = Factory.Payment.FaceToFace()
                .asyncNotify(notifyUrl)
                .preCreate(title, orderNo, transferAmount2String(amount));
        
        // 3.处理响应
        if (ResponseChecker.success(response)) {
            return PrepayResponse.builder().success(true).payUrl(response.getQrCode()).build();
        } else {
            return PrepayResponse.builder().success(false).code(response.getCode()).msg(response.getMsg()).build();
        }
    }
}

微信支付:

java 复制代码
@Service(PayConstants.WX_CHANNEL_CODE)
public class WxPayService implements IPayService {
    
    @Override
    public PrepayResponse createPrepayOrder(String title, String orderNo, Integer amount) {
        // 1.请求地址
        String requestPath = "https://api.mch.weixin.qq.com/v3/pay/transactions/native";
        
        // 2.构建参数
        ObjectNode baseParam = wxPayClient.baseParam(true, true, false)
                .put("out_trade_no", orderNo)
                .put("description", title);
        baseParam.putObject("amount").put("total", amount);
        
        // 3.发送请求
        String responseJson = wxPayClient.doPostJson(requestPath, baseParam);
        
        // 4.解析响应
        JSONObject result = JsonUtils.parseObj(responseJson);
        String codeUrl = result.getStr("code_url");
        
        if (codeUrl != null) {
            return PrepayResponse.builder().success(true).payUrl(codeUrl).build();
        } else {
            return PrepayResponse.builder().success(false).code(result.getStr("code")).msg(result.getStr("message")).build();
        }
    }
}

5.5、支付回调处理

java 复制代码
@RestController
@RequestMapping("notify")
public class NotifyController {
    
    // 支付宝回调
    @PostMapping("aliPay")
    public ResponseEntity<String> handleAliPayNotify(HttpServletRequest httpRequest){
        Map<String, String[]> parameterMap = httpRequest.getParameterMap();
        Map<String, String> request = parameterMap.entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, 
                        e -> StringUtils.join(",", e.getValue())));
        notifyService.handleAliPayNotify(request);
        return ResponseEntity.ok("success");  // 必须返回success
    }
    
    // 微信支付回调
    @PostMapping("wxPay")
    public ResponseEntity<Object> handleWxPayNotify(HttpEntity<String> httpEntity){
        NotificationRequest request = transformHttpEntityToNotificationRequest(httpEntity);
        notifyService.handleWxPayNotify(request);
        return ResponseEntity.ok().build();
    }
}
java 复制代码
@Override
public void handleWxPayNotify(NotificationRequest request) {
    // 1.验签(安全校验)
    Notification notification = checkWxNotifyRequest(request);
    if (!"TRANSACTION.SUCCESS".equals(notification.getEventType())) {
        return;  // 只处理支付成功通知
    }
    
    // 2.解析加密数据
    String decryptData = notification.getDecryptData();
    JSONObject data = JsonUtils.parseObj(decryptData);
    
    // 3.提取关键信息
    Long tradingOrderNo = data.getLong("out_trade_no");
    Integer amount = data.getJSONObject("amount").getInt("total");
    LocalDateTime successTime = data.getLocalDateTime("success_time", LocalDateTime.now());
    
    // 4.业务校验和幂等处理
    PayOrder payOrder = checkNotifyData(tradingOrderNo, amount, successTime);
    if (payOrder == null) return;
    
    // 5.发送MQ通知交易服务
    rabbitMqHelper.send(
            MqConstants.Exchange.PAY_EXCHANGE,
            MqConstants.Key.PAY_SUCCESS,
            PayResultDTO.builder()
                    .payOrderNo(payOrder.getPayOrderNo())
                    .bizOrderId(payOrder.getBizOrderNo())
                    .payChannel(payOrder.getPayChannelCode())
                    .successTime(successTime)
                    .build());
}

5.6、定时任务兜底机制之支付状态检查任务

java 复制代码
@XxlJob("payOrderCheckHandler")
public void checkPayOrderStatus() {
    // 1.获取分片信息(支持分布式部署)
    int index = XxlJobHelper.getShardIndex() + 1;
    int size = StringUtils.isNumeric(jobParam) ? Integer.parseInt(jobParam) : 10;
    
    // 2.查询待检查的支付订单(状态为WAIT_BUYER_PAY)
    PageDTO<PayOrder> result = payOrderService.queryPayingOrderByPage(index, size);
    
    // 3.逐个检查
    for (PayOrder payOrder : result.getList()) {
        try {
            payOrderService.checkPayOrder(payOrder);
        } catch (Exception e) {
            log.error("处理订单支付状态异常:", e);
        }
    }
}
java 复制代码
@Override
@Lock(name = PayConstants.RedisKeyFormatter.PAY_ORDER_CHECK_TASK, 
      lockStrategy = LockStrategy.SKIP_AFTER_RETRY_TIMEOUT)
public void checkPayOrder(PayOrder payOrder) {
    // 1.判断是否超时(120分钟)
    if (payOrder.getPayOverTime().isBefore(LocalDateTime.now())) {
        closeOrder(payOrder.getId());  // 关闭订单
        return;
    }
    
    // 2.调用第三方查询支付状态
    PayStatusResponse response = payService.queryPayOrderStatus(payOrder.getPayOrderNo().toString());
    
    // 3.状态变更处理
    if (PayStatus.TRADE_SUCCESS.equalsValue(response.getPayStatus())) {
        // 支付成功:更新状态,发送MQ
        updatePayStatus2DB(response, payOrder.getId());
        rabbitMqHelper.send(MqConstants.Exchange.PAY_EXCHANGE,
                MqConstants.Key.PAY_SUCCESS, PayResultDTO.builder()...build());
    }
}

兜底机制作用 :

  • 防止回调丢失导致订单状态不一致
  • 超时自动关闭订单(120分钟)
  • 支持分布式部署(XXL-Job分片)

5.7、退款流程

java 复制代码
@Override
@Lock(name = PayConstants.RedisKeyFormatter.REFUND_APPLY, leaseTime = 3)
public RefundResultDTO applyRefund(RefundApplyDTO refundApplyDTO) {
    // 1.校验支付单状态(必须已支付)
    PayOrder payOrder = payOrderService.queryByBizOrderNo(refundApplyDTO.getBizOrderNo());
    
    // 2.构建退款单
    RefundOrder refundOrder = buildRefundOrder(refundApplyDTO, payOrder);
    
    // 3.调用第三方申请退款
    RefundResponse response = payService.refundOrder(...);
    
    // 4.更新退款状态
    updateRefundStatus(refundOrder.getId(), response);
    
    // 5.发送MQ通知
    rabbitMqHelper.send(MqConstants.Exchange.PAY_EXCHANGE,
            MqConstants.Key.REFUND_CHANGE, RefundResultDTO.builder()...build());
}
java 复制代码
@Override
public void handleWxPayRefundNotify(NotificationRequest request) {
    Notification notification = checkWxNotifyRequest(request);
    
    // 解析退款通知
    String decryptData = notification.getDecryptData();
    JSONObject data = JsonUtils.parseObj(decryptData);
    Long refundOrderNo = data.getLong("out_refund_no");
    RefundStatus status = handleWxRefundStatus(notification.getEventType());
    
    // 幂等校验
    RefundOrder refundOrder = checkRefundData(refundOrderNo, status, null);
    
    // 发送MQ通知交易服务
    rabbitMqHelper.send(MqConstants.Exchange.PAY_EXCHANGE,
            MqConstants.Key.REFUND_CHANGE, RefundResultDTO.builder()...build());
}

5.8、数据库表结构

5.9、乐观锁的使用

5.9.1、核心代码

java 复制代码
@Override
public boolean markPayOrderSuccess(Long id, LocalDateTime successTime) {
    return lambdaUpdate()
            .set(PayOrder::getStatus, PayStatus.TRADE_SUCCESS.getValue())      // 设置新状态
            .set(PayOrder::getNotifyStatus, NotifyStatus.CALLING.getValue())   // 设置通知状态
            .set(PayOrder::getPaySuccessTime, successTime)                      // 设置成功时间
            .eq(PayOrder::getId, id)                                            // 主键条件
            // 乐观锁:只有状态为"未提交"或"待支付"时才能更新
            .in(PayOrder::getStatus, PayStatus.NOT_COMMIT.getValue(), PayStatus.WAIT_BUYER_PAY.getValue())
            .update();  // 返回是否更新成功
}

5.9.2、乐观锁工作原理

sql 复制代码
UPDATE pay_order 
SET status = 3, notify_status = 1, pay_success_time = '2026-06-27 10:30:00'
WHERE id = 123 
AND status IN (1, 2);  -- 乐观锁条件:只有状态为1或2时才能更新

5.9.3、乐观锁的作用

java 复制代码
┌─────────────────────────────────────────────────────────────────────────────────┐
│                        乐观锁防止并发问题的场景                                   │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                  │
│   场景1: 防止回调重复处理                                                        │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │                                                                          │  │
│   │   支付宝第一次回调 → 更新状态为成功 ✓                                       │  │
│   │   支付宝第二次回调 → 尝试更新,但状态已是3,不满足 IN(1,2),更新失败 ✗        │  │
│   │                                                                          │  │
│   │   结果:第一次成功,第二次被乐观锁拦截,防止重复处理                         │  │
│   │                                                                          │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
│   场景2: 防止已关闭订单被误更新                                                  │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │                                                                          │  │
│   │   订单超时被定时任务关闭 → 状态变为4                                       │  │
│   │   支付回调到达 → 尝试更新为成功,但状态是4,不满足 IN(1,2),更新失败 ✗       │  │
│   │                                                                          │  │
│   │   结果:已关闭订单不会被误更新为成功状态                                    │  │
│   │                                                                          │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
│   场景3: 防止并发回调                                                            │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │                                                                          │  │
│   │   时间线:                                                                │  │
│   │   T1: 回调A到达,status=2,满足 IN(1,2),开始处理                          │  │
│   │   T2: 回调B到达,status=2,满足 IN(1,2),开始处理                          │  │
│   │   T3: 回调A执行UPDATE,status变为3                                        │  │
│   │   T4: 回调B执行UPDATE,status已是3,不满足 IN(1,2),更新失败 ✗              │  │
│   │                                                                          │  │
│   │   结果:只有一个回调成功处理                                               │  │
│   │                                                                          │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
└─────────────────────────────────────────────────────────────────────────────────┘
java 复制代码
┌─────────────────────────────────────────────────────────────────────────────────┐
│                        markPayOrderSuccess 的调用链                              │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                  │
│   NotifyServiceImpl.checkNotifyData (支付回调处理)                              │
│   ┌──────────────────────────────────────────────────────────────────────────┐  │
│   │                                                                          │  │
│   │   @Lock(name = "pay:notify:payOrderNo:#{tradingOrderNo}")                │  │
│   │   private PayOrder checkNotifyData(Long tradingOrderNo, ...) {           │  │
│   │       // 1.查询支付单                                                     │  │
│   │       PayOrder payOrder = payOrderService.queryByPayOrderNo(...);        │  │
│   │                                                                          │  │
│   │       // 2.判断状态(重复通知)                                           │  │
│   │       if (payOrder.success() || payOrder.closed()) {                     │  │
│   │           return null;  // 已经成功或关闭,直接返回                        │  │
│   │       }                                                                   │  │
│   │                                                                          │  │
│   │       // 3.乐观锁更新(双重保障)                                          │  │
│   │       boolean success = payOrderService.markPayOrderSuccess(...);        │  │
│   │       if (!success) {                                                     │  │
│   │           return null;  // 更新失败,说明是重复通知                        │  │
│   │       }                                                                   │  │
│   │                                                                          │  │
│   │       return payOrder;                                                    │  │
│   │   }                                                                       │  │
│   │                                                                          │  │
│   │   双重保障机制:                                                            │  │
│   │   1. Redis分布式锁 (@Lock) → 防止并发进入                                  │  │
│   │   2. 数据库乐观锁 → 防止重复更新                                           │  │
│   │                                                                          │  │
│   └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
└─────────────────────────────────────────────────────────────────────────────────┘

六、文字转语音和语音转文字功能详解

6.1、整体架构设计和准备工作

6.1.1、依赖组件(需在pom.xml中添加)

java 复制代码
<!-- Spring AI OpenAI 集成 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>

<!-- WebFlux 用于流式响应 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

6.1.2、配置说明

yaml 复制代码
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}          # OpenAI API密钥
      base-url: ${OPENAI_BASE_URL}        # API地址(可配置代理)
    tts:
      openai:
        enabled: true                     # 启用TTS
        voice: alloy                      # 默认音色
        response-format: mp3              # 响应格式
        speed: 1.0                        # 语速
    transcription:
      openai:
        enabled: true                     # 启用ASR
        model: whisper-1                  # 识别模型

6.2、文字转语音功能

6.2.1、SpeechSynthesisDTO(文字转语音请求)

java 复制代码
@Data
@Builder
public class SpeechSynthesisDTO {
    private String text;           // 待转换的文本
    private String voice;          // 音色:alloy/echo/fable/onyx/nova/shimmer
    private String responseFormat; // 输出格式:mp3/opus/aac/flac
    private Double speed;          // 语速:0.25 ~ 4.0
}

6.2.2、接口代码

java 复制代码
@RestController
@RequestMapping("/speech")
public class SpeechController {

    private final ISpeechService speechService;

    // 1. 文字转语音 - 流式响应音频
    @PostMapping(value = "/synthesize", produces = "audio/mpeg")
    public Flux<byte[]> synthesize(@RequestBody SpeechSynthesisDTO dto) {
        return speechService.textToSpeech(dto);
    }

    // 2. 语音转文字 - 上传音频文件
    @PostMapping("/recognize")
    public SpeechRecognitionVO recognize(@RequestParam("file") MultipartFile file) {
        return speechService.speechToText(file);
    }
}

6.2.3、文字转语音流程

6.2.4、流式响应 (Streaming)

文字转语音使用流式响应的原因 :

  • 文本较长时,无需等待全部音频生成完毕
  • 前端可边接收边播放,提升用户体验
  • 减少内存占用,避免大文件一次性加载

6.3、语音转文字功能

6.3.1、SpeechRecognitionVO(语音转文字响应)

java 复制代码
@Data
@Builder
public class SpeechRecognitionVO {
    private String text;       // 识别结果文本
    private String language;   // 语言:zh/en/...
    private String model;      // 使用的模型:whisper-1
}
java 复制代码
public interface ISpeechService {
    Flux<byte[]> textToSpeech(SpeechSynthesisDTO dto);
    SpeechRecognitionVO speechToText(MultipartFile file);
}
java 复制代码
@Service
public class SpeechServiceImpl implements ISpeechService {

    private final OpenAiAudioSpeechClient speechClient;
    private final OpenAiAudioTranscriptionClient transcriptionClient;

    // ===== 文字转语音 (TTS) =====
    @Override
    public Flux<byte[]> textToSpeech(SpeechSynthesisDTO dto) {
        // 构建TTS请求
        SpeechPrompt prompt = SpeechPrompt.builder()
                .withText(dto.getText())                          // 文本内容
                .withVoice(dto.getVoice() != null ? dto.getVoice() : "alloy")
                .withResponseFormat(dto.getResponseFormat() != null ? dto.getResponseFormat() : "mp3")
                .withSpeed(dto.getSpeed() != null ? dto.getSpeed() : 1.0)
                .build();

        // 流式调用,返回音频字节流
        return speechClient.stream(prompt)
                .map(SpeechResponse::getAudio);
    }

    // ===== 语音转文字 (ASR) =====
    @Override
    public SpeechRecognitionVO speechToText(MultipartFile file) {
        try {
            // 构建识别请求
            TranscriptionPrompt prompt = TranscriptionPrompt.builder()
                    .withAudio(file.getInputStream())            // 音频流
                    .withModel(WhisperModel.WHISPER_1)           // 使用Whisper模型
                    .withLanguage("zh")                          // 指定中文
                    .withResponseFormat(WhisperAudioFormat.TEXT) // 返回纯文本
                    .withTemperature(0.0)                        // 确定性模式
                    .build();

            // 调用API获取结果
            TranscriptionResponse response = transcriptionClient.call(prompt);

            return SpeechRecognitionVO.builder()
                    .text(response.getResult())
                    .language("zh")
                    .model("whisper-1")
                    .build();

        } catch (IOException e) {
            throw new RuntimeException("读取音频文件失败", e);
        }
    }
}

6.3.2、接口代码

java 复制代码
@RestController
@RequestMapping("/speech")
public class SpeechController {

    private final ISpeechService speechService;

    // 1. 文字转语音 - 流式响应音频
    @PostMapping(value = "/synthesize", produces = "audio/mpeg")
    public Flux<byte[]> synthesize(@RequestBody SpeechSynthesisDTO dto) {
        return speechService.textToSpeech(dto);
    }

    // 2. 语音转文字 - 上传音频文件
    @PostMapping("/recognize")
    public SpeechRecognitionVO recognize(@RequestParam("file") MultipartFile file) {
        return speechService.speechToText(file);
    }
}
java 复制代码
public interface ISpeechService {
    Flux<byte[]> textToSpeech(SpeechSynthesisDTO dto);
    SpeechRecognitionVO speechToText(MultipartFile file);
}
java 复制代码
@Service
public class SpeechServiceImpl implements ISpeechService {

    private final OpenAiAudioSpeechClient speechClient;
    private final OpenAiAudioTranscriptionClient transcriptionClient;

    // ===== 文字转语音 (TTS) =====
    @Override
    public Flux<byte[]> textToSpeech(SpeechSynthesisDTO dto) {
        // 构建TTS请求
        SpeechPrompt prompt = SpeechPrompt.builder()
                .withText(dto.getText())                          // 文本内容
                .withVoice(dto.getVoice() != null ? dto.getVoice() : "alloy")
                .withResponseFormat(dto.getResponseFormat() != null ? dto.getResponseFormat() : "mp3")
                .withSpeed(dto.getSpeed() != null ? dto.getSpeed() : 1.0)
                .build();

        // 流式调用,返回音频字节流
        return speechClient.stream(prompt)
                .map(SpeechResponse::getAudio);
    }

    // ===== 语音转文字 (ASR) =====
    @Override
    public SpeechRecognitionVO speechToText(MultipartFile file) {
        try {
            // 构建识别请求
            TranscriptionPrompt prompt = TranscriptionPrompt.builder()
                    .withAudio(file.getInputStream())            // 音频流
                    .withModel(WhisperModel.WHISPER_1)           // 使用Whisper模型
                    .withLanguage("zh")                          // 指定中文
                    .withResponseFormat(WhisperAudioFormat.TEXT) // 返回纯文本
                    .withTemperature(0.0)                        // 确定性模式
                    .build();

            // 调用API获取结果
            TranscriptionResponse response = transcriptionClient.call(prompt);

            return SpeechRecognitionVO.builder()
                    .text(response.getResult())
                    .language("zh")
                    .model("whisper-1")
                    .build();

        } catch (IOException e) {
            throw new RuntimeException("读取音频文件失败", e);
        }
    }
}

6.3.3、语音转文字流程

6.3.4、语音转文字Whisper

Whisper 模型特点:

  • 多语言支持 :支持99种语言,包括中文
  • 实时识别 :可处理实时语音流
  • 准确率高 :对口音、噪音有较好的鲁棒性
  • 价格 :$0.042/分钟

功能总结:

如果要将语音功能与现有AI聊天结合:

java 复制代码
// 在 ChatServiceImpl 中扩展
public Flux<ChatEventVO> chatWithVoice(String audioBase64) {
    // 1. 先将语音转文字
    String text = speechService.speechToTextBase64(audioBase64).getText();
    
    // 2. 调用AI聊天
    return chatClient.prompt()
            .system(...)
            .user(text)
            .stream()
            .chatResponse()
            .map(...)
            .concatWith(Flux.just(/* 可选:将AI回复转语音 */));
}