fix(chat): 优化聊天流式接口异常处理与拦截器分发类型过滤

- talk 接口改为 TEXT_EVENT_STREAM 输出并包装 Flux 异常处理,返回 error/done 事件
- 登录与签名校验拦截器忽略 ASYNC/ERROR 分发,防止二次校验导致上下文丢失
- 更新嵌入模型与 Qdrant 配置,保持与线上环境一致
This commit is contained in:
2026-02-24 14:54:59 +08:00
parent d3abe32e1a
commit 590230a86b
6 changed files with 40 additions and 9 deletions

View File

@@ -2,6 +2,7 @@ package com.yolo.keyborad.interceptor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yolo.keyborad.utils.SignUtils;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.data.redis.core.StringRedisTemplate;
@@ -35,6 +36,10 @@ public class SignInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 异步/错误二次分发不重复验签,避免 nonce 重放误判
if (request.getDispatcherType() != DispatcherType.REQUEST) {
return true;
}
String appId = request.getHeader("X-App-Id");
String timestamp = request.getHeader("X-Timestamp");

View File

@@ -57,7 +57,7 @@ public class LLMConfig {
this.openAiApi(),
MetadataMode.EMBED,
OpenAiEmbeddingOptions.builder()
.model("text-embedding-v4")
.model("qwen/qwen3-embedding-8b")
.dimensions(1536)
.user("user-6")
.build(),

View File

@@ -12,11 +12,11 @@ import org.springframework.context.annotation.Configuration;
@Configuration
public class QdrantClientConfig {
private final String qdrantHost = "b0c7f1ee-0eb9-469e-83e0-654249d9bd04.us-east4-0.gcp.cloud.qdrant.io";
private final String qdrantHost = "00044004-d9d2-4705-8b7b-9defab34ca1b.us-west-1-0.aws.cloud.qdrant.io";
private final Integer qdrantPort = 6334;
private final String apiKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJtIn0.HX_GxjXCrnhw2DQbMnMFzvDeaHbmNpI2tj2hoUjkvVU";
private final String apiKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJtIn0.M9nnZ14QXQqI3sG8JzRzRwq48x9u5g-4MWF2jnxiOHA";
@Bean
public QdrantClient qdrantClient() {

View File

@@ -1,15 +1,18 @@
package com.yolo.keyborad.config;
import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import com.yolo.keyborad.interceptor.SignInterceptor;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -36,7 +39,17 @@ public class SaTokenConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
registry.addInterceptor(new HandlerInterceptor() {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 仅在首次 REQUEST 分发时做登录校验,避免 ASYNC/ERROR 二次分发阶段上下文丢失
if (request.getDispatcherType() != DispatcherType.REQUEST) {
return true;
}
StpUtil.checkLogin();
return true;
}
})
.addPathPatterns("/**")
.excludePathPatterns(getExcludePaths());
appSecretMap.put(appId, appSecret);
@@ -48,6 +61,7 @@ public class SaTokenConfigure implements WebMvcConfigurer {
return new String[]{
// Swagger & Knife4j 相关
"/doc.html",
"/error",
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs",
@@ -57,7 +71,6 @@ public class SaTokenConfigure implements WebMvcConfigurer {
"/swagger-ui/**",
"/favicon.ico",
// 你的其他放行路径,例如登录接口
"/error",
"/user/appleLogin",
"/user/logout",
"/tag/list",

View File

@@ -30,6 +30,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
@@ -90,10 +91,22 @@ public class ChatController {
return ResultUtils.success(result);
}
@PostMapping("/talk")
@PostMapping(value = "/talk", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@Operation(summary = "聊天润色接口", description = "聊天润色接口")
public Flux<ServerSentEvent<ChatStreamMessage>> talk(@RequestBody ChatReq chatReq){
return chatService.talk(chatReq);
return Flux.defer(() -> chatService.talk(chatReq))
.onErrorResume(e -> {
log.error("聊天流式接口异常", e);
String message = StrUtil.isBlank(e.getMessage()) ? "服务暂时不可用,请稍后重试" : e.getMessage();
return Flux.just(
ServerSentEvent.builder(new ChatStreamMessage("error", message))
.event("error")
.build(),
ServerSentEvent.builder(new ChatStreamMessage("done", null))
.event("done")
.build()
);
});
}

View File

@@ -10,7 +10,7 @@ spring:
model: google/gemini-2.5-flash-lite
embedding:
options:
model: text-embedding-v4
model: qwen/qwen3-embedding-8b
# model: qwen/qwen3-embedding-8b
dashscope:
api-key: 11