From 02076e24e5f149993202dc2b02a5883a937d7c2c Mon Sep 17 00:00:00 2001 From: Tim <135014430+nagisa77@users.noreply.github.com> Date: Tue, 12 Aug 2025 14:28:34 +0800 Subject: [PATCH] feat: allow updating notification prefs --- .../controller/NotificationController.java | 12 +++++ .../dto/NotificationPreferenceDto.java | 11 +++++ .../NotificationPreferenceUpdateRequest.java | 11 +++++ .../main/java/com/openisle/model/User.java | 8 ++++ .../openisle/service/NotificationService.java | 41 +++++++++++++++-- frontend_nuxt/pages/message.vue | 46 ++++++++++++++++--- frontend_nuxt/utils/notification.js | 32 +++++++++++++ 7 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 backend/src/main/java/com/openisle/dto/NotificationPreferenceDto.java create mode 100644 backend/src/main/java/com/openisle/dto/NotificationPreferenceUpdateRequest.java diff --git a/backend/src/main/java/com/openisle/controller/NotificationController.java b/backend/src/main/java/com/openisle/controller/NotificationController.java index 5462b94fc..d25d2a808 100644 --- a/backend/src/main/java/com/openisle/controller/NotificationController.java +++ b/backend/src/main/java/com/openisle/controller/NotificationController.java @@ -3,6 +3,8 @@ package com.openisle.controller; import com.openisle.dto.NotificationDto; import com.openisle.dto.NotificationMarkReadRequest; import com.openisle.dto.NotificationUnreadCountDto; +import com.openisle.dto.NotificationPreferenceDto; +import com.openisle.dto.NotificationPreferenceUpdateRequest; import com.openisle.mapper.NotificationMapper; import com.openisle.service.NotificationService; import lombok.RequiredArgsConstructor; @@ -40,4 +42,14 @@ public class NotificationController { public void markRead(@RequestBody NotificationMarkReadRequest req, Authentication auth) { notificationService.markRead(auth.getName(), req.getIds()); } + + @GetMapping("/prefs") + public List prefs(Authentication auth) { + return notificationService.listPreferences(auth.getName()); + } + + @PostMapping("/prefs") + public void updatePref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) { + notificationService.updatePreference(auth.getName(), req.getType(), req.isEnabled()); + } } diff --git a/backend/src/main/java/com/openisle/dto/NotificationPreferenceDto.java b/backend/src/main/java/com/openisle/dto/NotificationPreferenceDto.java new file mode 100644 index 000000000..4d4e6a4d7 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/NotificationPreferenceDto.java @@ -0,0 +1,11 @@ +package com.openisle.dto; + +import com.openisle.model.NotificationType; +import lombok.Data; + +/** User notification preference DTO. */ +@Data +public class NotificationPreferenceDto { + private NotificationType type; + private boolean enabled; +} diff --git a/backend/src/main/java/com/openisle/dto/NotificationPreferenceUpdateRequest.java b/backend/src/main/java/com/openisle/dto/NotificationPreferenceUpdateRequest.java new file mode 100644 index 000000000..ad68bf45f --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/NotificationPreferenceUpdateRequest.java @@ -0,0 +1,11 @@ +package com.openisle.dto; + +import com.openisle.model.NotificationType; +import lombok.Data; + +/** Request to update a single notification preference. */ +@Data +public class NotificationPreferenceUpdateRequest { + private NotificationType type; + private boolean enabled; +} diff --git a/backend/src/main/java/com/openisle/model/User.java b/backend/src/main/java/com/openisle/model/User.java index 43bcbe268..fef975633 100644 --- a/backend/src/main/java/com/openisle/model/User.java +++ b/backend/src/main/java/com/openisle/model/User.java @@ -7,6 +7,8 @@ import lombok.Setter; import org.hibernate.annotations.CreationTimestamp; import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; /** * Simple user entity with basic fields and a role. @@ -62,6 +64,12 @@ public class User { @Enumerated(EnumType.STRING) private MedalType displayMedal; + @ElementCollection(targetClass = NotificationType.class) + @CollectionTable(name = "user_disabled_notification_types", joinColumns = @JoinColumn(name = "user_id")) + @Column(name = "notification_type") + @Enumerated(EnumType.STRING) + private Set disabledNotificationTypes = new HashSet<>(); + @CreationTimestamp @Column(nullable = false, updatable = false, columnDefinition = "DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6)") diff --git a/backend/src/main/java/com/openisle/service/NotificationService.java b/backend/src/main/java/com/openisle/service/NotificationService.java index 86335151b..aabdb5b03 100644 --- a/backend/src/main/java/com/openisle/service/NotificationService.java +++ b/backend/src/main/java/com/openisle/service/NotificationService.java @@ -1,6 +1,7 @@ package com.openisle.service; import com.openisle.model.*; +import com.openisle.dto.NotificationPreferenceDto; import com.openisle.repository.NotificationRepository; import com.openisle.repository.ReactionRepository; import com.openisle.repository.UserRepository; @@ -20,7 +21,9 @@ import java.util.Set; import java.util.HashSet; import java.util.List; +import java.util.ArrayList; import java.util.concurrent.Executor; +import java.util.stream.Collectors; /** Service for creating and retrieving notifications. */ @Service @@ -138,13 +141,43 @@ public class NotificationService { } } + public List listPreferences(String username) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found")); + Set disabled = user.getDisabledNotificationTypes(); + List prefs = new ArrayList<>(); + for (NotificationType nt : NotificationType.values()) { + NotificationPreferenceDto dto = new NotificationPreferenceDto(); + dto.setType(nt); + dto.setEnabled(!disabled.contains(nt)); + prefs.add(dto); + } + return prefs; + } + + public void updatePreference(String username, NotificationType type, boolean enabled) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found")); + Set disabled = user.getDisabledNotificationTypes(); + if (enabled) { + disabled.remove(type); + } else { + disabled.add(type); + } + userRepository.save(user); + } + 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(); + List list; if (read == null) { - return notificationRepository.findByUserOrderByCreatedAtDesc(user); + list = notificationRepository.findByUserOrderByCreatedAtDesc(user); + } else { + list = notificationRepository.findByUserAndReadOrderByCreatedAtDesc(user, read); } - return notificationRepository.findByUserAndReadOrderByCreatedAtDesc(user, read); + return list.stream().filter(n -> !disabled.contains(n.getType())).collect(Collectors.toList()); } public void markRead(String username, List ids) { @@ -162,7 +195,9 @@ public class NotificationService { public long countUnread(String username) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found")); - return notificationRepository.countByUserAndRead(user, false); + Set disabled = user.getDisabledNotificationTypes(); + return notificationRepository.findByUserAndReadOrderByCreatedAtDesc(user, false).stream() + .filter(n -> !disabled.contains(n.getType())).count(); } public void notifyMentions(String content, User fromUser, Post post, Comment comment) { diff --git a/frontend_nuxt/pages/message.vue b/frontend_nuxt/pages/message.vue index 0864d5285..0b012775d 100644 --- a/frontend_nuxt/pages/message.vue +++ b/frontend_nuxt/pages/message.vue @@ -34,11 +34,15 @@
通知设置
-
已读通知
-
订阅者发帖通知
-
订阅者回复通知
-
关注者发帖通知
-
评论回复通知
+
+ {{ formatType(pref.type) }} +
@@ -482,7 +486,13 @@ import BaseTimeline from '../components/BaseTimeline.vue' import BasePlaceholder from '../components/BasePlaceholder.vue' import NotificationContainer from '../components/NotificationContainer.vue' import { getToken, authState } from '../utils/auth' -import { markNotificationsRead, fetchUnreadCount, notificationState } from '../utils/notification' +import { + markNotificationsRead, + fetchUnreadCount, + notificationState, + fetchNotificationPreferences, + updateNotificationPreference, +} from '../utils/notification' import { toast } from '../main' import { stripMarkdownLength } from '../utils/markdown' import TimeManager from '../utils/time' @@ -496,6 +506,7 @@ export default { const notifications = ref([]) const isLoadingMessage = ref(false) const selectedTab = ref('unread') + const notificationPrefs = ref([]) const filteredNotifications = computed(() => selectedTab.value === 'all' ? notifications.value @@ -568,6 +579,7 @@ export default { return } isLoadingMessage.value = true + notifications.value = [] const res = await fetch(`${API_BASE_URL}/api/notifications`, { headers: { Authorization: `Bearer ${token}`, @@ -705,6 +717,21 @@ export default { } } + const fetchPrefs = async () => { + notificationPrefs.value = await fetchNotificationPreferences() + } + + const togglePref = async (pref) => { + const ok = await updateNotificationPreference(pref.type, !pref.enabled) + if (ok) { + pref.enabled = !pref.enabled + await fetchNotifications() + await fetchUnreadCount() + } else { + toast.error('操作失败') + } + } + const approve = async (id, nid) => { const token = getToken() if (!token) return @@ -768,7 +795,10 @@ export default { } } - onMounted(fetchNotifications) + onMounted(() => { + fetchNotifications() + fetchPrefs() + }) return { notifications, @@ -783,6 +813,8 @@ export default { filteredNotifications, markAllRead, authState, + notificationPrefs, + togglePref, } }, } diff --git a/frontend_nuxt/utils/notification.js b/frontend_nuxt/utils/notification.js index 2e117cbac..16e37aa0f 100644 --- a/frontend_nuxt/utils/notification.js +++ b/frontend_nuxt/utils/notification.js @@ -46,3 +46,35 @@ export async function markNotificationsRead(ids) { return false } } + +export async function fetchNotificationPreferences() { + try { + const token = getToken() + if (!token) return [] + const res = await fetch(`${API_BASE_URL}/api/notifications/prefs`, { + headers: { Authorization: `Bearer ${token}` }, + }) + if (!res.ok) return [] + return await res.json() + } catch (e) { + return [] + } +} + +export async function updateNotificationPreference(type, enabled) { + try { + const token = getToken() + if (!token) return false + const res = await fetch(`${API_BASE_URL}/api/notifications/prefs`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ type, enabled }), + }) + return res.ok + } catch (e) { + return false + } +}