数据流转: 用户浏览器 (Vue) <-> Nginx <-> Spring Boot 网关 <-> Dify API <-> LLM 大模型
核心实现:
- SSE 流式响应:
- 原因:传统的 HTTP 请求需要等待 AI 生成完毕才能返回,用户体验极差
- 实现:后端使用WebClient异步调用Dify,并利用SseEmitter实现"打字机"效果
- 踩坑:Nginx 默认开启了
proxy_buffering,导致流数据被缓存 - 解决方案:在 Nginx 配置中加入
proxy_buffering off;,让每一个字都能即时“蹦”出来
- Redis分布式限流:
- 原因:不能让其他人无限刷接口
- 实现:使用 Redisson 的
RRateLimiter结合 Spring AOP 自定义注解。 - 踩坑:在nginx代理环境下,后端接收到的地址都是nginx的回环地址,无法获得真实ip
- 解决:通过解析
X-Forwarded-For请求头还原用户真实 IP,解决了限流器“误伤”代理节点导致全局失效的问题。 - 限制:不能完全解决问题,后期考虑图形验证与用户登录系统限制
- SSR 兼容:DOMPurify 的“环境陷阱”
- 现象:VitePress 打包时报
DOMPurify.sanitize is not a function。 - 排查:VitePress 在 Build 阶段运行在 Node.js 环境,没有浏览器的 DOM 树。
- 代码技巧
- 现象:VitePress 打包时报
js
if (typeof window !== 'undefined') {
// 仅在客户端执行清洗逻辑
}切面方法
java
@Aspect
@Component
public class RateLimitAspect {
private final RedissonClient redissonClient;
public RateLimitAspect(RedissonClient redissonClient){
this.redissonClient = redissonClient;
}
@Around("@annotation(rateLimit)") //贴了@rateLimite标签,就触发方法
public Object checkRateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable{ //ProceedingJoinPoint请求本身
HttpServletRequest request=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); //获取请求对象 HttpServletRequest //获取真实 IP(如果通过 Nginx 代理,需要取 X-Forwarded-For)
String ip=request.getHeader("X-FORWARDED-FOR"); //只有在X-Forwarded-For头里,才藏着客人真正的物理IP
if(ip==null||ip.isEmpty()){
ip=request.getRemoteAddr();
}
String key="rate_limit:chat:"+ip;
//限流器,紧盯ip
RRateLimiter limiter=redissonClient.getRateLimiter(key);
//初始化限流器。这里设置为:整体(OVERALL)限流,指定时间窗口内的次数
limiter.trySetRate(RateType.OVERALL, rateLimit.rate(), rateLimit.ratelnterval(), RateIntervalUnit.MINUTES);
if(!limiter.tryAcquire()){
throw new RuntimeException("提问频繁");
}
return joinPoint.proceed();
}
}实现: 目前只投喂了dify相关笔记,后期有时间考虑更多投入 