ChapterTaskApplicationService.java

package com.yumu.noveltranslator.application.service;

import com.yumu.noveltranslator.port.dto.collab.ChapterTaskResponse;
import com.yumu.noveltranslator.port.dto.common.PageResponse;
import com.yumu.noveltranslator.domain.model.CollabChapterTask;
import com.yumu.noveltranslator.domain.model.CollabProject;
import com.yumu.noveltranslator.domain.model.User;
import com.yumu.noveltranslator.enums.CollabProjectStatus;
import com.yumu.noveltranslator.enums.ChapterTaskStatus;
import com.yumu.noveltranslator.enums.ProjectMemberRole;
import com.yumu.noveltranslator.exception.BusinessException;
import com.yumu.noveltranslator.enums.ErrorCodeEnum;
import com.yumu.noveltranslator.port.out.CollaborationRepositoryPort;
import com.yumu.noveltranslator.port.out.UserRepositoryPort;
import com.yumu.noveltranslator.port.dto.common.PageResult;
import com.yumu.noveltranslator.domain.service.CollabStateMachine;
import com.yumu.noveltranslator.domain.service.CollabEventPublisher;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 章节任务管理应用服务
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class ChapterTaskApplicationService implements com.yumu.noveltranslator.port.in.ChapterTaskPort {

    private final CollaborationRepositoryPort collabPort;
    private final UserRepositoryPort userPort;
    private final CollabStateMachine collabStateMachine;
    private final CollabEventPublisher collabEventPublisher;

    /**
     * 创建章节
     */
    @Transactional
    public ChapterTaskResponse createChapter(Long projectId, Integer chapterNumber, String title, String sourceText, Long creatorId) {
        CollabProject project = collabPort.findProjectById(projectId).orElse(null);
        if (project == null) {
            throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "项目不存在: " + projectId);
        }

        CollabChapterTask task = new CollabChapterTask();
        task.setProjectId(projectId);
        task.setChapterNumber(chapterNumber);
        task.setTitle(title);
        task.setSourceText(sourceText);
        task.setStatus(ChapterTaskStatus.UNASSIGNED.getValue());
        task.setProgress(0);
        if (sourceText != null) {
            task.setSourceWordCount(sourceText.length());
        }
        collabPort.saveChapterTask(task);

        log.info("创建章节: projectId={}, chapterNumber={}", projectId, chapterNumber);
        return toChapterResponse(task);
    }

    /**
     * 获取项目章节列表(分页)
     */
    public PageResponse<ChapterTaskResponse> listByProjectId(Long projectId, int page, int pageSize) {
        PageResult<CollabChapterTask> resultPage = collabPort.findChapterTasksByProjectIdPaged(projectId, page, pageSize);

        // 批量加载关联用户,避免 N+1
        Set<Long> userIds = new HashSet<>();
        for (CollabChapterTask task : resultPage.getRecords()) {
            if (task.getAssigneeId() != null) userIds.add(task.getAssigneeId());
            if (task.getReviewerId() != null) userIds.add(task.getReviewerId());
        }
        Map<Long, User> userMap = new HashMap<>();
        for (Long uid : userIds) {
            userPort.findById(uid).ifPresent(u -> userMap.put(uid, u));
        }

        List<ChapterTaskResponse> list = resultPage.getRecords().stream()
                .map(task -> toChapterResponse(task, userMap))
                .collect(Collectors.toList());
        return PageResponse.of(page, pageSize, resultPage.getTotal(), list);
    }

    /**
     * 获取章节详情(含权限检查)
     */
    public ChapterTaskResponse getChapterById(Long chapterId, Long userId) {
        CollabChapterTask task = collabPort.findChapterTaskById(chapterId).orElse(null);
        if (task == null) {
            throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "章节不存在: " + chapterId);
        }
        var member = collabPort.findMemberByProjectAndUser(task.getProjectId(), userId);
        if (member == null) {
            throw new BusinessException(ErrorCodeEnum.FORBIDDEN, "无权访问该章节");
        }
        return toChapterResponse(task);
    }

    /**
     * 分配译者
     */
    @Transactional
    public ChapterTaskResponse assignChapter(Long chapterId, Long assigneeId, Long assignerId) {
        CollabChapterTask task = collabPort.findChapterTaskById(chapterId).orElse(null);
        if (task == null) {
            throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "章节不存在: " + chapterId);
        }
        // 权限校验:分配者必须是项目OWNER
        var assigner = collabPort.findMemberByProjectAndUser(task.getProjectId(), assignerId);
        if (assigner == null || !ProjectMemberRole.OWNER.getValue().equals(assigner.getRole())) {
            throw new BusinessException(ErrorCodeEnum.FORBIDDEN, "无权分配章节,只有项目所有者可以分配");
        }

        collabStateMachine.transitionChapter(task, ChapterTaskStatus.TRANSLATING);

        task.setAssigneeId(assigneeId);
        task.setAssignedTime(LocalDateTime.now());
        task.setProgress(0);
        collabPort.updateChapterTask(task);

        final Long finalProjectId = task.getProjectId();
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    collabEventPublisher.publishChapterUpdate(finalProjectId, chapterId, String.valueOf(assigneeId), "assigned");
                }
            });
        }

        log.info("分配章节: chapterId={}, assigneeId={}", chapterId, assigneeId);
        return toChapterResponse(task);
    }

    /**
     * 译者提交章节
     */
    @Transactional
    public ChapterTaskResponse submitChapter(Long chapterId, String translatedText) {
        CollabChapterTask task = collabPort.findChapterTaskById(chapterId).orElse(null);
        if (task == null) {
            throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "章节不存在: " + chapterId);
        }

        ChapterTaskStatus current = ChapterTaskStatus.fromValue(task.getStatus());
        // 已提交状态再次提交:译者更新译文(尚未审核),允许幂等覆盖
        if (current == ChapterTaskStatus.SUBMITTED) {
            log.info("译者重新提交已提交的章节: chapterId={}", chapterId);
            task.setTargetText(translatedText);
            task.setSubmittedTime(LocalDateTime.now());
            task.setProgress(100);
            if (translatedText != null) {
                task.setTargetWordCount(translatedText.length());
            }
            collabPort.updateChapterTask(task);
            return toChapterResponse(task);
        }

        collabStateMachine.transitionChapter(task, ChapterTaskStatus.SUBMITTED);

        task.setTargetText(translatedText);
        task.setSubmittedTime(LocalDateTime.now());
        task.setProgress(100);
        if (translatedText != null) {
            task.setTargetWordCount(translatedText.length());
        }
        collabPort.updateChapterTask(task);

        final Long finalProjectId = task.getProjectId();
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    collabEventPublisher.publishChapterUpdate(finalProjectId, chapterId, String.valueOf(task.getAssigneeId()), "submitted");
                }
            });
        }

        log.info("提交章节: chapterId={}", chapterId);
        return toChapterResponse(task);
    }

    /**
     * 审校审核章节
     */
    @Transactional
    public ChapterTaskResponse reviewChapter(Long chapterId, Boolean approved, String comment, Long reviewerId) {
        CollabChapterTask task = collabPort.findChapterTaskById(chapterId).orElse(null);
        if (task == null) {
            throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "章节不存在: " + chapterId);
        }
        // 权限校验:审核者必须是项目REVIEWER或OWNER
        var reviewer = collabPort.findMemberByProjectAndUser(task.getProjectId(), reviewerId);
        if (reviewer == null) {
            throw new BusinessException(ErrorCodeEnum.FORBIDDEN, "无权访问该项目");
        }
        if (!ProjectMemberRole.REVIEWER.getValue().equals(reviewer.getRole())
                && !ProjectMemberRole.OWNER.getValue().equals(reviewer.getRole())) {
            throw new BusinessException(ErrorCodeEnum.FORBIDDEN, "无权审核该章节,只有审校或项目所有者可以审核");
        }

        // 先转入 REVIEWING 中间状态
        collabStateMachine.transitionChapter(task, ChapterTaskStatus.REVIEWING);

        task.setReviewerId(reviewerId);
        task.setReviewComment(comment);
        task.setReviewedTime(LocalDateTime.now());

        if (approved) {
            collabStateMachine.transitionChapter(task, ChapterTaskStatus.APPROVED);
            task.setCompletedTime(LocalDateTime.now());
            log.info("审核通过: chapterId={}", chapterId);
        } else {
            collabStateMachine.transitionChapter(task, ChapterTaskStatus.REJECTED);
            task.setProgress(0);
            task.setSubmittedTime(null);
            log.info("审核驳回: chapterId={}, reason={}", chapterId, comment);
        }

        collabPort.updateChapterTask(task);

        // 如果 APPROVED,自动转为 COMPLETED
        if (approved) {
            collabStateMachine.transitionChapter(task, ChapterTaskStatus.COMPLETED);
            collabPort.updateChapterTask(task);
            updateProjectProgress(task.getProjectId());
        }

        final Long finalProjectId = task.getProjectId();
        final String action = approved ? "updated" : "submitted";
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    collabEventPublisher.publishChapterUpdate(finalProjectId, chapterId, String.valueOf(reviewerId), action);
                    if (comment != null && !comment.isBlank()) {
                        collabEventPublisher.publishCommentAdded(finalProjectId, chapterId, reviewerId, comment);
                    }
                }
            });
        }

        return toChapterResponse(task);
    }

    /**
     * 获取用户待处理的章节列表(分页)
     */
    public PageResponse<ChapterTaskResponse> listByAssigneeId(Long assigneeId, int page, int pageSize) {
        PageResult<CollabChapterTask> resultPage = collabPort.findChapterTasksByAssigneeIdPaged(
                assigneeId,
                List.of(ChapterTaskStatus.TRANSLATING.getValue(), ChapterTaskStatus.SUBMITTED.getValue()),
                page, pageSize);

        Set<Long> userIds = new HashSet<>();
        for (CollabChapterTask task : resultPage.getRecords()) {
            if (task.getAssigneeId() != null) userIds.add(task.getAssigneeId());
            if (task.getReviewerId() != null) userIds.add(task.getReviewerId());
        }
        Map<Long, User> userMap = new HashMap<>();
        for (Long uid : userIds) {
            userPort.findById(uid).ifPresent(u -> userMap.put(uid, u));
        }

        List<ChapterTaskResponse> list = resultPage.getRecords().stream()
                .map(task -> toChapterResponse(task, userMap))
                .collect(Collectors.toList());
        return PageResponse.of(page, pageSize, resultPage.getTotal(), list);
    }

    /**
     * 更新项目整体进度
     */
    private void updateProjectProgress(Long projectId) {
        List<CollabChapterTask> tasks = collabPort.findChapterTasksByProjectId(projectId);
        if (tasks.isEmpty()) {
            return;
        }

        long completed = tasks.stream()
                .filter(t -> ChapterTaskStatus.COMPLETED.getValue().equals(t.getStatus()))
                .count();

        int progress = (int) Math.round((double) completed / tasks.size() * 100);

        CollabProject project = collabPort.findProjectById(projectId).orElse(null);
        if (project != null) {
            project.setProgress(progress);
            if (progress == 100) {
                try {
                    collabStateMachine.transitionProject(project, CollabProjectStatus.COMPLETED);
                } catch (IllegalStateException e) {
                    log.debug("项目无法转换为 COMPLETED 状态(当前状态不允许): projectId={}", projectId);
                }
            }
            collabPort.updateProject(project);
        }
    }

    private ChapterTaskResponse toChapterResponse(CollabChapterTask task) {
        return toChapterResponse(task, Map.of());
    }

    private ChapterTaskResponse toChapterResponse(CollabChapterTask task, Map<Long, User> userMap) {
        ChapterTaskResponse resp = new ChapterTaskResponse();
        resp.setId(task.getId());
        resp.setChapterNumber(task.getChapterNumber());
        resp.setTitle(task.getTitle());
        resp.setSourceText(task.getSourceText());
        resp.setTranslatedText(task.getTargetText());
        resp.setStatus(task.getStatus());
        resp.setProgress(task.getProgress());
        resp.setAssigneeId(task.getAssigneeId());
        resp.setReviewerId(task.getReviewerId());
        resp.setReviewComment(task.getReviewComment());
        resp.setSourceWordCount(task.getSourceWordCount());
        resp.setTargetWordCount(task.getTargetWordCount());
        resp.setAssignedTime(task.getAssignedTime());
        resp.setSubmittedTime(task.getSubmittedTime());
        resp.setReviewedTime(task.getReviewedTime());
        resp.setCompletedTime(task.getCompletedTime());

        // 设置译员名称
        if (task.getAssigneeId() != null) {
            User assignee = userMap.get(task.getAssigneeId());
            if (assignee == null) {
                assignee = userPort.findById(task.getAssigneeId()).orElse(null);
            }
            if (assignee != null) {
                resp.setAssigneeName(assignee.getUsername());
            }
        }
        // 设置审校名称
        if (task.getReviewerId() != null) {
            User reviewer = userMap.get(task.getReviewerId());
            if (reviewer == null) {
                reviewer = userPort.findById(task.getReviewerId()).orElse(null);
            }
            if (reviewer != null) {
                resp.setReviewerName(reviewer.getUsername());
            }
        }

        return resp;
    }
}