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

原因

uniapp小程序受fetch影响,无法使用,环境限制,改用websocket

依赖

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

java接口

WebSocketConfig配置

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    private final AiWebSocketHandler aiWebSocketHandler;

    public WebSocketConfig(AiWebSocketHandler aiWebSocketHandler) {
        this.aiWebSocketHandler = aiWebSocketHandler;
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(aiWebSocketHandler, "/ai/stream-ws")
                .setAllowedOrigins("*"); // 生产环境应限制 origin
    }
}

调用ai实现

import com.fan.model.paiRecord.PaiRecordService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fan.entity.PaiRecord;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import reactor.core.scheduler.Schedulers;

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

@Slf4j
@Component
public class AiWebSocketHandler extends TextWebSocketHandler {

    private static final String API_ENDPOINT = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions";
    private static final String API_KEY = "xxxxxxxxxxxxx";
    private static final ObjectMapper objectMapper = new ObjectMapper();

    @Resource
    private PaiRecordService paiRecordService;

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        log.info("🟢 WebSocket 连接已建立 - Session ID: {}, Remote Address: {}",
                session.getId(), session.getRemoteAddress());
        super.afterConnectionEstablished(session);
    }

    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        log.debug("📥 收到客户端消息 - Session ID: {}, Message: {}", session.getId(), message.getPayload());
        super.handleMessage(session, message);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        log.info("🔴 WebSocket 连接已关闭 - Session ID: {}, Reason: {} (Code: {})",
                session.getId(),
                status.getReason(),
                status.getCode());
        super.afterConnectionClosed(session, status);
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        // 假设前端发送的是 JSON: { "id": "2008925332902686722" }
        JsonNode jsonNode = objectMapper.readTree(payload);
        String idStr = jsonNode.path("id").asText();

        if (idStr == null || idStr.trim().isEmpty()) {
            session.sendMessage(new TextMessage("{\"error\":\"id 不能为空\"}"));
            session.close();
            return;
        }

        PaiRecord record = paiRecordService.getById(idStr);
        if (record == null) {
            session.sendMessage(new TextMessage("{\"error\":\"记录不存在\"}"));
            session.close();
            return;
        }

        // 如果已有缓存,直接返回
        if (record.getAiStr() != null) {
            session.sendMessage(new TextMessage(record.getAiStr()));
            session.sendMessage(new TextMessage("[DONE]"));
            session.close();
            return;
        }

        // 启动流式请求
        streamAndPush(session, record, idStr);
    }

    private void streamAndPush(WebSocketSession session, PaiRecord record, String idStr) {
        StringBuilder fullResponse = new StringBuilder();
        String liuBody = record.getLiuBody();
        String userMessage = liuBody + "\n根据专业解卦思维来分析此卦...(你的 prompt)";

        // 构建 DashScope 请求体
        Map<String, Object> systemMsg = Map.of("role", "system", "content",
                "请用纯文本回答,不要使用任何 Markdown、HTML 或富文本格式。");

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

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

        // 使用 WebClient 发起流式请求
        var client = org.springframework.web.reactive.function.client.WebClient.builder()
                .baseUrl(API_ENDPOINT)
                .defaultHeader("Authorization", "Bearer " + API_KEY)
                .defaultHeader("Content-Type", "application/json")
                .build();

        var responseFlux = client.post()
                .bodyValue(requestBody)
                .accept(org.springframework.http.MediaType.TEXT_EVENT_STREAM)
                .retrieve()
                .bodyToFlux(org.springframework.core.io.buffer.DataBuffer.class);

        responseFlux
                .publishOn(Schedulers.boundedElastic())
                .subscribe(
                        dataBuffer -> {
                            try {
                                String chunk = dataBuffer.toString(StandardCharsets.UTF_8);
                                org.springframework.core.io.buffer.DataBufferUtils.release(dataBuffer);

                                // 解析 SSE 格式
                                String[] lines = chunk.split("\n");
                                for (String line : lines) {
                                    if (line.startsWith("data: ")) {
                                        String jsonData = line.substring(6).trim();
                                        if ("[DONE]".equals(jsonData)) {
                                            // 推送结束标记
                                            if (session.isOpen()) {
                                                session.sendMessage(new TextMessage("[DONE]"));
                                            }
                                            saveToDatabaseAsync(userMessage, fullResponse.toString(), idStr);
                                            return;
                                        }

                                        try {
                                            JsonNode root = objectMapper.readTree(jsonData);
                                            var choices = root.path("choices");
                                            if (choices.isArray() && !choices.isEmpty()) {
                                                var delta = choices.get(0).path("delta");
                                                if (delta.has("content")) {
                                                    String content = delta.get("content").asText();
                                                    fullResponse.append(content);
                                                    if (session.isOpen()) {
                                                        session.sendMessage(new TextMessage(content));
                                                    }
                                                }
                                            }
                                        } catch (Exception e) {
                                            // ignore parse error
                                        }
                                    }
                                }
                            } catch (Exception e) {
                                if (session.isOpen()) {
                                    try {
                                        session.sendMessage(new TextMessage("{\"error\":\"内部错误\"}"));
                                        session.close();
                                    } catch (Exception ignored) {}
                                }
                            }
                        },
                        error -> {
                            try {
                                if (session.isOpen()) {
                                    session.sendMessage(new TextMessage("{\"error\":\"流请求失败\"}"));
                                    session.close();
                                }
                            } catch (Exception ignored) {}
                        }
                );
    }

    private void saveToDatabaseAsync(String userMessage, String aiResponse, String idStr) {
        CompletableFuture.runAsync(() -> {
            try {
                PaiRecord record = paiRecordService.getById(idStr);
                if (record != null) {
                    record.setAiStr(aiResponse.trim());
                    record.setUpdateTime(new Date());
                    paiRecordService.updateById(record); // 确保你的 service 有 update 方法
                }
            } catch (Exception e) {
                System.err.println("保存失败: " + e.getMessage());
            }
        }, Executors.newCachedThreadPool());
    }
}

h5方式连接

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>AI 流式 WebSocket 测试</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      padding: 20px;
      background: #f9f9f9;
    }
    #output {
      white-space: pre-wrap;
      word-break: break-word;
      background: white;
      padding: 16px;
      border-radius: 8px;
      border: 1px solid #ddd;
      min-height: 100px;
      margin-top: 10px;
    }
    button {
      padding: 8px 16px;
      font-size: 16px;
      cursor: pointer;
    }
    .status {
      margin-top: 10px;
      color: #555;
    }
  </style>
</head>
<body>
  <h2>AI 流式响应测试(WebSocket)</h2>
  <button id="connectBtn">开始连接并请求 AI</button>
  <div class="status">状态: <span id="status">未连接</span></div>
  <div id="output"></div>

  <script>
    const output = document.getElementById('output');
    const statusEl = document.getElementById('status');
    const connectBtn = document.getElementById('connectBtn');

    let socket = null;

    // 替换为你的实际域名(必须是 HTTPS + WSS)
    // const WS_URL = 'wss://your-domain.com/ai/stream-ws';
    // 如果本地测试(且后端支持),可用:
    const WS_URL = 'wss://xishangkeji.com/liuyao/ai/stream-ws';

    const TEST_ID = "2008925332902686722"; // 替换为你的真实 ID

    function updateStatus(text) {
      statusEl.textContent = text;
    }

    function appendText(text) {
      output.textContent += text;
      // 自动滚动到底部
      output.scrollTop = output.scrollHeight;
    }

    connectBtn.addEventListener('click', () => {
      if (socket && socket.readyState === WebSocket.OPEN) {
        alert('已连接,请先刷新页面');
        return;
      }

      output.textContent = '';
      updateStatus('正在连接...');

      socket = new WebSocket(WS_URL);

      socket.onopen = () => {
        updateStatus('已连接,正在发送请求...');
        socket.send(JSON.stringify({ id: TEST_ID }));
      };

      socket.onmessage = (event) => {
        const data = event.data;

        if (data === '[DONE]') {
          updateStatus('✅ 流结束');
          console.log('流结束');
          return;
        }

        if (typeof data === 'string' && data.startsWith('{"error":')) {
          try {
            const err = JSON.parse(data);
            updateStatus(`❌ 错误: ${err.error}`);
          } catch {
            updateStatus('❌ 返回错误格式');
          }
          return;
        }

        // 正常内容片段
        appendText(data);
      };

      socket.onerror = (error) => {
        updateStatus('❌ 连接出错');
        console.error('WebSocket 错误:', error);
      };

      socket.onclose = () => {
        updateStatus('🔌 连接已关闭');
      };
    });
  </script>
</body>
</html>

uniapp方式连接

<template>
  <scroll-view class="org-container" scroll-y enhanced :show-scrollbar="false">
    <!-- 如果样式设置在全局,不需要背景的不使用class="com-container" -->
    <view class="com-container">
      <!-- placeholder:固定在顶部时,是否生成一个等高元素,以防止塌陷 -->
      <up-navbar title="" :placeholder="true" leftIcon="" leftText="我的">
      </up-navbar>


      <!--  <view>
        <web-view :fullscreen="false" :update-title="false" src="http://127.0.0.1:5500/ai.html"></web-view>
      </view> -->

      <view>
        <text>{{ content }}</text>
      </view>

    </view>
    <up-toast ref="uToastRef"></up-toast>
  </scroll-view>
</template>

<script setup>
  import {
    ref,
    onMounted,
    onUnmounted
  } from 'vue';
  import {
    onLoad,
    onUnload
  } from '@dcloudio/uni-app';

  const content = ref('');
  
  let socketTask = null; // 用于保存连接任务(可选)

  onLoad(() => {
    // 检查平台
    const platform = uni.getSystemInfoSync().platform
    
    // uni-app中的WebSocket需要特别注意以下几点:
    
    // 1. 确保在合法域名列表中(小程序需要配置)
    // 2. 使用完整的URL(包括协议)
    const wsUrl = 'wss://xishangkeji.com/liuyao/ai/stream-ws'
    
    console.log('当前平台:', platform)
    console.log('连接URL:', wsUrl)
    
    // 建立连接
    socketTask = uni.connectSocket({
      url: wsUrl,
      complete: (res) => {
        console.log('connectSocket complete:', res)
      }
    })
    
    // 注意:uni-app中需要确保socketTask不为null再添加事件监听
    if (socketTask) {
      socketTask.onOpen((res) => {
        console.log('WebSocket连接已打开', res)
        // 发送请求
        socketTask.send({
          data: JSON.stringify({
            id: "2008925332902686722"
          }),
          success: () => {
            console.log('消息发送成功')
          },
          fail: (err) => {
            console.error('发送消息失败', err)
          }
        })
      })
      
      socketTask.onMessage((res) => {
        console.log('收到消息:', res.data)
        // ... 处理消息
        content.value += res.data
      })
      
      socketTask.onError((err) => {
        console.error('WebSocket错误:', err)
      })
      
      socketTask.onClose((res) => {
        console.log('WebSocket已关闭:', res)
      })
    }
  })
</script>

<style lang="scss">
  // 背景图片  
  $container-bg-url: 'https://xs-face.oss-cn-shanghai.aliyuncs.com/202601/2008440539537674241_61252.png';

  .com-container {
    background: url($container-bg-url);
    width: 100%;
    min-height: 100vh;
    background-repeat: no-repeat;
    background-size: cover;
    background-attachment: fixed;
    box-sizing: border-box;
    padding: 16rpx 32rpx 24rpx 32rpx;

    // 导航栏默认背景替换
    .u-navbar--fixed {
      background: url($container-bg-url);
      background-repeat: no-repeat;
      background-size: cover;
      background-attachment: fixed;

      .u-navbar__content,
      .u-status-bar {
        background-color: transparent !important;
      }
    }
  }
</style>

公众平台配置wss

nginx配置

location /liuyao {
    proxy_pass http://localhost:10999;
    rewrite ^/liuyao/(.*) /$1 break;
    proxy_http_version 1.1;                         # 重要
    proxy_set_header Upgrade $http_upgrade;         # 重要
    proxy_set_header Connection "upgrade";          # 重要
}
原因
依赖
java接口
h5方式连接
uniapp方式连接
公众平台配置wss
nginx配置