"""测试账号:免验证码登录 + 每次重走新手引导 + 每日使用上限。 为打通 **release 包全流程**(无 SIM 卡、不走极光一键登录)而设的一个固定测试手机号。 对这个号(且仅这个号)放开三件事: 1. **免短信验证码**:`/sms/send` 不真发、`/sms/login` 跳过 `verify_code`,real 模式下同样生效 —— 测试时直接填任意验证码即可登入,不用等真短信。 2. **每次都走新手引导**:登录响应 `onboarding_completed` 恒为 false,不读/不依赖 onboarding_completion 表,所以反复登录都能体验引导。 3. **每日使用次数上限**:防被人猜到这个号后写脚本一直刷。当天登录数超过上限即拒绝(429), 次日自动归零。 手机号与上限都在 .env 配(`TEST_ACCOUNT_PHONE` / `TEST_ACCOUNT_DAILY_LIMIT`),随时可改。 `TEST_ACCOUNT_PHONE` 留空 = 整个功能关闭(生产默认态),`is_test_account()` 对任何号都返回 False,登录/短信回到原逻辑,零影响。 计数存**进程内存**(单 worker 够用,与 sms.py 同款约定):重启清零、多 worker 不共享。作为 一个测试号的粗粒度防滥用闸够用;且因所有登录都落同一个 phone → 同一个 user,滥用面天然只 限于这一个账号。要严格持久化/跨进程再迁 DB 或 Redis(同 sms.py 的技术债)。 """ from __future__ import annotations import logging from datetime import datetime from threading import Lock from app.core.config import settings logger = logging.getLogger("shagua.test_account") # 进程内每日计数:(date_str, 当日已登录次数)。单 worker 有效,重启清零(见模块 docstring)。 _lock = Lock() _usage: tuple[str, int] = ("", 0) def _today() -> str: return datetime.now().strftime("%Y-%m-%d") def is_enabled() -> bool: """功能总开关:配了 TEST_ACCOUNT_PHONE 才启用(空=关闭)。""" return bool(settings.test_account_phone) def is_test_account(phone: str) -> bool: """该手机号是否为配置的测试账号。功能关闭时对任何号都返回 False。""" return is_enabled() and phone == settings.test_account_phone def try_consume_quota() -> bool: """测试账号登录时调:当日计数 +1。 Returns: True —— 未超当日上限,本次放行(计数已 +1); False —— 已达上限,本次拒绝(计数不变)。 线程安全;跨天自动归零。上限取自 settings.TEST_ACCOUNT_DAILY_LIMIT(可在 .env 改)。 """ global _usage limit = settings.TEST_ACCOUNT_DAILY_LIMIT with _lock: today = _today() day, cnt = _usage if day != today: # 跨天归零 cnt = 0 if cnt >= limit: _usage = (today, cnt) # 已满,保持不变 logger.warning( "测试账号 %s 今日登录数已达上限 %d,拒绝", settings.test_account_phone, limit ) return False _usage = (today, cnt + 1) logger.info("测试账号 %s 第 %d/%d 次登录", settings.test_account_phone, cnt + 1, limit) return True def _reset_for_test() -> None: """仅供单测:清空进程内计数,隔离用例间状态。""" global _usage with _lock: _usage = ("", 0)