refactor: 优化API路由和响应模型 feat(admin): 添加App用户管理接口 feat(sms): 实现阿里云短信服务集成 feat(email): 添加SMTP邮件发送功能 feat(upload): 支持文件上传接口 feat(rate-limiter): 实现手机号限流器 fix: 修复计算步骤入库问题 docs: 更新API文档和测试计划 chore: 更新依赖和配置
5.2 KiB
5.2 KiB
目标与范围
- 接入阿里云短信服务,封装发送客户端
- 提供两类发送接口:验证码通知、报告生成通知,供 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)
- 初始化 Dysms 客户端(读取
- 新增:
app/services/rate_limiter.py- 类:
PhoneRateLimiter,键为手机号,值为最近一次发送时间戳;判定 60s 内拒绝
- 类:
- 新增路由:
app/api/v1/sms/sms.pyPOST /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_498190229TemplateParam:{"code": "<动态验证码>"}与${code}正确对应
- 报告通知模板:
SMS_498140213- 不含变量,可传空对象
{}或不传TemplateParam
- 不含变量,可传空对象
- 签名:
ALIYUN_SMS_SIGN_NAME读取为“成都文化产权交易所”且不在代码中硬编码
日志与审计
- 路由层:审计中间件自动记录请求/响应(
module=短信服务,summary=验证码发送/报告通知发送) - 服务层:
from app.log import logger- 发送开始、Provider 请求入参(不含敏感信息)、返回码、
RequestId、耗时、失败异常
- 发送开始、Provider 请求入参(不含敏感信息)、返回码、
- 敏感信息不入日志:AccessKey、完整模板内容不打印
频率限制
- 策略:同一手机号在 60 秒内全模板合并限 1 次(共享窗口)
- 实现:进程内
dict[phone]=last_ts;进入路由先校验再发送;返回 429(或业务错误码) - 进阶:如需多实例一致性,后续接入 Redis,键:
sms:limit:{phone}TTL=60s
安全与配置
- 环境变量:
ALIBABA_CLOUD_ACCESS_KEY_IDALIBABA_CLOUD_ACCESS_KEY_SECRETALIYUN_SMS_SIGN_NAMEALIYUN_SMS_ENDPOINT(默认dysmsapi.aliyuncs.com)
- Pydantic Settings 统一读取,避免硬编码,并在
/docs与审计中隐藏敏感字段
依赖与安装
- 在
pyproject.toml添加:alibabacloud_dysmsapi20170525alibabacloud_tea_openapialibabacloud_tea_util
- 与
requirements.txt保持一致版本钉死策略;Python 3.11 兼容性验证
测试与验证
- 单元测试:
- Mock SDK 客户端,校验
TemplateCode与TemplateParam的正确构造 - 限流:同号 60s 内第二次返回限制错误
- Mock SDK 客户端,校验
- 集成测试:
- 使用
httpx.AsyncClient调用两个接口并断言响应结构 - 在预设测试手机号上进行真实发送,观察到达与模板内容正确
- 使用
- 观测:
- 查看应用日志与审计表(
AuditLog)记录
- 查看应用日志与审计表(
风险与回滚
- 进程内限流仅在单实例有效,多实例需 Redis(后续迭代)
- SDK 版本冲突,采用独立最小版本并逐项验证;必要时锁版本
- 若出现发送失败,保留错误码与
RequestId,按官方错误码表排查(见 SendSms 文档)
交付物
- 新增短信客户端与路由模块
- 两个可调用接口(验证码发送、报告通知发送)
- 限流与日志落地,配置基于环境变量