# 阿里云短信服务部署指南

> 注意：这份文档是早期短信接入草稿，里面包含“修改 config.py / 替换 sms.py”等旧步骤。当前项目已经内置 `SMS_MODE=local/aliyun` 和阿里云短信发送逻辑，正式部署请以 `docs/正式部署操作手册.md` 的“短信验证码配置”章节为准。

## 前置条件

- 阿里云账号（已实名认证）
- 已备案的域名（短信签名需要）
- 服务器（已部署后端服务）

## 第一步：开通短信服务

1. 登录阿里云控制台：https://console.aliyun.com
2. 搜索「短信服务」或访问：https://dysms.console.aliyun.com
3. 点击「免费开通」
4. 阅读并同意服务协议
5. 点击「立即开通」

## 第二步：申请短信签名

短信签名是短信开头的标识，如【培训结业系统】。

1. 进入短信服务控制台
2. 左侧菜单选择「国内消息」→「签名管理」
3. 点击「添加签名」
4. 填写信息：
   - **签名名称**：培训结业证书系统（或其他名称，2-12个字符）
   - **适用场景**：选择「自用-验证码」
   - **签名来源**：选择「企事业单位的全称或简称」
   - **上传证明**：营业执照照片
   - **申请说明**：用于学员查询培训结业证书时的短信验证码
5. 点击「提交」
6. 等待审核（通常1-2小时）

## 第三步：申请短信模板

短信模板是短信内容的格式。

1. 左侧菜单选择「国内消息」→「模板管理」
2. 点击「添加模板」
3. 填写信息：
   - **模板名称**：验证码模板
   - **模板类型**：选择「验证码」
   - **模板内容**：`您的验证码是${code}，5分钟内有效，请勿泄露给他人。`
   - **申请说明**：用于学员查询培训结业证书时的身份验证
4. 点击「提交」
5. 等待审核（通常1-2小时）

**注意**：模板变量格式必须是 `${变量名}`，不能是其他格式。

## 第四步：获取 AccessKey

AccessKey 用于后端调用阿里云 API。

1. 点击右上角头像 →「AccessKey管理」
2. 或直接访问：https://ram.console.aliyun.com/manage/ak
3. 点击「创建AccessKey」
4. 进行身份验证（短信验证码或邮箱验证）
5. 创建成功后，保存：
   - **AccessKey ID**
   - **AccessKey Secret**

**重要**：AccessKey Secret 只显示一次，请立即保存到安全的地方。

**安全建议**：
- 不要将 AccessKey 写在代码里
- 使用 RAM 子账号的 AccessKey，并只授予短信服务权限
- 定期轮换 AccessKey

## 第五步：配置环境变量

在服务器上配置环境变量，编辑 `.env` 文件或系统环境变量：

```bash
# 阿里云短信服务配置
ALIYUN_ACCESS_KEY_ID=你的AccessKeyID
ALIYUN_ACCESS_KEY_SECRET=你的AccessKeySecret
ALIYUN_SMS_SIGN_NAME=你的短信签名
ALIYUN_SMS_TEMPLATE_CODE=你的模板CODE
```

**示例**：
```bash
ALIYUN_ACCESS_KEY_ID=LTAI5tQkB3VRg8xR
ALIYUN_ACCESS_KEY_SECRET=your_secret_key_here
ALIYUN_SMS_SIGN_NAME=培训结业系统
ALIYUN_SMS_TEMPLATE_CODE=SMS_123456789
```

## 第六步：安装依赖

```bash
cd backend
pip install alibabacloud-dysmsapi20170525==3.0.0
```

或在 `requirements.txt` 中添加：
```
alibabacloud-dysmsapi20170525==3.0.0
```

然后执行：
```bash
pip install -r requirements.txt
```

## 第七步：配置后端代码

编辑 `backend/app/core/config.py`，添加短信配置：

```python
import os

class Settings:
    # ... 其他配置 ...
    
    # 阿里云短信配置
    ALIYUN_ACCESS_KEY_ID: str = os.getenv("ALIYUN_ACCESS_KEY_ID", "")
    ALIYUN_ACCESS_KEY_SECRET: str = os.getenv("ALIYUN_ACCESS_KEY_SECRET", "")
    ALIYUN_SMS_SIGN_NAME: str = os.getenv("ALIYUN_SMS_SIGN_NAME", "")
    ALIYUN_SMS_TEMPLATE_CODE: str = os.getenv("ALIYUN_SMS_TEMPLATE_CODE", "")

settings = Settings()
```

## 第八步：修改短信发送服务

编辑 `backend/app/services/sms.py`，替换为真实的短信发送逻辑：

```python
import logging
import random
import secrets
import string
import time

from alibabacloud_dysmsapi20170525.client import Client
from alibabacloud_dysmsapi20170525 import models as dysmsapi_models
from alibabacloud_tea_openapi import models as open_api_models

from app.core.config import settings

logger = logging.getLogger(__name__)

SMS_CODE_TTL_SECONDS = 300  # 5分钟有效期
_sms_codes: dict[str, tuple[str, float]] = {}


def _get_sms_client() -> Client:
    """获取阿里云短信客户端"""
    config = open_api_models.Config(
        access_key_id=settings.ALIYUN_ACCESS_KEY_ID,
        access_key_secret=settings.ALIYUN_ACCESS_KEY_SECRET,
    )
    config.endpoint = "dysmsapi.aliyuncs.com"
    return Client(config)


def send_sms(phone: str, code: str) -> bool:
    """发送短信验证码"""
    try:
        client = _get_sms_client()
        request = dysmsapi_models.SendSmsRequest(
            phone_numbers=phone,
            sign_name=settings.ALIYUN_SMS_SIGN_NAME,
            template_code=settings.ALIYUN_SMS_TEMPLATE_CODE,
            template_param=f'{{"code":"{code}"}}',
        )
        response = client.send_sms(request)
        
        if response.body.code == "OK":
            logger.info(f"[短信发送成功] 手机号: {phone}")
            return True
        else:
            logger.error(f"[短信发送失败] 手机号: {phone}, 错误: {response.body.message}")
            return False
    except Exception as e:
        logger.error(f"[短信发送异常] 手机号: {phone}, 异常: {str(e)}")
        return False


def generate_sms_code(phone: str) -> str:
    """生成短信验证码"""
    clean_expired_codes()
    code = "".join(random.choices(string.digits, k=6))
    sms_id = secrets.token_urlsafe(16)
    _sms_codes[sms_id] = (code, time.time() + SMS_CODE_TTL_SECONDS)
    
    # 发送短信
    send_sms(phone, code)
    
    # 开发环境下记录日志
    logger.info(f"[短信验证码] 手机号: {phone}, 验证码: {code}, 有效期: {SMS_CODE_TTL_SECONDS}秒")
    
    return sms_id


def verify_sms_code(sms_id: str, code: str) -> bool:
    """验证短信验证码"""
    clean_expired_codes()
    record = _sms_codes.pop(sms_id, None)
    if not record:
        return False
    expected, expires_at = record
    if expires_at < time.time():
        return False
    return expected == code.strip()


def clean_expired_codes() -> None:
    """清理过期的验证码"""
    now = time.time()
    expired = [sms_id for sms_id, (_, expires_at) in _sms_codes.items() if expires_at < now]
    for sms_id in expired:
        _sms_codes.pop(sms_id, None)
```

## 第九步：测试验证

### 1. 检查配置

```bash
# 查看环境变量
echo $ALIYUN_ACCESS_KEY_ID
echo $ALIYUN_SMS_SIGN_NAME
echo $ALIYUN_SMS_TEMPLATE_CODE
```

### 2. 重启服务

```bash
# 重启后端服务
systemctl restart backend
# 或
pm2 restart backend
```

### 3. 查看日志

```bash
# 查看日志
tail -f /var/log/backend/app.log
```

### 4. 测试发送

访问公开查询页面，输入手机号，点击「获取验证码」，查看：
- 是否收到短信
- 日志是否有发送记录

## 常见问题排查

### 1. 签名或模板审核不通过

**原因**：
- 签名与备案域名不一致
- 模板内容不符合规范
- 证明材料不清晰

**解决**：
- 确保签名与备案域名一致
- 模板变量格式使用 `${变量名}`
- 上传清晰的营业执照照片

### 2. 发送失败，返回错误码

**常见错误码**：
- `isv.BUSINESS_LIMIT_CONTROL`：发送频率超限
- `isv.INVALID_PARAMETERS`：参数错误
- `isv.SMS_SIGNATURE_ILLEGAL`：签名不合法
- `isv.SMS_TEMPLATE_ILLEGAL`：模板不合法

**解决**：
- 检查签名和模板是否审核通过
- 检查手机号格式是否正确
- 检查模板变量格式是否正确

### 3. 发送成功但收不到短信

**可能原因**：
- 手机号被运营商拦截
- 短信内容触发风控
- 手机号在黑名单中

**解决**：
- 换个手机号测试
- 检查短信内容是否合规
- 联系阿里云客服

### 4. AccessKey 权限不足

**错误信息**：
```
Code: InvalidAccessKeyId.NotFound
```

**解决**：
- 检查 AccessKey ID 是否正确
- 检查 AccessKey 是否已启用
- 检查 RAM 子账号权限

## 费用说明

- **短信费用**：0.045元/条（国内）
- **免费额度**：新用户有100条免费额度
- **计费方式**：按条计费，发送成功才计费

## 安全建议

1. **使用 RAM 子账号**：
   - 创建专门的 RAM 子账号
   - 只授予「短信服务」权限
   - 不使用主账号的 AccessKey

2. **限制发送频率**：
   - 同一手机号：1条/分钟，5条/小时
   - 同一IP：5条/分钟

3. **监控告警**：
   - 设置短信发送量告警
   - 监控异常发送行为

4. **定期轮换 AccessKey**：
   - 建议每3个月轮换一次
   - 轮换时先创建新 Key，再删除旧 Key

## 相关文档

- [阿里云短信服务官方文档](https://help.aliyun.com/product/44286.html)
- [短信服务API参考](https://help.aliyun.com/document_detail/101414.html)
- [短信服务错误码](https://help.aliyun.com/document_detail/101345.html)
