"""短信验证码服务。 当前实现:**mock 模式**。 - send_code(): 不真发短信,仅 log。记录 phone → 发送时间(防 60s 内重复发) - verify_code(): 任意 6 位数字均通过(配合前台 demo 行为) 后续接真供应商(阿里云 / 腾讯云)时: - send_code() 改为调供应商 API,把生成的 6 位码存到 cache(redis / sqlite) - verify_code() 比对 cache 里的码,且验过即作废 进程内存方案占坑期够用;切真实供应商时一并切 redis。 """ from __future__ import annotations import logging import time from threading import Lock from app.core.config import settings logger = logging.getLogger("shagua.sms") # 进程内最近发送时间表:{phone: epoch_seconds},用于 60s 内拒发 _last_sent: dict[str, float] = {} _lock = Lock() class SmsError(Exception): """业务异常:发送过频 / 验证码不对。api 层 catch 后翻成 4xx。""" def send_code(phone: str) -> int: """发送(或 mock 发送)验证码。 Returns: 距下一次可发的秒数(=0 表示刚刚已发,需等 SMS_SEND_INTERVAL_SEC 秒) Raises: SmsError 如果上次发送在 SMS_SEND_INTERVAL_SEC 内 """ now = time.time() with _lock: last = _last_sent.get(phone, 0.0) elapsed = now - last if elapsed < settings.SMS_SEND_INTERVAL_SEC: remain = int(settings.SMS_SEND_INTERVAL_SEC - elapsed) raise SmsError(f"send too frequent, retry in {remain}s") _last_sent[phone] = now if settings.SMS_MOCK: logger.info("[SMS-MOCK] send to %s****, code=any-6-digits", phone[:3]) else: # TODO: 接真实短信供应商 logger.warning("[SMS] real provider not implemented yet, phone=%s****", phone[:3]) raise SmsError("sms provider not configured") return settings.SMS_SEND_INTERVAL_SEC def verify_code(phone: str, code: str) -> bool: """校验验证码。mock 模式下任意 6 位数字均通过。""" if settings.SMS_MOCK: if len(code) == 6 and code.isdigit(): logger.info("[SMS-MOCK] verify ok for %s****", phone[:3]) return True logger.info("[SMS-MOCK] verify fail (need 6 digits) for %s****", phone[:3]) return False # TODO: 比对供应商发出的真实验证码 logger.warning("[SMS] real provider not implemented yet, phone=%s****", phone[:3]) return False