guzhi/.trae/documents/接入阿里云短信并提供两个接口.md
邹方成 cc352d3184 feat: 重构后端服务并添加新功能
refactor: 优化API路由和响应模型
feat(admin): 添加App用户管理接口
feat(sms): 实现阿里云短信服务集成
feat(email): 添加SMTP邮件发送功能
feat(upload): 支持文件上传接口
feat(rate-limiter): 实现手机号限流器
fix: 修复计算步骤入库问题
docs: 更新API文档和测试计划
chore: 更新依赖和配置
2025-11-19 19:36:03 +08:00

5.2 KiB
Raw Blame History

目标与范围

  • 接入阿里云短信服务,封装发送客户端
  • 提供两类发送接口:验证码通知、报告生成通知,供 App 调用
  • 支持模板动态调用与验证码变量 ${code} 的正确替换
  • 记录发送日志并融入现有审计体系
  • 实现同一手机号每分钟不超过 1 条的频率限制
  • 安全存储 AccessKey 等敏感信息(环境变量/配置)

技术选型

代码改动

  • 新增:app/services/sms_client.py
    • 初始化 Dysms 客户端(读取 ALIBABA_CLOUD_ACCESS_KEY_IDALIBABA_CLOUD_ACCESS_KEY_SECRETALIYUN_SMS_SIGN_NAMEALIYUN_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.pyPydantic 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
    • 返回同上
  • 校验:手机号格式(支持无前缀或 +86code 为 48 位数字(可按需约束)

模板与变量替换

  • 验证码模板: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或业务错误码
  • 进阶:如需多实例一致性,后续接入 Redissms: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 客户端,校验 TemplateCodeTemplateParam 的正确构造
    • 限流:同号 60s 内第二次返回限制错误
  • 集成测试:
    • 使用 httpx.AsyncClient 调用两个接口并断言响应结构
    • 在预设测试手机号上进行真实发送,观察到达与模板内容正确
  • 观测:
    • 查看应用日志与审计表(AuditLog)记录

风险与回滚

  • 进程内限流仅在单实例有效,多实例需 Redis后续迭代
  • SDK 版本冲突,采用独立最小版本并逐项验证;必要时锁版本
  • 若出现发送失败,保留错误码与 RequestId,按官方错误码表排查(见 SendSms 文档)

交付物

  • 新增短信客户端与路由模块
  • 两个可调用接口(验证码发送、报告通知发送)
  • 限流与日志落地,配置基于环境变量