Skip to content
DAILY QUOTE

“ ”

数据流转: 用户浏览器 (Vue) <-> Nginx <-> Spring Boot 网关 <-> Dify API <-> LLM 大模型

核心实现:

  • SSE 流式响应:
    • 原因:传统的 HTTP 请求需要等待 AI 生成完毕才能返回,用户体验极差
    • 实现:后端使用WebClient异步调用Dify,并利用SseEmitter实现"打字机"效果
    • 踩坑:Nginx 默认开启了 proxy_buffering,导致流数据被缓存
    • 解决方案:在 Nginx 配置中加入 proxy_buffering off;,让每一个字都能即时“蹦”出来
  • Redis分布式限流:
    • 原因:不能让其他人无限刷接口
    • 实现:使用 RedissonRRateLimiter 结合 Spring AOP 自定义注解。
    • 踩坑:在nginx代理环境下,后端接收到的地址都是nginx的回环地址,无法获得真实ip
    • 解决:通过解析 X-Forwarded-For 请求头还原用户真实 IP,解决了限流器“误伤”代理节点导致全局失效的问题。
    • 限制:不能完全解决问题,后期考虑图形验证与用户登录系统限制
  • SSR 兼容:DOMPurify 的“环境陷阱”
    • 现象:VitePress 打包时报 DOMPurify.sanitize is not a function
    • 排查:VitePress 在 Build 阶段运行在 Node.js 环境,没有浏览器的 DOM 树。
    • 代码技巧
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相关笔记,后期有时间考虑更多投入