CollabCommentApplicationService.java

package com.yumu.noveltranslator.application.service;

import com.yumu.noveltranslator.port.dto.collab.CommentResponse;
import com.yumu.noveltranslator.port.dto.collab.CreateCommentRequest;
import com.yumu.noveltranslator.domain.model.CollabComment;
import com.yumu.noveltranslator.domain.model.User;
import com.yumu.noveltranslator.enums.ErrorCodeEnum;
import com.yumu.noveltranslator.exception.BusinessException;
import com.yumu.noveltranslator.port.out.CollaborationRepositoryPort;
import com.yumu.noveltranslator.port.out.UserRepositoryPort;
import com.yumu.noveltranslator.port.dto.common.PageResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
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 CollabCommentApplicationService implements com.yumu.noveltranslator.port.in.CollabCommentPort {

    private final CollaborationRepositoryPort collabPort;
    private final UserRepositoryPort userPort;

    /**
     * 创建评论
     */
    public CommentResponse createComment(Long chapterTaskId, Long userId, CreateCommentRequest request) {
        var task = collabPort.findChapterTaskById(chapterTaskId);
        if (task.isEmpty()) {
            throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "章节不存在: " + chapterTaskId);
        }
        var member = collabPort.findMemberByProjectAndUser(task.get().getProjectId(), userId);
        if (member == null) {
            throw new BusinessException(ErrorCodeEnum.FORBIDDEN, "无权访问该项目");
        }

        // 如果是回复,验证父评论是否存在
        if (request.getParentId() != null) {
            var parent = collabPort.findCommentById(request.getParentId());
            if (parent.isEmpty() || !parent.get().getChapterTaskId().equals(chapterTaskId)) {
                throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "父评论不存在");
            }
        }

        CollabComment comment = new CollabComment();
        comment.setChapterTaskId(chapterTaskId);
        comment.setUserId(userId);
        comment.setSourceText(request.getSourceText());
        comment.setTargetText(request.getTargetText());
        comment.setContent(request.getContent());
        comment.setParentId(request.getParentId());
        comment.setResolved(false);
        collabPort.saveComment(comment);

        log.info("创建评论: chapterTaskId={}, userId={}, parentId={}", chapterTaskId, userId, request.getParentId());
        return toCommentResponse(comment, Map.of());
    }

    /**
     * 获取章节评论列表(分页,树形结构)
     */
    public PageResult<CommentResponse> getCommentsByChapterPage(Long chapterTaskId, Long userId, int page, int size) {
        var task = collabPort.findChapterTaskById(chapterTaskId);
        if (task.isEmpty()) {
            throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "章节不存在: " + chapterTaskId);
        }
        var member = collabPort.findMemberByProjectAndUser(task.get().getProjectId(), userId);
        if (member == null) {
            throw new BusinessException(ErrorCodeEnum.FORBIDDEN, "无权访问该项目");
        }

        // 分页查询根评论
        var rootPage = collabPort.findCommentsByChapterTaskIdPaged(chapterTaskId, page, size);

        // 批量加载回复和用户,避免 N+1
        Set<Long> userIds = new HashSet<>();
        Map<Long, List<CollabComment>> repliesByRoot = new HashMap<>();
        for (CollabComment root : rootPage.getRecords()) {
            userIds.add(root.getUserId());
            List<CollabComment> replies = collabPort.findRepliesByParentId(root.getId());
            repliesByRoot.put(root.getId(), replies);
            for (CollabComment reply : replies) {
                userIds.add(reply.getUserId());
            }
        }

        Map<Long, User> userMap = new HashMap<>();
        if (!userIds.isEmpty()) {
            for (Long uid : userIds) {
                userPort.findById(uid).ifPresent(u -> userMap.put(uid, u));
            }
        }

        List<CommentResponse> responses = rootPage.getRecords().stream()
                .map(root -> {
                    CommentResponse resp = toCommentResponse(root, userMap);
                    List<CommentResponse> replyResponses = repliesByRoot.getOrDefault(root.getId(), List.of()).stream()
                            .map(reply -> toCommentResponse(reply, userMap))
                            .collect(Collectors.toList());
                    resp.setReplies(replyResponses);
                    return resp;
                })
                .collect(Collectors.toList());

        PageResult<CommentResponse> resultPage = new PageResult<>(responses, rootPage.getTotal(), (long) page, (long) size);
        return resultPage;
    }

    /**
     * 标记评论已解决
     */
    public void resolveComment(Long commentId, Long userId) {
        CollabComment comment = collabPort.findCommentById(commentId).orElse(null);
        if (comment == null) {
            throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "评论不存在");
        }
        var task = collabPort.findChapterTaskById(comment.getChapterTaskId());
        if (task.isEmpty()) {
            throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "章节不存在");
        }
        var member = collabPort.findMemberByProjectAndUser(task.get().getProjectId(), userId);
        if (member == null) {
            throw new BusinessException(ErrorCodeEnum.FORBIDDEN, "无权访问该项目");
        }
        comment.setResolved(true);
        collabPort.updateComment(comment);
    }

    /**
     * 删除评论
     */
    public void deleteComment(Long commentId, Long userId) {
        CollabComment comment = collabPort.findCommentById(commentId).orElse(null);
        if (comment == null) {
            throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "评论不存在");
        }
        var task = collabPort.findChapterTaskById(comment.getChapterTaskId());
        if (task.isEmpty()) {
            throw new BusinessException(ErrorCodeEnum.NOT_FOUND, "章节不存在");
        }
        var member = collabPort.findMemberByProjectAndUser(task.get().getProjectId(), userId);
        if (member == null) {
            throw new BusinessException(ErrorCodeEnum.FORBIDDEN, "无权访问该项目");
        }
        // 仅创建者可以删除
        if (!comment.getUserId().equals(userId)) {
            throw new BusinessException(ErrorCodeEnum.FORBIDDEN, "无权删除他人评论");
        }
        collabPort.deleteComment(commentId);
    }

    private CommentResponse toCommentResponse(CollabComment comment, Map<Long, User> userMap) {
        CommentResponse resp = new CommentResponse();
        resp.setId(comment.getId());
        resp.setUserId(comment.getUserId());
        resp.setSourceText(comment.getSourceText());
        resp.setTargetText(comment.getTargetText());
        resp.setContent(comment.getContent());
        resp.setResolved(comment.getResolved());
        resp.setCreateTime(comment.getCreateTime());

        User user = userMap.get(comment.getUserId());
        if (user != null) {
            resp.setUsername(user.getUsername());
            resp.setAvatar(user.getAvatar());
        }
        resp.setReplies(new ArrayList<>());
        return resp;
    }
}