diff --git a/backend/src/main/java/com/openisle/controller/CommentController.java b/backend/src/main/java/com/openisle/controller/CommentController.java index 2de863617..d4542d999 100644 --- a/backend/src/main/java/com/openisle/controller/CommentController.java +++ b/backend/src/main/java/com/openisle/controller/CommentController.java @@ -8,6 +8,7 @@ import com.openisle.service.ReactionService; import com.openisle.model.CommentSort; import lombok.Data; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; @@ -20,6 +21,7 @@ import java.util.stream.Collectors; @RestController @RequestMapping("/api") @RequiredArgsConstructor +@Slf4j public class CommentController { private final CommentService commentService; private final LevelService levelService; @@ -36,12 +38,15 @@ public class CommentController { public ResponseEntity createComment(@PathVariable Long postId, @RequestBody CommentRequest req, Authentication auth) { + log.debug("createComment called by user {} for post {}", auth.getName(), postId); if (captchaEnabled && commentCaptchaEnabled && !captchaService.verify(req.getCaptcha())) { + log.debug("Captcha verification failed for user {} on post {}", auth.getName(), postId); return ResponseEntity.badRequest().build(); } Comment comment = commentService.addComment(auth.getName(), postId, req.getContent()); CommentDto dto = toDto(comment); dto.setReward(levelService.awardForComment(auth.getName())); + log.debug("createComment succeeded for comment {}", comment.getId()); return ResponseEntity.ok(dto); } @@ -49,21 +54,27 @@ public class CommentController { public ResponseEntity replyComment(@PathVariable Long commentId, @RequestBody CommentRequest req, Authentication auth) { + log.debug("replyComment called by user {} for comment {}", auth.getName(), commentId); if (captchaEnabled && commentCaptchaEnabled && !captchaService.verify(req.getCaptcha())) { + log.debug("Captcha verification failed for user {} on comment {}", auth.getName(), commentId); return ResponseEntity.badRequest().build(); } Comment comment = commentService.addReply(auth.getName(), commentId, req.getContent()); CommentDto dto = toDto(comment); dto.setReward(levelService.awardForComment(auth.getName())); + log.debug("replyComment succeeded for comment {}", comment.getId()); return ResponseEntity.ok(dto); } @GetMapping("/posts/{postId}/comments") public List listComments(@PathVariable Long postId, @RequestParam(value = "sort", required = false, defaultValue = "OLDEST") CommentSort sort) { - return commentService.getCommentsForPost(postId, sort).stream() + log.debug("listComments called for post {} with sort {}", postId, sort); + List list = commentService.getCommentsForPost(postId, sort).stream() .map(this::toDtoWithReplies) .collect(Collectors.toList()); + log.debug("listComments returning {} comments", list.size()); + return list; } private CommentDto toDtoWithReplies(Comment comment) { @@ -81,7 +92,9 @@ public class CommentController { @DeleteMapping("/comments/{id}") public void deleteComment(@PathVariable Long id, Authentication auth) { + log.debug("deleteComment called by user {} for comment {}", auth.getName(), id); commentService.deleteComment(auth.getName(), id); + log.debug("deleteComment completed for comment {}", id); } private CommentDto toDto(Comment comment) { diff --git a/backend/src/main/java/com/openisle/service/CommentService.java b/backend/src/main/java/com/openisle/service/CommentService.java index 70eb764f6..ddae361f9 100644 --- a/backend/src/main/java/com/openisle/service/CommentService.java +++ b/backend/src/main/java/com/openisle/service/CommentService.java @@ -16,6 +16,7 @@ import com.openisle.service.SubscriptionService; import com.openisle.model.Role; import com.openisle.exception.RateLimitException; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.List; @@ -25,6 +26,7 @@ import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor +@Slf4j public class CommentService { private final CommentRepository commentRepository; private final PostRepository postRepository; @@ -38,9 +40,11 @@ public class CommentService { @Transactional public Comment addComment(String username, Long postId, String content) { + log.debug("addComment called by user {} for post {}", username, postId); long recent = commentRepository.countByAuthorAfter(username, java.time.LocalDateTime.now().minusMinutes(1)); if (recent >= 3) { + log.debug("Rate limit exceeded for user {}", username); throw new RateLimitException("Too many comments"); } User author = userRepository.findByUsername(username) @@ -52,6 +56,7 @@ public class CommentService { comment.setPost(post); comment.setContent(content); comment = commentRepository.save(comment); + log.debug("Comment {} saved for post {}", comment.getId(), postId); imageUploader.addReferences(imageUploader.extractUrls(content)); if (!author.getId().equals(post.getAuthor().getId())) { notificationService.createNotification(post.getAuthor(), NotificationType.COMMENT_REPLY, post, comment, null, null, null, null); @@ -67,14 +72,17 @@ public class CommentService { } } notificationService.notifyMentions(content, author, post, comment); + log.debug("addComment finished for comment {}", comment.getId()); return comment; } @Transactional public Comment addReply(String username, Long parentId, String content) { + log.debug("addReply called by user {} for parent comment {}", username, parentId); long recent = commentRepository.countByAuthorAfter(username, java.time.LocalDateTime.now().minusMinutes(1)); if (recent >= 3) { + log.debug("Rate limit exceeded for user {}", username); throw new RateLimitException("Too many comments"); } User author = userRepository.findByUsername(username) @@ -87,6 +95,7 @@ public class CommentService { comment.setParent(parent); comment.setContent(content); comment = commentRepository.save(comment); + log.debug("Reply {} saved for parent {}", comment.getId(), parentId); imageUploader.addReferences(imageUploader.extractUrls(content)); if (!author.getId().equals(parent.getAuthor().getId())) { notificationService.createNotification(parent.getAuthor(), NotificationType.COMMENT_REPLY, parent.getPost(), comment, null, null, null, null); @@ -107,10 +116,12 @@ public class CommentService { } } notificationService.notifyMentions(content, author, parent.getPost(), comment); + log.debug("addReply finished for comment {}", comment.getId()); return comment; } public List getCommentsForPost(Long postId, CommentSort sort) { + log.debug("getCommentsForPost called for post {} with sort {}", postId, sort); Post post = postRepository.findById(postId) .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); List list = commentRepository.findByPostAndParentIsNullOrderByCreatedAtAsc(post); @@ -119,56 +130,76 @@ public class CommentService { } else if (sort == CommentSort.MOST_INTERACTIONS) { list.sort((a, b) -> Integer.compare(interactionCount(b), interactionCount(a))); } + log.debug("getCommentsForPost returning {} comments", list.size()); return list; } public List getReplies(Long parentId) { + log.debug("getReplies called for parent {}", parentId); Comment parent = commentRepository.findById(parentId) .orElseThrow(() -> new IllegalArgumentException("Comment not found")); - return commentRepository.findByParentOrderByCreatedAtAsc(parent); + List replies = commentRepository.findByParentOrderByCreatedAtAsc(parent); + log.debug("getReplies returning {} replies for parent {}", replies.size(), parentId); + return replies; } public List getRecentCommentsByUser(String username, int limit) { + log.debug("getRecentCommentsByUser called for user {} with limit {}", username, limit); User user = userRepository.findByUsername(username) .orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found")); Pageable pageable = PageRequest.of(0, limit); - return commentRepository.findByAuthorOrderByCreatedAtDesc(user, pageable); + List comments = commentRepository.findByAuthorOrderByCreatedAtDesc(user, pageable); + log.debug("getRecentCommentsByUser returning {} comments for user {}", comments.size(), username); + return comments; } public java.util.List getParticipants(Long postId, int limit) { + log.debug("getParticipants called for post {} with limit {}", postId, limit); Post post = postRepository.findById(postId) .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); java.util.LinkedHashSet set = new java.util.LinkedHashSet<>(); set.add(post.getAuthor()); set.addAll(commentRepository.findDistinctAuthorsByPost(post)); java.util.List list = new java.util.ArrayList<>(set); - return list.subList(0, Math.min(limit, list.size())); + java.util.List result = list.subList(0, Math.min(limit, list.size())); + log.debug("getParticipants returning {} users for post {}", result.size(), postId); + return result; } public java.util.List getCommentsByIds(java.util.List ids) { - return commentRepository.findAllById(ids); + log.debug("getCommentsByIds called for ids {}", ids); + java.util.List comments = commentRepository.findAllById(ids); + log.debug("getCommentsByIds returning {} comments", comments.size()); + return comments; } public java.time.LocalDateTime getLastCommentTime(Long postId) { + log.debug("getLastCommentTime called for post {}", postId); Post post = postRepository.findById(postId) .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); - return commentRepository.findLastCommentTime(post); + java.time.LocalDateTime time = commentRepository.findLastCommentTime(post); + log.debug("getLastCommentTime for post {} is {}", postId, time); + return time; } @Transactional public void deleteComment(String username, Long id) { + log.debug("deleteComment called by user {} for comment {}", username, id); User user = userRepository.findByUsername(username) .orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found")); Comment comment = commentRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("Comment not found")); if (!user.getId().equals(comment.getAuthor().getId()) && user.getRole() != Role.ADMIN) { + log.debug("User {} not authorized to delete comment {}", username, id); throw new IllegalArgumentException("Unauthorized"); } deleteCommentCascade(comment); + log.debug("deleteComment completed for comment {}", id); } @Transactional public void deleteCommentCascade(Comment comment) { + log.debug("deleteCommentCascade called for comment {}", comment.getId()); List replies = commentRepository.findByParentOrderByCreatedAtAsc(comment); for (Comment c : replies) { deleteCommentCascade(c); @@ -178,6 +209,7 @@ public class CommentService { notificationRepository.deleteAll(notificationRepository.findByComment(comment)); imageUploader.removeReferences(imageUploader.extractUrls(comment.getContent())); commentRepository.delete(comment); + log.debug("deleteCommentCascade removed comment {}", comment.getId()); } private int interactionCount(Comment comment) { diff --git a/frontend/src/components/CommentEditor.vue b/frontend/src/components/CommentEditor.vue index db42bf4ee..15bab89d6 100644 --- a/frontend/src/components/CommentEditor.vue +++ b/frontend/src/components/CommentEditor.vue @@ -66,6 +66,7 @@ export default { const submit = () => { if (!vditorInstance.value || isDisabled.value) return const value = vditorInstance.value.getValue() + console.debug('CommentEditor submit', value) emit('submit', value) vditorInstance.value.setValue('') text.value = '' diff --git a/frontend/src/components/CommentItem.vue b/frontend/src/components/CommentItem.vue index 7322b8bcd..8173c8613 100644 --- a/frontend/src/components/CommentItem.vue +++ b/frontend/src/components/CommentItem.vue @@ -144,10 +144,12 @@ const CommentItem = { toast.error('请先登录') return } + console.debug('Deleting comment', props.comment.id) const res = await fetch(`${API_BASE_URL}/api/comments/${props.comment.id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` } }) + console.debug('Delete comment response status', res.status) if (res.ok) { toast.success('已删除') emit('deleted', props.comment.id) @@ -164,14 +166,17 @@ const CommentItem = { isWaitingForReply.value = false return } + console.debug('Submitting reply', { parentId: props.comment.id, text }) try { const res = await fetch(`${API_BASE_URL}/api/comments/${props.comment.id}/replies`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ content: text }) }) + console.debug('Submit reply response status', res.status) if (res.ok) { const data = await res.json() + console.debug('Submit reply response data', data) const replyList = props.comment.reply || (props.comment.reply = []) replyList.push({ id: data.id, @@ -202,6 +207,7 @@ const CommentItem = { toast.error('回复失败') } } catch (e) { + console.debug('Submit reply error', e) toast.error('回复失败') } finally { isWaitingForReply.value = false diff --git a/frontend/src/views/PostPageView.vue b/frontend/src/views/PostPageView.vue index 89d6967ce..ebe090b65 100644 --- a/frontend/src/views/PostPageView.vue +++ b/frontend/src/views/PostPageView.vue @@ -370,6 +370,7 @@ export default { const postComment = async (text) => { if (!text.trim()) return + console.debug('Posting comment', { postId, text }) isWaitingPostingComment.value = true const token = getToken() if (!token) { @@ -383,8 +384,10 @@ export default { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ content: text }) }) + console.debug('Post comment response status', res.status) if (res.ok) { const data = await res.json() + console.debug('Post comment response data', data) await fetchComments() if (data.reward && data.reward > 0) { toast.success(`评论成功,获得 ${data.reward} 经验值`) @@ -397,6 +400,7 @@ export default { toast.error('评论失败') } } catch (e) { + console.debug('Post comment error', e) toast.error('评论失败') } finally { isWaitingPostingComment.value = false @@ -538,20 +542,23 @@ export default { const fetchComments = async () => { isFetchingComments.value = true + console.debug('Fetching comments', { postId, sort: commentSort.value }) try { const token = getToken() const res = await fetch(`${API_BASE_URL}/api/posts/${postId}/comments?sort=${commentSort.value}`, { headers: { Authorization: token ? `Bearer ${token}` : '' } }) + console.debug('Fetch comments response status', res.status) if (res.ok) { const data = await res.json() + console.debug('Fetched comments count', data.length) comments.value = data.map(mapComment) isFetchingComments.value = false await nextTick() gatherPostItems() } - } catch { - // ignore + } catch (e) { + console.debug('Fetch comments error', e) } finally { isFetchingComments.value = false }