Files
ylhp-ai-project-manager/docs/frontend-sse-integration.md
JiaoTianBo 6d91be8af5 feat(project): 实现异步项目初始化及SSE进度推送功能
- 新增异步任务线程池配置,支持项目初始化异步执行
- 定义异步任务状态枚举,统一管理任务生命周期状态
- 实现通用SSE通道管理器,支持用户绑定及多业务消息推送
- 创建统一SSE消息结构,支持多业务类型及事件分类
- 提供基础SSE连接管理接口,支持连接建立、状态查询及关闭
- 提供项目初始化异步任务服务接口及实现,支持进度回调和任务取消
- 添加项目初始化异步预览任务接口,支持异步提交、状态查询、结果获取及取消
- 新增项目初始化任务SSE接口,实现任务异步提交与实时进度推送
- 设计前端SSE集成文档,详细说明SSE连接、消息格式和对接步骤
- 添加Spring工具类,方便非Spring管理类获取Bean实例
- 优化项目控制器,整合异步任务相关API接口支持异步项目初始化工作流
2026-03-28 16:57:55 +08:00

358 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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();
}
```