diff --git a/backend/src/main/java/com/openisle/controller/NotificationController.java b/backend/src/main/java/com/openisle/controller/NotificationController.java index d5813304c..d25d2a808 100644 --- a/backend/src/main/java/com/openisle/controller/NotificationController.java +++ b/backend/src/main/java/com/openisle/controller/NotificationController.java @@ -23,19 +23,9 @@ public class NotificationController { private final NotificationMapper notificationMapper; @GetMapping - public List list(@RequestParam(value = "page", defaultValue = "0") int page, - @RequestParam(value = "size", defaultValue = "30") int size, + public List list(@RequestParam(value = "read", required = false) Boolean read, Authentication auth) { - return notificationService.listNotifications(auth.getName(), page, size).stream() - .map(notificationMapper::toDto) - .collect(Collectors.toList()); - } - - @GetMapping("/unread") - public List listUnread(@RequestParam(value = "page", defaultValue = "0") int page, - @RequestParam(value = "size", defaultValue = "30") int size, - Authentication auth) { - return notificationService.listUnreadNotifications(auth.getName(), page, size).stream() + return notificationService.listNotifications(auth.getName(), read).stream() .map(notificationMapper::toDto) .collect(Collectors.toList()); } diff --git a/backend/src/main/java/com/openisle/repository/NotificationRepository.java b/backend/src/main/java/com/openisle/repository/NotificationRepository.java index 3434c083f..1e897b3a0 100644 --- a/backend/src/main/java/com/openisle/repository/NotificationRepository.java +++ b/backend/src/main/java/com/openisle/repository/NotificationRepository.java @@ -5,8 +5,6 @@ import com.openisle.model.User; import com.openisle.model.Post; import com.openisle.model.Comment; import com.openisle.model.NotificationType; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; @@ -15,8 +13,6 @@ import java.util.List; public interface NotificationRepository extends JpaRepository { List findByUserOrderByCreatedAtDesc(User user); List findByUserAndReadOrderByCreatedAtDesc(User user, boolean read); - Page findByUser(User user, Pageable pageable); - Page findByUserAndRead(User user, boolean read, Pageable pageable); long countByUserAndRead(User user, boolean read); List findByPost(Post post); List findByComment(Comment comment); diff --git a/backend/src/main/java/com/openisle/service/NotificationService.java b/backend/src/main/java/com/openisle/service/NotificationService.java index a43a1c1c4..6ecf571e8 100644 --- a/backend/src/main/java/com/openisle/service/NotificationService.java +++ b/backend/src/main/java/com/openisle/service/NotificationService.java @@ -24,10 +24,6 @@ import java.util.List; import java.util.ArrayList; import java.util.concurrent.Executor; import java.util.stream.Collectors; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; /** Service for creating and retrieving notifications. */ @Service @@ -184,24 +180,15 @@ public class NotificationService { userRepository.save(user); } - public List listNotifications(String username, int page, int size) { - return listNotifications(username, null, page, size); - } - - public List listUnreadNotifications(String username, int page, int size) { - return listNotifications(username, false, page, size); - } - - private List listNotifications(String username, Boolean read, int page, int size) { + public List listNotifications(String username, Boolean read) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found")); Set disabled = user.getDisabledNotificationTypes(); - Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); - Page list; + List list; if (read == null) { - list = notificationRepository.findByUser(user, pageable); + list = notificationRepository.findByUserOrderByCreatedAtDesc(user); } else { - list = notificationRepository.findByUserAndRead(user, read, pageable); + list = notificationRepository.findByUserAndReadOrderByCreatedAtDesc(user, read); } return list.stream().filter(n -> !disabled.contains(n.getType())).collect(Collectors.toList()); } diff --git a/backend/src/test/java/com/openisle/controller/NotificationControllerTest.java b/backend/src/test/java/com/openisle/controller/NotificationControllerTest.java index 89d28d892..6b74440a1 100644 --- a/backend/src/test/java/com/openisle/controller/NotificationControllerTest.java +++ b/backend/src/test/java/com/openisle/controller/NotificationControllerTest.java @@ -45,7 +45,7 @@ class NotificationControllerTest { p.setId(2L); n.setPost(p); n.setCreatedAt(LocalDateTime.now()); - when(notificationService.listNotifications("alice", 0, 30)) + when(notificationService.listNotifications("alice", null)) .thenReturn(List.of(n)); NotificationDto dto = new NotificationDto(); @@ -62,23 +62,6 @@ class NotificationControllerTest { .andExpect(jsonPath("$[0].post.id").value(2)); } - @Test - void listUnreadNotifications() throws Exception { - Notification n = new Notification(); - n.setId(1L); - when(notificationService.listUnreadNotifications("alice", 0, 30)) - .thenReturn(List.of(n)); - - NotificationDto dto = new NotificationDto(); - dto.setId(1L); - when(notificationMapper.toDto(n)).thenReturn(dto); - - mockMvc.perform(get("/api/notifications/unread") - .principal(new UsernamePasswordAuthenticationToken("alice","p"))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].id").value(1)); - } - @Test void markReadEndpoint() throws Exception { mockMvc.perform(post("/api/notifications/read") diff --git a/backend/src/test/java/com/openisle/service/NotificationServiceTest.java b/backend/src/test/java/com/openisle/service/NotificationServiceTest.java index 2f8292e0e..a01d3c95b 100644 --- a/backend/src/test/java/com/openisle/service/NotificationServiceTest.java +++ b/backend/src/test/java/com/openisle/service/NotificationServiceTest.java @@ -11,7 +11,6 @@ import org.mockito.Mockito; import java.util.List; import java.util.Optional; -import org.springframework.data.domain.Pageable; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -66,12 +65,12 @@ class NotificationServiceTest { when(uRepo.findByUsername("bob")).thenReturn(Optional.of(user)); Notification n = new Notification(); - when(nRepo.findByUser(eq(user), any(Pageable.class))).thenReturn(new org.springframework.data.domain.PageImpl<>(List.of(n))); + when(nRepo.findByUserOrderByCreatedAtDesc(user)).thenReturn(List.of(n)); - List list = service.listNotifications("bob", 0, 30); + List list = service.listNotifications("bob", null); assertEquals(1, list.size()); - verify(nRepo).findByUser(eq(user), any(Pageable.class)); + verify(nRepo).findByUserOrderByCreatedAtDesc(user); } @Test diff --git a/frontend_nuxt/pages/message.vue b/frontend_nuxt/pages/message.vue index bdc05a19d..061eedd37 100644 --- a/frontend_nuxt/pages/message.vue +++ b/frontend_nuxt/pages/message.vue @@ -53,13 +53,13 @@ -
- +
+ -
diff --git a/frontend_nuxt/utils/notification.js b/frontend_nuxt/utils/notification.js index c31ba4d81..7c0245b94 100644 --- a/frontend_nuxt/utils/notification.js +++ b/frontend_nuxt/utils/notification.js @@ -118,189 +118,175 @@ export async function updateNotificationPreference(type, enabled) { function createFetchNotifications() { const notifications = ref([]) const isLoadingMessage = ref(false) - const page = ref(0) - const pageSize = 30 - const readFilter = ref(null) - const fetchNotifications = async ({ reset = false, read = null } = {}) => { + const fetchNotifications = async () => { const config = useRuntimeConfig() const API_BASE_URL = config.public.apiBaseUrl - if (isLoadingMessage.value) return false - try { - const token = getToken() - if (!token) { - toast.error('请先登录') - return true - } - if (reset) { - notifications.value = [] - page.value = 0 - readFilter.value = read - } - isLoadingMessage.value = true - let url = `${API_BASE_URL}/api/notifications` - if (readFilter.value === false) url += '/unread' - url += `?page=${page.value}&size=${pageSize}` - const res = await fetch(url, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - isLoadingMessage.value = false - if (!res.ok) { - toast.error('获取通知失败') - return true - } - const data = await res.json() - - for (const n of data) { - if (n.type === 'COMMENT_REPLY') { - notifications.value.push({ - ...n, - src: n.comment.author.avatar, - iconClick: () => { - markRead(n.id) - navigateTo(`/users/${n.comment.author.id}`, { replace: true }) - }, - }) - } else if (n.type === 'REACTION') { - notifications.value.push({ - ...n, - emoji: reactionEmojiMap[n.reactionType], - iconClick: () => { - if (n.fromUser) { - markRead(n.id) - navigateTo(`/users/${n.fromUser.id}`, { replace: true }) - } - }, - }) - } else if (n.type === 'POST_VIEWED') { - notifications.value.push({ - ...n, - src: n.fromUser ? n.fromUser.avatar : null, - icon: n.fromUser ? undefined : iconMap[n.type], - iconClick: () => { - if (n.fromUser) { - markRead(n.id) - navigateTo(`/users/${n.fromUser.id}`, { replace: true }) - } - }, - }) - } else if (n.type === 'LOTTERY_WIN') { - notifications.value.push({ - ...n, - icon: iconMap[n.type], - iconClick: () => { - if (n.post) { - markRead(n.id) - router.push(`/posts/${n.post.id}`) - } - }, - }) - } else if (n.type === 'LOTTERY_DRAW') { - notifications.value.push({ - ...n, - icon: iconMap[n.type], - iconClick: () => { - if (n.post) { - markRead(n.id) - router.push(`/posts/${n.post.id}`) - } - }, - }) - } else if (n.type === 'POST_UPDATED') { - notifications.value.push({ - ...n, - src: n.comment.author.avatar, - iconClick: () => { - markRead(n.id) - navigateTo(`/users/${n.comment.author.id}`, { replace: true }) - }, - }) - } else if (n.type === 'USER_ACTIVITY') { - notifications.value.push({ - ...n, - src: n.comment.author.avatar, - iconClick: () => { - markRead(n.id) - navigateTo(`/users/${n.comment.author.id}`, { replace: true }) - }, - }) - } else if (n.type === 'MENTION') { - notifications.value.push({ - ...n, - icon: iconMap[n.type], - iconClick: () => { - if (n.fromUser) { - markRead(n.id) - navigateTo(`/users/${n.fromUser.id}`, { replace: true }) - } - }, - }) - } else if (n.type === 'USER_FOLLOWED' || n.type === 'USER_UNFOLLOWED') { - notifications.value.push({ - ...n, - icon: iconMap[n.type], - iconClick: () => { - if (n.fromUser) { - markRead(n.id) - navigateTo(`/users/${n.fromUser.id}`, { replace: true }) - } - }, - }) - } else if (n.type === 'FOLLOWED_POST') { - notifications.value.push({ - ...n, - icon: iconMap[n.type], - iconClick: () => { - if (n.post) { - markRead(n.id) - navigateTo(`/posts/${n.post.id}`, { replace: true }) - } - }, - }) - } else if (n.type === 'POST_SUBSCRIBED' || n.type === 'POST_UNSUBSCRIBED') { - notifications.value.push({ - ...n, - icon: iconMap[n.type], - iconClick: () => { - if (n.post) { - markRead(n.id) - navigateTo(`/posts/${n.post.id}`, { replace: true }) - } - }, - }) - } else if (n.type === 'POST_REVIEW_REQUEST') { - notifications.value.push({ - ...n, - src: n.fromUser ? n.fromUser.avatar : null, - icon: n.fromUser ? undefined : iconMap[n.type], - iconClick: () => { - if (n.post) { - markRead(n.id) - navigateTo(`/posts/${n.post.id}`, { replace: true }) - } - }, - }) - } else if (n.type === 'REGISTER_REQUEST') { - notifications.value.push({ - ...n, - icon: iconMap[n.type], - iconClick: () => {}, - }) - } else { - notifications.value.push({ - ...n, - icon: iconMap[n.type], - }) + if (isLoadingMessage && notifications && markRead) { + try { + const token = getToken() + if (!token) { + toast.error('请先登录') + return } + isLoadingMessage.value = true + notifications.value = [] + const res = await fetch(`${API_BASE_URL}/api/notifications`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + isLoadingMessage.value = false + if (!res.ok) { + toast.error('获取通知失败') + return + } + const data = await res.json() + + for (const n of data) { + if (n.type === 'COMMENT_REPLY') { + notifications.value.push({ + ...n, + src: n.comment.author.avatar, + iconClick: () => { + markRead(n.id) + navigateTo(`/users/${n.comment.author.id}`, { replace: true }) + }, + }) + } else if (n.type === 'REACTION') { + notifications.value.push({ + ...n, + emoji: reactionEmojiMap[n.reactionType], + iconClick: () => { + if (n.fromUser) { + markRead(n.id) + navigateTo(`/users/${n.fromUser.id}`, { replace: true }) + } + }, + }) + } else if (n.type === 'POST_VIEWED') { + notifications.value.push({ + ...n, + src: n.fromUser ? n.fromUser.avatar : null, + icon: n.fromUser ? undefined : iconMap[n.type], + iconClick: () => { + if (n.fromUser) { + markRead(n.id) + navigateTo(`/users/${n.fromUser.id}`, { replace: true }) + } + }, + }) + } else if (n.type === 'LOTTERY_WIN') { + notifications.value.push({ + ...n, + icon: iconMap[n.type], + iconClick: () => { + if (n.post) { + markRead(n.id) + router.push(`/posts/${n.post.id}`) + } + }, + }) + } else if (n.type === 'LOTTERY_DRAW') { + notifications.value.push({ + ...n, + icon: iconMap[n.type], + iconClick: () => { + if (n.post) { + markRead(n.id) + router.push(`/posts/${n.post.id}`) + } + }, + }) + } else if (n.type === 'POST_UPDATED') { + notifications.value.push({ + ...n, + src: n.comment.author.avatar, + iconClick: () => { + markRead(n.id) + navigateTo(`/users/${n.comment.author.id}`, { replace: true }) + }, + }) + } else if (n.type === 'USER_ACTIVITY') { + notifications.value.push({ + ...n, + src: n.comment.author.avatar, + iconClick: () => { + markRead(n.id) + navigateTo(`/users/${n.comment.author.id}`, { replace: true }) + }, + }) + } else if (n.type === 'MENTION') { + notifications.value.push({ + ...n, + icon: iconMap[n.type], + iconClick: () => { + if (n.fromUser) { + markRead(n.id) + navigateTo(`/users/${n.fromUser.id}`, { replace: true }) + } + }, + }) + } else if (n.type === 'USER_FOLLOWED' || n.type === 'USER_UNFOLLOWED') { + notifications.value.push({ + ...n, + icon: iconMap[n.type], + iconClick: () => { + if (n.fromUser) { + markRead(n.id) + navigateTo(`/users/${n.fromUser.id}`, { replace: true }) + } + }, + }) + } else if (n.type === 'FOLLOWED_POST') { + notifications.value.push({ + ...n, + icon: iconMap[n.type], + iconClick: () => { + if (n.post) { + markRead(n.id) + navigateTo(`/posts/${n.post.id}`, { replace: true }) + } + }, + }) + } else if (n.type === 'POST_SUBSCRIBED' || n.type === 'POST_UNSUBSCRIBED') { + notifications.value.push({ + ...n, + icon: iconMap[n.type], + iconClick: () => { + if (n.post) { + markRead(n.id) + navigateTo(`/posts/${n.post.id}`, { replace: true }) + } + }, + }) + } else if (n.type === 'POST_REVIEW_REQUEST') { + notifications.value.push({ + ...n, + src: n.fromUser ? n.fromUser.avatar : null, + icon: n.fromUser ? undefined : iconMap[n.type], + iconClick: () => { + if (n.post) { + markRead(n.id) + navigateTo(`/posts/${n.post.id}`, { replace: true }) + } + }, + }) + } else if (n.type === 'REGISTER_REQUEST') { + notifications.value.push({ + ...n, + icon: iconMap[n.type], + iconClick: () => {}, + }) + } else { + notifications.value.push({ + ...n, + icon: iconMap[n.type], + }) + } + } + } catch (e) { + console.error(e) } - const done = data.length < pageSize - if (!done) page.value++ - return done - } catch (e) { - console.error(e) - isLoadingMessage.value = false - return true } } @@ -349,6 +335,7 @@ function createFetchNotifications() { markRead, notifications, isLoadingMessage, + markRead, markAllRead, } }