feat(project): 实现异步项目初始化及SSE进度推送功能
- 新增异步任务线程池配置,支持项目初始化异步执行 - 定义异步任务状态枚举,统一管理任务生命周期状态 - 实现通用SSE通道管理器,支持用户绑定及多业务消息推送 - 创建统一SSE消息结构,支持多业务类型及事件分类 - 提供基础SSE连接管理接口,支持连接建立、状态查询及关闭 - 提供项目初始化异步任务服务接口及实现,支持进度回调和任务取消 - 添加项目初始化异步预览任务接口,支持异步提交、状态查询、结果获取及取消 - 新增项目初始化任务SSE接口,实现任务异步提交与实时进度推送 - 设计前端SSE集成文档,详细说明SSE连接、消息格式和对接步骤 - 添加Spring工具类,方便非Spring管理类获取Bean实例 - 优化项目控制器,整合异步任务相关API接口支持异步项目初始化工作流
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
package cn.yinlihupo.service.project;
|
||||
|
||||
import cn.yinlihupo.domain.vo.ProjectInitResult;
|
||||
import cn.yinlihupo.domain.vo.ProjectInitTaskVO;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 项目初始化异步任务服务接口
|
||||
*/
|
||||
public interface ProjectInitAsyncService {
|
||||
|
||||
/**
|
||||
* 提交异步项目初始化预览任务
|
||||
*
|
||||
* @param file 项目资料文件
|
||||
* @return 任务ID
|
||||
*/
|
||||
String submitPreviewTask(MultipartFile file);
|
||||
|
||||
/**
|
||||
* 提交异步项目初始化预览任务(带进度回调)
|
||||
*
|
||||
* @param file 项目资料文件
|
||||
* @param progressCallback 进度回调函数
|
||||
* @return 任务ID
|
||||
*/
|
||||
String submitPreviewTask(MultipartFile file, Consumer<ProjectInitTaskVO> progressCallback);
|
||||
|
||||
/**
|
||||
* 获取任务状态
|
||||
*
|
||||
* @param taskId 任务ID
|
||||
* @return 任务状态VO
|
||||
*/
|
||||
ProjectInitTaskVO getTaskStatus(String taskId);
|
||||
|
||||
/**
|
||||
* 获取任务结果
|
||||
*
|
||||
* @param taskId 任务ID
|
||||
* @return 项目初始化结果
|
||||
*/
|
||||
ProjectInitResult getTaskResult(String taskId);
|
||||
|
||||
/**
|
||||
* 取消任务
|
||||
*
|
||||
* @param taskId 任务ID
|
||||
* @return 是否取消成功
|
||||
*/
|
||||
boolean cancelTask(String taskId);
|
||||
|
||||
/**
|
||||
* 清理过期任务
|
||||
*
|
||||
* @param expireHours 过期时间(小时)
|
||||
*/
|
||||
void cleanExpiredTasks(int expireHours);
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package cn.yinlihupo.service.project.impl;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.yinlihupo.common.enums.AsyncTaskStatus;
|
||||
import cn.yinlihupo.domain.vo.ProjectInitResult;
|
||||
import cn.yinlihupo.domain.vo.ProjectInitTaskVO;
|
||||
import cn.yinlihupo.service.oss.OssService;
|
||||
import cn.yinlihupo.service.project.ProjectInitAsyncService;
|
||||
import cn.yinlihupo.service.project.ProjectService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 项目初始化异步任务服务实现类
|
||||
* 使用内存存储任务状态
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ProjectInitAsyncServiceImpl implements ProjectInitAsyncService {
|
||||
|
||||
private final ProjectService projectService;
|
||||
private final OssService ossService;
|
||||
|
||||
/**
|
||||
* 任务存储(内存存储)
|
||||
*/
|
||||
private final Map<String, ProjectInitTaskVO> taskStore = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 进度回调存储(内存存储,仅当前实例有效)
|
||||
*/
|
||||
private final Map<String, Consumer<ProjectInitTaskVO>> progressCallbacks = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public String submitPreviewTask(MultipartFile file) {
|
||||
return submitPreviewTask(file, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String submitPreviewTask(MultipartFile file, Consumer<ProjectInitTaskVO> progressCallback) {
|
||||
// 生成任务ID
|
||||
String taskId = IdUtil.fastSimpleUUID();
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
|
||||
log.info("提交项目初始化预览任务, taskId: {}, 文件名: {}", taskId, originalFilename);
|
||||
|
||||
// 创建任务记录
|
||||
ProjectInitTaskVO taskVO = new ProjectInitTaskVO();
|
||||
taskVO.setTaskId(taskId);
|
||||
taskVO.setStatus(AsyncTaskStatus.PENDING.getCode());
|
||||
taskVO.setStatusDesc(AsyncTaskStatus.PENDING.getDescription());
|
||||
taskVO.setProgress(0);
|
||||
taskVO.setProgressMessage("任务已提交,等待处理...");
|
||||
taskVO.setOriginalFilename(originalFilename);
|
||||
taskVO.setCreateTime(LocalDateTime.now());
|
||||
|
||||
// 存储到内存
|
||||
taskStore.put(taskId, taskVO);
|
||||
|
||||
// 保存进度回调
|
||||
if (progressCallback != null) {
|
||||
progressCallbacks.put(taskId, progressCallback);
|
||||
}
|
||||
|
||||
// 异步执行任务
|
||||
executePreviewTaskAsync(taskId, file);
|
||||
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步执行预览任务
|
||||
*/
|
||||
@Async("projectInitTaskExecutor")
|
||||
public CompletableFuture<Void> executePreviewTaskAsync(String taskId, MultipartFile file) {
|
||||
ProjectInitTaskVO taskVO = taskStore.get(taskId);
|
||||
if (taskVO == null) {
|
||||
log.error("任务不存在, taskId: {}", taskId);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
try {
|
||||
// 更新状态为处理中
|
||||
updateTaskProgress(taskId, AsyncTaskStatus.PROCESSING, 10, "正在上传文件...");
|
||||
|
||||
// 1. 上传文件到OSS
|
||||
String fileUrl = ossService.uploadFile(file, file.getOriginalFilename());
|
||||
log.info("文件上传成功, taskId: {}, URL: {}", taskId, fileUrl);
|
||||
updateTaskProgress(taskId, AsyncTaskStatus.PROCESSING, 30, "文件上传完成,正在读取内容...");
|
||||
|
||||
// 2. 读取文件内容
|
||||
String content = ossService.readFileAsString(fileUrl);
|
||||
if (content == null || content.isEmpty()) {
|
||||
throw new RuntimeException("无法读取文件内容: " + fileUrl);
|
||||
}
|
||||
updateTaskProgress(taskId, AsyncTaskStatus.PROCESSING, 50, "文件读取完成,AI正在分析...");
|
||||
|
||||
// 3. 调用AI生成项目预览数据
|
||||
updateTaskProgress(taskId, AsyncTaskStatus.PROCESSING, 60, "AI正在解析项目结构...");
|
||||
ProjectInitResult result = projectService.generateProjectFromContent(content);
|
||||
|
||||
// 4. 更新任务完成状态
|
||||
updateTaskProgress(taskId, AsyncTaskStatus.COMPLETED, 100, "项目预览数据生成成功");
|
||||
taskVO.setResult(result);
|
||||
taskVO.setCompleteTime(LocalDateTime.now());
|
||||
|
||||
log.info("项目初始化预览任务完成, taskId: {}", taskId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("项目初始化预览任务失败, taskId: {}, error: {}", taskId, e.getMessage(), e);
|
||||
updateTaskProgress(taskId, AsyncTaskStatus.FAILED, 0, "任务执行失败");
|
||||
taskVO.setErrorMessage(e.getMessage());
|
||||
taskVO.setCompleteTime(LocalDateTime.now());
|
||||
} finally {
|
||||
// 清理回调(仅清理内存中的回调)
|
||||
progressCallbacks.remove(taskId);
|
||||
// 注意:Redis中的任务数据保留,供后续查询,24小时后自动过期
|
||||
}
|
||||
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务进度
|
||||
*/
|
||||
private void updateTaskProgress(String taskId, AsyncTaskStatus status, int progress, String message) {
|
||||
ProjectInitTaskVO taskVO = taskStore.get(taskId);
|
||||
if (taskVO == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
taskVO.setStatus(status.getCode());
|
||||
taskVO.setStatusDesc(status.getDescription());
|
||||
taskVO.setProgress(progress);
|
||||
taskVO.setProgressMessage(message);
|
||||
|
||||
if (status == AsyncTaskStatus.PROCESSING && taskVO.getStartTime() == null) {
|
||||
taskVO.setStartTime(LocalDateTime.now());
|
||||
}
|
||||
|
||||
// 更新内存存储
|
||||
taskStore.put(taskId, taskVO);
|
||||
|
||||
// 触发进度回调
|
||||
Consumer<ProjectInitTaskVO> callback = progressCallbacks.get(taskId);
|
||||
if (callback != null) {
|
||||
try {
|
||||
callback.accept(taskVO);
|
||||
} catch (Exception e) {
|
||||
log.warn("进度回调执行失败, taskId: {}", taskId, e);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("任务进度更新, taskId: {}, status: {}, progress: {}%, message: {}",
|
||||
taskId, status.getCode(), progress, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProjectInitTaskVO getTaskStatus(String taskId) {
|
||||
ProjectInitTaskVO taskVO = taskStore.get(taskId);
|
||||
if (taskVO == null) {
|
||||
return null;
|
||||
}
|
||||
// 返回副本,避免外部修改
|
||||
return copyTaskVO(taskVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProjectInitResult getTaskResult(String taskId) {
|
||||
ProjectInitTaskVO taskVO = taskStore.get(taskId);
|
||||
if (taskVO == null || !AsyncTaskStatus.COMPLETED.getCode().equals(taskVO.getStatus())) {
|
||||
return null;
|
||||
}
|
||||
return taskVO.getResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancelTask(String taskId) {
|
||||
ProjectInitTaskVO taskVO = taskStore.get(taskId);
|
||||
if (taskVO == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 只能取消待处理或处理中的任务
|
||||
if (AsyncTaskStatus.PENDING.getCode().equals(taskVO.getStatus()) ||
|
||||
AsyncTaskStatus.PROCESSING.getCode().equals(taskVO.getStatus())) {
|
||||
updateTaskProgress(taskId, AsyncTaskStatus.CANCELLED, 0, "任务已取消");
|
||||
taskVO.setCompleteTime(LocalDateTime.now());
|
||||
taskStore.put(taskId, taskVO);
|
||||
progressCallbacks.remove(taskId);
|
||||
log.info("任务已取消, taskId: {}", taskId);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanExpiredTasks(int expireHours) {
|
||||
// 清理已完成的任务,释放内存
|
||||
LocalDateTime expireTime = LocalDateTime.now().minusHours(expireHours);
|
||||
int count = 0;
|
||||
for (Map.Entry<String, ProjectInitTaskVO> entry : taskStore.entrySet()) {
|
||||
ProjectInitTaskVO task = entry.getValue();
|
||||
if (task.getCompleteTime() != null && task.getCompleteTime().isBefore(expireTime)) {
|
||||
taskStore.remove(entry.getKey());
|
||||
progressCallbacks.remove(entry.getKey());
|
||||
count++;
|
||||
}
|
||||
}
|
||||
log.info("已清理 {} 个过期任务", count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制任务VO
|
||||
*/
|
||||
private ProjectInitTaskVO copyTaskVO(ProjectInitTaskVO source) {
|
||||
ProjectInitTaskVO copy = new ProjectInitTaskVO();
|
||||
copy.setTaskId(source.getTaskId());
|
||||
copy.setStatus(source.getStatus());
|
||||
copy.setStatusDesc(source.getStatusDesc());
|
||||
copy.setProgress(source.getProgress());
|
||||
copy.setProgressMessage(source.getProgressMessage());
|
||||
copy.setOriginalFilename(source.getOriginalFilename());
|
||||
copy.setCreateTime(source.getCreateTime());
|
||||
copy.setStartTime(source.getStartTime());
|
||||
copy.setCompleteTime(source.getCompleteTime());
|
||||
copy.setResult(source.getResult());
|
||||
copy.setErrorMessage(source.getErrorMessage());
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user