logo 范 · 拾光录
网址收集 关于作者 Github Gitee
杂文随笔5
Hexo博客:基础使用Hexo博客:Next主题Hexo博客:Next进阶使用Hexo博客:Next高级配置基于Node的WIKI管理
前端知识16
HTML常用知识CSS常用知识CSS美化checkbox复选框JavaScript常用知识JavaScript格式化时间戳JavaScript窗口宽高处理JavaScript黑夜主题切换实现方案JavaScript数字转大写简易图片查看器TypeScript基础知识Threejs基础三要素Threejs网格辅助和轨道控制器Threejs物体绘制Electron基础使用Nodejs基础知识animate.css页面动画
Vue框架19
Vite的使用及扩展Vue3父子组件Vue3使用Marked解析MarkdownMermaid图表生成库初始化页面加载动画Axios表单提交二维码解决方案NProgress加载进度条Vue3动态菜单实现Vue3使用ECharts图表Vue3处理Excel导入导出keep-alive页面缓存及setup问题Element:文件上传Element:结合Pinia实现动态菜单Element:图片上传组件Element:自定义统一弹窗组件Element:表格自定义指令控制按钮显示(鉴权)可视化大屏使用缩放适配分辨率
UniApp15
UniApp的基础使用封装网络请求工具及文件上传uni-app的开发记录微信小程序分享原生文件上传Pinia取消滚动条(兼容小程序)tabbar消息数量显示scroll-view上滑到底部加载数据状态栏高度动态设配数据共享与传递uview-plus导航栏实现背景融合Wot UIWot UI实现顶部背景图融合uni-app x
Java基础知识10
基础知识面向对象Lambda表达式常用API常用知识积累try-with-resource注解反射多线程经纬度距离计算
SpringBoot31
application配置Maven创建聚合项目全局异常处理锁机制项目启动初始化数据方式邮件功能集成原生定时任务异步集成阿里云OSS阿里OSS预签名上传基于hutool读excelJSR303WebSocketWebSocket版AI接口流式调用Smart-Doc接口文档生成器application配置信息加密雪花算法工具AOP实现请求参数脱敏思路JWT生成Token及工具类SpringBoot默认JSON与对象转换若依框架:安装使用若依框架:优化和调整文件上传若依框架:管理后台页面优化若依框架:后端接口代码优化SpringAISpringBoot实现AI接口流式调用服务启动时创建MySQL连接自建项目工程树形结构处理工具微信支付代码微信手机号登录
SpringMVC14
跨域处理拦截器RESTful风格伪前后端分离Jackson转换器调整Thymeleaf基于拦截器做权限校验AOP打印接口请求响应日志AOP打印接口请求响应耗时文件上传和回显POST请求加解密实现(AES)POST请求加解密实现(RSA+AES)参数动态校验实现方案真实IP和归属地
MyBatis8
MyBatis基本使用与配置Mapper使用相关MaBatis多数据源配置MyBatisPlus数据统计类处理方案MyBatisPlus条件查询正向工程的实现(H2)mybatis-plus-join
SpringCloud15
Netflix:微服务与搭建Netflix:服务的消费与提供Netflix:EurekaNetflix:ActuatorNetflix:RibbonNetflix:FeignNetflix:HystrixNetflix:ZuulAlibaba:简介与搭建Alibaba:Nacos注册中心Alibaba:RibbonAlibaba:OpenFeignAlibaba:Nacos配置中心Alibaba:GetewayAlibaba:Sentinel
MySQL6
MySQL基础知识MySQL多表查询与事务MySQL常用函数及解决方案MySQL视图MySQL索引安装MySQL
Redis7
Redis介绍和安装Redis配置文件Redis持久化Redis集群Redis语法基础Redis相关问题及解决方案SpringBoot集成Redis使用记录
MongoDB10
Linux安装MongoDBMongoDB基础语法MongoTemplate及SpringBoot配置MongoTemplate中Update操作MongoTemplate中聚合查询MongoTemplate日期归档示例项目使用相关知识归纳地理位置存储与距离查询MongoDB副本集与事务获取类名和属性名工具类
其他数据库1
H2数据库
Python编程6
Python基础知识Python语法yolo目标检测OpenCV的使用及树莓派平台condauv
工具集合13
IDEAMavenGradleGitNginx安装Nginx配置VSCodeJMeter压测DockerOllamaRustFSPicGoObs录制
Linux知识11
Linux常用命令Jar启动脚本VirtualBox安装CentOSVirtualBox安装Ubuntu树莓派安装及使用frp内网穿透ArchLinux:基础系统安装ArchLInux:图形化界面安装ArchLinux:常用软件ArchLinux:深度优化ArchLinux:Niri
创意设计2
Blender:入门知识UI设计基础知识
AI相关9
Claude CodeHermes AgentOpenAI基本使用OpenAI工具调用OpenAI记忆管理OpenAI推理执行OpenAI开发框架Langchainllama.cpp

依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

java

package com.fan.model.paiRecord;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;

@RestController
@RequestMapping("/ai")
public class AiStreamController {


    private static final String API_ENDPOINT = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions";
    private static final String API_KEY = "xxxxxxxxxxxxxxx"; // 建议从配置读取
    private static final ObjectMapper objectMapper = new ObjectMapper();


    @PostMapping("/stream")
    public Flux<String> streamChat(@RequestBody Map<String, String> request) {
        String userMessage = request.get("message");
        String idStr = request.get("id");
        if (idStr == null || idStr.trim().isEmpty()) {
            return Flux.error(new IllegalArgumentException("id 不能为空"));
        }
        if (userMessage == null || userMessage.trim().isEmpty()) {
            return Flux.error(new IllegalArgumentException("消息不能为空"));
        }

        // 用于收集完整 AI 回答
        StringBuilder fullResponse = new StringBuilder();

        WebClient client = WebClient.builder()
                .baseUrl(API_ENDPOINT)
                .defaultHeader("Authorization", "Bearer " + API_KEY)
                .defaultHeader("Content-Type", "application/json")
                .build();

        Map<String, Object> systemMsg = Map.of("role", "system", "content",
                "请用纯文本回答,不要使用任何 Markdown、HTML 或富文本格式。不要加粗、不要列表、不要代码块,只输出自然语言文本。");

        Map<String, Object> userMsg = Map.of("role", "user", "content", userMessage);
        List<Map<String, Object>> messages = List.of(systemMsg, userMsg);

        Map<String, Object> requestBody = Map.of(
                "model", "qwen-plus",
                "messages", messages,
                "stream", true,
                "stream_options", Map.of("include_usage", true)
        );

        Flux<DataBuffer> dashScopeStream = client.post()
                .body(BodyInserters.fromValue(requestBody))
                .accept(MediaType.TEXT_EVENT_STREAM)
                .retrieve()
                .bodyToFlux(DataBuffer.class);

        StringBuilder buffer = new StringBuilder();

        Flux<String> contentStream = dashScopeStream
                .publishOn(Schedulers.boundedElastic())
                .flatMap(dataBuffer -> {
                    try {
                        String chunk = dataBuffer.toString(StandardCharsets.UTF_8);
                        DataBufferUtils.release(dataBuffer);

                        buffer.append(chunk);
                        List<String> contents = new ArrayList<>();

                        while (buffer.indexOf("\n\n") != -1) {
                            int endIndex = buffer.indexOf("\n\n");
                            String event = buffer.substring(0, endIndex).trim();
                            buffer.delete(0, endIndex + 2);

                            if (event.startsWith("data: ")) {
                                String jsonData = event.substring(6).trim();
                                if ("[DONE]".equals(jsonData)) {
                                    return Flux.empty();
                                }
                                try {
                                    JsonNode root = objectMapper.readTree(jsonData);
                                    JsonNode choices = root.path("choices");
                                    if (choices.isArray() && !choices.isEmpty()) {
                                        JsonNode delta = choices.get(0).path("delta");
                                        if (delta.has("content")) {
                                            String content = delta.get("content").asText();
                                            contents.add(content);
                                            fullResponse.append(content); // 收集完整回答
                                        }
                                    }
                                } catch (Exception e) {
                                    // ignore
                                }
                            }
                        }
                        return Flux.fromIterable(contents);
                    } catch (Exception e) {
                        return Flux.error(e);
                    }
                });

        // 在流结束时保存记录(异步,不阻塞响应)
        return contentStream
                .doOnComplete(() -> {
                    String aiResponse = fullResponse.toString().trim();
                    // 异步保存,避免阻塞响应流
                    saveToDatabaseAsync(userMessage, aiResponse, idStr);
                })
                .doOnError(throwable -> {
                    // 可选:记录错误日志
                    System.err.println("流处理出错: " + throwable.getMessage());
                });
    }

    // 假设你有一个 service 或 repository
    private void saveToDatabaseAsync(String userMessage, String aiResponse, String idStr) {
        // 注意:这里不能直接调用同步 DB 方法(会阻塞 IO 线程)
        // 应该提交到业务线程池或使用响应式 Repository

        // 示例:使用 CompletableFuture 异步保存(如果你用 JPA)
        CompletableFuture.runAsync(() -> {
            try {
                // 调用你的 service 保存逻辑
                // paiRecordService.saveRecord(userMessage, aiResponse);
                System.out.println("【保存数据库】用户: " + userMessage);
                System.out.println("【保存数据库】AI: " + aiResponse);
            } catch (Exception e) {
                System.err.println("保存失败: " + e.getMessage());
            }
        }, Executors.newCachedThreadPool()); // 更好的方式是注入一个 Bean 的线程池
    }
}

前端

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>AI 流式对话</title>
</head>
<body>
<h2>AI 流式回复(六爻解释)</h2>
<div id="output" style="white-space: pre-wrap; font-family: monospace;"></div>

<script>
    const output = document.getElementById('output');
    const message = `1+1=?`;

    // 使用 fetch + ReadableStream(更灵活)
    fetch('', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message: message,id:'2008856506596999170' })
    })
    .then(response => {
        const reader = response.body.getReader();
        const decoder = new TextDecoder();

        function read() {
            reader.read().then(({ done, value }) => {
                if (done) {
                    console.log('流结束');
                    return;
                }
                const chunk = decoder.decode(value, { stream: true });
                output.textContent += chunk; // 逐字追加
                read(); // 继续读
            }).catch(err => {
                console.error('流读取错误:', err);
            });
        }
        read();
    })
    .catch(err => {
        console.error('请求失败:', err);
    });
</script>
</body>
</html>
依赖
java
前端