From 1ecb285fd090e6bddc286188344be70736e98e57 Mon Sep 17 00:00:00 2001 From: OpenCode Date: Thu, 23 Jul 2026 13:03:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=82=AE=E7=AE=B1=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E7=AE=A1=E7=90=86+=E9=82=AE=E7=AE=B1=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E7=A0=81=E4=BF=AE=E6=94=B9=E5=AF=86=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 BaseSysEmailConfigEntity 邮箱配置实体及CRUD - 新增 EmailSendService 邮件发送服务(动态SMTP) - 新增 sendEmailCode/updatePasswordByEmail 接口 - App修改密码改用邮箱验证码(原短信验证码保留) - 邮箱配置支持多条,仅一条可设为当前使用 - 新增测试邮件发送接口 - menu.json 新增邮箱配置菜单 - pom.xml 新增 spring-boot-starter-mail 依赖 --- pom.xml | 4 + .../AdminBaseSysEmailConfigController.java | 76 ++++++++++++++++ .../entity/sys/BaseSysEmailConfigEntity.java | 40 +++++++++ .../mapper/sys/BaseSysEmailConfigMapper.java | 7 ++ .../sys/BaseSysEmailConfigService.java | 9 ++ .../base/service/sys/EmailSendService.java | 90 +++++++++++++++++++ .../impl/BaseSysEmailConfigServiceImpl.java | 32 +++++++ .../controller/app/AppUserInfoController.java | 20 ++++- .../modules/user/service/UserInfoService.java | 33 ++----- .../service/impl/UserInfoServiceImpl.java | 45 ++++++++++ src/main/resources/cool/data/menu/menu.json | 74 +++++++++++++++ 11 files changed, 403 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysEmailConfigController.java create mode 100644 src/main/java/com/cool/modules/base/entity/sys/BaseSysEmailConfigEntity.java create mode 100644 src/main/java/com/cool/modules/base/mapper/sys/BaseSysEmailConfigMapper.java create mode 100644 src/main/java/com/cool/modules/base/service/sys/BaseSysEmailConfigService.java create mode 100644 src/main/java/com/cool/modules/base/service/sys/EmailSendService.java create mode 100644 src/main/java/com/cool/modules/base/service/sys/impl/BaseSysEmailConfigServiceImpl.java diff --git a/pom.xml b/pom.xml index f43c908..c707575 100644 --- a/pom.xml +++ b/pom.xml @@ -71,6 +71,10 @@ org.springframework.boot spring-boot-starter-aop + + org.springframework.boot + spring-boot-starter-mail + com.github.ben-manes.caffeine diff --git a/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysEmailConfigController.java b/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysEmailConfigController.java new file mode 100644 index 0000000..88a22c5 --- /dev/null +++ b/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysEmailConfigController.java @@ -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 { + + 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 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("测试邮件发送成功"); + } +} diff --git a/src/main/java/com/cool/modules/base/entity/sys/BaseSysEmailConfigEntity.java b/src/main/java/com/cool/modules/base/entity/sys/BaseSysEmailConfigEntity.java new file mode 100644 index 0000000..9743db7 --- /dev/null +++ b/src/main/java/com/cool/modules/base/entity/sys/BaseSysEmailConfigEntity.java @@ -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 { + + @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; +} diff --git a/src/main/java/com/cool/modules/base/mapper/sys/BaseSysEmailConfigMapper.java b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysEmailConfigMapper.java new file mode 100644 index 0000000..d234a67 --- /dev/null +++ b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysEmailConfigMapper.java @@ -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 { +} diff --git a/src/main/java/com/cool/modules/base/service/sys/BaseSysEmailConfigService.java b/src/main/java/com/cool/modules/base/service/sys/BaseSysEmailConfigService.java new file mode 100644 index 0000000..2aecc1b --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/BaseSysEmailConfigService.java @@ -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 getActiveConfig(); +} diff --git a/src/main/java/com/cool/modules/base/service/sys/EmailSendService.java b/src/main/java/com/cool/modules/base/service/sys/EmailSendService.java new file mode 100644 index 0000000..b51b5e3 --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/EmailSendService.java @@ -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("
" + + "

验证码

" + + "

您的验证码为:" + code + "

" + + "

验证码10分钟内有效,请勿泄露给他人。

" + + "
", 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("
" + + "

测试邮件

" + + "

这是一封测试邮件,如果您收到了说明邮箱配置正确。

" + + "

发送时间:" + java.time.LocalDateTime.now() + "

" + + "
", 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; + } +} diff --git a/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysEmailConfigServiceImpl.java b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysEmailConfigServiceImpl.java new file mode 100644 index 0000000..1155399 --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysEmailConfigServiceImpl.java @@ -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 + 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, + "新增时无法设置活跃状态"); + } + } + } +} diff --git a/src/main/java/com/cool/modules/user/controller/app/AppUserInfoController.java b/src/main/java/com/cool/modules/user/controller/app/AppUserInfoController.java index b3cc7f0..2b1729f 100644 --- a/src/main/java/com/cool/modules/user/controller/app/AppUserInfoController.java +++ b/src/main/java/com/cool/modules/user/controller/app/AppUserInfoController.java @@ -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(); + } } diff --git a/src/main/java/com/cool/modules/user/service/UserInfoService.java b/src/main/java/com/cool/modules/user/service/UserInfoService.java index 0efb133..3482419 100644 --- a/src/main/java/com/cool/modules/user/service/UserInfoService.java +++ b/src/main/java/com/cool/modules/user/service/UserInfoService.java @@ -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 { - /** - * 用户个人信息 - * @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 { void checkEmailUnique(String email, Long excludeId); void checkUsernameUnique(String username, Long excludeId); + + void updateEmail(Long currentUserId, String email); } diff --git a/src/main/java/com/cool/modules/user/service/impl/UserInfoServiceImpl.java b/src/main/java/com/cool/modules/user/service/impl/UserInfoServiceImpl.java index c14901e..09e62d6 100644 --- a/src/main/java/com/cool/modules/user/service/impl/UserInfoServiceImpl.java +++ b/src/main/java/com/cool/modules/user/service/impl/UserInfoServiceImpl.java @@ -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