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