# 阿里云SMS短信服务集成文档

## 目录
1. [概述](#概述)
2. [Maven依赖](#maven依赖)
3. [配置文件](#配置文件)
4. [代码实现](#代码实现)
5. [使用示例](#使用示例)
6. [常见问题](#常见问题)

---

## 概述

本文档提供阿里云短信服务（SMS）的完整集成方案，包括验证码发送、验证码校验、发送频率限制等功能。

### 功能特性
- ✅ 发送6位数字验证码
- ✅ 验证码5分钟有效期
- ✅ 每个手机号每天最多发送10次
- ✅ 使用Redis存储验证码和发送次数
- ✅ 完整的错误处理和日志记录

---

## Maven依赖

### pom.xml

```xml
<dependencies>
    <!-- 阿里云短信SDK -->
    <dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>dysmsapi20170525</artifactId>
        <version>2.0.24</version>
    </dependency>
    
    <!-- 阿里云核心SDK -->
    <dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>tea-openapi</artifactId>
        <version>0.2.8</version>
    </dependency>
    
    <!-- 阿里云Tea工具 -->
    <dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>tea-util</artifactId>
        <version>0.2.21</version>
    </dependency>
    
    <!-- Spring Boot Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>
```

---

## 配置文件

### application.yml

```yaml
spring:
  data:
    redis:
      host: localhost
      port: 6379
      database: 0
      password: # 如果有密码则填写

# 阿里云短信服务配置
aliyun:
  sms:
    # 短信签名（需要在阿里云控制台申请）
    sign-name: 你的短信签名
    # 短信模板CODE（需要在阿里云控制台申请）
    template-code: SMS_xxxxxxxx
    # 阿里云AccessKey ID
    access-key-id: ${ALIYUN_ACCESS_KEY_ID:your-access-key-id}
    # 阿里云AccessKey Secret
    access-key-secret: ${ALIYUN_ACCESS_KEY_SECRET:your-access-key-secret}
```

### 环境变量配置（推荐）

**Linux/Mac**:
```bash
export ALIYUN_ACCESS_KEY_ID=your-access-key-id
export ALIYUN_ACCESS_KEY_SECRET=your-access-key-secret
```

**Windows**:
```cmd
set ALIYUN_ACCESS_KEY_ID=your-access-key-id
set ALIYUN_ACCESS_KEY_SECRET=your-access-key-secret
```

---

## 代码实现

### 1. Service接口

**SmsService.java**

```java
package com.example.service;

/**
 * 短信服务接口
 */
public interface SmsService {
    
    /**
     * 发送短信验证码
     * 
     * @param phoneNumber 手机号码
     * @return 是否发送成功
     * @throws Exception 发送异常
     */
    boolean sendVerificationCode(String phoneNumber) throws Exception;
    
    /**
     * 验证短信验证码
     * 
     * @param phoneNumber 手机号码
     * @param code 验证码
     * @return 是否验证通过
     */
    boolean verifyCode(String phoneNumber, String code);
}
```

### 2. Service实现类

**SmsServiceImpl.java**

```java
package com.example.service.impl;

import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.tea.TeaException;
import com.aliyun.teautil.models.RuntimeOptions;
import com.example.service.SmsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * 阿里云短信服务实现类
 */
@Service
public class SmsServiceImpl implements SmsService {
    
    private static final Logger logger = LoggerFactory.getLogger(SmsServiceImpl.class);
    
    @Value("${aliyun.sms.sign-name}")
    private String signName;
    
    @Value("${aliyun.sms.template-code}")
    private String templateCode;
    
    @Value("${aliyun.sms.access-key-id}")
    private String accessKeyId;
    
    @Value("${aliyun.sms.access-key-secret}")
    private String accessKeySecret;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 短信验证码Redis前缀
    private static final String SMS_CODE_PREFIX = "sms:code:";
    // 短信发送次数Redis前缀
    private static final String SMS_COUNT_PREFIX = "sms:count:";
    // 短信验证码有效期（分钟）
    private static final long SMS_CODE_EXPIRE = 5;
    // 每天最大发送次数
    private static final int MAX_SMS_COUNT_PER_DAY = 10;
    
    /**
     * 创建阿里云短信客户端
     */
    private Client createSmsClient() throws Exception {
        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
        config.accessKeyId = accessKeyId;
        config.accessKeySecret = accessKeySecret;
        config.endpoint = "dysmsapi.aliyuncs.com";
        return new Client(config);
    }
    
    /**
     * 生成6位随机数字验证码
     */
    private String generateVerificationCode() {
        Random random = new Random();
        StringBuilder code = new StringBuilder();
        for (int i = 0; i < 6; i++) {
            code.append(random.nextInt(10));
        }
        return code.toString();
    }
    
    /**
     * 获取当天结束时间的剩余秒数
     */
    private long getSecondsUntilEndOfDay() {
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime endOfDay = now.toLocalDate().atTime(23, 59, 59);
        Duration duration = Duration.between(now, endOfDay);
        return duration.getSeconds();
    }
    
    @Override
    public boolean sendVerificationCode(String phoneNumber) throws Exception {
        // 1. 检查当天发送次数是否达到上限
        String countKey = SMS_COUNT_PREFIX + phoneNumber + ":" + 
                         LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        Integer count = (Integer) redisTemplate.opsForValue().get(countKey);
        
        if (count != null && count >= MAX_SMS_COUNT_PER_DAY) {
            logger.warn("手机号{}今日短信发送次数已达上限: {}", phoneNumber, MAX_SMS_COUNT_PER_DAY);
            return false;
        }
        
        // 2. 生成6位随机验证码
        String verificationCode = generateVerificationCode();
        
        // 3. 构建短信请求
        Client client = createSmsClient();
        SendSmsRequest sendSmsRequest = new SendSmsRequest()
                .setSignName(signName)
                .setTemplateCode(templateCode)
                .setPhoneNumbers(phoneNumber)
                .setTemplateParam("{\"code\":\"" + verificationCode + "\"}");
        
        RuntimeOptions runtime = new RuntimeOptions();
        
        try {
            // 4. 发送短信
            client.sendSmsWithOptions(sendSmsRequest, runtime);
            logger.info("短信验证码发送成功，手机号: {}", phoneNumber);
            
            // 5. 将验证码保存到Redis，设置过期时间
            String codeKey = SMS_CODE_PREFIX + phoneNumber;
            redisTemplate.opsForValue().set(codeKey, verificationCode, SMS_CODE_EXPIRE, TimeUnit.MINUTES);
            
            // 6. 增加当天发送次数，并设置过期时间为当天结束
            if (count == null) {
                count = 0;
            }
            redisTemplate.opsForValue().set(countKey, count + 1, getSecondsUntilEndOfDay(), TimeUnit.SECONDS);
            
            return true;
            
        } catch (TeaException error) {
            logger.error("短信发送失败, 手机号: {}, 错误信息: {}, 诊断信息: {}", 
                    phoneNumber, error.getMessage(), error.getData().get("Recommend"));
            return false;
        } catch (Exception error) {
            logger.error("短信发送异常, 手机号: {}", phoneNumber, error);
            return false;
        }
    }
    
    @Override
    public boolean verifyCode(String phoneNumber, String code) {
        if (phoneNumber == null || code == null) {
            return false;
        }
        
        String codeKey = SMS_CODE_PREFIX + phoneNumber;
        String savedCode = (String) redisTemplate.opsForValue().get(codeKey);
        
        if (savedCode != null && savedCode.equals(code)) {
            // 验证成功后删除验证码
            redisTemplate.delete(codeKey);
            return true;
        }
        
        return false;
    }
}
```

### 3. Controller控制器

**SmsController.java**

```java
package com.example.controller;

import com.example.common.ApiResponse;
import com.example.common.ResultUtils;
import com.example.service.SmsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * 短信控制器
 */
@RestController
@RequestMapping("/sms")
@Tag(name = "短信接口", description = "提供短信验证码相关功能")
public class SmsController {

    @Autowired
    private SmsService smsService;

    /**
     * 发送短信验证码
     *
     * @param phoneNumber 手机号
     * @return 发送结果
     */
    @PostMapping("/sendCode")
    @Operation(summary = "发送短信验证码", description = "向指定手机号发送验证码，每个手机号每天最多发送10次")
    public ApiResponse<Boolean> sendVerificationCode(
            @Parameter(description = "手机号码", required = true) 
            @RequestParam String phoneNumber) {
        try {
            boolean success = smsService.sendVerificationCode(phoneNumber);
            if (success) {
                return ResultUtils.success(true, "验证码发送成功");
            } else {
                return ResultUtils.error(40001, "发送验证码失败，请稍后重试");
            }
        } catch (Exception e) {
            return ResultUtils.error(50000, "发送验证码异常：" + e.getMessage());
        }
    }
    
    /**
     * 验证短信验证码
     *
     * @param phoneNumber 手机号
     * @param code 验证码
     * @return 验证结果
     */
    @PostMapping("/verifyCode")
    @Operation(summary = "验证短信验证码", description = "验证手机号和验证码是否匹配")
    public ApiResponse<Boolean> verifyCode(
            @Parameter(description = "手机号码", required = true) 
            @RequestParam String phoneNumber,
            @Parameter(description = "验证码", required = true) 
            @RequestParam String code) {
        boolean valid = smsService.verifyCode(phoneNumber, code);
        if (valid) {
            return ResultUtils.success(true, "验证成功");
        } else {
            return ResultUtils.error(40002, "验证码错误或已过期");
        }
    }
}
```

### 4. 通用响应类（可选）

**ApiResponse.java**

```java
package com.example.common;

import lombok.Data;

/**
 * 通用API响应类
 */
@Data
public class ApiResponse<T> {
    private int code;
    private String message;
    private T data;
    
    public ApiResponse(int code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }
}
```

**ResultUtils.java**

```java
package com.example.common;

/**
 * 响应工具类
 */
public class ResultUtils {
    
    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(200, "success", data);
    }
    
    public static <T> ApiResponse<T> success(T data, String message) {
        return new ApiResponse<>(200, message, data);
    }
    
    public static <T> ApiResponse<T> error(int code, String message) {
        return new ApiResponse<>(code, message, null);
    }
}
```

---

## 使用示例

### 1. 发送验证码

**请求**:
```bash
curl -X POST http://localhost:8080/sms/sendCode \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "phoneNumber=13800138000"
```

**响应**:
```json
{
  "code": 200,
  "message": "验证码发送成功",
  "data": true
}
```

### 2. 验证验证码

**请求**:
```bash
curl -X POST http://localhost:8080/sms/verifyCode \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "phoneNumber=13800138000&code=123456"
```

**响应**:
```json
{
  "code": 200,
  "message": "验证成功",
  "data": true
}
```

### 3. 在业务代码中使用

```java
@Service
public class UserService {
    
    @Autowired
    private SmsService smsService;
    
    /**
     * 用户注册
     */
    public void register(String phoneNumber, String code, String password) {
        // 1. 验证验证码
        if (!smsService.verifyCode(phoneNumber, code)) {
            throw new BusinessException("验证码错误或已过期");
        }
        
        // 2. 执行注册逻辑
        // ...
    }
    
    /**
     * 找回密码
     */
    public void resetPassword(String phoneNumber, String code, String newPassword) {
        // 1. 验证验证码
        if (!smsService.verifyCode(phoneNumber, code)) {
            throw new BusinessException("验证码错误或已过期");
        }
        
        // 2. 重置密码
        // ...
    }
}
```

---

## 常见问题

### 1. 如何申请阿里云短信服务？

1. 登录[阿里云控制台](https://www.aliyun.com/)
2. 进入"短信服务"产品页面
3. 申请短信签名（需要企业资质或个人认证）
4. 申请短信模板（需要审核，一般1-2个工作日）
5. 获取AccessKey ID和AccessKey Secret

### 2. 短信模板格式

**模板示例**:
```
您的验证码是${code}，请在5分钟内完成验证。
```

**模板变量**:
- 使用 `${变量名}` 格式
- 代码中传递JSON格式：`{"code":"123456"}`

### 3. 常见错误码

| 错误码 | 说明 | 解决方案 |
|--------|------|---------|
| isv.BUSINESS_LIMIT_CONTROL | 触发业务流控 | 降低发送频率 |
| isv.MOBILE_NUMBER_ILLEGAL | 手机号码格式错误 | 检查手机号格式 |
| isv.TEMPLATE_MISSING_PARAMETERS | 模板参数缺失 | 检查模板参数 |
| isv.INVALID_PARAMETERS | 参数异常 | 检查所有参数 |
| isv.AMOUNT_NOT_ENOUGH | 账户余额不足 | 充值 |
| isv.TEMPLATE_PARAMS_ILLEGAL | 模板变量值非法 | 检查变量值格式 |

### 4. 如何修改验证码位数？

修改 `generateVerificationCode()` 方法中的循环次数：

```java
private String generateVerificationCode() {
    Random random = new Random();
    StringBuilder code = new StringBuilder();
    // 修改这里的数字，比如改为4位验证码
    for (int i = 0; i < 4; i++) {
        code.append(random.nextInt(10));
    }
    return code.toString();
}
```

### 5. 如何修改验证码有效期？

修改常量 `SMS_CODE_EXPIRE`：

```java
// 修改为10分钟
private static final long SMS_CODE_EXPIRE = 10;
```

### 6. 如何修改每日发送次数限制？

修改常量 `MAX_SMS_COUNT_PER_DAY`：

```java
// 修改为5次
private static final int MAX_SMS_COUNT_PER_DAY = 5;
```

### 7. Redis连接失败怎么办？

确保Redis服务已启动：

```bash
# Linux/Mac
redis-server

# 检查Redis是否运行
redis-cli ping
# 应该返回 PONG
```

### 8. 如何测试短信发送？

阿里云提供测试环境，可以使用测试手机号：

```java
// 测试环境配置
config.endpoint = "dysmsapi.aliyuncs.com"; // 正式环境
// config.endpoint = "dysmsapi-test.aliyuncs.com"; // 测试环境（如果有）
```

### 9. 生产环境安全建议

1. **不要硬编码密钥**：使用环境变量或配置中心
2. **启用HTTPS**：保护API通信安全
3. **添加图形验证码**：防止恶意刷短信
4. **IP限流**：防止单个IP频繁请求
5. **手机号验证**：验证手机号格式和归属地
6. **监控告警**：设置短信发送量告警

### 10. 性能优化建议

1. **Redis连接池**：配置合理的连接池参数
2. **异步发送**：使用 `@Async` 异步发送短信
3. **批量发送**：如果需要群发，使用批量接口
4. **缓存优化**：合理设置Redis过期时间

---

## 完整项目结构

```
src/
├── main/
│   ├── java/
│   │   └── com/
│   │       └── example/
│   │           ├── controller/
│   │           │   └── SmsController.java
│   │           ├── service/
│   │           │   ├── SmsService.java
│   │           │   └── impl/
│   │           │       └── SmsServiceImpl.java
│   │           ├── common/
│   │           │   ├── ApiResponse.java
│   │           │   └── ResultUtils.java
│   │           └── Application.java
│   └── resources/
│       └── application.yml
└── test/
    └── java/
        └── com/
            └── example/
                └── service/
                    └── SmsServiceTest.java
```

---

## 单元测试示例

**SmsServiceTest.java**

```java
package com.example.service;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class SmsServiceTest {
    
    @Autowired
    private SmsService smsService;
    
    @Test
    void testSendVerificationCode() throws Exception {
        // 使用测试手机号
        String phoneNumber = "13800138000";
        boolean result = smsService.sendVerificationCode(phoneNumber);
        assertTrue(result, "验证码发送应该成功");
    }
    
    @Test
    void testVerifyCode() throws Exception {
        String phoneNumber = "13800138000";
        
        // 发送验证码
        smsService.sendVerificationCode(phoneNumber);
        
        // 验证错误的验证码
        boolean result1 = smsService.verifyCode(phoneNumber, "000000");
        assertFalse(result1, "错误的验证码应该验证失败");
        
        // 注意：无法测试正确的验证码，因为验证码是随机生成的
    }
}
```

---

## 总结

本文档提供了阿里云SMS短信服务的完整集成方案，包括：

✅ Maven依赖配置
✅ 完整的代码实现
✅ 配置文件示例
✅ 使用示例和测试方法
✅ 常见问题解答
✅ 安全和性能优化建议

将此文档保存到你的项目中，以后需要集成阿里云SMS时可以直接参考使用。

---

## 相关链接

- [阿里云短信服务官网](https://www.aliyun.com/product/sms)
- [阿里云短信服务API文档](https://help.aliyun.com/document_detail/101414.html)
- [阿里云短信服务SDK](https://help.aliyun.com/document_detail/215759.html)
- [阿里云短信服务控制台](https://dysms.console.aliyun.com/)
