# DraftProjectRecoveryTask.java

    package com.yumu.noveltranslator.task;

    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.yumu.noveltranslator.adapter.out.persistence.converter.CollabConverter;
    import com.yumu.noveltranslator.adapter.out.persistence.entity.CollabProject;
    import com.yumu.noveltranslator.enums.CollabProjectStatus;
    import com.yumu.noveltranslator.adapter.out.persistence.mapper.CollabChapterTaskMapper;
    import com.yumu.noveltranslator.adapter.out.persistence.mapper.CollabProjectMapper;
    import com.yumu.noveltranslator.domain.service.CollabStateMachine;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;

    import java.time.LocalDateTime;
    import java.util.List;

    /**
     * 恢复停滞在 DRAFT 状态的协作项目。
     * 每 5 分钟运行一次，检测 DRAFT 状态超过阈值的项目：
     * - 如果已有章节：说明异步插入已完成但激活失败，将其转为 ACTIVE
     * - 如果无章节：说明异步任务未执行或失败，记录为陈旧项目
     */
    @Component
    @RequiredArgsConstructor
    @Slf4j
    public class DraftProjectRecoveryTask {

        /**
         * DRAFT 项目被视为停滞的时间阈值（分钟）
         */
        private static final int STALE_THRESHOLD_MINUTES = 10;

        private final CollabProjectMapper collabProjectMapper;
        private final CollabChapterTaskMapper collabChapterTaskMapper;
        private final CollabStateMachine collabStateMachine;

        @Scheduled(fixedRate = 300_000)
        public void recoverStaleDraftProjects() {
            LocalDateTime cutoff = LocalDateTime.now().minusMinutes(STALE_THRESHOLD_MINUTES);

            // 查询 DRAFT 状态且创建时间早于阈值的项目
            List<CollabProject> staleDrafts = collabProjectMapper.selectList(
                    new QueryWrapper<CollabProject>()
                            .eq("status", CollabProjectStatus.DRAFT.getValue())
                            .eq("deleted", 0)
                            .lt("create_time", cutoff)
            );

            if (staleDrafts.isEmpty()) {
                return;
            }

            log.info("发现 {} 个停滞的 DRAFT 项目，开始恢复", staleDrafts.size());

            for (CollabProject entity : staleDrafts) {
                try {
                    recoverSingleProject(entity);
                } catch (Exception e) {
                    log.error("恢复项目失败: projectId={}, error={}", entity.getId(), e.getMessage(), e);
                }
            }
        }

        private void recoverSingleProject(CollabProject entity) {
            Long projectId = entity.getId();

            // 检查该项目是否已有章节
            int chapterCount = collabChapterTaskMapper.countByProjectId(projectId);

            if (chapterCount > 0) {
                // 有章节存在：异步任务可能已完成但激活失败
                transitionToActive(entity);
            } else {
                // 无章节：异步任务未执行或完全失败
                logStaleProject(entity);
            }
        }

        private void transitionToActive(CollabProject entity) {
            Long projectId = entity.getId();
            try {
                com.yumu.noveltranslator.domain.model.CollabProject model = CollabConverter.toProjectModel(entity);
                collabStateMachine.transitionProject(model, CollabProjectStatus.ACTIVE);
                collabProjectMapper.updateById(entity);
                log.info("恢复停滞项目（有章节）: projectId={}, chapters exist, transitioned to ACTIVE", projectId);
            } catch (IllegalStateException e) {
                // 状态转移不合法（如已是 ACTIVE），跳过
                log.warn("无法转换项目状态: projectId={}, reason={}", projectId, e.getMessage());
            }
        }

        private void logStaleProject(CollabProject entity) {
            Long projectId = entity.getId();
            log.warn("发现陈旧 DRAFT 项目（无章节插入）: projectId={}, name={}, ownerId={}, createTime={}. "
                    + "可能原因：异步章节拆分任务未执行或事件发布失败。"
                    + "保持 DRAFT 状态，等待人工介入。",
                    projectId, entity.getName(), entity.getOwnerId(), entity.getCreateTime());
        }
    }