sms-mail
SKILL.md
短信与邮件开发指南
通用模板。如果项目有专属技能,优先使用。
设计原则
- 接口抽象:定义统一的发送接口,具体渠道通过策略模式实现,方便切换供应商。
- 异步发送:发送操作应异步执行,不阻塞主业务流程。
- 结果校验:每次发送必须检查返回结果,失败需记录日志并决策是否重试。
- 安全防护:验证码需限频限量、设置过期时间,防止短信轰炸和暴力破解。
实现模式
一、抽象接口设计
// 统一短信发送接口
public interface SmsSender {
SmsResult send(String phone, String templateId, Map<String, String> params);
SmsResult batchSend(List<String> phones, String templateId, Map<String, String> params);
}
// 统一邮件发送接口
public interface MailSender {
String sendText(String to, String subject, String content);
String sendHtml(String to, String subject, String htmlContent);
String sendWithAttachment(String to, String subject, String content, File... files);
}
// 发送结果
@Data
public class SmsResult {
private boolean success;
private String messageId;
private String errorMessage;
}
二、短信服务
多渠道策略模式
// 阿里云实现
@Service("aliyunSmsSender")
public class AliyunSmsSender implements SmsSender {
@Override
public SmsResult send(String phone, String templateId, Map<String, String> params) {
// 调用阿里云 SMS SDK
}
}
// 腾讯云实现
@Service("tencentSmsSender")
public class TencentSmsSender implements SmsSender {
@Override
public SmsResult send(String phone, String templateId, Map<String, String> params) {
// 调用腾讯云 SMS SDK
}
}
// 工厂/路由
@Service
public class SmsService {
@Autowired
private Map<String, SmsSender> senderMap;
@Value("${sms.default-channel:aliyunSmsSender}")
private String defaultChannel;
public SmsResult send(String phone, String templateId, Map<String, String> params) {
SmsSender sender = senderMap.get(defaultChannel);
if (sender == null) {
throw new [你的异常类]("短信渠道未配置: " + defaultChannel);
}
SmsResult result = sender.send(phone, templateId, params);
if (!result.isSuccess()) {
log.error("短信发送失败: phone={}, error={}", phone, result.getErrorMessage());
}
return result;
}
}
配置示例
sms:
default-channel: aliyunSmsSender
aliyun:
access-key-id: ${ALIYUN_SMS_KEY:}
access-key-secret: ${ALIYUN_SMS_SECRET:}
sign-name: [你的签名]
tencent:
secret-id: ${TENCENT_SMS_ID:}
secret-key: ${TENCENT_SMS_KEY:}
sdk-app-id: "1400000000"
sign-name: [你的签名]
验证码短信完整示例
@RestController
@RequestMapping("/captcha")
public class CaptchaController {
@Autowired
private SmsService smsService;
@Autowired
private StringRedisTemplate redisTemplate;
@GetMapping("/sms")
public Result<?> sendSmsCode(@RequestParam String phone) {
// 1. 限频检查(60秒内只能发一次)
String limitKey = "sms:limit:" + phone;
if (Boolean.TRUE.equals(redisTemplate.hasKey(limitKey))) {
return Result.fail("请60秒后重试");
}
// 2. 生成验证码
String code = String.valueOf(ThreadLocalRandom.current().nextInt(100000, 999999));
// 3. 存入 Redis(5分钟有效)
String codeKey = "sms:code:" + phone;
redisTemplate.opsForValue().set(codeKey, code, 5, TimeUnit.MINUTES);
redisTemplate.opsForValue().set(limitKey, "1", 60, TimeUnit.SECONDS);
// 4. 发送短信
Map<String, String> params = new LinkedHashMap<>();
params.put("code", code);
SmsResult result = smsService.send(phone, "SMS_VERIFY_CODE", params);
if (!result.isSuccess()) {
redisTemplate.delete(codeKey);
return Result.fail("短信发送失败");
}
return Result.ok("验证码已发送");
}
@PostMapping("/verify")
public Result<Boolean> verify(@RequestParam String phone, @RequestParam String code) {
String codeKey = "sms:code:" + phone;
String cached = redisTemplate.opsForValue().get(codeKey);
if (cached == null) return Result.fail("验证码已过期");
if (!cached.equals(code)) return Result.fail("验证码错误");
redisTemplate.delete(codeKey);
return Result.ok(true);
}
}
三、邮件服务
Spring Boot 原生邮件
@Service
public class MailService {
@Autowired
private JavaMailSender mailSender;
@Value("${spring.mail.username}")
private String from;
// 文本邮件
public void sendText(String to, String subject, String content) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setSubject(subject);
message.setText(content);
mailSender.send(message);
}
// HTML 邮件
public void sendHtml(String to, String subject, String htmlContent) throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(htmlContent, true);
mailSender.send(message);
}
// 带附件邮件
public void sendWithAttachment(String to, String subject, String content,
File... attachments) throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
for (File file : attachments) {
helper.addAttachment(file.getName(), file);
}
mailSender.send(message);
}
// 群发
public void sendHtml(List<String> toList, String subject, String htmlContent)
throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(from);
helper.setTo(toList.toArray(new String[0]));
helper.setSubject(subject);
helper.setText(htmlContent, true);
mailSender.send(message);
}
}
配置示例
spring:
mail:
host: smtp.163.com
port: 465
username: system@example.com
password: ${MAIL_PASSWORD} # 授权码,非登录密码
properties:
mail:
smtp:
auth: true
ssl:
enable: true
timeout: 10000
connectiontimeout: 10000
常用 SMTP 服务器:
| 提供商 | Host | 端口(SSL) |
|---|---|---|
| 163 | smtp.163.com | 465 |
| smtp.qq.com | 465 | |
| Gmail | smtp.gmail.com | 465 |
| 阿里企业邮箱 | smtp.qiye.aliyun.com | 465 |
四、多渠道消息通知
@Service
public class MessageService {
@Autowired
private SmsService smsService;
@Autowired
private MailService mailService;
// 可扩展:站内信、推送、企业微信等
public void sendNotification(List<String> channels, String subject,
String content, List<UserDTO> users) {
for (UserDTO user : users) {
try {
if (channels.contains("sms") && StringUtils.hasText(user.getPhone())) {
Map<String, String> params = Map.of("content", content);
smsService.send(user.getPhone(), "SMS_NOTIFY", params);
}
if (channels.contains("email") && StringUtils.hasText(user.getEmail())) {
mailService.sendHtml(user.getEmail(), subject,
"<div style='padding:20px;'><h3>" + subject + "</h3><p>" + content + "</p></div>");
}
} catch (Exception e) {
log.error("消息发送失败, channel={}, userId={}, error={}",
channels, user.getUserId(), e.getMessage());
// 不抛出,继续发送其他用户
}
}
}
}
选型建议
| 维度 | 自研抽象层 | SMS4j | 云 SDK 直接调用 |
|---|---|---|---|
| 灵活性 | 最高 | 高 | 中 |
| 开发成本 | 中 | 低 | 低 |
| 多渠道切换 | 需自行实现 | 内置支持 | 需改代码 |
| 维护成本 | 中 | 低(社区维护) | 低 |
| 适用场景 | 定制需求高 | 通用 | 单一渠道 |
常见错误
// 1. 未配置就使用,NPE
SmsSender sender = senderMap.get("xxx");
sender.send(phone, template, params); // sender 为 null -> NPE
// 应先判空
// 2. 模板参数名与模板定义不匹配
params.put("verifyCode", "123456"); // 模板中变量名是 code
// 应确认模板变量名
// 3. 不检查发送结果
smsService.send(phone, templateId, params); // 发送可能失败
// 应检查 SmsResult.isSuccess()
// 4. 验证码无过期时间
redisTemplate.opsForValue().set("sms:code:" + phone, code); // 永不过期
// 应设置 5-10 分钟过期
// 5. 无限频控制(短信轰炸)
// 应限制:同号码 60秒 1 次、每天最多 10 次
// 6. 邮件密码用登录密码而非授权码
// 大多数邮件服务商需要使用"授权码"而非"登录密码"
// 7. 同步发送阻塞主线程
// 短信/邮件发送应使用 @Async 异步执行
Weekly Installs
4
Repository
xu-cell/ai-engi…ing-initGitHub Stars
8
First Seen
6 days ago
Security Audits
Installed on
gemini-cli4
github-copilot4
codex4
kimi-cli4
cursor4
amp4