60c8fece23
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
73 lines
2.9 KiB
Python
73 lines
2.9 KiB
Python
"""短信验证码服务 —— provider 分派入口(极光主 + 可选创蓝备)。
|
||
|
||
对外暴露 `send_code`(返回 SendResult) / `verify_code` / `SmsError` / `SendResult`。
|
||
主 provider = settings.SMS_PROVIDER(默认 jiguang);备 = settings.SMS_FALLBACK_PROVIDER
|
||
(默认空=无备)。**每次调用读 settings**,支持运行时切换 / 灰度回退。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
|
||
from app.core.config import settings
|
||
|
||
from . import aliyun, chuanglan, jiguang
|
||
from .base import SendResult, SmsError
|
||
|
||
__all__ = ["SmsError", "SendResult", "send_code", "verify_code"]
|
||
|
||
logger = logging.getLogger("shagua.sms")
|
||
|
||
# provider 名 <-> 模块。未知/缺省主 provider 回退 jiguang(防误配把登录打挂)。
|
||
_ALL = {"jiguang": jiguang, "aliyun": aliyun, "chuanglan": chuanglan}
|
||
_NAME = {jiguang: "jiguang", aliyun: "aliyun", chuanglan: "chuanglan"}
|
||
|
||
|
||
def _primary():
|
||
"""当前主 provider 模块(未知/缺省回退 jiguang)。"""
|
||
return _ALL.get(settings.SMS_PROVIDER, jiguang)
|
||
|
||
|
||
def _fallback():
|
||
"""启用的备 provider 模块;未配置 / 同主 / 未知名 → None(= 不启用 fallback)。"""
|
||
name = (settings.SMS_FALLBACK_PROVIDER or "").strip()
|
||
if not name or name == settings.SMS_PROVIDER:
|
||
return None
|
||
return _ALL.get(name)
|
||
|
||
|
||
def send_code(phone: str) -> SendResult:
|
||
"""发码:主成功即返回;仅主「供应商不可用(503)」且配置了备时转备补发。
|
||
|
||
429(本地冷却/超频)、400(手机号无效)不转——不绕过防刷、不为无效号白烧。
|
||
备也失败则抛备的 SmsError。返回 SendResult(cooldown + 实际渠道 + 是否 fallback)。
|
||
"""
|
||
primary = _primary()
|
||
fb = _fallback()
|
||
try:
|
||
cooldown = primary.send_code(phone)
|
||
return SendResult(cooldown_sec=cooldown, provider=_NAME[primary], fallback=False)
|
||
except SmsError as e:
|
||
if fb is not None and e.status_code == 503:
|
||
logger.warning("[SMS] primary=%s 不可用(%s),fallback→%s",
|
||
_NAME[primary], e, _NAME[fb])
|
||
cooldown = fb.send_code(phone) # 备的冷却/错误码原样透出
|
||
return SendResult(cooldown_sec=cooldown, provider=_NAME[fb], fallback=True)
|
||
raise
|
||
|
||
|
||
def verify_code(phone: str, code: str) -> bool:
|
||
"""校验:try-both,遍历「启用的 fallback 链」(主→备),任一命中即 True。
|
||
|
||
码只存在实际发码那家(fallback 前主已 pop 掉自己的码),另一家 rec is None 即 False、
|
||
不误判、不累加其防爆破计数。关闭 fallback 时链中只有主,备完全不参与。
|
||
"""
|
||
chain = [_primary()]
|
||
fb = _fallback()
|
||
if fb is not None:
|
||
chain.append(fb)
|
||
for prov in chain:
|
||
if prov.verify_code(phone, code): # Mode B:纯本地内存比对,不联网
|
||
logger.info("[SMS] verify hit provider=%s", _NAME[prov])
|
||
return True
|
||
return False
|