mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-22 19:01:13 +08:00
Compare commits
6 Commits
codex/fix-
...
codex/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8767aa31d6 | ||
|
|
a428f472f2 | ||
|
|
8544803e62 | ||
|
|
54874cea7a | ||
|
|
098d82a6a0 | ||
|
|
90eee03198 |
@@ -62,4 +62,14 @@ public class NotificationController {
|
|||||||
public void updatePref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
|
public void updatePref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
|
||||||
notificationService.updatePreference(auth.getName(), req.getType(), req.isEnabled());
|
notificationService.updatePreference(auth.getName(), req.getType(), req.isEnabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/email-prefs")
|
||||||
|
public List<NotificationPreferenceDto> emailPrefs(Authentication auth) {
|
||||||
|
return notificationService.listEmailPreferences(auth.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/email-prefs")
|
||||||
|
public void updateEmailPref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
|
||||||
|
notificationService.updateEmailPreference(auth.getName(), req.getType(), req.isEnabled());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ public class InviteToken {
|
|||||||
@Id
|
@Id
|
||||||
private String token;
|
private String token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Short token used in invite links. Existing records may have this field null
|
||||||
|
* and fall back to {@link #token} for backward compatibility.
|
||||||
|
*/
|
||||||
|
@Column(unique = true)
|
||||||
|
private String shortToken;
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
private User inviter;
|
private User inviter;
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,12 @@ public class User {
|
|||||||
NotificationType.USER_ACTIVITY
|
NotificationType.USER_ACTIVITY
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ElementCollection(targetClass = NotificationType.class)
|
||||||
|
@CollectionTable(name = "user_disabled_email_notification_types", joinColumns = @JoinColumn(name = "user_id"))
|
||||||
|
@Column(name = "notification_type")
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private Set<NotificationType> disabledEmailNotificationTypes = EnumSet.noneOf(NotificationType.class);
|
||||||
|
|
||||||
@CreationTimestamp
|
@CreationTimestamp
|
||||||
@Column(nullable = false, updatable = false,
|
@Column(nullable = false, updatable = false,
|
||||||
columnDefinition = "DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6)")
|
columnDefinition = "DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6)")
|
||||||
|
|||||||
@@ -9,4 +9,8 @@ import java.util.Optional;
|
|||||||
|
|
||||||
public interface InviteTokenRepository extends JpaRepository<InviteToken, String> {
|
public interface InviteTokenRepository extends JpaRepository<InviteToken, String> {
|
||||||
Optional<InviteToken> findByInviterAndCreatedDate(User inviter, LocalDate createdDate);
|
Optional<InviteToken> findByInviterAndCreatedDate(User inviter, LocalDate createdDate);
|
||||||
|
|
||||||
|
Optional<InviteToken> findByShortToken(String shortToken);
|
||||||
|
|
||||||
|
boolean existsByShortToken(String shortToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,33 +30,53 @@ public class InviteService {
|
|||||||
LocalDate today = LocalDate.now();
|
LocalDate today = LocalDate.now();
|
||||||
Optional<InviteToken> existing = inviteTokenRepository.findByInviterAndCreatedDate(inviter, today);
|
Optional<InviteToken> existing = inviteTokenRepository.findByInviterAndCreatedDate(inviter, today);
|
||||||
if (existing.isPresent()) {
|
if (existing.isPresent()) {
|
||||||
return existing.get().getToken();
|
InviteToken inviteToken = existing.get();
|
||||||
|
return inviteToken.getShortToken() != null ? inviteToken.getShortToken() : inviteToken.getToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
String token = jwtService.generateInviteToken(username);
|
String token = jwtService.generateInviteToken(username);
|
||||||
|
String shortToken;
|
||||||
|
do {
|
||||||
|
shortToken = java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 8);
|
||||||
|
} while (inviteTokenRepository.existsByShortToken(shortToken));
|
||||||
|
|
||||||
InviteToken inviteToken = new InviteToken();
|
InviteToken inviteToken = new InviteToken();
|
||||||
inviteToken.setToken(token);
|
inviteToken.setToken(token);
|
||||||
|
inviteToken.setShortToken(shortToken);
|
||||||
inviteToken.setInviter(inviter);
|
inviteToken.setInviter(inviter);
|
||||||
inviteToken.setCreatedDate(today);
|
inviteToken.setCreatedDate(today);
|
||||||
inviteToken.setUsageCount(0);
|
inviteToken.setUsageCount(0);
|
||||||
inviteTokenRepository.save(inviteToken);
|
inviteTokenRepository.save(inviteToken);
|
||||||
return token;
|
return shortToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public InviteValidateResult validate(String token) {
|
public InviteValidateResult validate(String token) {
|
||||||
if (token == null || token.isEmpty()) {
|
if (token == null || token.isEmpty()) {
|
||||||
return new InviteValidateResult(null, false);
|
return new InviteValidateResult(null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InviteToken invite = inviteTokenRepository.findById(token).orElse(null);
|
||||||
|
String realToken = token;
|
||||||
|
if (invite == null) {
|
||||||
|
invite = inviteTokenRepository.findByShortToken(token).orElse(null);
|
||||||
|
if (invite == null) {
|
||||||
|
return new InviteValidateResult(null, false);
|
||||||
|
}
|
||||||
|
realToken = invite.getToken();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
jwtService.validateAndGetSubjectForInvite(token);
|
jwtService.validateAndGetSubjectForInvite(realToken);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return new InviteValidateResult(null, false);
|
return new InviteValidateResult(null, false);
|
||||||
}
|
}
|
||||||
InviteToken invite = inviteTokenRepository.findById(token).orElse(null);
|
|
||||||
return new InviteValidateResult(invite, invite != null && invite.getUsageCount() < 3);
|
return new InviteValidateResult(invite, invite.getUsageCount() < 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void consume(String token, String newUserName) {
|
public void consume(String token, String newUserName) {
|
||||||
InviteToken invite = inviteTokenRepository.findById(token).orElseThrow();
|
InviteToken invite = inviteTokenRepository.findById(token)
|
||||||
|
.orElseGet(() -> inviteTokenRepository.findByShortToken(token).orElseThrow());
|
||||||
invite.setUsageCount(invite.getUsageCount() + 1);
|
invite.setUsageCount(invite.getUsageCount() + 1);
|
||||||
inviteTokenRepository.save(invite);
|
inviteTokenRepository.save(invite);
|
||||||
pointService.awardForInvite(invite.getInviter().getUsername(), newUserName);
|
pointService.awardForInvite(invite.getInviter().getUsername(), newUserName);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import java.util.regex.Pattern;
|
|||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -40,6 +41,12 @@ public class NotificationService {
|
|||||||
|
|
||||||
private static final Pattern MENTION_PATTERN = Pattern.compile("@\\[([^\\]]+)\\]");
|
private static final Pattern MENTION_PATTERN = Pattern.compile("@\\[([^\\]]+)\\]");
|
||||||
|
|
||||||
|
private static final Set<NotificationType> EMAIL_TYPES = EnumSet.of(
|
||||||
|
NotificationType.COMMENT_REPLY,
|
||||||
|
NotificationType.LOTTERY_WIN,
|
||||||
|
NotificationType.LOTTERY_DRAW
|
||||||
|
);
|
||||||
|
|
||||||
private String buildPayload(String body, String url) {
|
private String buildPayload(String body, String url) {
|
||||||
// Ensure push notifications contain a link to the related resource so
|
// Ensure push notifications contain a link to the related resource so
|
||||||
// that verifications can assert its presence and users can navigate
|
// that verifications can assert its presence and users can navigate
|
||||||
@@ -75,7 +82,8 @@ public class NotificationService {
|
|||||||
n = notificationRepository.save(n);
|
n = notificationRepository.save(n);
|
||||||
|
|
||||||
// Runnable asyncTask = () -> {
|
// Runnable asyncTask = () -> {
|
||||||
if (type == NotificationType.COMMENT_REPLY && user.getEmail() != null && post != null && comment != null) {
|
if (type == NotificationType.COMMENT_REPLY && user.getEmail() != null && post != null && comment != null
|
||||||
|
&& !user.getDisabledEmailNotificationTypes().contains(NotificationType.COMMENT_REPLY)) {
|
||||||
String url = String.format("%s/posts/%d#comment-%d", websiteUrl, post.getId(), comment.getId());
|
String url = String.format("%s/posts/%d#comment-%d", websiteUrl, post.getId(), comment.getId());
|
||||||
emailSender.sendEmail(user.getEmail(), "有人回复了你", url);
|
emailSender.sendEmail(user.getEmail(), "有人回复了你", url);
|
||||||
sendCustomPush(user, "有人回复了你", url);
|
sendCustomPush(user, "有人回复了你", url);
|
||||||
@@ -187,6 +195,35 @@ public class NotificationService {
|
|||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<NotificationPreferenceDto> listEmailPreferences(String username) {
|
||||||
|
User user = userRepository.findByUsername(username)
|
||||||
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||||
|
Set<NotificationType> disabled = user.getDisabledEmailNotificationTypes();
|
||||||
|
List<NotificationPreferenceDto> prefs = new ArrayList<>();
|
||||||
|
for (NotificationType nt : EMAIL_TYPES) {
|
||||||
|
NotificationPreferenceDto dto = new NotificationPreferenceDto();
|
||||||
|
dto.setType(nt);
|
||||||
|
dto.setEnabled(!disabled.contains(nt));
|
||||||
|
prefs.add(dto);
|
||||||
|
}
|
||||||
|
return prefs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateEmailPreference(String username, NotificationType type, boolean enabled) {
|
||||||
|
if (!EMAIL_TYPES.contains(type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
User user = userRepository.findByUsername(username)
|
||||||
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||||
|
Set<NotificationType> disabled = user.getDisabledEmailNotificationTypes();
|
||||||
|
if (enabled) {
|
||||||
|
disabled.remove(type);
|
||||||
|
} else {
|
||||||
|
disabled.add(type);
|
||||||
|
}
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
public List<Notification> listNotifications(String username, Boolean read, int page, int size) {
|
public List<Notification> listNotifications(String username, Boolean read, int page, int size) {
|
||||||
User user = userRepository.findByUsername(username)
|
User user = userRepository.findByUsername(username)
|
||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||||
|
|||||||
@@ -374,14 +374,16 @@ public class PostService {
|
|||||||
lp.setWinners(winners);
|
lp.setWinners(winners);
|
||||||
lotteryPostRepository.save(lp);
|
lotteryPostRepository.save(lp);
|
||||||
for (User w : winners) {
|
for (User w : winners) {
|
||||||
if (w.getEmail() != null) {
|
if (w.getEmail() != null &&
|
||||||
|
!w.getDisabledEmailNotificationTypes().contains(NotificationType.LOTTERY_WIN)) {
|
||||||
emailSender.sendEmail(w.getEmail(), "你中奖了", "恭喜你在抽奖贴 \"" + lp.getTitle() + "\" 中获奖");
|
emailSender.sendEmail(w.getEmail(), "你中奖了", "恭喜你在抽奖贴 \"" + lp.getTitle() + "\" 中获奖");
|
||||||
}
|
}
|
||||||
notificationService.createNotification(w, NotificationType.LOTTERY_WIN, lp, null, null, lp.getAuthor(), null, null);
|
notificationService.createNotification(w, NotificationType.LOTTERY_WIN, lp, null, null, lp.getAuthor(), null, null);
|
||||||
notificationService.sendCustomPush(w, "你中奖了", String.format("%s/posts/%d", websiteUrl, lp.getId()));
|
notificationService.sendCustomPush(w, "你中奖了", String.format("%s/posts/%d", websiteUrl, lp.getId()));
|
||||||
}
|
}
|
||||||
if (lp.getAuthor() != null) {
|
if (lp.getAuthor() != null) {
|
||||||
if (lp.getAuthor().getEmail() != null) {
|
if (lp.getAuthor().getEmail() != null &&
|
||||||
|
!lp.getAuthor().getDisabledEmailNotificationTypes().contains(NotificationType.LOTTERY_DRAW)) {
|
||||||
emailSender.sendEmail(lp.getAuthor().getEmail(), "抽奖已开奖", "您的抽奖贴 \"" + lp.getTitle() + "\" 已开奖");
|
emailSender.sendEmail(lp.getAuthor().getEmail(), "抽奖已开奖", "您的抽奖贴 \"" + lp.getTitle() + "\" 已开奖");
|
||||||
}
|
}
|
||||||
notificationService.createNotification(lp.getAuthor(), NotificationType.LOTTERY_DRAW, lp, null, null, null, null, null);
|
notificationService.createNotification(lp.getAuthor(), NotificationType.LOTTERY_DRAW, lp, null, null, null, null, null);
|
||||||
|
|||||||
@@ -334,6 +334,9 @@ onMounted(async () => {
|
|||||||
if (currentUser.value) {
|
if (currentUser.value) {
|
||||||
await fetchMessages(0)
|
await fetchMessages(0)
|
||||||
await markConversationAsRead()
|
await markConversationAsRead()
|
||||||
|
await nextTick()
|
||||||
|
// 初次进入频道时,平滑滚动到底部
|
||||||
|
scrollToBottomSmooth()
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
if (token && !isConnected.value) {
|
if (token && !isConnected.value) {
|
||||||
connect(token)
|
connect(token)
|
||||||
|
|||||||
@@ -23,6 +23,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="message-control-container">
|
||||||
|
<div class="message-control-title">邮件通知设置</div>
|
||||||
|
<div class="message-control-item-container">
|
||||||
|
<div v-for="pref in emailPrefs" :key="pref.type" class="message-control-item">
|
||||||
|
<div class="message-control-item-label">{{ formatType(pref.type) }}</div>
|
||||||
|
<BaseSwitch
|
||||||
|
:model-value="pref.enabled"
|
||||||
|
@update:modelValue="(val) => toggleEmailPref(pref, val)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -579,6 +591,8 @@ import {
|
|||||||
hasMore,
|
hasMore,
|
||||||
fetchNotificationPreferences,
|
fetchNotificationPreferences,
|
||||||
updateNotificationPreference,
|
updateNotificationPreference,
|
||||||
|
fetchEmailNotificationPreferences,
|
||||||
|
updateEmailNotificationPreference,
|
||||||
} from '~/utils/notification'
|
} from '~/utils/notification'
|
||||||
import TimeManager from '~/utils/time'
|
import TimeManager from '~/utils/time'
|
||||||
import BaseSwitch from '~/components/BaseSwitch.vue'
|
import BaseSwitch from '~/components/BaseSwitch.vue'
|
||||||
@@ -595,6 +609,7 @@ const tabs = [
|
|||||||
{ key: 'control', label: '消息设置' },
|
{ key: 'control', label: '消息设置' },
|
||||||
]
|
]
|
||||||
const notificationPrefs = ref([])
|
const notificationPrefs = ref([])
|
||||||
|
const emailPrefs = ref([])
|
||||||
const page = ref(0)
|
const page = ref(0)
|
||||||
const pageSize = 30
|
const pageSize = 30
|
||||||
|
|
||||||
@@ -619,6 +634,10 @@ const fetchPrefs = async () => {
|
|||||||
notificationPrefs.value = await fetchNotificationPreferences()
|
notificationPrefs.value = await fetchNotificationPreferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchEmailPrefs = async () => {
|
||||||
|
emailPrefs.value = await fetchEmailNotificationPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
const togglePref = async (pref, value) => {
|
const togglePref = async (pref, value) => {
|
||||||
const ok = await updateNotificationPreference(pref.type, value)
|
const ok = await updateNotificationPreference(pref.type, value)
|
||||||
if (ok) {
|
if (ok) {
|
||||||
@@ -634,6 +653,15 @@ const togglePref = async (pref, value) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleEmailPref = async (pref, value) => {
|
||||||
|
const ok = await updateEmailNotificationPreference(pref.type, value)
|
||||||
|
if (ok) {
|
||||||
|
pref.enabled = value
|
||||||
|
} else {
|
||||||
|
toast.error('操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const markRead = async (id) => {
|
const markRead = async (id) => {
|
||||||
markNotificationRead(id)
|
markNotificationRead(id)
|
||||||
if (selectedTab.value === 'unread') {
|
if (selectedTab.value === 'unread') {
|
||||||
@@ -729,6 +757,7 @@ onActivated(async () => {
|
|||||||
page.value = 0
|
page.value = 0
|
||||||
await fetchNotifications({ page: 0, size: pageSize, unread: selectedTab.value === 'unread' })
|
await fetchNotifications({ page: 0, size: pageSize, unread: selectedTab.value === 'unread' })
|
||||||
fetchPrefs()
|
fetchPrefs()
|
||||||
|
fetchEmailPrefs()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,43 @@ export async function updateNotificationPreference(type, enabled) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchEmailNotificationPreferences() {
|
||||||
|
try {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
|
const token = getToken()
|
||||||
|
if (!token) return []
|
||||||
|
const res = await fetch(`${API_BASE_URL}/api/notifications/email-prefs`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
if (!res.ok) return []
|
||||||
|
return await res.json()
|
||||||
|
} catch (e) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateEmailNotificationPreference(type, enabled) {
|
||||||
|
try {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
const token = getToken()
|
||||||
|
if (!token) return false
|
||||||
|
const res = await fetch(`${API_BASE_URL}/api/notifications/email-prefs`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ type, enabled }),
|
||||||
|
})
|
||||||
|
return res.ok
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理信息的高阶函数
|
* 处理信息的高阶函数
|
||||||
* @returns
|
* @returns
|
||||||
|
|||||||
Reference in New Issue
Block a user