Files
1818web-hoduan/WebSocket任务通知接收示例.md
2025-11-14 17:41:15 +08:00

20 KiB
Raw Blame History

WebSocket 任务通知接收示例

一、WebSocket 配置说明

后端配置

  • 连接端点: /user/websocket(支持 SockJS 备用方案)
  • 用户前缀: /user
  • 订阅目的地: /user/queue/tasks-progress
  • 协议: STOMP over WebSocket

消息格式TaskProgressDto

interface TaskProgressDto {
  taskNo: string;           // 任务编号
  status: string;           // 任务状态: created/queued/processing/completed/failed
  progress: number;         // 进度百分比 0-100
  message: string;          // 进度消息
  resultUrl?: string;       // 结果URL完成时
  errorMessage?: string;    // 错误信息(失败时)
}

二、前端依赖安装

使用 npm

npm install @stomp/stompjs sockjs-client

使用 yarn

yarn add @stomp/stompjs sockjs-client

CDN 引入HTML

<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@7.0.0/bundles/stomp.umd.min.js"></script>

三、基础连接示例

1. 原生 JavaScript + STOMP.js

// 引入依赖(如果使用模块化)
import SockJS from 'sockjs-client';
import { Client } from '@stomp/stompjs';

// WebSocket 配置
const WEBSOCKET_URL = 'http://localhost:8081/ws';
const AUTH_TOKEN = 'YOUR_JWT_TOKEN';

// 创建 STOMP 客户端
const stompClient = new Client({
  // 使用 SockJS 作为 WebSocket 实现
  webSocketFactory: () => new SockJS(WEBSOCKET_URL),
  
  // 连接头(携带认证信息)
  connectHeaders: {
    Authorization: `Bearer ${AUTH_TOKEN}`
  },
  
  // 连接成功回调
  onConnect: (frame) => {
    console.log('WebSocket 连接成功:', frame);
    
    // 订阅任务进度更新
    stompClient.subscribe('/user/queue/tasks-progress', (message) => {
      const notification = JSON.parse(message.body);
      console.log('收到任务通知:', notification);
      
      // 处理通知
      handleTaskNotification(notification);
    });
  },
  
  // 连接失败回调
  onStompError: (frame) => {
    console.error('STOMP 错误:', frame);
  },
  
  // WebSocket 错误回调
  onWebSocketError: (error) => {
    console.error('WebSocket 错误:', error);
  },
  
  // WebSocket 关闭回调
  onWebSocketClose: (event) => {
    console.log('WebSocket 连接关闭:', event);
  },
  
  // 自动重连配置
  reconnectDelay: 5000,  // 5秒后重连
  heartbeatIncoming: 4000,
  heartbeatOutgoing: 4000
});

// 激活连接
stompClient.activate();

// 处理任务通知
function handleTaskNotification(notification) {
  const { taskNo, status, progress, message, resultUrl, errorMessage } = notification;
  
  switch(status) {
    case 'created':
      console.log(`[${taskNo}] 任务已创建`);
      break;
    case 'queued':
      console.log(`[${taskNo}] 任务排队中: ${message}`);
      break;
    case 'processing':
      console.log(`[${taskNo}] 处理中 ${progress}%: ${message}`);
      updateProgressBar(taskNo, progress);
      break;
    case 'completed':
      console.log(`[${taskNo}] 任务完成: ${resultUrl}`);
      showCompletedNotification(taskNo, resultUrl);
      break;
    case 'failed':
      console.error(`[${taskNo}] 任务失败: ${errorMessage || message}`);
      showErrorNotification(taskNo, errorMessage || message);
      break;
  }
}

// 断开连接
function disconnect() {
  if (stompClient.connected) {
    stompClient.deactivate();
    console.log('WebSocket 已断开');
  }
}

四、完整封装类(推荐)

TaskWebSocketClient.js

import SockJS from 'sockjs-client';
import { Client } from '@stomp/stompjs';

class TaskWebSocketClient {
  constructor(baseUrl = 'http://localhost:8081', token) {
    this.baseUrl = baseUrl;
    this.token = token;
    this.client = null;
    this.listeners = {
      onTaskUpdate: [],
      onConnect: [],
      onDisconnect: [],
      onError: []
    };
  }
  
  /**
   * 连接 WebSocket
   */
  connect() {
    if (this.client && this.client.connected) {
      console.warn('WebSocket 已连接');
      return Promise.resolve();
    }
    
    return new Promise((resolve, reject) => {
      this.client = new Client({
        webSocketFactory: () => new SockJS(`${this.baseUrl}/ws`),
        
        connectHeaders: {
          Authorization: `Bearer ${this.token}`
        },
        
        onConnect: (frame) => {
          console.log('✅ WebSocket 连接成功');
          
          // 订阅任务进度更新
          this.client.subscribe('/user/queue/tasks-progress', (message) => {
            try {
              const notification = JSON.parse(message.body);
              this._notifyListeners('onTaskUpdate', notification);
            } catch (error) {
              console.error('解析消息失败:', error);
            }
          });
          
          this._notifyListeners('onConnect', frame);
          resolve(frame);
        },
        
        onStompError: (frame) => {
          console.error('❌ STOMP 错误:', frame);
          this._notifyListeners('onError', frame);
          reject(frame);
        },
        
        onWebSocketError: (error) => {
          console.error('❌ WebSocket 错误:', error);
          this._notifyListeners('onError', error);
        },
        
        onWebSocketClose: (event) => {
          console.log('🔌 WebSocket 连接关闭');
          this._notifyListeners('onDisconnect', event);
        },
        
        reconnectDelay: 5000,
        heartbeatIncoming: 4000,
        heartbeatOutgoing: 4000
      });
      
      this.client.activate();
    });
  }
  
  /**
   * 断开 WebSocket
   */
  disconnect() {
    if (this.client && this.client.connected) {
      this.client.deactivate();
      console.log('WebSocket 已断开');
    }
  }
  
  /**
   * 添加任务更新监听器
   */
  onTaskUpdate(callback) {
    this.listeners.onTaskUpdate.push(callback);
    return () => this._removeListener('onTaskUpdate', callback);
  }
  
  /**
   * 添加连接监听器
   */
  onConnect(callback) {
    this.listeners.onConnect.push(callback);
    return () => this._removeListener('onConnect', callback);
  }
  
  /**
   * 添加断开连接监听器
   */
  onDisconnect(callback) {
    this.listeners.onDisconnect.push(callback);
    return () => this._removeListener('onDisconnect', callback);
  }
  
  /**
   * 添加错误监听器
   */
  onError(callback) {
    this.listeners.onError.push(callback);
    return () => this._removeListener('onError', callback);
  }
  
  /**
   * 通知所有监听器
   */
  _notifyListeners(event, data) {
    this.listeners[event].forEach(callback => {
      try {
        callback(data);
      } catch (error) {
        console.error(`监听器执行错误 (${event}):`, error);
      }
    });
  }
  
  /**
   * 移除监听器
   */
  _removeListener(event, callback) {
    const index = this.listeners[event].indexOf(callback);
    if (index > -1) {
      this.listeners[event].splice(index, 1);
    }
  }
  
  /**
   * 检查连接状态
   */
  isConnected() {
    return this.client && this.client.connected;
  }
}

export default TaskWebSocketClient;

五、使用示例

1. React 组件示例

import React, { useEffect, useState } from 'react';
import TaskWebSocketClient from './TaskWebSocketClient';

function TaskMonitor() {
  const [tasks, setTasks] = useState({});
  const [wsClient, setWsClient] = useState(null);
  
  useEffect(() => {
    // 获取 Token从 localStorage 或其他地方)
    const token = localStorage.getItem('jwt_token');
    
    // 创建 WebSocket 客户端
    const client = new TaskWebSocketClient('http://localhost:8081', token);
    
    // 监听任务更新
    const unsubscribe = client.onTaskUpdate((notification) => {
      console.log('任务更新:', notification);
      
      // 更新任务状态
      setTasks(prev => ({
        ...prev,
        [notification.taskNo]: notification
      }));
      
      // 根据状态显示不同提示
      if (notification.status === 'completed') {
        showSuccessToast(`任务 ${notification.taskNo} 已完成!`);
      } else if (notification.status === 'failed') {
        showErrorToast(`任务 ${notification.taskNo} 失败: ${notification.errorMessage}`);
      }
    });
    
    // 连接 WebSocket
    client.connect().catch(error => {
      console.error('连接失败:', error);
    });
    
    setWsClient(client);
    
    // 清理函数
    return () => {
      unsubscribe();
      client.disconnect();
    };
  }, []);
  
  return (
    <div>
      <h2>任务监控</h2>
      {Object.values(tasks).map(task => (
        <div key={task.taskNo} className="task-card">
          <h3>{task.taskNo}</h3>
          <p>状态: {task.status}</p>
          <p>进度: {task.progress}%</p>
          <p>{task.message}</p>
          {task.resultUrl && (
            <a href={task.resultUrl} target="_blank" rel="noopener noreferrer">
              查看结果
            </a>
          )}
        </div>
      ))}
    </div>
  );
}

export default TaskMonitor;

2. Vue 3 组件示例

<template>
  <div class="task-monitor">
    <h2>任务监控</h2>
    <div v-for="task in taskList" :key="task.taskNo" class="task-card">
      <h3>{{ task.taskNo }}</h3>
      <p>状态: {{ task.status }}</p>
      <div v-if="task.status === 'processing'">
        <progress :value="task.progress" max="100"></progress>
        <span>{{ task.progress }}%</span>
      </div>
      <p>{{ task.message }}</p>
      <a v-if="task.resultUrl" :href="task.resultUrl" target="_blank">
        查看结果
      </a>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import TaskWebSocketClient from './TaskWebSocketClient';

const tasks = ref({});
const taskList = computed(() => Object.values(tasks.value));
let wsClient = null;

onMounted(() => {
  const token = localStorage.getItem('jwt_token');
  wsClient = new TaskWebSocketClient('http://localhost:8081', token);
  
  // 监听任务更新
  wsClient.onTaskUpdate((notification) => {
    tasks.value[notification.taskNo] = notification;
    
    if (notification.status === 'completed') {
      ElMessage.success(`任务 ${notification.taskNo} 已完成!`);
    } else if (notification.status === 'failed') {
      ElMessage.error(`任务失败: ${notification.errorMessage}`);
    }
  });
  
  // 连接
  wsClient.connect();
});

onUnmounted(() => {
  if (wsClient) {
    wsClient.disconnect();
  }
});
</script>

3. 原生 JavaScript 完整示例

<!DOCTYPE html>
<html>
<head>
  <title>任务监控</title>
  <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@7.0.0/bundles/stomp.umd.min.js"></script>
</head>
<body>
  <h1>AI 任务实时监控</h1>
  <div id="connection-status">未连接</div>
  <div id="tasks-container"></div>
  
  <script>
    const AUTH_TOKEN = localStorage.getItem('jwt_token');
    const tasks = {};
    
    // 创建 STOMP 客户端
    const client = new StompJs.Client({
      webSocketFactory: () => new SockJS('http://localhost:8081/ws'),
      connectHeaders: {
        Authorization: `Bearer ${AUTH_TOKEN}`
      },
      onConnect: (frame) => {
        document.getElementById('connection-status').textContent = '✅ 已连接';
        
        // 订阅任务通知
        client.subscribe('/user/queue/tasks-progress', (message) => {
          const notification = JSON.parse(message.body);
          handleTaskNotification(notification);
        });
      },
      onStompError: (frame) => {
        document.getElementById('connection-status').textContent = '❌ 连接错误';
        console.error('STOMP 错误:', frame);
      },
      reconnectDelay: 5000,
      heartbeatIncoming: 4000,
      heartbeatOutgoing: 4000
    });
    
    // 处理任务通知
    function handleTaskNotification(notification) {
      const { taskNo, status, progress, message, resultUrl, errorMessage } = notification;
      
      // 更新任务数据
      tasks[taskNo] = notification;
      
      // 渲染任务列表
      renderTasks();
      
      // 显示浏览器通知(如果支持)
      if (status === 'completed' && 'Notification' in window && Notification.permission === 'granted') {
        new Notification('任务完成', {
          body: `任务 ${taskNo} 已成功完成!`,
          icon: '/icon.png'
        });
      }
    }
    
    // 渲染任务列表
    function renderTasks() {
      const container = document.getElementById('tasks-container');
      container.innerHTML = '';
      
      Object.values(tasks).forEach(task => {
        const taskDiv = document.createElement('div');
        taskDiv.className = 'task-card';
        taskDiv.innerHTML = `
          <h3>${task.taskNo}</h3>
          <p>状态: ${task.status}</p>
          <p>进度: ${task.progress}%</p>
          <progress value="${task.progress}" max="100"></progress>
          <p>${task.message}</p>
          ${task.resultUrl ? `<a href="${task.resultUrl}" target="_blank">查看结果</a>` : ''}
          ${task.errorMessage ? `<p style="color:red">错误: ${task.errorMessage}</p>` : ''}
        `;
        container.appendChild(taskDiv);
      });
    }
    
    // 激活连接
    client.activate();
    
    // 页面关闭时断开连接
    window.addEventListener('beforeunload', () => {
      if (client.connected) {
        client.deactivate();
      }
    });
    
    // 请求浏览器通知权限
    if ('Notification' in window && Notification.permission === 'default') {
      Notification.requestPermission();
    }
  </script>
  
  <style>
    .task-card {
      border: 1px solid #ddd;
      padding: 15px;
      margin: 10px 0;
      border-radius: 8px;
    }
    progress {
      width: 100%;
      height: 20px;
    }
  </style>
</body>
</html>

六、完整业务流程示例

import TaskWebSocketClient from './TaskWebSocketClient';
import SuChuangImageGenerator from './SuChuangImageGenerator';

class AITaskManager {
  constructor(token, baseUrl = 'http://localhost:8081') {
    this.wsClient = new TaskWebSocketClient(baseUrl, token);
    this.generator = new SuChuangImageGenerator(token, baseUrl);
    this.pendingTasks = new Map();
  }
  
  /**
   * 初始化(连接 WebSocket 并设置监听器)
   */
  async init() {
    // 监听任务更新
    this.wsClient.onTaskUpdate((notification) => {
      this.handleTaskUpdate(notification);
    });
    
    // 连接 WebSocket
    await this.wsClient.connect();
    console.log('AI 任务管理器已初始化');
  }
  
  /**
   * 提交文生图任务
   */
  async submitTextToImage(prompt, aspectRatio = '1:1', onProgress, onComplete, onError) {
    try {
      // 提交任务
      const taskNo = await this.generator.generateImage(prompt, aspectRatio);
      
      // 注册回调
      this.pendingTasks.set(taskNo, { onProgress, onComplete, onError });
      
      return taskNo;
    } catch (error) {
      if (onError) onError(error);
      throw error;
    }
  }
  
  /**
   * 提交图生图任务
   */
  async submitImageToImage(prompt, imageUrl, aspectRatio = '1:1', onProgress, onComplete, onError) {
    try {
      const taskNo = await this.generator.transformImage(prompt, imageUrl, aspectRatio);
      this.pendingTasks.set(taskNo, { onProgress, onComplete, onError });
      return taskNo;
    } catch (error) {
      if (onError) onError(error);
      throw error;
    }
  }
  
  /**
   * 处理任务更新
   */
  handleTaskUpdate(notification) {
    const { taskNo, status, progress, message, resultUrl, errorMessage } = notification;
    const callbacks = this.pendingTasks.get(taskNo);
    
    if (!callbacks) return;
    
    const { onProgress, onComplete, onError } = callbacks;
    
    switch(status) {
      case 'processing':
        if (onProgress) onProgress(progress, message);
        break;
      
      case 'completed':
        if (onComplete) onComplete(resultUrl);
        this.pendingTasks.delete(taskNo);
        break;
      
      case 'failed':
        if (onError) onError(new Error(errorMessage || message));
        this.pendingTasks.delete(taskNo);
        break;
    }
  }
  
  /**
   * 清理资源
   */
  destroy() {
    this.wsClient.disconnect();
    this.pendingTasks.clear();
  }
}

// 使用示例
const taskManager = new AITaskManager(YOUR_JWT_TOKEN);

// 初始化
await taskManager.init();

// 提交任务并监听进度
const taskNo = await taskManager.submitTextToImage(
  '一只可爱的柴犬',
  '1:1',
  (progress, message) => {
    console.log(`进度: ${progress}% - ${message}`);
    updateProgressBar(progress);
  },
  (resultUrl) => {
    console.log('生成完成:', resultUrl);
    displayImage(resultUrl);
  },
  (error) => {
    console.error('生成失败:', error);
    showErrorMessage(error.message);
  }
);

console.log('任务已提交:', taskNo);

七、常见问题

1. 跨域问题

如果前端和后端不在同一域名,确保后端已配置 CORS

// WebSocketConfig.java 中已配置
registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS();

2. 认证失败

确保在连接时传递了正确的 JWT Token

connectHeaders: {
  Authorization: `Bearer ${YOUR_JWT_TOKEN}`
}

3. 连接断开自动重连

STOMP 客户端已配置自动重连:

reconnectDelay: 5000  // 5秒后自动重连

4. 心跳检测

防止连接超时:

heartbeatIncoming: 4000,  // 接收心跳间隔
heartbeatOutgoing: 4000   // 发送心跳间隔

八、调试技巧

1. 开启详细日志

const client = new Client({
  // ... 其他配置
  debug: (str) => {
    console.log('STOMP Debug:', str);
  }
});

2. 监控连接状态

client.onConnect = () => console.log('✅ 已连接');
client.onDisconnect = () => console.log('🔌 已断开');
client.onWebSocketError = (error) => console.error('❌ 错误:', error);

3. 测试消息接收

client.subscribe('/user/queue/tasks-progress', (message) => {
  console.log('收到原始消息:', message.body);
  const data = JSON.parse(message.body);
  console.log('解析后的数据:', data);
});

九、安全建议

  1. 不要在前端暴露敏感信息

    • Token 应通过 localStorage 或 sessionStorage 安全存储
    • 避免在 URL 中传递 Token
  2. 设置合理的超时时间

    • 避免长时间保持空闲连接
  3. 处理连接断开

    • 实现重连逻辑
    • 提示用户连接状态
  4. 验证消息来源

    • 确认 taskNo 是否为当前用户的任务

十、完整目录结构

src/
├── websocket/
│   ├── TaskWebSocketClient.js       # WebSocket 封装类
│   └── AITaskManager.js             # 任务管理器
├── api/
│   └── SuChuangImageGenerator.js    # 任务提交API
├── components/
│   ├── TaskMonitor.jsx              # React 任务监控组件
│   └── TaskMonitor.vue              # Vue 任务监控组件
└── utils/
    └── notification.js              # 浏览器通知工具

总结

前端通过以下步骤接收 WebSocket 通知:

  1. 连接: new SockJS('http://localhost:8081/ws')
  2. 认证: 在 connectHeaders 中传递 JWT Token
  3. 订阅: client.subscribe('/user/queue/tasks-progress', callback)
  4. 处理: 根据 status 字段处理不同状态的通知

关键点

  • 端点以 /ws 开头(不是 /user/ws
  • 订阅地址为 /user/queue/tasks-progress
  • Spring 会自动将消息路由到当前用户
  • 支持自动重连和心跳检测