790 lines
20 KiB
Markdown
790 lines
20 KiB
Markdown
# WebSocket 任务通知接收示例
|
||
|
||
## 一、WebSocket 配置说明
|
||
|
||
### 后端配置
|
||
- **连接端点**: `/user/websocket`(支持 SockJS 备用方案)
|
||
- **用户前缀**: `/user`
|
||
- **订阅目的地**: `/user/queue/tasks-progress`
|
||
- **协议**: STOMP over WebSocket
|
||
|
||
### 消息格式(TaskProgressDto)
|
||
```typescript
|
||
interface TaskProgressDto {
|
||
taskNo: string; // 任务编号
|
||
status: string; // 任务状态: created/queued/processing/completed/failed
|
||
progress: number; // 进度百分比 0-100
|
||
message: string; // 进度消息
|
||
resultUrl?: string; // 结果URL(完成时)
|
||
errorMessage?: string; // 错误信息(失败时)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 二、前端依赖安装
|
||
|
||
### 使用 npm
|
||
```bash
|
||
npm install @stomp/stompjs sockjs-client
|
||
```
|
||
|
||
### 使用 yarn
|
||
```bash
|
||
yarn add @stomp/stompjs sockjs-client
|
||
```
|
||
|
||
### CDN 引入(HTML)
|
||
```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
|
||
|
||
```javascript
|
||
// 引入依赖(如果使用模块化)
|
||
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
|
||
|
||
```javascript
|
||
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 组件示例
|
||
|
||
```jsx
|
||
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 组件示例
|
||
|
||
```vue
|
||
<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 完整示例
|
||
|
||
```html
|
||
<!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>
|
||
```
|
||
|
||
---
|
||
|
||
## 六、完整业务流程示例
|
||
|
||
```javascript
|
||
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:
|
||
|
||
```java
|
||
// WebSocketConfig.java 中已配置
|
||
registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS();
|
||
```
|
||
|
||
### 2. 认证失败
|
||
确保在连接时传递了正确的 JWT Token:
|
||
|
||
```javascript
|
||
connectHeaders: {
|
||
Authorization: `Bearer ${YOUR_JWT_TOKEN}`
|
||
}
|
||
```
|
||
|
||
### 3. 连接断开自动重连
|
||
STOMP 客户端已配置自动重连:
|
||
|
||
```javascript
|
||
reconnectDelay: 5000 // 5秒后自动重连
|
||
```
|
||
|
||
### 4. 心跳检测
|
||
防止连接超时:
|
||
|
||
```javascript
|
||
heartbeatIncoming: 4000, // 接收心跳间隔
|
||
heartbeatOutgoing: 4000 // 发送心跳间隔
|
||
```
|
||
|
||
---
|
||
|
||
## 八、调试技巧
|
||
|
||
### 1. 开启详细日志
|
||
```javascript
|
||
const client = new Client({
|
||
// ... 其他配置
|
||
debug: (str) => {
|
||
console.log('STOMP Debug:', str);
|
||
}
|
||
});
|
||
```
|
||
|
||
### 2. 监控连接状态
|
||
```javascript
|
||
client.onConnect = () => console.log('✅ 已连接');
|
||
client.onDisconnect = () => console.log('🔌 已断开');
|
||
client.onWebSocketError = (error) => console.error('❌ 错误:', error);
|
||
```
|
||
|
||
### 3. 测试消息接收
|
||
```javascript
|
||
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 会自动将消息路由到当前用户
|
||
- ✅ 支持自动重连和心跳检测
|
||
|