目录:
- 一、项目架构分析
- 二、网关服务实现详解
-
- 1、技术选型与依赖
- 2、项目结构
- 3、核心实现详解
-
- 3.1、启动类详解
- 3.2、application.yml路由规则定义
- 3.3、配置属性 ( AuthProperties.java )
- 3.4、请求ID追踪过滤器 ( RequestIdRelayFilter.java )
- [3.5、 账号认证过滤器 ( AccountAuthFilter.java )](#3.5、 账号认证过滤器 ( AccountAuthFilter.java ))
- [3.6、 全局异常处理器 ( GatewayExceptionHandler.java )](#3.6、 全局异常处理器 ( GatewayExceptionHandler.java ))
- [3.7、 Nacos配置中心 ( application-local.yml )](#3.7、 Nacos配置中心 ( application-local.yml ))
- 三、AI问答助手服务实现详解
-
- 1、技术选型
- 2、项目结构
- 3、核心实现流程
-
- [3.1 对话请求入口 ( ChatController.java )](#3.1 对话请求入口 ( ChatController.java ))
- [3.2 流式对话服务 ( ChatServiceImpl.java )](#3.2 流式对话服务 ( ChatServiceImpl.java ))
- [3.3 系统提示词热加载 ( SystemPromptConfig.java )](#3.3 系统提示词热加载 ( SystemPromptConfig.java ))
- [3.4 Spring AI配置 ( SpringAIConfig.java )](#3.4 Spring AI配置 ( SpringAIConfig.java ))
- 3.5、完整请求流程
- 四、会话管理在AI问答中的作用详解
-
- 1、会话管理的四大核心作用
- 2、详细流程图(分步骤讲解)
- [3、sessionId 在整个流程中的作用总结](#3、sessionId 在整个流程中的作用总结)
- 4、会话总结
-
- 4.1、问题汇总
- 4.2、用户与会话的关联关系
- [4.3、sessionId 的传递机制](#4.3、sessionId 的传递机制)
- 4.3、前端客户新开一个对话完整处理流程
- 4.4、创建一个会话完整创建流程
- 4.5、如何区分用户是哪个会话
- 4.6、代码示例
-
- [1、会话实体 ( ChatSession.java ):](#1、会话实体 ( ChatSession.java ):)
- 2、创建会话 ( SessionController.java )
- 3、创建会话服务 ( ChatSessionServiceImpl.java )
- 4、会话配置属性 ( SessionProperties.java )
- 5、返回数据模型 ( SessionVO.java )
- 五、支付服务粗略讲解
- 六、文字转语音和语音转文字功能详解
一、项目架构分析
典型的 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回复转语音 */));
}
