feat(project): 实现异步项目初始化及SSE进度推送功能

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

View File

@@ -0,0 +1,118 @@
package cn.yinlihupo.controller.project;
import cn.yinlihupo.common.core.BaseResponse;
import cn.yinlihupo.common.enums.AsyncTaskStatus;
import cn.yinlihupo.common.sse.SseChannelManager;
import cn.yinlihupo.common.sse.SseMessage;
import cn.yinlihupo.common.util.ResultUtils;
import cn.yinlihupo.service.project.ProjectInitAsyncService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.Map;
/**
* 项目初始化 SSE 控制器
* 使用通用 SSE 通道管理器,通过 userId 绑定type 字段区分业务
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/project-init")
@RequiredArgsConstructor
public class ProjectInitSseController {
private final ProjectInitAsyncService projectInitAsyncService;
private final SseChannelManager sseChannelManager;
/**
* 消息类型常量
*/
private static final String MESSAGE_TYPE = "project-init";
/**
* 通过 SSE 提交项目初始化任务
* 使用通用 SSE 通道,通过 userId 推送进度
*
* @param userId 用户ID
* @param file 项目资料文件
* @return 提交结果
*/
@PostMapping("/sse/submit-task")
public BaseResponse<Map<String, Object>> submitTaskWithSse(@RequestParam("userId") String userId,
@RequestParam("file") MultipartFile file) {
log.info("用户通过SSE提交任务, userId: {}, 文件名: {}", userId, file.getOriginalFilename());
if (file.isEmpty()) {
return ResultUtils.error("上传文件不能为空");
}
// 检查用户是否在线
if (!sseChannelManager.isOnline(userId)) {
return ResultUtils.error("用户未建立SSE连接请先调用 /api/v1/sse/connect/" + userId);
}
try {
// 提交异步任务,带进度回调
String taskId = projectInitAsyncService.submitPreviewTask(file, taskVO -> {
// 构建消息并推送
SseMessage message = SseMessage.of(MESSAGE_TYPE, "progress", userId, taskVO);
sseChannelManager.send(userId, message);
// 任务完成或失败,推送完成事件
if (isTaskFinished(taskVO.getStatus())) {
SseMessage completeMessage = SseMessage.of(MESSAGE_TYPE, "complete", userId, taskVO);
sseChannelManager.send(userId, completeMessage);
}
});
// 推送任务提交成功事件
Map<String, Object> submittedData = new HashMap<>();
submittedData.put("taskId", taskId);
submittedData.put("message", "任务已提交");
SseMessage submittedMessage = SseMessage.of(MESSAGE_TYPE, "submitted", userId, submittedData);
sseChannelManager.send(userId, submittedMessage);
log.info("任务提交成功并通过SSE推送, userId: {}, taskId: {}", userId, taskId);
Map<String, Object> result = new HashMap<>();
result.put("taskId", taskId);
result.put("message", "任务已提交进度将通过SSE推送");
return ResultUtils.success("提交成功", result);
} catch (Exception e) {
log.error("提交任务失败, userId: {}, error: {}", userId, e.getMessage(), e);
// 推送错误事件
Map<String, Object> errorData = new HashMap<>();
errorData.put("error", e.getMessage());
SseMessage errorMessage = SseMessage.of(MESSAGE_TYPE, "error", userId, errorData);
sseChannelManager.send(userId, errorMessage);
return ResultUtils.error("提交失败: " + e.getMessage());
}
}
// ==================== 工具方法 ====================
private boolean isTaskFinished(String status) {
return AsyncTaskStatus.COMPLETED.getCode().equals(status) ||
AsyncTaskStatus.FAILED.getCode().equals(status) ||
AsyncTaskStatus.CANCELLED.getCode().equals(status);
}
private void sendErrorAndClose(String userId, String errorMessage) {
Map<String, Object> errorData = new HashMap<>();
errorData.put("error", errorMessage);
SseMessage errorMessage_obj = SseMessage.of(MESSAGE_TYPE, "error", userId, errorData);
sseChannelManager.send(userId, errorMessage_obj);
sseChannelManager.closeChannel(userId);
}
}