## 目标与范围 - 接入阿里云短信服务,封装发送客户端 - 提供两类发送接口:验证码通知、报告生成通知,供 App 调用 - 支持模板动态调用与验证码变量 `${code}` 的正确替换 - 记录发送日志并融入现有审计体系 - 实现同一手机号每分钟不超过 1 条的频率限制 - 安全存储 AccessKey 等敏感信息(环境变量/配置) ## 技术选型 - 后端框架:FastAPI(现有工程) - 短信 SDK:Alibaba Cloud SMS Python SDK(Tea/OpenAPI V2,`alibabacloud_dysmsapi20170525`) - 端点(中国站):`dysmsapi.aliyuncs.com` - 关键请求字段:`PhoneNumbers`、`SignName`、`TemplateCode`、`TemplateParam` - 日志:沿用 `app/log` 的 Loguru 与审计中间件 - 频率限制:服务内共享的内存限流(后续可升级为 Redis) - 安全:通过环境变量注入凭证,Pydantic Settings 读取 - 参考文档: - Alibaba Cloud SDK V2(Python)示例(SendSms):https://www.alibabacloud.com/help/en/sdk/developer-reference/v2-python-integrated-sdk - 短信服务 SendSms 接口(2017-05-25):https://help.aliyun.com/zh/sms/developer-reference/api-dysmsapi-2017-05-25-sendsms ## 代码改动 - 新增:`app/services/sms_client.py` - 初始化 Dysms 客户端(读取 `ALIBABA_CLOUD_ACCESS_KEY_ID`、`ALIBABA_CLOUD_ACCESS_KEY_SECRET`、`ALIYUN_SMS_SIGN_NAME`、`ALIYUN_SMS_ENDPOINT`) - 方法:`send_by_template(phone, template_code, template_param_json)` - 方法:`send_code(phone, code)`(模板:`SMS_498190229`) - 方法:`send_report(phone)`(模板:`SMS_498140213`) - 新增:`app/services/rate_limiter.py` - 类:`PhoneRateLimiter`,键为手机号,值为最近一次发送时间戳;判定 60s 内拒绝 - 新增路由:`app/api/v1/sms/sms.py` - `POST /api/v1/sms/send-code`(无鉴权,用于登录场景) - `POST /api/v1/sms/send-report`(需要鉴权,防滥用) - 统一返回结构:`{status, message, request_id}` - 路由聚合:在 `app/api/v1/__init__.py` 注册 `sms_router(prefix="/sms", tags=["短信服务"])` - 配置:扩展 `app/settings/config.py`(Pydantic Settings)增加短信相关字段并从环境读入 ## 接口设计 - `POST /api/v1/sms/send-code` - 请求体:`{ "phone": "1390000****", "code": "123456" }` - 处理:限流校验 → 构造 `TemplateParam` 为 `{"code": "123456"}` → 调用 `SMS_498190229` - 成功:`{ "status": "OK", "message": "sent", "request_id": "..." }` - 失败:`{ "status": "ERROR", "message": "..." }` - `POST /api/v1/sms/send-report` - 请求体:`{ "phone": "1390000****" }` - 处理:鉴权(`DependAuth`)→ 限流校验 → 调用 `SMS_498140213` - 返回同上 - 校验:手机号格式(支持无前缀或 `+86`),`code` 为 4–8 位数字(可按需约束) ## 模板与变量替换 - 验证码模板:`SMS_498190229` - `TemplateParam`:`{"code": "<动态验证码>"}` 与 `${code}` 正确对应 - 报告通知模板:`SMS_498140213` - 不含变量,可传空对象 `{}` 或不传 `TemplateParam` - 签名:`ALIYUN_SMS_SIGN_NAME` 读取为“成都文化产权交易所”且不在代码中硬编码 ## 日志与审计 - 路由层:审计中间件自动记录请求/响应(`module=短信服务`,`summary=验证码发送/报告通知发送`) - 服务层:`from app.log import logger` - 发送开始、Provider 请求入参(不含敏感信息)、返回码、`RequestId`、耗时、失败异常 - 敏感信息不入日志:AccessKey、完整模板内容不打印 ## 频率限制 - 策略:同一手机号在 60 秒内全模板合并限 1 次(共享窗口) - 实现:进程内 `dict[phone]=last_ts`;进入路由先校验再发送;返回 429(或业务错误码) - 进阶:如需多实例一致性,后续接入 Redis,键:`sms:limit:{phone}` TTL=60s ## 安全与配置 - 环境变量: - `ALIBABA_CLOUD_ACCESS_KEY_ID` - `ALIBABA_CLOUD_ACCESS_KEY_SECRET` - `ALIYUN_SMS_SIGN_NAME` - `ALIYUN_SMS_ENDPOINT`(默认 `dysmsapi.aliyuncs.com`) - Pydantic Settings 统一读取,避免硬编码,并在 `/docs` 与审计中隐藏敏感字段 ## 依赖与安装 - 在 `pyproject.toml` 添加: - `alibabacloud_dysmsapi20170525` - `alibabacloud_tea_openapi` - `alibabacloud_tea_util` - 与 `requirements.txt` 保持一致版本钉死策略;Python 3.11 兼容性验证 ## 测试与验证 - 单元测试: - Mock SDK 客户端,校验 `TemplateCode` 与 `TemplateParam` 的正确构造 - 限流:同号 60s 内第二次返回限制错误 - 集成测试: - 使用 `httpx.AsyncClient` 调用两个接口并断言响应结构 - 在预设测试手机号上进行真实发送,观察到达与模板内容正确 - 观测: - 查看应用日志与审计表(`AuditLog`)记录 ## 风险与回滚 - 进程内限流仅在单实例有效,多实例需 Redis(后续迭代) - SDK 版本冲突,采用独立最小版本并逐项验证;必要时锁版本 - 若出现发送失败,保留错误码与 `RequestId`,按官方错误码表排查(见 SendSms 文档) ## 交付物 - 新增短信客户端与路由模块 - 两个可调用接口(验证码发送、报告通知发送) - 限流与日志落地,配置基于环境变量