refactor: 两个「数据泥团」收成 dataclass / 字典

为什么:上一轮报告「11 个参数 > 6」的清单实测只有 7 个 ≥7 参数,其中真正
该改的是 2 个(另 5 个是 HTTP/下载原语的参数透传,合并反而降低清晰度,有意
保留)。

1) auth_system.create_user(11 参 → 6 参)
   前 5 个是账号属性(username/password/email/credits/role,同属一个概念),
   后 6 个才是泥团——它们是「注册风控上下文」(同 IP 频率闸 + 设备赠送闸)。
   收成 frozen dataclass RegistrationGuard,默认值等价于「不做任何风控」,故
   老调用点语义不变。

2) web/routers/download.py._single_task(11 参 → 2 参)
   11 个位置参数改成一个 params 字典 + device_id,字典原样透传给 worker。
   顺带删掉原本就**未被使用**的 data 参数(11 个里有 1 个是死的)。
   理由不是「参数多」而是这 11 个位置参数没有名字,调用点极易错序。

教训:create_user 签名重构时漏改 web/routers/admin_site.py:155,pytest 189 例
全绿(该端点无测试覆盖)但 pyright 报出 2 errors —— 两道门禁互补的实证,
说明类型检查不是冗余。
This commit is contained in:
snowy-yang committed 2026-09-26 18:54:17 +08:00
1 parent 0569b50737
commit 3de75f7479
5 files changed
+53 -27

No files matched your search

+2
View File
@@ -124,6 +124,7 @@ from .settings import (
set_setting,
)
from .users import (
RegistrationGuard,
_last_province,
_user_public,
ban_user,
@@ -166,6 +167,7 @@ __all__ = [
'_PBKDF2_ITERS',
'_PROVINCE_CACHE',
'_SMTP_ENC_PREFIX',
'RegistrationGuard',
'_auto_backup_loop',
'_backup_is_valid_db',
'_backup_thread_started',
+30 -12
View File
@@ -7,6 +7,7 @@
import json
import sqlite3
import time
from dataclasses import dataclass
from typing import Any
from .db import (
@@ -22,10 +23,26 @@ from .security import (
)
@dataclass(frozen=True)
class RegistrationGuard:
"""注册风控上下文:同 IP 频率闸 + 设备赠送闸。
这些检查必须与建号 INSERT 在**同一把 _DB_LOCK 事务内**完成——锁外预检存在
TOCTOU(并发同设备双注册各得一次赠送、并发同 IP 挤过限额),所以它们不是
「可选的附加参数」,而是事务内必须可见的输入。管理建号无风控需求,不传即可。
"""
device_id: str = ''
ip: str = ''
ua: str = ''
ip_limit: int = 0
ip_window: int = 86400
device_once: bool = False
def create_user(username: str, password: str, email: str = '', credits: int = 0,
role: str = 'user', device_id: str = '', ip: str = '', ua: str = '',
register_ip_limit: int = 0, register_ip_window: int = 86400,
register_device_once: bool = False) -> sqlite3.Row | None:
role: str = 'user', *,
guard: RegistrationGuard | None = None) -> sqlite3.Row | None:
# 余额只接受 -1(不限)或 >= 0;非法负值一律按 0 处理,禁止出现 -2 这类"假不限"
try:
credits = int(credits)
@@ -33,23 +50,24 @@ def create_user(username: str, password: str, email: str = '', credits: int = 0,
credits = 0
if credits < -1:
credits = 0
# 注册入口传 register_ip_limit=3 —— 同 IP 24h 注册数检查挪进
# _DB_LOCK 事务内与 INSERT 原子完成,堵住"锁外检查+并发插入"的 TOCTOU 刷号;
# 管理建号与不限制场景传 0 关闭。超限抛 ValueError 由调用方转 400/429。
# 注册入口传 guard=RegistrationGuard(ip_limit=3, device_once=True)——同 IP 24h
# 注册数检查在 _DB_LOCK 事务内与 INSERT 原子完成,堵住「锁外检查 + 并发插入」
# 的 TOCTOU 刷号;管理建号不传 guard(即不限制)。超限抛 ValueError 由调用方转 400/429。
guard = guard or RegistrationGuard()
now = int(time.time())
with _DB_LOCK:
conn = get_conn()
try:
if register_ip_limit > 0 and ip:
if guard.ip_limit > 0 and guard.ip:
n = conn.execute('SELECT COUNT(*) AS n FROM users WHERE register_ip = ? AND created_at > ?',
(ip, now - register_ip_window)).fetchone()[0]
if n >= register_ip_limit:
(guard.ip, now - guard.ip_window)).fetchone()[0]
if n >= guard.ip_limit:
raise ValueError('当前网络注册过于频繁,请 24 小时后再试或联系管理员')
if register_device_once and device_id:
if guard.device_once and guard.device_id:
# 设备赠送闸并入同一事务——锁外 device_used 预检有 TOCTOU,
# 并发同设备双注册可各得一次赠送;锁内复查命中则本次建号不赠送(账号照常创建)
n = conn.execute("SELECT COUNT(*) AS n FROM users WHERE trial_device = ? AND role != 'admin'",
(device_id,)).fetchone()[0]
(guard.device_id,)).fetchone()[0]
if n > 0:
credits = 0
conn.execute(
@@ -58,7 +76,7 @@ def create_user(username: str, password: str, email: str = '', credits: int = 0,
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
(username, _hash_new(password), '', email, role,
-1 if role == 'admin' else int(credits),
device_id, ip, ua, now, now))
guard.device_id, guard.ip, guard.ua, now, now))
conn.commit()
finally:
conn.close()