diff --git a/src/main/java/com/youlai/system/config/MailConfig.java b/src/main/java/com/youlai/system/config/MailConfig.java new file mode 100644 index 00000000..a7d06cc4 --- /dev/null +++ b/src/main/java/com/youlai/system/config/MailConfig.java @@ -0,0 +1,51 @@ +package com.youlai.system.config; + +import com.youlai.system.config.property.MailProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +/** + * MailConfig 配置类,用于手动配置和注入 JavaMailSender。 + * 通过读取 MailProperties 类中配置的邮件相关属性来初始化 JavaMailSender。 + *

+ * 手动注入的原因是为了避免在使用 application-dev.yml 或其他非 application.yml 配置文件时, + * IDEA 提示无法找到 JavaMailSender 的 bean。 + * + * @author Ray + * @since 2024/8/17 + */ +@Configuration +@EnableConfigurationProperties(MailProperties.class) +public class MailConfig { + + private final MailProperties mailProperties; + + public MailConfig(MailProperties mailProperties) { + this.mailProperties = mailProperties; + } + + /** + * 创建并配置 JavaMailSender bean。 + * + * @return 配置好的 JavaMailSender 实例 + */ + @Bean + public JavaMailSender javaMailSender() { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(mailProperties.getHost()); + mailSender.setPort(mailProperties.getPort()); + mailSender.setUsername(mailProperties.getUsername()); + mailSender.setPassword(mailProperties.getPassword()); + + Properties properties = mailSender.getJavaMailProperties(); + properties.put("mail.smtp.auth", mailProperties.getProperties().getSmtp().isAuth()); + properties.put("mail.smtp.starttls.enable", mailProperties.getProperties().getSmtp().getStarttls().isEnable()); + + return mailSender; + } +} diff --git a/src/main/java/com/youlai/system/config/property/AliyunSmsProperties.java b/src/main/java/com/youlai/system/config/property/AliyunSmsProperties.java new file mode 100644 index 00000000..80df9de2 --- /dev/null +++ b/src/main/java/com/youlai/system/config/property/AliyunSmsProperties.java @@ -0,0 +1,50 @@ +package com.youlai.system.config.property; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.Map; + +/** + * 阿里云短信配置 + * + * @author Ray + * @since 2024/8/17 + */ +@Configuration +@ConfigurationProperties(prefix = "sms.aliyun") +@Data +public class AliyunSmsProperties { + + /** + * 阿里云账户的Access Key ID,用于API请求认证 + */ + private String accessKeyId; + + /** + *阿里云账户的Access Key Secret,用于API请求认证 + */ + private String accessKeySecret; + + /** + * 阿里云短信服务API的域名 eg: dysmsapi.aliyuncs.com + */ + private String domain; + + /** + * 阿里云服务的区域ID,如cn-shanghai + */ + private String regionId; + + /** + * 短信签名,必须是已经在阿里云短信服务中注册并通过审核的 + */ + private String signName; + + /** + * 模板编码 + */ + private Map templateCodes; + +} diff --git a/src/main/java/com/youlai/system/config/property/MailProperties.java b/src/main/java/com/youlai/system/config/property/MailProperties.java new file mode 100644 index 00000000..77e3c230 --- /dev/null +++ b/src/main/java/com/youlai/system/config/property/MailProperties.java @@ -0,0 +1,89 @@ +package com.youlai.system.config.property; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 邮件配置类,用于接收和存储邮件相关的配置属性。 + * + * @author Ray + * @since 2024/8/17 + */ +@ConfigurationProperties(prefix = "spring.mail") +@Data +public class MailProperties { + + /** + * 邮件服务器主机名或 IP 地址。 + * 例如:smtp.example.com + */ + private String host; + + /** + * 邮件服务器端口号。 + * 例如:587 + */ + private int port; + + /** + * 用于连接邮件服务器的用户名。 + * 例如:your_email@example.com + */ + private String username; + + /** + * 用于连接邮件服务器的密码。 + * 该密码应安全存储,不应在代码中硬编码。 + */ + private String password; + + /** + * 邮件发送者地址。 + */ + private String from; + + /** + * 邮件服务器的其他属性配置。 + * 这些配置通常用于进一步定制邮件发送行为。 + */ + private Properties properties = new Properties(); + + /** + * 内部类,用于封装邮件服务器的详细配置。 + * 包含 SMTP 相关的配置选项。 + */ + @Data + public static class Properties { + + /** + * SMTP 配置选项类。 + * 包含认证、加密等与 SMTP 协议相关的配置。 + */ + private Smtp smtp = new Smtp(); + + @Data + public static class Smtp { + + /** + * 是否启用 SMTP 认证。 + * 如果为 `true`,则需要提供有效的用户名和密码进行认证。 + */ + private boolean auth; + + /** + * STARTTLS 加密配置选项。 + */ + private StartTls starttls = new StartTls(); + + @Data + public static class StartTls { + + /** + * 是否启用 STARTTLS 加密。 + * 如果为 `true`,在发送邮件时将启用 STARTTLS 协议进行加密传输。 + */ + private boolean enable; + } + } + } +} diff --git a/src/main/java/com/youlai/system/controller/SysUserController.java b/src/main/java/com/youlai/system/controller/SysUserController.java index d484bc70..46a61f67 100644 --- a/src/main/java/com/youlai/system/controller/SysUserController.java +++ b/src/main/java/com/youlai/system/controller/SysUserController.java @@ -6,8 +6,8 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.youlai.system.common.result.PageResult; import com.youlai.system.common.result.Result; +import com.youlai.system.enums.ContactType; import com.youlai.system.model.form.PasswordChangeForm; -import com.youlai.system.model.form.PasswordResetForm; import com.youlai.system.model.form.UserProfileForm; import com.youlai.system.model.vo.UserProfileVO; import com.youlai.system.security.util.SecurityUtils; @@ -167,7 +167,7 @@ public class SysUserController { @Operation(summary = "获取个人中心用户信息") @GetMapping("/{userId}/profile") public Result getUserProfile( - @PathVariable Long userId + @Parameter(description = "用户ID") @PathVariable Long userId ) { UserProfileVO userProfile = userService.getUserProfile(userId); return Result.success(userProfile); @@ -188,20 +188,31 @@ public class SysUserController { @PreAuthorize("@ss.hasPerm('sys:user:password:reset')") public Result resetPassword( @Parameter(description = "用户ID") @PathVariable Long userId, - @RequestParam String password + @RequestParam String password ) { boolean result = userService.resetPassword(userId, password); return Result.judge(result); } - @Operation(summary = "修改用户密码") + @Operation(summary = "修改密码") @PutMapping(value = "/password") public Result changePassword( - @RequestParam PasswordChangeForm data + @RequestBody PasswordChangeForm data ) { Long currUserId = SecurityUtils.getUserId(); boolean result = userService.changePassword(currUserId, data); return Result.judge(result); } + @Operation(summary = "发送短信/邮箱验证码") + @PostMapping(value = "/send-verification-code") + public Result sendVerificationCode( + @Parameter(description = "联系方式(手机号码或邮箱地址)", required = true) @RequestParam String contact, + @Parameter(description = "联系方式类型(Mobile或Email)", required = true) @RequestParam ContactType contactType + ) { + boolean result = userService.sendVerificationCode(contact, contactType); + return Result.judge(result); + } + + } diff --git a/src/main/java/com/youlai/system/enums/ContactType.java b/src/main/java/com/youlai/system/enums/ContactType.java new file mode 100644 index 00000000..e0f1be45 --- /dev/null +++ b/src/main/java/com/youlai/system/enums/ContactType.java @@ -0,0 +1,19 @@ +package com.youlai.system.enums; + +/** + * 联系方式类型 + * + * @author Ray + * @since 2.10.0 + */ +public enum ContactType { + /** + * 手机 + */ + MOBILE, + + /** + * 邮箱 + */ + EMAIL +} diff --git a/src/main/java/com/youlai/system/service/MailService.java b/src/main/java/com/youlai/system/service/MailService.java new file mode 100644 index 00000000..cf629a1d --- /dev/null +++ b/src/main/java/com/youlai/system/service/MailService.java @@ -0,0 +1,31 @@ +package com.youlai.system.service; + +/** + * 邮件服务接口层 + * + * @author Ray + * @since 2024/8/17 + */ +public interface MailService { + + + /** + * 发送简单文本邮件 + * + * @param to 收件人地址 + * @param subject 邮件主题 + * @param text 邮件内容 + */ + boolean sendSimpleMail(String to, String subject, String text) ; + + /** + * 发送带附件的邮件 + * + * @param to 收件人地址 + * @param subject 邮件主题 + * @param text 邮件内容 + * @param filePath 附件路径 + */ + boolean sendMailWithAttachment(String to, String subject, String text, String filePath); + +} diff --git a/src/main/java/com/youlai/system/service/SmsService.java b/src/main/java/com/youlai/system/service/SmsService.java new file mode 100644 index 00000000..9a327eed --- /dev/null +++ b/src/main/java/com/youlai/system/service/SmsService.java @@ -0,0 +1,22 @@ +package com.youlai.system.service; + +/** + * 短信服务接口层 + *

+ * SMS = Short Message Service 短信服务 + * + * @author Ray + * @since 2024/8/17 + */ +public interface SmsService { + + /** + * 发送短信 + * + * @param mobile 手机号 13388886666 + * @param templateCode 短信模板 SMS_194640010 + * @param templateParam 模板参数 "[{"code":"123456"}]" + * @return boolean 是否发送成功 + */ + boolean sendSms(String mobile, String templateCode, String templateParam); +} diff --git a/src/main/java/com/youlai/system/service/SysUserService.java b/src/main/java/com/youlai/system/service/SysUserService.java index 8f3d83da..93d81d18 100644 --- a/src/main/java/com/youlai/system/service/SysUserService.java +++ b/src/main/java/com/youlai/system/service/SysUserService.java @@ -3,9 +3,9 @@ package com.youlai.system.service; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.service.IService; +import com.youlai.system.enums.ContactType; import com.youlai.system.model.entity.SysUser; import com.youlai.system.model.form.PasswordChangeForm; -import com.youlai.system.model.form.PasswordResetForm; import com.youlai.system.model.form.UserForm; import com.youlai.system.model.dto.UserAuthInfo; import com.youlai.system.model.form.UserProfileForm; @@ -127,4 +127,13 @@ public interface SysUserService extends IService { * @return */ boolean resetPassword(Long userId, String password); + + /** + * 发送验证码 + * + * @param contact 联系方式 + * @param type 联系方式类型 + * @return + */ + boolean sendVerificationCode(String contact, ContactType type); } diff --git a/src/main/java/com/youlai/system/service/impl/SysUserServiceImpl.java b/src/main/java/com/youlai/system/service/impl/SysUserServiceImpl.java index 3dea7517..4b1227a6 100644 --- a/src/main/java/com/youlai/system/service/impl/SysUserServiceImpl.java +++ b/src/main/java/com/youlai/system/service/impl/SysUserServiceImpl.java @@ -8,8 +8,11 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.youlai.system.common.constant.RedisConstants; import com.youlai.system.common.constant.SystemConstants; +import com.youlai.system.config.property.AliyunSmsProperties; import com.youlai.system.converter.UserConverter; +import com.youlai.system.enums.ContactType; import com.youlai.system.exception.BusinessException; import com.youlai.system.model.form.PasswordChangeForm; import com.youlai.system.model.form.PasswordResetForm; @@ -28,6 +31,8 @@ import com.youlai.system.model.vo.UserPageVO; import com.youlai.system.security.service.PermissionService; import com.youlai.system.service.*; import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -35,6 +40,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.Arrays; import java.util.List; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** @@ -59,6 +65,14 @@ public class SysUserServiceImpl extends ServiceImpl impl private final PermissionService permissionService; + private final SmsService smsService; + + private final MailService mailService; + + private final AliyunSmsProperties aliyunSmsProperties; + + private final StringRedisTemplate redisTemplate; + /** * 获取用户分页列表 * @@ -286,6 +300,10 @@ public class SysUserServiceImpl extends ServiceImpl impl if (!passwordEncoder.matches(oldPassword, user.getPassword())) { throw new BusinessException("原密码错误"); } + // 新旧密码不能相同 + if (passwordEncoder.matches(data.getNewPassword(), user.getPassword())) { + throw new BusinessException("新密码不能与原密码相同"); + } String newPassword = data.getNewPassword(); return this.update(new LambdaUpdateWrapper() @@ -297,8 +315,8 @@ public class SysUserServiceImpl extends ServiceImpl impl /** * 重置密码 * - * @param userId 用户ID - * @param password 密码重置表单数据 + * @param userId 用户ID + * @param password 密码重置表单数据 * @return */ @Override @@ -308,4 +326,38 @@ public class SysUserServiceImpl extends ServiceImpl impl .set(SysUser::getPassword, passwordEncoder.encode(password)) ); } + + /** + * 发送验证码 + * + * @param contact 联系方式 手机号/邮箱 + * @param type 联系方式类型 {@link ContactType} + * @return + */ + @Override + public boolean sendVerificationCode(String contact, ContactType type) { + + // 随机生成4位验证码 + String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000)); + // 发送验证码 + + String verificationCodePrefix = null; + switch (type) { + case MOBILE: + // 获取修改密码的模板code + String changePasswordSmsTemplateCode = aliyunSmsProperties.getTemplateCodes().get("changePassword"); + smsService.sendSms(contact, changePasswordSmsTemplateCode, "[{\"code\":\"" + code + "\"}]"); + verificationCodePrefix = RedisConstants.MOBILE_VERIFICATION_CODE_PREFIX; + break; + case EMAIL: + mailService.sendSimpleMail(contact, "验证码", "您的验证码是:" + code); + verificationCodePrefix = RedisConstants.EMAIL_VERIFICATION_CODE_PREFIX; + break; + default: + throw new BusinessException("不支持的联系方式类型"); + } + // 存入 redis 用于校验, 5分钟有效 + redisTemplate.opsForValue().set(verificationCodePrefix + contact, code, 5, TimeUnit.MINUTES ); + return true; + } } diff --git a/src/main/java/com/youlai/system/service/impl/mail/MailServiceImpl.java b/src/main/java/com/youlai/system/service/impl/mail/MailServiceImpl.java new file mode 100644 index 00000000..a5a55e38 --- /dev/null +++ b/src/main/java/com/youlai/system/service/impl/mail/MailServiceImpl.java @@ -0,0 +1,83 @@ +package com.youlai.system.service.impl.mail; + +import com.youlai.system.config.property.MailProperties; +import com.youlai.system.service.MailService; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.FileSystemResource; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +import java.io.File; + +/** + * 邮件服务实现类 + * + * @author Ray + * @since 2024/8/17 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class MailServiceImpl implements MailService { + + private final JavaMailSender mailSender; + + private final MailProperties mailProperties; + + /** + * 发送简单文本邮件 + * + * @param to 收件人地址 + * @param subject 邮件主题 + * @param text 邮件内容 + */ + @Override + public boolean sendSimpleMail(String to, String subject, String text) { + try { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(mailProperties.getFrom()); + message.setTo(to); + message.setSubject(subject); + message.setText(text); + mailSender.send(message); + return true; + } catch (Exception e) { + e.printStackTrace(); + log.error("发送邮件失败{}", e.getMessage()); + return false; + } + } + + /** + * 发送带附件的邮件 + * + * @param to 收件人地址 + * @param subject 邮件主题 + * @param text 邮件内容 + * @param filePath 附件路径 + */ + @Override + public boolean sendMailWithAttachment(String to, String subject, String text, String filePath) { + MimeMessage message = mailSender.createMimeMessage(); + try { + MimeMessageHelper helper = new MimeMessageHelper(message, true); + helper.setFrom(mailProperties.getFrom()); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(text, true); // true表示支持HTML内容 + + FileSystemResource file = new FileSystemResource(new File(filePath)); + helper.addAttachment(file.getFilename(), file); + + mailSender.send(message); + return true; + } catch (MessagingException e) { + return false; + } + } +} diff --git a/src/main/java/com/youlai/system/service/impl/sms/AliyunSmsService.java b/src/main/java/com/youlai/system/service/impl/sms/AliyunSmsService.java new file mode 100644 index 00000000..2cd6533f --- /dev/null +++ b/src/main/java/com/youlai/system/service/impl/sms/AliyunSmsService.java @@ -0,0 +1,77 @@ +package com.youlai.system.service.impl.sms; + +import com.aliyuncs.CommonRequest; +import com.aliyuncs.CommonResponse; +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.exceptions.ClientException; +import com.aliyuncs.exceptions.ServerException; +import com.aliyuncs.http.MethodType; +import com.aliyuncs.profile.DefaultProfile; +import com.youlai.system.config.property.AliyunSmsProperties; +import com.youlai.system.service.SmsService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 阿里云短信业务类 + * + * @author Ray + * @since 2024/8/17 + */ +@Service +@RequiredArgsConstructor +public class AliyunSmsService implements SmsService { + + private final AliyunSmsProperties aliyunSmsProperties; + + /** + * 发送短信验证码 + * + * @param mobile 手机号 13388886666 + * @param templateCode 短信模板 SMS_194640010 + * @param templateParam 模板参数 "[{"code":"123456"}]" + * + * @return boolean 是否发送成功 + */ + @Override + public boolean sendSms(String mobile,String templateCode,String templateParam) { + + DefaultProfile profile = DefaultProfile.getProfile(aliyunSmsProperties.getRegionId(), + aliyunSmsProperties.getAccessKeyId(), aliyunSmsProperties.getAccessKeySecret()); + IAcsClient client = new DefaultAcsClient(profile); + + // 创建通用的请求对象 + CommonRequest request = new CommonRequest(); + // 指定请求方式 + request.setSysMethod(MethodType.POST); + // 短信api的请求地址(固定) + request.setSysDomain(aliyunSmsProperties.getDomain()); + // 签名算法版(固定) + request.setSysVersion("2017-05-25"); + // 请求 API 的名称(固定) + request.setSysAction("SendSms"); + // 指定地域名称 + request.putQueryParameter("RegionId", aliyunSmsProperties.getRegionId()); + // 要给哪个手机号发送短信 指定手机号 + request.putQueryParameter("PhoneNumbers", mobile); + // 您的申请签名 + request.putQueryParameter("SignName", aliyunSmsProperties.getSignName()); + // 您申请的模板 code + request.putQueryParameter("TemplateCode", templateCode); + + request.putQueryParameter("TemplateParam", templateParam); + + try { + CommonResponse response = client.getCommonResponse(request); + return response.getHttpResponse().isSuccess(); + } catch (ServerException e) { + e.printStackTrace(); + } catch (ClientException e) { + e.printStackTrace(); + } + return false; + } + + +}