mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-20 22:11:01 +08:00
Compare commits
9 Commits
codex/set-
...
feature/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6730b2882 | ||
|
|
21b1c3317a | ||
|
|
72a915af2e | ||
|
|
f000011994 | ||
|
|
d48c9dc27a | ||
|
|
94f955e50f | ||
|
|
bf94707914 | ||
|
|
209f0ef1f8 | ||
|
|
e2d900759a |
@@ -57,6 +57,9 @@ cd OpenIsle
|
||||
--profile dev up -d --force-recreate
|
||||
```
|
||||
|
||||
数据初始化sql会创建几个帐户供大家测试使用
|
||||
> username:admin/user1/user2 password:123456
|
||||
|
||||
3. 查看服务状态:
|
||||
```shell
|
||||
docker compose -f docker/docker-compose.yaml --env-file .env ps
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<Map<String, String>> entity = new HttpEntity<>(body, headers);
|
||||
restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
|
||||
try {
|
||||
ResponseEntity<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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); // 五分钟后验证码过期
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,10 +45,10 @@ class DailyNewsBot extends BotFather {
|
||||
请立即在 https://www.open-isle.com 使用 create_post 发布一篇名为「OpenIsle 每日新闻速递|${dateLabel}」的帖子,并遵循以下要求:
|
||||
1. 发布类型为 NORMAL,categoryId = ${categoryId},tagIds = ${tagIdsText}。
|
||||
2. 正文以简洁问候开头, 不用再重复标题
|
||||
3. 使用 web_search 工具按以下顺序收集资讯,并在正文中以 Markdown 小节呈现:
|
||||
- 「全球区块链与加密」:汇总 CoinDesk 所有重点新闻, 列出至少5条
|
||||
- 「国际新闻速览」:汇总 Reuters 重点头条,关注宏观经济、市场波动或政策变化。列出至少5条
|
||||
- 「AI 行业快讯」:检索全球 AI 领域的重要发布或事件(例如 OpenAI、Google、Meta、国内大模型厂商等)。列出至少5条
|
||||
3. 使用 web_search 工具按以下顺序收集资讯,并在正文中以 Markdown 小节呈现, 需要调用3次web_search:
|
||||
- 「全球区块链与加密」:汇总 coindesk.com 版面所有重点新闻, 列出至少5条
|
||||
- 「国际新闻速览」:汇总 reuters.com 版面重点头条,关注宏观经济、市场波动或政策变化。列出至少5条
|
||||
- 「AI 行业快讯」:检索今天全球 AI 领域的重要发布或事件(例如 OpenAI、Google、Meta、国内大模型厂商等)。列出至少5条
|
||||
4. 每条新闻采用项目符号,先写结论再给出关键数字或细节,末尾添加来源超链接,格式示例:「**结论** —— 关键细节。(来源:[Reuters](URL))」
|
||||
5. 资讯整理完毕后,调用 weather_mcp_server.get_current_weather,列出北京、上海、广州、深圳今日天气,放置在「城市天气」小节下, 本小节可加emoji。
|
||||
6. 最后一节为「今日提醒」,给出 2-3 条与新闻或天气相关的行动建议。
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -533,7 +533,7 @@ const {
|
||||
} catch (err) {}
|
||||
},
|
||||
{
|
||||
server: false,
|
||||
server: true,
|
||||
lazy: false,
|
||||
},
|
||||
)
|
||||
@@ -1395,10 +1395,6 @@ onMounted(async () => {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.reaction-action.copy-link:hover {
|
||||
background-color: #e2e2e2;
|
||||
}
|
||||
|
||||
.comment-editor-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user