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());
    }
}