feat: 邮箱配置管理+邮箱验证码修改密码

- 新增 BaseSysEmailConfigEntity 邮箱配置实体及CRUD
- 新增 EmailSendService 邮件发送服务(动态SMTP)
- 新增 sendEmailCode/updatePasswordByEmail 接口
- App修改密码改用邮箱验证码(原短信验证码保留)
- 邮箱配置支持多条,仅一条可设为当前使用
- 新增测试邮件发送接口
- menu.json 新增邮箱配置菜单
- pom.xml 新增 spring-boot-starter-mail 依赖
This commit is contained in:
OpenCode
2026-07-23 13:03:14 +08:00
parent ef9c06edf6
commit 1ecb285fd0
11 changed files with 403 additions and 27 deletions
+4
View File
@@ -71,6 +71,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- springframework end -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
@@ -0,0 +1,76 @@
package com.cool.modules.base.controller.admin.sys;
import static com.cool.modules.base.entity.sys.table.BaseSysEmailConfigEntityTableDef.BASE_SYS_EMAIL_CONFIG_ENTITY;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.json.JSONObject;
import com.cool.core.annotation.CoolRestController;
import com.cool.core.base.BaseController;
import com.cool.core.exception.CoolPreconditions;
import com.cool.core.request.R;
import com.cool.modules.base.entity.sys.BaseSysEmailConfigEntity;
import com.cool.modules.base.service.sys.BaseSysEmailConfigService;
import com.cool.modules.base.service.sys.EmailSendService;
import com.mybatisflex.core.query.QueryWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
@Tag(name = "邮箱配置管理", description = "邮箱配置管理")
@CoolRestController(api = {"add", "delete", "update", "page", "info"}, cname = "emailConfig")
@RequiredArgsConstructor
public class AdminBaseSysEmailConfigController extends BaseController<BaseSysEmailConfigService, BaseSysEmailConfigEntity> {
private final EmailSendService emailSendService;
@Override
protected void init(HttpServletRequest request, JSONObject requestParams) {
setPageOption(createOp().fieldEq(BASE_SYS_EMAIL_CONFIG_ENTITY.ENABLED)
.keyWordLikeFields(BASE_SYS_EMAIL_CONFIG_ENTITY.EMAIL, BASE_SYS_EMAIL_CONFIG_ENTITY.REMARK));
}
@Override
protected void checkPermission(String action) {
}
@Operation(summary = "设为当前使用")
@PostMapping("/setActive")
public R setActive(@RequestAttribute JSONObject requestParams) {
Long id = requestParams.getLong("id");
CoolPreconditions.checkEmpty(id, "id不能为空");
BaseSysEmailConfigEntity entity = service.getById(id);
CoolPreconditions.checkEmpty(entity, "邮箱配置不存在");
CoolPreconditions.check(ObjUtil.equals(entity.getEnabled(), 0), "该邮箱配置已禁用,无法设为使用");
List<BaseSysEmailConfigEntity> activeList = service.list(
QueryWrapper.create().eq(BaseSysEmailConfigEntity::getIsActive, 1));
for (BaseSysEmailConfigEntity active : activeList) {
active.setIsActive(0);
active.updateById();
}
entity.setIsActive(1);
entity.updateById();
return R.ok();
}
@Operation(summary = "发送测试邮件")
@PostMapping("/test")
public R test(@RequestAttribute JSONObject requestParams) {
Long id = requestParams.getLong("id");
CoolPreconditions.checkEmpty(id, "id不能为空");
BaseSysEmailConfigEntity entity = service.getById(id);
CoolPreconditions.checkEmpty(entity, "邮箱配置不存在");
emailSendService.sendTestMail(entity);
return R.okMsg("测试邮件发送成功");
}
}
@@ -0,0 +1,40 @@
package com.cool.modules.base.entity.sys;
import com.cool.core.base.BaseEntity;
import com.mybatisflex.annotation.Table;
import com.cool.core.annotation.ColumnDefine;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Table(value = "base_sys_email_config", comment = "邮箱配置")
public class BaseSysEmailConfigEntity extends BaseEntity<BaseSysEmailConfigEntity> {
@ColumnDefine(comment = "邮箱地址", notNull = true)
private String email;
@ColumnDefine(comment = "SMTP主机", notNull = true)
private String host;
@ColumnDefine(comment = "SMTP端口", notNull = true)
private Integer port;
@ColumnDefine(comment = "用户名", notNull = true)
private String username;
@ColumnDefine(comment = "密码/授权码", notNull = true)
private String password;
@ColumnDefine(comment = "是否启用SSL", defaultValue = "1")
private Integer sslEnable;
@ColumnDefine(comment = "是否启用 0-禁用 1-启用", defaultValue = "1")
private Integer enabled;
@ColumnDefine(comment = "是否为当前使用 0-否 1-是", defaultValue = "0")
private Integer isActive;
@ColumnDefine(comment = "备注")
private String remark;
}
@@ -0,0 +1,7 @@
package com.cool.modules.base.mapper.sys;
import com.cool.modules.base.entity.sys.BaseSysEmailConfigEntity;
import com.mybatisflex.core.BaseMapper;
public interface BaseSysEmailConfigMapper extends BaseMapper<BaseSysEmailConfigEntity> {
}
@@ -0,0 +1,9 @@
package com.cool.modules.base.service.sys;
import com.cool.core.base.BaseService;
import com.cool.modules.base.entity.sys.BaseSysEmailConfigEntity;
public interface BaseSysEmailConfigService extends BaseService<BaseSysEmailConfigEntity> {
BaseSysEmailConfigEntity getActiveConfig();
}
@@ -0,0 +1,90 @@
package com.cool.modules.base.service.sys;
import com.cool.modules.base.entity.sys.BaseSysEmailConfigEntity;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class EmailSendService {
private final BaseSysEmailConfigService emailConfigService;
public void sendCode(String toEmail, String code) {
BaseSysEmailConfigEntity config = emailConfigService.getActiveConfig();
if (config == null) {
throw new RuntimeException("未配置可用的邮箱服务,请在系统管理中配置邮箱");
}
JavaMailSender mailSender = buildMailSender(config);
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(config.getEmail());
helper.setTo(toEmail);
helper.setSubject("验证码");
helper.setText("<div style=\"padding:20px;font-family:sans-serif;\">"
+ "<h2>验证码</h2>"
+ "<p>您的验证码为:<strong style=\"font-size:24px;color:#14b8a6;\">" + code + "</strong></p>"
+ "<p style=\"color:#999;font-size:12px;\">验证码10分钟内有效,请勿泄露给他人。</p>"
+ "</div>", true);
mailSender.send(message);
log.info("验证码邮件发送成功: to={}", toEmail);
} catch (Exception e) {
log.error("验证码邮件发送失败: to={}, error={}", toEmail, e.getMessage());
throw new RuntimeException("邮件发送失败,请检查邮箱配置");
}
}
public void sendTestMail(BaseSysEmailConfigEntity config) {
JavaMailSender mailSender = buildMailSender(config);
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(config.getEmail());
helper.setTo(config.getEmail());
helper.setSubject("测试邮件");
helper.setText("<div style=\"padding:20px;font-family:sans-serif;\">"
+ "<h2>测试邮件</h2>"
+ "<p>这是一封测试邮件,如果您收到了说明邮箱配置正确。</p>"
+ "<p style=\"color:#999;font-size:12px;\">发送时间:" + java.time.LocalDateTime.now() + "</p>"
+ "</div>", true);
mailSender.send(message);
log.info("测试邮件发送成功: to={}", config.getEmail());
} catch (Exception e) {
log.error("测试邮件发送失败: to={}, error={}", config.getEmail(), e.getMessage());
throw new RuntimeException("邮件发送失败,请检查邮箱配置:" + e.getMessage());
}
}
private JavaMailSender buildMailSender(BaseSysEmailConfigEntity config) {
JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost(config.getHost());
sender.setPort(config.getPort());
sender.setUsername(config.getUsername());
sender.setPassword(config.getPassword());
sender.setDefaultEncoding("UTF-8");
java.util.Properties props = sender.getJavaMailProperties();
props.put("mail.transport.protocol", "smtp");
props.put("mail.smtp.auth", "true");
if (config.getSslEnable() != null && config.getSslEnable() == 1) {
props.put("mail.smtp.ssl.enable", "true");
props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
} else {
props.put("mail.smtp.starttls.enable", "true");
}
props.put("mail.smtp.timeout", "5000");
props.put("mail.smtp.connectiontimeout", "5000");
return sender;
}
}
@@ -0,0 +1,32 @@
package com.cool.modules.base.service.sys.impl;
import cn.hutool.core.util.ObjUtil;
import com.cool.core.base.BaseServiceImpl;
import com.cool.core.exception.CoolPreconditions;
import com.cool.modules.base.entity.sys.BaseSysEmailConfigEntity;
import com.cool.modules.base.mapper.sys.BaseSysEmailConfigMapper;
import com.cool.modules.base.service.sys.BaseSysEmailConfigService;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
@Service
public class BaseSysEmailConfigServiceImpl extends BaseServiceImpl<BaseSysEmailConfigMapper, BaseSysEmailConfigEntity>
implements BaseSysEmailConfigService {
@Override
public BaseSysEmailConfigEntity getActiveConfig() {
return getOne(QueryWrapper.create()
.eq(BaseSysEmailConfigEntity::getIsActive, 1)
.eq(BaseSysEmailConfigEntity::getEnabled, 1));
}
@Override
public void modifyAfter(cn.hutool.json.JSONObject requestParams, BaseSysEmailConfigEntity entity, com.cool.core.base.ModifyEnum type) {
if (type == com.cool.core.base.ModifyEnum.ADD || type == com.cool.core.base.ModifyEnum.UPDATE) {
if (ObjUtil.isNotEmpty(entity.getIsActive()) && entity.getIsActive() == 1) {
CoolPreconditions.check(entity.getId() == null && type == com.cool.core.base.ModifyEnum.UPDATE,
"新增时无法设置活跃状态");
}
}
}
}
@@ -5,6 +5,7 @@ import com.cool.core.annotation.CoolRestController;
import com.cool.core.request.R;
import com.cool.core.util.CoolSecurityUtil;
import com.cool.core.util.EntityUtils;
import com.cool.modules.base.service.sys.EmailSendService;
import com.cool.modules.user.entity.UserInfoEntity;
import com.cool.modules.user.service.UserInfoService;
import io.swagger.v3.oas.annotations.Operation;
@@ -20,6 +21,7 @@ import org.springframework.web.bind.annotation.RequestAttribute;
public class AppUserInfoController {
private final UserInfoService userInfoService;
private final EmailSendService emailSendService;
@Operation(summary = "用户个人信息", description = "获得App、小程序或者其他应用的用户个人信息")
@GetMapping("/person")
@@ -42,6 +44,14 @@ public class AppUserInfoController {
);
}
@Operation(summary = "发送邮箱验证码")
@PostMapping("/sendEmailCode")
public R sendEmailCode() {
Long userId = CoolSecurityUtil.getCurrentUserId();
userInfoService.sendEmailCode(userId, emailSendService);
return R.ok();
}
@Operation(summary = "更新用户密码")
@PostMapping("/updatePassword")
public R updatePassword(
@@ -49,7 +59,7 @@ public class AppUserInfoController {
) {
String password = requestParams.get("password", String.class);
String code = requestParams.get("code", String.class);
userInfoService.updatePassword(CoolSecurityUtil.getCurrentUserId(), password, code);
userInfoService.updatePasswordByEmail(CoolSecurityUtil.getCurrentUserId(), password, code);
return R.ok();
}
@@ -85,4 +95,12 @@ public class AppUserInfoController {
userInfoService.bindUsername(CoolSecurityUtil.getCurrentUserId(), username);
return R.ok();
}
@Operation(summary = "修改邮箱")
@PostMapping("/updateEmail")
public R updateEmail(@RequestAttribute JSONObject requestParams) {
String email = requestParams.getStr("email");
userInfoService.updateEmail(CoolSecurityUtil.getCurrentUserId(), email);
return R.ok();
}
}
@@ -1,49 +1,28 @@
package com.cool.modules.user.service;
import com.cool.core.base.BaseService;
import com.cool.modules.base.service.sys.EmailSendService;
import com.cool.modules.user.entity.UserInfoEntity;
public interface UserInfoService extends BaseService<UserInfoEntity> {
/**
* 用户个人信息
* @param userId
* @return
*/
UserInfoEntity person(Long userId);
/**
* 更新用户密码
*/
void updatePassword(Long userId, String password, String code);
/**
* 注销
*/
void sendEmailCode(Long userId, EmailSendService emailSendService);
void updatePasswordByEmail(Long userId, String password, String code);
void logoff(Long currentUserId);
/**
* 绑定手机号
*/
void bindPhone(Long currentUserId, String phone, String code);
/**
* 绑定邮箱
*/
void bindEmail(Long currentUserId, String email);
/**
* 绑定用户名
*/
void bindUsername(Long currentUserId, String username);
/**
* 注销(管理端)
*/
void adminLogoff(Long userId);
/**
* 删除前校验:必须已注销
*/
void checkLogoffBeforeDelete(Long userId);
String resetPassword(Long userId);
@@ -51,4 +30,6 @@ public interface UserInfoService extends BaseService<UserInfoEntity> {
void checkEmailUnique(String email, Long excludeId);
void checkUsernameUnique(String username, Long excludeId);
void updateEmail(Long currentUserId, String email);
}
@@ -2,9 +2,12 @@ package com.cool.modules.user.service.impl;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.MD5;
import com.cool.core.base.BaseServiceImpl;
import com.cool.core.cache.CoolCache;
import com.cool.core.exception.CoolPreconditions;
import com.cool.modules.base.service.sys.EmailSendService;
import com.cool.modules.user.entity.UserInfoEntity;
import com.cool.modules.user.mapper.UserInfoMapper;
import com.cool.modules.user.service.UserInfoService;
@@ -20,6 +23,9 @@ public class UserInfoServiceImpl extends BaseServiceImpl<UserInfoMapper, UserInf
UserInfoService {
private final UserSmsUtil userSmsUtil;
private final CoolCache coolCache;
private static final String EMAIL_CODE_CACHE_PREFIX = "email_code:";
@Override
public UserInfoEntity person(Long userId) {
@@ -36,6 +42,36 @@ public class UserInfoServiceImpl extends BaseServiceImpl<UserInfoMapper, UserInf
info.updateById();
}
@Override
public void sendEmailCode(Long userId, EmailSendService emailSendService) {
UserInfoEntity info = getById(userId);
CoolPreconditions.checkEmpty(info, "用户不存在");
CoolPreconditions.check(StrUtil.isEmpty(info.getEmail()), "请先设置邮箱");
String code = RandomUtil.randomNumbers(6);
String cacheKey = EMAIL_CODE_CACHE_PREFIX + userId;
coolCache.set(cacheKey, code, 600);
emailSendService.sendCode(info.getEmail(), code);
}
@Override
public void updatePasswordByEmail(Long userId, String password, String code) {
UserInfoEntity info = getById(userId);
CoolPreconditions.checkEmpty(info, "用户不存在");
String cacheKey = EMAIL_CODE_CACHE_PREFIX + userId;
Object cachedCode = coolCache.get(cacheKey);
CoolPreconditions.check(ObjUtil.isEmpty(cachedCode), "验证码已过期,请重新获取");
CoolPreconditions.check(!ObjUtil.equals(String.valueOf(cachedCode), code), "验证码错误");
coolCache.del(cacheKey);
info.setPassword(MD5.create().digestHex(password));
info.updateById();
}
@Override
public void logoff(Long userId) {
UserInfoEntity info = new UserInfoEntity();
@@ -128,4 +164,13 @@ public class UserInfoServiceImpl extends BaseServiceImpl<UserInfoMapper, UserInf
.ne(UserInfoEntity::getId, excludeId));
CoolPreconditions.check(ObjUtil.isNotEmpty(existing), "该用户名已被其他用户使用");
}
@Override
public void updateEmail(Long userId, String email) {
UserInfoEntity info = getById(userId);
CoolPreconditions.checkEmpty(info, "用户不存在");
checkEmailUnique(email, userId);
info.setEmail(email);
info.updateById();
}
}
@@ -364,6 +364,80 @@
}
]
},
{
"name": "邮箱配置",
"router": null,
"perms": null,
"type": 0,
"icon": "icon-message",
"orderNum": 4,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "邮箱列表",
"router": "/sys/email-config",
"perms": null,
"type": 1,
"icon": "icon-message",
"orderNum": 0,
"viewPath": "cool/modules/base/views/email-config.vue",
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "新增",
"router": null,
"perms": "base:sys:emailConfig:add",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "修改",
"router": null,
"perms": "base:sys:emailConfig:info,base:sys:emailConfig:update",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "删除",
"router": null,
"perms": "base:sys:emailConfig:delete",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "查看",
"router": null,
"perms": "base:sys:emailConfig:page,base:sys:emailConfig:list,base:sys:emailConfig:info",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
}
]
}
]
},
{
"name": "监控管理",
"router": null,