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

9.3 KiB
Raw Blame History

SSE 前端对接文档

概述

本文档描述前端如何与后端 SSE (Server-Sent Events) 服务进行对接,实现异步任务的实时进度推送。

核心特性

  • 用户绑定SSE 通道通过 userId 绑定,一个用户只需建立一个连接
  • 多业务复用:同一连接可接收多种业务类型的消息(项目初始化、系统通知等)
  • 类型区分:通过消息中的 type 字段区分不同业务

消息格式

所有 SSE 消息采用统一格式:

{
  "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 连接

// 使用用户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. 监听业务消息

// 监听项目初始化进度
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. 提交项目初始化任务

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. 关闭连接

// 页面卸载时关闭连接
window.addEventListener('beforeunload', () => {
    // 可选:调用后端关闭接口
    fetch(`/api/v1/sse/close/${userId}`, { method: 'POST' });
    eventSource.close();
});

完整示例

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

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 项目预览数据生成成功

错误处理

连接错误

eventSource.onerror = (error) => {
    console.error('SSE连接错误:', error);
    // 可尝试重连
};

提交任务错误

// 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. 重连机制:建议实现自动重连机制,当连接断开时自动重新建立连接

// 简单重连示例
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();
}