Files
server-core/app/services/sms.py
T
Pine b90539a013 feat(server): 统一登录+算力打通(port 阶段0-1)落地四层架构
- 登录:/auth/send-code·phone-login·wx-login·wx-phone(复用 RBAC 签发)
- 算力:services/compute_client(loopback :3000)+ /admin/compute/ping·provision
- /v1 relay 改走 compute-engine(前端不直连引擎)
- 模型:User 补 wx_openid/phone;仓储 async create+新方法
- 契约:api/schemas(auth/compute)Pydantic
2026-08-24 14:16:34 +08:00

68 lines
2.3 KiB
Python

# -*- coding: utf-8 -*-
"""短信验证码服务(统一登录)。
默认 ``stub`` provider 仅打印验证码不发真实短信,便于无短信资质联调;
接入真实短信(阿里云 / 腾讯云)时新增 provider 分支即可,接口不变。
验证码存内存(带 TTL 与尝试次数),进程重启即失效——生产应换
Redis 存储(见 云超服一体化架构设计方案 §8.4 缓存服务)。
"""
from __future__ import annotations
import secrets
import threading
import time
from .. import config
# phone -> {"code": str, "expires_at": float, "attempts": int}
_STORE: dict[str, dict] = {}
_LOCK = threading.Lock()
_CODE_TTL = config.SMS_CODE_TTL_SECONDS
_MAX_ATTEMPTS = config.SMS_RATE_LIMIT
class SmsError(Exception):
"""短信验证码业务异常(校验失败 / 限流等)。"""
def _send(phone: str, code: str) -> None:
"""按 provider 发送验证码。stub 只打印,供联调与测试。"""
if config.SMS_PROVIDER == "stub":
print(f"[sms:stub] verification-code -> {phone}: {code}")
return
# 预留:接入阿里云 / 腾讯云短信 SDK(环境注入凭据),按 provider 分发。
raise SmsError(f"unsupported sms provider: {config.SMS_PROVIDER}")
def issue(phone: str) -> str:
"""为手机号生成并发送 6 位验证码,返回明文(stub 下发时打印)。"""
code = f"{secrets.randbelow(1_000_000):06d}"
with _LOCK:
_STORE[phone] = {
"code": code,
"expires_at": time.monotonic() + _CODE_TTL,
"attempts": 0,
}
_send(phone, code)
return code
def verify(phone: str, code: str) -> bool:
"""校验验证码;成功即作废,失败累计次数超限则不可再试。"""
with _LOCK:
rec = _STORE.get(phone)
if not rec:
raise SmsError("verification code not issued")
if time.monotonic() > rec["expires_at"]:
_STORE.pop(phone, None)
raise SmsError("verification code expired")
if rec["attempts"] >= _MAX_ATTEMPTS:
raise SmsError("too many attempts, request a new code")
if rec["code"] != code.strip():
rec["attempts"] += 1
raise SmsError("invalid verification code")
_STORE.pop(phone, None)
return True