feat: 添加短信和邮件发送

This commit is contained in:
ray
2024-08-18 23:58:16 +08:00
parent 082e8012f4
commit b9917e96f5
11 changed files with 502 additions and 8 deletions

View File

@@ -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。
* <p>
* 手动注入的原因是为了避免在使用 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;
}
}

View File

@@ -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<String, String> templateCodes;
}

View File

@@ -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;
}
}
}
}

View File

@@ -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<UserProfileVO> 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);
}
}

View File

@@ -0,0 +1,19 @@
package com.youlai.system.enums;
/**
* 联系方式类型
*
* @author Ray
* @since 2.10.0
*/
public enum ContactType {
/**
* 手机
*/
MOBILE,
/**
* 邮箱
*/
EMAIL
}

View File

@@ -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);
}

View File

@@ -0,0 +1,22 @@
package com.youlai.system.service;
/**
* 短信服务接口层
* <p>
* 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);
}

View File

@@ -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<SysUser> {
* @return
*/
boolean resetPassword(Long userId, String password);
/**
* 发送验证码
*
* @param contact 联系方式
* @param type 联系方式类型
* @return
*/
boolean sendVerificationCode(String contact, ContactType type);
}

View File

@@ -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<SysUserMapper, SysUser> 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<SysUserMapper, SysUser> 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<SysUser>()
@@ -297,8 +315,8 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
/**
* 重置密码
*
* @param userId 用户ID
* @param password 密码重置表单数据
* @param userId 用户ID
* @param password 密码重置表单数据
* @return
*/
@Override
@@ -308,4 +326,38 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> 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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}