"""短信 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()