# SSE 前端对接文档 ## 概述 本文档描述前端如何与后端 SSE (Server-Sent Events) 服务进行对接,实现异步任务的实时进度推送。 ### 核心特性 - **用户绑定**:SSE 通道通过 `userId` 绑定,一个用户只需建立一个连接 - **多业务复用**:同一连接可接收多种业务类型的消息(项目初始化、系统通知等) - **类型区分**:通过消息中的 `type` 字段区分不同业务 --- ## 消息格式 所有 SSE 消息采用统一格式: ```json { "type": "project-init", // 业务类型 "event": "progress", // 事件名称 "userId": "user_123", // 用户ID "data": { ... }, // 业务数据 "timestamp": "2024-01-01T10:00:00" } ``` ### 业务类型 (type) | 类型 | 说明 | |------|------| | `project-init` | 项目初始化任务进度 | | `system-notification` | 系统通知 | | `task-notification` | 任务通知 | | `system` | 系统事件(连接成功等) | ### 事件名称 (event) #### 项目初始化 (type=project-init) | 事件 | 说明 | 数据结构 | |------|------|----------| | `submitted` | 任务已提交 | `{ taskId, message }` | | `progress` | 进度更新 | `ProjectInitTaskVO` | | `complete` | 任务完成 | `ProjectInitTaskVO` | | `error` | 执行错误 | `{ error }` | #### 系统事件 (type=system) | 事件 | 说明 | |------|------| | `connected` | SSE 连接成功 | --- ## 对接步骤 ### 1. 建立 SSE 连接 ```javascript // 使用用户ID建立连接 const userId = 'user_123'; // 当前登录用户ID const eventSource = new EventSource(`/api/v1/sse/connect/${userId}`); // 监听连接成功事件 eventSource.addEventListener('connected', (e) => { const message = JSON.parse(e.data); console.log('SSE连接成功:', message); // { type: "system", event: "connected", userId: "user_123", data: {...} } }); ``` ### 2. 监听业务消息 ```javascript // 监听项目初始化进度 eventSource.addEventListener('progress', (e) => { const message = JSON.parse(e.data); // 根据 type 字段处理不同业务 switch(message.type) { case 'project-init': handleProjectInitProgress(message.data); break; case 'system-notification': handleSystemNotification(message.data); break; case 'task-notification': handleTaskNotification(message.data); break; } }); // 监听任务完成 eventSource.addEventListener('complete', (e) => { const message = JSON.parse(e.data); if (message.type === 'project-init') { console.log('项目初始化完成:', message.data); // data 包含完整的 ProjectInitTaskVO,包括 result 字段 } }); // 监听错误 eventSource.addEventListener('error', (e) => { const message = JSON.parse(e.data); console.error('任务执行错误:', message.data.error); }); ``` ### 3. 提交项目初始化任务 ```javascript async function submitProjectInitTask(file) { const formData = new FormData(); formData.append('userId', userId); // 必须与 SSE 连接时的 userId 一致 formData.append('file', file); const response = await fetch('/api/v1/project-init/sse/submit-task', { method: 'POST', body: formData }); const result = await response.json(); if (result.code === 200) { console.log('任务提交成功:', result.data.taskId); // 进度将通过已建立的 SSE 连接推送 } else { console.error('提交失败:', result.message); } } ``` ### 4. 关闭连接 ```javascript // 页面卸载时关闭连接 window.addEventListener('beforeunload', () => { // 可选:调用后端关闭接口 fetch(`/api/v1/sse/close/${userId}`, { method: 'POST' }); eventSource.close(); }); ``` --- ## 完整示例 ```javascript class SseClient { constructor(userId) { this.userId = userId; this.eventSource = null; this.listeners = new Map(); } // 建立连接 connect() { this.eventSource = new EventSource(`/api/v1/sse/connect/${this.userId}`); // 系统事件 this.eventSource.addEventListener('connected', (e) => { console.log('SSE连接成功'); this.emit('connected', JSON.parse(e.data)); }); // 项目初始化事件 this.eventSource.addEventListener('submitted', (e) => { const msg = JSON.parse(e.data); if (msg.type === 'project-init') { this.emit('project-init-submitted', msg.data); } }); this.eventSource.addEventListener('progress', (e) => { const msg = JSON.parse(e.data); if (msg.type === 'project-init') { this.emit('project-init-progress', msg.data); } }); this.eventSource.addEventListener('complete', (e) => { const msg = JSON.parse(e.data); if (msg.type === 'project-init') { this.emit('project-init-complete', msg.data); } }); this.eventSource.addEventListener('error', (e) => { const msg = JSON.parse(e.data); this.emit('error', msg.data); }); } // 提交项目初始化任务 async submitProjectInitTask(file) { const formData = new FormData(); formData.append('userId', this.userId); formData.append('file', file); const response = await fetch('/api/v1/project-init/sse/submit-task', { method: 'POST', body: formData }); return response.json(); } // 事件监听 on(event, callback) { if (!this.listeners.has(event)) { this.listeners.set(event, []); } this.listeners.get(event).push(callback); } emit(event, data) { const callbacks = this.listeners.get(event); if (callbacks) { callbacks.forEach(cb => cb(data)); } } // 关闭连接 close() { if (this.eventSource) { this.eventSource.close(); } } } // 使用示例 const sseClient = new SseClient('user_123'); // 监听进度 sseClient.on('project-init-progress', (data) => { console.log(`进度: ${data.progress}%, ${data.progressMessage}`); // 更新进度条 }); sseClient.on('project-init-complete', (data) => { console.log('完成:', data.result); // 显示结果 }); // 建立连接 sseClient.connect(); // 提交任务 document.getElementById('uploadBtn').addEventListener('click', async () => { const file = document.getElementById('fileInput').files[0]; const result = await sseClient.submitProjectInitTask(file); console.log('提交结果:', result); }); ``` --- ## 数据结构 ### ProjectInitTaskVO ```typescript interface ProjectInitTaskVO { taskId: string; // 任务ID status: string; // 状态: pending/processing/completed/failed statusDesc: string; // 状态描述 progress: number; // 进度百分比 (0-100) progressMessage: string; // 进度描述 originalFilename: string; // 原始文件名 createTime: string; // 创建时间 startTime: string; // 开始时间 completeTime: string; // 完成时间 result?: ProjectInitResult; // 结果数据(完成时) errorMessage?: string; // 错误信息(失败时) } ``` ### 进度阶段说明 | 进度 | 阶段 | 说明 | |------|------|------| | 0% | pending | 任务已提交,等待处理 | | 10% | processing | 开始处理,正在上传文件 | | 30% | processing | 文件上传完成,读取内容 | | 50% | processing | 文件读取完成,AI分析中 | | 60% | processing | AI解析项目结构 | | 100% | completed | 项目预览数据生成成功 | --- ## 错误处理 ### 连接错误 ```javascript eventSource.onerror = (error) => { console.error('SSE连接错误:', error); // 可尝试重连 }; ``` ### 提交任务错误 ```javascript // HTTP 响应错误 if (response.code !== 200) { console.error('提交失败:', response.message); // 可能的错误: // - "上传文件不能为空" // - "用户未建立SSE连接,请先调用 /api/v1/sse/connect/{userId}" } // 任务执行错误(通过 SSE 推送) eventSource.addEventListener('error', (e) => { const msg = JSON.parse(e.data); console.error('任务执行错误:', msg.data.error); }); ``` --- ## 注意事项 1. **用户ID一致性**:SSE 连接和提交任务时必须使用相同的 `userId` 2. **连接超时**:默认 30 分钟超时,超时后需要重新建立连接 3. **单用户单连接**:一个 `userId` 同时只能有一个 SSE 连接,新建连接会自动关闭旧连接 4. **文件大小限制**:建议前端先做文件大小校验,避免上传过大文件 5. **重连机制**:建议实现自动重连机制,当连接断开时自动重新建立连接 ```javascript // 简单重连示例 function connectWithRetry(userId, maxRetries = 3) { let retries = 0; const connect = () => { const es = new EventSource(`/api/v1/sse/connect/${userId}`); es.onerror = (e) => { es.close(); retries++; if (retries < maxRetries) { setTimeout(connect, 3000); // 3秒后重试 } }; return es; }; return connect(); } ```