邮件-腾讯云

This commit is contained in:
2025-06-20 15:35:29 +08:00
parent df5ea406e0
commit 18384c277b
13 changed files with 343 additions and 28 deletions

View File

@@ -0,0 +1,19 @@
package com.accompany.email.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
@Data
@RefreshScope
@Configuration
@ConfigurationProperties(prefix = "tencent.ses")
public class TencentSesConfig {
private String secretId;
private String secretKey;
private String region;
}

View File

@@ -8,4 +8,6 @@ package com.accompany.email.enums;
public enum SesTypeEnum {
AWS,
TENCENT,
;
}

View File

@@ -0,0 +1,23 @@
package com.accompany.email.strategy;
import lombok.Data;
/**
* @author: liaozetao
* @date: 2023/8/14 14:57
* @description:
*/
@Data
public class EmailContext {
/**
* 手机号码
*/
private String email;
/**
* 验证码
*/
private String code;
}

View File

@@ -0,0 +1,22 @@
package com.accompany.email.strategy;
import lombok.Data;
/**
* @author: liaozetao
* @date: 2023/8/14 15:06
* @description:
*/
@Data
public class EmailResponse {
/**
* 异常编码
*/
private String code;
/**
* 异常信息
*/
private String message;
}

View File

@@ -26,6 +26,12 @@
<version>2.30.37</version>
</dependency>
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<version>${tencentcloud-sdk-java.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,61 @@
package com.accompany.email.config;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.ses.v20201002.SesClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TencentSecClientConfig {
@Autowired
private TencentSesConfig config;
@Bean
public Credential credential() {
/* 必要步骤:
* 实例化一个认证对象入参需要传入腾讯云账户密钥对secretIdsecretKey。
* 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。
* 你也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人,
* 以免泄露密钥对危及你的财产安全。
* SecretId、SecretKey 查询: https://console.cloud.tencent.com/cam/capi */
return new Credential(config.getSecretId(), config.getSecretKey());
}
@Bean
public HttpProfile httpProfile() {
// 实例化一个http选项可选没有特殊需求可以跳过
HttpProfile httpProfile = new HttpProfile();
// 设置代理(无需要直接忽略)
// httpProfile.setProxyHost("真实代理ip");
// httpProfile.setProxyPort(真实代理端口);
/* SDK默认使用POST方法。
* 如果你一定要使用GET方法可以在这里设置。GET方法无法处理一些较大的请求 */
httpProfile.setReqMethod("POST");
/* SDK有默认的超时时间非必要请不要进行调整
* 如有需要请在代码中查阅以获取最新的默认值 */
httpProfile.setConnTimeout(60);
/* 指定接入地域域名,默认就近地域接入域名为 sms.tencentcloudapi.com ,也支持指定地域域名访问,例如广州地域的域名为 sms.ap-guangzhou.tencentcloudapi.com */
httpProfile.setEndpoint("ses.tencentcloudapi.com");
return httpProfile;
}
@Bean
public SesClient sesClient(Credential credential, HttpProfile httpProfile) {
/* 非必要步骤:
* 实例化一个客户端配置对象,可以指定超时时间等配置 */
ClientProfile clientProfile = new ClientProfile();
/* SDK默认用TC3-HMAC-SHA256进行签名
* 非必要请不要修改这个字段 */
clientProfile.setSignMethod("HmacSHA256");
clientProfile.setHttpProfile(httpProfile);
/* 实例化要请求产品(以sms为例)的client对象
* 第二个参数是地域信息可以直接填写字符串ap-guangzhou支持的地域列表参考 https://cloud.tencent.com/document/api/382/52071#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8 */
//return new SesClient(credential, config.getRegion(), clientProfile);
return new SesClient(credential, config.getRegion(), clientProfile);
}
}

View File

@@ -9,15 +9,15 @@ import com.accompany.common.utils.RandomUtil;
import com.accompany.common.utils.StringUtils;
import com.accompany.core.exception.ServiceException;
import com.accompany.core.service.base.BaseService;
import com.accompany.email.enums.SesTypeEnum;
import com.accompany.email.strategy.EmailContext;
import com.accompany.email.strategy.EmailFactory;
import com.accompany.email.strategy.EmailResponse;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
import software.amazon.awssdk.awscore.internal.AwsErrorCode;
import software.amazon.awssdk.services.sesv2.model.SendEmailResponse;
import software.amazon.awssdk.services.sesv2.model.SesV2Exception;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
@@ -34,8 +34,6 @@ public class EmailService extends BaseService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private AwsSecService awsSecService;
@Autowired
private EmailRecordService emailRecordService;
private final String EMAIL_REGEX = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
@@ -67,31 +65,21 @@ public class EmailService extends BaseService {
}
}
String responseCode = null;
String responseMsg = null;
EmailContext emailContext = new EmailContext();
emailContext.setEmail(emailAddress);
emailContext.setCode(code);
try {
SendEmailResponse response = awsSecService.sendMailCode(emailAddress, code, EmailConstant.EXPIRE_MINUTE);
responseCode = String.valueOf(response.sdkHttpResponse().statusCode());
responseMsg = response.sdkHttpResponse().statusText().orElse(null);
EmailResponse response = EmailFactory.getInstance(SesTypeEnum.TENCENT.name()).sendEmailCode(emailContext);
log.info("[email] to {} code {} responseCode {} responseMsg {}", emailAddress, code, responseCode, responseMsg);
log.info("[email] to {} code {} responseCode {} responseMsg {}", emailAddress, code, response.getCode(), response.getMessage());
if (rateLimiter != null) {
rateLimiter.tryAcquire();
rateLimiter.expire(Duration.of(1, ChronoUnit.DAYS));
}
} catch (SesV2Exception e){
log.error("[email] error {}", e.getMessage(), e);
AwsErrorDetails awsErrorDetails = e.awsErrorDetails();
responseCode = awsErrorDetails.errorCode();
responseMsg = awsErrorDetails.errorMessage();
if (rateLimiter != null) {
rateLimiter.tryAcquire();
rateLimiter.expire(Duration.of(1, ChronoUnit.DAYS));
}
//新增短信记录
emailRecordService.saveRecord(emailAddress, deviceInfo, ip, code, type, responseCode, responseMsg);
emailRecordService.saveRecord(emailAddress, deviceInfo, ip, code, type, response.getCode(), response.getMessage());
//todo 响应编码校验
// 写入短信发送频率

View File

@@ -0,0 +1,51 @@
package com.accompany.email.service;
import com.accompany.email.config.EmailConfig;
import com.alibaba.fastjson.JSON;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.ses.v20201002.SesClient;
import com.tencentcloudapi.ses.v20201002.models.SendEmailRequest;
import com.tencentcloudapi.ses.v20201002.models.SendEmailResponse;
import com.tencentcloudapi.ses.v20201002.models.Simple;
import com.tencentcloudapi.ses.v20201002.models.Template;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class TencentSecService {
@Autowired
private EmailConfig emailConfig;
@Autowired
private SesClient client;
public SendEmailResponse sendMailCode(String emailAddress, String code, Integer expire) throws TencentCloudSDKException {
String appName = emailConfig.getAppName();
//String logoUrl = emailConfig.getLogoUrl();
String sender = emailConfig.getSender();
String title = String.format("%s verification code", appName);
String subject = String.format("%s is %s", title, code);
SendEmailRequest request = new SendEmailRequest();
request.setFromEmailAddress(sender);
request.setDestination(new String[]{emailAddress});
request.setSubject(subject);
Map<String, Object> params = new HashMap<>();
params.put("vvvv", code);
Template template = new Template();
template.setTemplateID(66001L);
template.setTemplateData(JSON.toJSONString(params));
request.setTemplate(template);
request.setTriggerType(1L);
return client.SendEmail(request);
}
}

View File

@@ -0,0 +1,43 @@
package com.accompany.email.strategy;
import com.accompany.common.constant.EmailConstant;
import com.accompany.core.base.SpringContextHolder;
import com.accompany.email.enums.SesTypeEnum;
import com.accompany.email.service.AwsSecService;
import lombok.extern.slf4j.Slf4j;
import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
import software.amazon.awssdk.services.sesv2.model.SendEmailResponse;
import software.amazon.awssdk.services.sesv2.model.SesV2Exception;
@Slf4j
public class AwsEmailStrategy extends EmailStrategy{
@Override
public String type() {
return SesTypeEnum.AWS.name();
}
@Override
public EmailResponse sendEmailCode(EmailContext context) {
EmailResponse r = new EmailResponse();
try {
SendEmailResponse response = SpringContextHolder.getBean(AwsSecService.class).sendMailCode(context.getEmail(), context.getCode(), EmailConstant.EXPIRE_MINUTE);
r.setCode(String.valueOf(response.sdkHttpResponse().statusCode()));
r.setMessage(response.sdkHttpResponse().statusText().orElse(null));
} catch (SesV2Exception e){
log.error("[email] error {}", e.getMessage(), e);
AwsErrorDetails awsErrorDetails = e.awsErrorDetails();
r.setCode(awsErrorDetails.errorCode());
r.setMessage(awsErrorDetails.errorMessage());
}
return r;
}
@Override
public void validResCode(String resCode) {
}
}

View File

@@ -0,0 +1,24 @@
package com.accompany.email.strategy;
import com.accompany.email.enums.SesTypeEnum;
import lombok.extern.slf4j.Slf4j;
/**
* @author: liaozetao
* @date: 2023/8/14 16:05
* @description:
*/
@Slf4j
public class EmailFactory {
public static EmailStrategy getInstance(String type) {
EmailStrategy strategy = null;
if (SesTypeEnum.AWS.name().equalsIgnoreCase(type)) {
strategy = new AwsEmailStrategy();
} else if (SesTypeEnum.TENCENT.name().equalsIgnoreCase(type)){
strategy = new TencentEmailStrategy();
}
log.info("email sdk type : {}", type);
return strategy;
}
}

View File

@@ -0,0 +1,31 @@
package com.accompany.email.strategy;
/**
* @author: liaozetao
* @date: 2023/8/14 14:59
* @description:
*/
public abstract class EmailStrategy {
/**
* 类型
*
* @return
*/
public abstract String type();
/**
* 发送验证码
*
* @param context
* @return
*/
public abstract EmailResponse sendEmailCode(EmailContext context);
/**
* 校验返回结果
*
* @param resCode
*/
public abstract void validResCode(String resCode);
}

View File

@@ -0,0 +1,48 @@
package com.accompany.email.strategy;
import com.accompany.common.constant.EmailConstant;
import com.accompany.core.base.SpringContextHolder;
import com.accompany.email.enums.SesTypeEnum;
import com.accompany.email.service.TencentSecService;
import com.alibaba.fastjson2.JSON;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.ses.v20201002.models.SendEmailResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TencentEmailStrategy extends EmailStrategy{
@Override
public String type() {
return SesTypeEnum.TENCENT.name();
}
@Override
public EmailResponse sendEmailCode(EmailContext context) {
EmailResponse r = new EmailResponse();
try {
SendEmailResponse response = SpringContextHolder.getBean(TencentSecService.class).sendMailCode(context.getEmail(), context.getCode(), EmailConstant.EXPIRE_MINUTE);
r.setCode("200");
r.setMessage("OK");
log.info("sendEmailCode success {}", JSON.toJSONString(response));
} catch (TencentCloudSDKException e) {
//todo log
log.error("sendEmailCode exception", e);
r.setCode(e.getErrorCode());
r.setMessage(e.getMessage());
}
return r;
}
@Override
public void validResCode(String resCode) {
}
}

View File

@@ -1,8 +1,5 @@
package com.accompany.sms.strategy;
import com.accompany.sms.strategy.SmsContext;
import com.accompany.sms.strategy.SmsResponse;
/**
* @author: liaozetao
* @date: 2023/8/14 14:59