31bff63ed4
背景 短信验证码已是可切换 provider 架构(极光 / 创蓝)。极光(默认)一旦供应商侧故障(欠费 / 网络 / 服务异常),/sms/send 直接 503 → 用户收不到码、登录中断。本 PR 把极光设为主、创蓝设为备,在极光供应商不可用时自动转创蓝补发,并让后台可区分每次实际走的渠道。 方案(4 个关键决策) # | 决策 | 结论 -- | -- | -- A | fallback 触发范围 | 仅主返回「供应商不可用」(SmsError.status_code == 503:网络 / 余额 / 服务故障)才转备。本地冷却 & 超频(429)、手机号无效(400)不转——不绕过防刷、不为无效号白烧 B | 校验路由 | try-both:极光转创蓝后码在创蓝内存,校验遍历「启用的 fallback 链」(主→备),任一命中即通过;关闭 fallback 时链中只有极光、创蓝零参与 C | 后台可见性 | 成功侧 EVENT_SMS_SEND.details 记 provider / fallback + 分派层日志,风控后台可按号/设备查本次走哪家、是否 fallback D | 默认开关 | SMS_FALLBACK_PROVIDER 默认空=关(保持现状零风险),生产设 chuanglan 开启,置空即秒回退。仅 Mode B(jiguang/chuanglan)互为主备 --------- Co-authored-by: guke <guke@autohome.com.cn> Reviewed-on: #204
40 lines
1.5 KiB
Python
40 lines
1.5 KiB
Python
"""短信 provider 共享基座:业务异常 + provider 无关的 mock 校验。
|
||
|
||
各 provider(jiguang / aliyun)都 `from .base import SmsError`,api 层也从包入口拿到同一个
|
||
`SmsError` —— 保证无论用哪个 provider,异常类型与 HTTP 码映射语义一致。
|
||
`SendResult` 是分派层 `send_code` 的返回类型,携带 cooldown、实际 provider 及是否走了 fallback。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
|
||
from app.core.config import settings
|
||
|
||
|
||
class SmsError(Exception):
|
||
"""业务异常。`status_code` 决定 api 层翻成哪个 HTTP 码:
|
||
过频/每日超限 = 429(客户端等会再来),供应商不可用 = 503,手机号无效 = 400。
|
||
"""
|
||
|
||
def __init__(self, message: str, status_code: int = 429) -> None:
|
||
super().__init__(message)
|
||
self.status_code = status_code
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class SendResult:
|
||
"""发码结果:距下次可发秒数 + 实际发码的 provider 名 + 是否走了备(fallback)。
|
||
|
||
分派层 send_code 的返回类型(provider 各自的 send_code 仍返回 int cooldown,
|
||
由分派层包装)。auth 层据此把渠道记入风控流水 details。
|
||
"""
|
||
|
||
cooldown_sec: int
|
||
provider: str
|
||
fallback: bool = False
|
||
|
||
|
||
def mock_verify(code: str) -> bool:
|
||
"""mock 模式校验:放行任意 SMS_CODE_LENGTH 位数字(provider 无关,测试/开发便利,不真校验)。"""
|
||
return len(code) == settings.SMS_CODE_LENGTH and code.isdigit()
|