From 21b1c3317aade15b5f3bb51b155fb93a17a50362 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 16 Jan 2026 11:12:20 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=A7=A3=E5=86=B3=E9=82=AE=E4=BB=B6?= =?UTF-8?q?=E5=8F=91=E9=80=81=E9=94=99=E8=AF=AF=EF=BC=8C=E4=BD=86=E6=98=AF?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E6=98=BE=E7=A4=BA=E5=B7=B2=E5=8F=91=E9=80=81?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminUserController.java | 31 +++++--- .../openisle/controller/AuthController.java | 73 ++++++++++++++++--- .../exception/EmailSendException.java | 15 ++++ .../openisle/service/NotificationService.java | 9 ++- .../com/openisle/service/PostService.java | 33 ++++++--- .../openisle/service/ResendEmailSender.java | 21 +++++- .../com/openisle/service/UserService.java | 4 +- frontend_nuxt/pages/login.vue | 11 ++- frontend_nuxt/pages/signup.vue | 11 ++- 9 files changed, 165 insertions(+), 43 deletions(-) create mode 100644 backend/src/main/java/com/openisle/exception/EmailSendException.java diff --git a/backend/src/main/java/com/openisle/controller/AdminUserController.java b/backend/src/main/java/com/openisle/controller/AdminUserController.java index 02d04696f..32eaa1b00 100644 --- a/backend/src/main/java/com/openisle/controller/AdminUserController.java +++ b/backend/src/main/java/com/openisle/controller/AdminUserController.java @@ -6,10 +6,12 @@ import com.openisle.model.User; import com.openisle.repository.NotificationRepository; import com.openisle.repository.UserRepository; import com.openisle.service.EmailSender; +import com.openisle.exception.EmailSendException; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -17,6 +19,7 @@ import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/admin/users") @RequiredArgsConstructor +@Slf4j public class AdminUserController { private final UserRepository userRepository; @@ -35,11 +38,15 @@ public class AdminUserController { user.setApproved(true); userRepository.save(user); markRegisterRequestNotificationsRead(user); - emailSender.sendEmail( - user.getEmail(), - "您的注册已审核通过", - "🎉您的注册已经审核通过, 点击以访问网站: " + websiteUrl - ); + try { + emailSender.sendEmail( + user.getEmail(), + "您的注册已审核通过", + "🎉您的注册已经审核通过, 点击以访问网站: " + websiteUrl + ); + } catch (EmailSendException e) { + log.warn("Failed to send approve email to {}: {}", user.getEmail(), e.getMessage()); + } return ResponseEntity.ok().build(); } @@ -52,11 +59,15 @@ public class AdminUserController { user.setApproved(false); userRepository.save(user); markRegisterRequestNotificationsRead(user); - emailSender.sendEmail( - user.getEmail(), - "您的注册已被管理员拒绝", - "您的注册被管理员拒绝, 点击链接可以重新填写理由申请: " + websiteUrl - ); + try { + emailSender.sendEmail( + user.getEmail(), + "您的注册已被管理员拒绝", + "您的注册被管理员拒绝, 点击链接可以重新填写理由申请: " + websiteUrl + ); + } catch (EmailSendException e) { + log.warn("Failed to send reject email to {}: {}", user.getEmail(), e.getMessage()); + } return ResponseEntity.ok().build(); } diff --git a/backend/src/main/java/com/openisle/controller/AuthController.java b/backend/src/main/java/com/openisle/controller/AuthController.java index 1014eb72a..4cc3b8b11 100644 --- a/backend/src/main/java/com/openisle/controller/AuthController.java +++ b/backend/src/main/java/com/openisle/controller/AuthController.java @@ -2,6 +2,7 @@ package com.openisle.controller; import com.openisle.config.CachingConfig; import com.openisle.dto.*; +import com.openisle.exception.EmailSendException; import com.openisle.exception.FieldException; import com.openisle.model.RegisterMode; import com.openisle.model.User; @@ -19,6 +20,7 @@ import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -83,6 +85,17 @@ public class AuthController { "INVITE_APPROVED" ) ); + } catch (EmailSendException e) { + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body( + Map.of( + "error", + "邮件发送失败: " + e.getMessage(), + "reason_code", + "EMAIL_SEND_FAILED" + ) + ); } catch (FieldException e) { return ResponseEntity.badRequest().body( Map.of("field", e.getField(), "error", e.getMessage()) @@ -97,7 +110,20 @@ public class AuthController { registerModeService.getRegisterMode() ); // 发送确认邮件 - userService.sendVerifyMail(user, VerifyType.REGISTER); + try { + userService.sendVerifyMail(user, VerifyType.REGISTER); + } catch (EmailSendException e) { + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body( + Map.of( + "error", + "邮件发送失败: " + e.getMessage(), + "reason_code", + "EMAIL_SEND_FAILED" + ) + ); + } if (!user.isApproved()) { notificationService.createRegisterRequestNotifications(user, user.getRegisterReason()); } @@ -169,14 +195,28 @@ public class AuthController { } User user = userOpt.get(); if (!user.isVerified()) { - user = userService.register( - user.getUsername(), - user.getEmail(), - user.getPassword(), - user.getRegisterReason(), - registerModeService.getRegisterMode() - ); - userService.sendVerifyMail(user, VerifyType.REGISTER); + user = + userService.register( + user.getUsername(), + user.getEmail(), + user.getPassword(), + user.getRegisterReason(), + registerModeService.getRegisterMode() + ); + try { + userService.sendVerifyMail(user, VerifyType.REGISTER); + } catch (EmailSendException e) { + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body( + Map.of( + "error", + "Failed to send verification email: " + e.getMessage(), + "reason_code", + "EMAIL_SEND_FAILED" + ) + ); + } return ResponseEntity.badRequest().body( Map.of( "error", @@ -663,7 +703,20 @@ public class AuthController { if (userOpt.isEmpty()) { return ResponseEntity.badRequest().body(Map.of("error", "User not found")); } - userService.sendVerifyMail(userOpt.get(), VerifyType.RESET_PASSWORD); + try { + userService.sendVerifyMail(userOpt.get(), VerifyType.RESET_PASSWORD); + } catch (EmailSendException e) { + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body( + Map.of( + "error", + "邮件发送失败: " + e.getMessage(), + "reason_code", + "EMAIL_SEND_FAILED" + ) + ); + } return ResponseEntity.ok(Map.of("message", "Verification code sent")); } diff --git a/backend/src/main/java/com/openisle/exception/EmailSendException.java b/backend/src/main/java/com/openisle/exception/EmailSendException.java new file mode 100644 index 000000000..b4a0fa59d --- /dev/null +++ b/backend/src/main/java/com/openisle/exception/EmailSendException.java @@ -0,0 +1,15 @@ +package com.openisle.exception; + +/** + * Thrown when email sending fails so callers can surface a clear error upstream. + */ +public class EmailSendException extends RuntimeException { + + public EmailSendException(String message) { + super(message); + } + + public EmailSendException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/openisle/service/NotificationService.java b/backend/src/main/java/com/openisle/service/NotificationService.java index c46045fa9..ac114f12d 100644 --- a/backend/src/main/java/com/openisle/service/NotificationService.java +++ b/backend/src/main/java/com/openisle/service/NotificationService.java @@ -7,6 +7,7 @@ import com.openisle.repository.NotificationRepository; import com.openisle.repository.ReactionRepository; import com.openisle.repository.UserRepository; import com.openisle.service.EmailSender; +import com.openisle.exception.EmailSendException; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashSet; @@ -17,6 +18,7 @@ import java.util.concurrent.Executor; import java.util.regex.Matcher; import java.util.regex.Pattern; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,6 +28,7 @@ import org.springframework.transaction.support.TransactionSynchronizationManager /** Service for creating and retrieving notifications. */ @Service @RequiredArgsConstructor +@Slf4j public class NotificationService { private final NotificationRepository notificationRepository; @@ -108,7 +111,11 @@ public class NotificationService { post.getId(), comment.getId() ); - emailSender.sendEmail(user.getEmail(), "有人回复了你", url); + try { + emailSender.sendEmail(user.getEmail(), "有人回复了你", url); + } catch (EmailSendException e) { + log.warn("Failed to send notification email to {}: {}", user.getEmail(), e.getMessage()); + } sendCustomPush(user, "有人回复了你", url); } else if (type == NotificationType.REACTION && comment != null) { // long count = reactionRepository.countReceived(comment.getAuthor().getUsername()); diff --git a/backend/src/main/java/com/openisle/service/PostService.java b/backend/src/main/java/com/openisle/service/PostService.java index 1d0d0f2df..a55dcc4da 100644 --- a/backend/src/main/java/com/openisle/service/PostService.java +++ b/backend/src/main/java/com/openisle/service/PostService.java @@ -19,6 +19,7 @@ import com.openisle.repository.TagRepository; import com.openisle.repository.UserRepository; import com.openisle.search.SearchIndexEventPublisher; import com.openisle.service.EmailSender; +import com.openisle.exception.EmailSendException; import java.time.Duration; import java.time.LocalDateTime; import java.time.ZoneId; @@ -663,11 +664,15 @@ public class PostService { w.getEmail() != null && !w.getDisabledEmailNotificationTypes().contains(NotificationType.LOTTERY_WIN) ) { - emailSender.sendEmail( - w.getEmail(), - "你中奖了", - "恭喜你在抽奖贴 \"" + lp.getTitle() + "\" 中获奖" - ); + try { + emailSender.sendEmail( + w.getEmail(), + "你中奖了", + "恭喜你在抽奖贴 \"" + lp.getTitle() + "\" 中获奖" + ); + } catch (EmailSendException e) { + log.warn("Failed to send lottery win email to {}: {}", w.getEmail(), e.getMessage()); + } } notificationService.createNotification( w, @@ -693,11 +698,19 @@ public class PostService { .getDisabledEmailNotificationTypes() .contains(NotificationType.LOTTERY_DRAW) ) { - emailSender.sendEmail( - lp.getAuthor().getEmail(), - "抽奖已开奖", - "您的抽奖贴 \"" + lp.getTitle() + "\" 已开奖" - ); + try { + emailSender.sendEmail( + lp.getAuthor().getEmail(), + "抽奖已开奖", + "您的抽奖贴 \"" + lp.getTitle() + "\" 已开奖" + ); + } catch (EmailSendException e) { + log.warn( + "Failed to send lottery draw email to {}: {}", + lp.getAuthor().getEmail(), + e.getMessage() + ); + } } notificationService.createNotification( lp.getAuthor(), diff --git a/backend/src/main/java/com/openisle/service/ResendEmailSender.java b/backend/src/main/java/com/openisle/service/ResendEmailSender.java index c205584c0..f735ec76b 100644 --- a/backend/src/main/java/com/openisle/service/ResendEmailSender.java +++ b/backend/src/main/java/com/openisle/service/ResendEmailSender.java @@ -1,5 +1,6 @@ package com.openisle.service; +import com.openisle.exception.EmailSendException; import java.util.HashMap; import java.util.Map; import org.springframework.beans.factory.annotation.Value; @@ -7,8 +8,9 @@ import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; -import org.springframework.scheduling.annotation.Async; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; @Service @@ -23,7 +25,6 @@ public class ResendEmailSender extends EmailSender { private final RestTemplate restTemplate = new RestTemplate(); @Override - @Async("notificationExecutor") public void sendEmail(String to, String subject, String text) { String url = "https://api.resend.com/emails"; // hypothetical endpoint @@ -38,6 +39,20 @@ public class ResendEmailSender extends EmailSender { body.put("from", "openisle <" + fromEmail + ">"); HttpEntity> entity = new HttpEntity<>(body, headers); - restTemplate.exchange(url, HttpMethod.POST, entity, String.class); + try { + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.POST, + entity, + String.class + ); + if (!response.getStatusCode().is2xxSuccessful()) { + throw new EmailSendException( + "Email service returned status " + response.getStatusCodeValue() + ); + } + } catch (RestClientException e) { + throw new EmailSendException("Failed to send email: " + e.getMessage(), e); + } } } diff --git a/backend/src/main/java/com/openisle/service/UserService.java b/backend/src/main/java/com/openisle/service/UserService.java index b41b8fd46..5eaaa1823 100644 --- a/backend/src/main/java/com/openisle/service/UserService.java +++ b/backend/src/main/java/com/openisle/service/UserService.java @@ -118,7 +118,6 @@ public class UserService { * @param user */ public void sendVerifyMail(User user, VerifyType verifyType) { - // 缓存验证码 String code = genCode(); String key; String subject; @@ -133,8 +132,9 @@ public class UserService { subject = "请填写验证码以重置密码(有效期为5分钟)"; } - redisTemplate.opsForValue().set(key, code, 5, TimeUnit.MINUTES); // 五分钟后验证码过期 emailService.sendEmail(user.getEmail(), subject, content); + // 邮件发送成功后再缓存验证码,避免发送失败时用户收不到但验证被要求 + redisTemplate.opsForValue().set(key, code, 5, TimeUnit.MINUTES); // 五分钟后验证码过期 } /** diff --git a/frontend_nuxt/pages/login.vue b/frontend_nuxt/pages/login.vue index 013e22642..2ff133a2f 100644 --- a/frontend_nuxt/pages/login.vue +++ b/frontend_nuxt/pages/login.vue @@ -58,12 +58,15 @@ const submitLogin = async () => { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username.value, password: password.value }), }) - const data = await res.json() + const data = await res.json().catch(() => ({})) if (res.ok && data.token) { setToken(data.token) toast.success('登录成功') registerPush() await navigateTo('/', { replace: true }) + } else if (data.reason_code === 'EMAIL_SEND_FAILED') { + const msg = data.error || data.message || res.statusText || '登录失败' + toast.error(`${res.status} ${msg} (${data.reason_code})`) } else if (data.reason_code === 'NOT_VERIFIED') { toast.info('当前邮箱未验证,已经为您重新发送验证码') await navigateTo( @@ -76,10 +79,12 @@ const submitLogin = async () => { } else if (data.reason_code === 'NOT_APPROVED') { await navigateTo({ path: '/signup-reason', query: { token: data.token } }, { replace: true }) } else { - toast.error(data.error || '登录失败') + const msg = data.error || data.message || res.statusText || '登录失败' + const reason = data.reason_code ? ` (${data.reason_code})` : '' + toast.error(`${res.status} ${msg}${reason}`) } } catch (e) { - toast.error('登录失败') + toast.error(`登录失败: ${e.message}`) } finally { isWaitingForLogin.value = false } diff --git a/frontend_nuxt/pages/signup.vue b/frontend_nuxt/pages/signup.vue index 73459f1b9..7dcbd2d71 100644 --- a/frontend_nuxt/pages/signup.vue +++ b/frontend_nuxt/pages/signup.vue @@ -139,8 +139,7 @@ const sendVerification = async () => { inviteToken: inviteToken.value, }), }) - isWaitingForEmailSent.value = false - const data = await res.json() + const data = await res.json().catch(() => ({})) if (res.ok) { emailStep.value = 1 toast.success('验证码已发送,请查看邮箱') @@ -149,10 +148,14 @@ const sendVerification = async () => { if (data.field === 'email') emailError.value = data.error if (data.field === 'password') passwordError.value = data.error } else { - toast.error(data.error || '发送失败') + const msg = data.error || data.message || res.statusText || '发送失败' + const reason = data.reason_code ? ` (${data.reason_code})` : '' + toast.error(`${res.status} ${msg}${reason}`) } } catch (e) { - toast.error('发送失败') + toast.error(`发送失败: ${e.message}`) + } finally { + isWaitingForEmailSent.value = false } }