diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f4775f57a..31a401273 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,11 +76,13 @@ cp .env.staging.example .env ```yaml ; 本地部署后端 -NUXT_PUBLIC_API_BASE_URL=https://127.0.0.1:8081 +NUXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8081 ; 预发环境后端 ; NUXT_PUBLIC_API_BASE_URL=https://staging.open-isle.com ; 生产环境后端 ; NUXT_PUBLIC_API_BASE_URL=https://www.open-isle.com +; 开发环境 +NUXT_PUBLIC_WEBSITE_BASE_URL=localhost:3000 ``` 2. 依赖预发环境后台环境 diff --git a/backend/src/main/java/com/openisle/service/CommentService.java b/backend/src/main/java/com/openisle/service/CommentService.java index e37ded111..1076aa5e2 100644 --- a/backend/src/main/java/com/openisle/service/CommentService.java +++ b/backend/src/main/java/com/openisle/service/CommentService.java @@ -4,6 +4,7 @@ import com.openisle.model.Comment; import com.openisle.model.Post; import com.openisle.model.User; import com.openisle.model.NotificationType; +import com.openisle.model.PointHistory; import com.openisle.model.CommentSort; import com.openisle.repository.CommentRepository; import com.openisle.repository.PostRepository; @@ -14,6 +15,7 @@ import com.openisle.repository.NotificationRepository; import com.openisle.repository.PointHistoryRepository; import com.openisle.service.NotificationService; import com.openisle.service.SubscriptionService; +import com.openisle.service.PointService; import com.openisle.model.Role; import com.openisle.exception.RateLimitException; import lombok.RequiredArgsConstructor; @@ -21,6 +23,9 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.List; +import java.util.Set; +import java.util.HashSet; +import java.util.stream.Collectors; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.transaction.annotation.Transactional; @@ -39,6 +44,7 @@ public class CommentService { private final CommentSubscriptionRepository commentSubscriptionRepository; private final NotificationRepository notificationRepository; private final PointHistoryRepository pointHistoryRepository; + private final PointService pointService; private final ImageUploader imageUploader; @Transactional @@ -65,16 +71,19 @@ public class CommentService { 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); + notificationService.createNotification(post.getAuthor(), NotificationType.COMMENT_REPLY, post, comment, + null, null, null, null); } for (User u : subscriptionService.getPostSubscribers(postId)) { if (!u.getId().equals(author.getId())) { - notificationService.createNotification(u, NotificationType.POST_UPDATED, post, comment, null, null, null, null); + notificationService.createNotification(u, NotificationType.POST_UPDATED, post, comment, null, null, + null, null); } } for (User u : subscriptionService.getSubscribers(author.getUsername())) { if (!u.getId().equals(author.getId())) { - notificationService.createNotification(u, NotificationType.USER_ACTIVITY, post, comment, null, null, null, null); + notificationService.createNotification(u, NotificationType.USER_ACTIVITY, post, comment, null, null, + null, null); } } notificationService.notifyMentions(content, author, post, comment); @@ -111,21 +120,25 @@ public class CommentService { 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); + notificationService.createNotification(parent.getAuthor(), NotificationType.COMMENT_REPLY, parent.getPost(), + comment, null, null, null, null); } for (User u : subscriptionService.getCommentSubscribers(parentId)) { if (!u.getId().equals(author.getId())) { - notificationService.createNotification(u, NotificationType.COMMENT_REPLY, parent.getPost(), comment, null, null, null, null); + notificationService.createNotification(u, NotificationType.COMMENT_REPLY, parent.getPost(), comment, + null, null, null, null); } } for (User u : subscriptionService.getPostSubscribers(parent.getPost().getId())) { if (!u.getId().equals(author.getId())) { - notificationService.createNotification(u, NotificationType.POST_UPDATED, parent.getPost(), comment, null, null, null, null); + notificationService.createNotification(u, NotificationType.POST_UPDATED, parent.getPost(), comment, + null, null, null, null); } } for (User u : subscriptionService.getSubscribers(author.getUsername())) { if (!u.getId().equals(author.getId())) { - notificationService.createNotification(u, NotificationType.USER_ACTIVITY, parent.getPost(), comment, null, null, null, null); + notificationService.createNotification(u, NotificationType.USER_ACTIVITY, parent.getPost(), comment, + null, null, null, null); } } notificationService.notifyMentions(content, author, parent.getPost(), comment); @@ -237,15 +250,33 @@ public class CommentService { for (Comment c : replies) { deleteCommentCascade(c); } - // 逻辑删除相关的积分历史记录 - pointHistoryRepository.findByComment(comment).forEach(pointHistoryRepository::delete); + + // 逻辑删除相关的积分历史记录,并收集受影响的用户 + List pointHistories = pointHistoryRepository.findByComment(comment); + // 收集需要重新计算积分的用户 + Set usersToRecalculate = pointHistories.stream().map(PointHistory::getUser).collect(Collectors.toSet()); + // 删除其他相关数据 reactionRepository.findByComment(comment).forEach(reactionRepository::delete); commentSubscriptionRepository.findByComment(comment).forEach(commentSubscriptionRepository::delete); notificationRepository.deleteAll(notificationRepository.findByComment(comment)); imageUploader.removeReferences(imageUploader.extractUrls(comment.getContent())); + // 逻辑删除评论 commentRepository.delete(comment); + // 删除积分历史 + pointHistoryRepository.deleteAll(pointHistories); + + // 重新计算受影响用户的积分 + if (!usersToRecalculate.isEmpty()) { + for (User user : usersToRecalculate) { + int newPoints = pointService.recalculateUserPoints(user); + user.setPoint(newPoints); + log.debug("Recalculated points for user {}: {}", user.getUsername(), newPoints); + } + userRepository.saveAll(usersToRecalculate); + } + log.debug("deleteCommentCascade removed comment {}", comment.getId()); } diff --git a/backend/src/main/java/com/openisle/service/PointService.java b/backend/src/main/java/com/openisle/service/PointService.java index 4bfe410af..677fd0a0d 100644 --- a/backend/src/main/java/com/openisle/service/PointService.java +++ b/backend/src/main/java/com/openisle/service/PointService.java @@ -219,4 +219,32 @@ public class PointService { return result; } + /** + * 重新计算用户的积分总数 + * 通过累加所有积分历史记录来重新计算用户的当前积分 + */ + public int recalculateUserPoints(User user) { + // 获取用户所有的积分历史记录(由于@Where注解,已删除的记录会被自动过滤) + List histories = pointHistoryRepository.findByUserOrderByIdDesc(user); + + int totalPoints = 0; + for (PointHistory history : histories) { + totalPoints += history.getAmount(); + } + + // 更新用户积分 + user.setPoint(totalPoints); + userRepository.save(user); + + return totalPoints; + } + + /** + * 重新计算用户的积分总数(通过用户名) + */ + public int recalculateUserPoints(String userName) { + User user = userRepository.findByUsername(userName).orElseThrow(); + return recalculateUserPoints(user); + } + } diff --git a/backend/src/test/java/com/openisle/service/CommentServiceTest.java b/backend/src/test/java/com/openisle/service/CommentServiceTest.java index 9d22c32aa..0e05c052c 100644 --- a/backend/src/test/java/com/openisle/service/CommentServiceTest.java +++ b/backend/src/test/java/com/openisle/service/CommentServiceTest.java @@ -7,6 +7,7 @@ import com.openisle.repository.ReactionRepository; import com.openisle.repository.CommentSubscriptionRepository; import com.openisle.repository.NotificationRepository; import com.openisle.repository.PointHistoryRepository; +import com.openisle.service.PointService; import com.openisle.exception.RateLimitException; import org.junit.jupiter.api.Test; @@ -26,10 +27,11 @@ class CommentServiceTest { CommentSubscriptionRepository subRepo = mock(CommentSubscriptionRepository.class); NotificationRepository nRepo = mock(NotificationRepository.class); PointHistoryRepository pointHistoryRepo = mock(PointHistoryRepository.class); + PointService pointService = mock(PointService.class); ImageUploader imageUploader = mock(ImageUploader.class); CommentService service = new CommentService(commentRepo, postRepo, userRepo, - notifService, subService, reactionRepo, subRepo, nRepo, pointHistoryRepo, imageUploader); + notifService, subService, reactionRepo, subRepo, nRepo, pointHistoryRepo, pointService, imageUploader); when(commentRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(3L); diff --git a/frontend_nuxt/package.json b/frontend_nuxt/package.json index 241d1a077..277fb3f4d 100644 --- a/frontend_nuxt/package.json +++ b/frontend_nuxt/package.json @@ -2,6 +2,9 @@ "name": "frontend_nuxt", "private": true, "type": "module", + "engines": { + "node": ">=20.0.0" + }, "scripts": { "dev": "nuxt dev", "build": "nuxt build", diff --git a/frontend_nuxt/pages/message-box/[id].vue b/frontend_nuxt/pages/message-box/[id].vue index a1d69c686..546add168 100644 --- a/frontend_nuxt/pages/message-box/[id].vue +++ b/frontend_nuxt/pages/message-box/[id].vue @@ -371,11 +371,12 @@ watch(isConnected, (newValue) => { }) onActivated(async () => { - // 返回页面时:刷新数据与已读,不做强制滚动,保持用户当前位置 + // 返回页面时:刷新数据与已读,并滚动到底部 if (currentUser.value) { await fetchMessages(0) await markConversationAsRead() await nextTick() + scrollToBottomSmooth() updateNearBottom() if (!isConnected.value) { const token = getToken() diff --git a/frontend_nuxt/pages/message.vue b/frontend_nuxt/pages/message.vue index e7456b780..9cccbebe3 100644 --- a/frontend_nuxt/pages/message.vue +++ b/frontend_nuxt/pages/message.vue @@ -14,13 +14,15 @@
通知设置
-
-
{{ formatType(pref.type) }}
- -
+
@@ -753,6 +755,14 @@ const formatType = (t) => { } } +const isAdmin = computed(() => authState.role === 'ADMIN') + +const needAdminSet = new Set(['POST_REVIEW_REQUEST','REGISTER_REQUEST', 'POINT_REDEEM', 'ACTIVITY_REDEEM']) + +const canShowNotification = (type) => { + return !needAdminSet.has(type) || isAdmin.value +} + onActivated(async () => { page.value = 0 await fetchNotifications({ page: 0, size: pageSize, unread: selectedTab.value === 'unread' })