Archived
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:
1 parent
0569b50737
commit
3de75f7479
5 files changed
+53
-27
No files matched your search
@@ -124,6 +124,7 @@ from .settings import (
|
|||||||
set_setting,
|
set_setting,
|
||||||
)
|
)
|
||||||
from .users import (
|
from .users import (
|
||||||
|
RegistrationGuard,
|
||||||
_last_province,
|
_last_province,
|
||||||
_user_public,
|
_user_public,
|
||||||
ban_user,
|
ban_user,
|
||||||
@@ -166,6 +167,7 @@ __all__ = [
|
|||||||
'_PBKDF2_ITERS',
|
'_PBKDF2_ITERS',
|
||||||
'_PROVINCE_CACHE',
|
'_PROVINCE_CACHE',
|
||||||
'_SMTP_ENC_PREFIX',
|
'_SMTP_ENC_PREFIX',
|
||||||
|
'RegistrationGuard',
|
||||||
'_auto_backup_loop',
|
'_auto_backup_loop',
|
||||||
'_backup_is_valid_db',
|
'_backup_is_valid_db',
|
||||||
'_backup_thread_started',
|
'_backup_thread_started',
|
||||||
|
|||||||
+30
-12
@@ -7,6 +7,7 @@
|
|||||||
import json
|
import json
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import time
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .db import (
|
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,
|
def create_user(username: str, password: str, email: str = '', credits: int = 0,
|
||||||
role: str = 'user', device_id: str = '', ip: str = '', ua: str = '',
|
role: str = 'user', *,
|
||||||
register_ip_limit: int = 0, register_ip_window: int = 86400,
|
guard: RegistrationGuard | None = None) -> sqlite3.Row | None:
|
||||||
register_device_once: bool = False) -> sqlite3.Row | None:
|
|
||||||
# 余额只接受 -1(不限)或 >= 0;非法负值一律按 0 处理,禁止出现 -2 这类"假不限"
|
# 余额只接受 -1(不限)或 >= 0;非法负值一律按 0 处理,禁止出现 -2 这类"假不限"
|
||||||
try:
|
try:
|
||||||
credits = int(credits)
|
credits = int(credits)
|
||||||
@@ -33,23 +50,24 @@ def create_user(username: str, password: str, email: str = '', credits: int = 0,
|
|||||||
credits = 0
|
credits = 0
|
||||||
if credits < -1:
|
if credits < -1:
|
||||||
credits = 0
|
credits = 0
|
||||||
# 注册入口传 register_ip_limit=3 —— 同 IP 24h 注册数检查挪进
|
# 注册入口传 guard=RegistrationGuard(ip_limit=3, device_once=True)——同 IP 24h
|
||||||
# _DB_LOCK 事务内与 INSERT 原子完成,堵住"锁外检查+并发插入"的 TOCTOU 刷号;
|
# 注册数检查在 _DB_LOCK 事务内与 INSERT 原子完成,堵住「锁外检查 + 并发插入」
|
||||||
# 管理建号与不限制场景传 0 关闭。超限抛 ValueError 由调用方转 400/429。
|
# 的 TOCTOU 刷号;管理建号不传 guard(即不限制)。超限抛 ValueError 由调用方转 400/429。
|
||||||
|
guard = guard or RegistrationGuard()
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
with _DB_LOCK:
|
with _DB_LOCK:
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
try:
|
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 > ?',
|
n = conn.execute('SELECT COUNT(*) AS n FROM users WHERE register_ip = ? AND created_at > ?',
|
||||||
(ip, now - register_ip_window)).fetchone()[0]
|
(guard.ip, now - guard.ip_window)).fetchone()[0]
|
||||||
if n >= register_ip_limit:
|
if n >= guard.ip_limit:
|
||||||
raise ValueError('当前网络注册过于频繁,请 24 小时后再试或联系管理员')
|
raise ValueError('当前网络注册过于频繁,请 24 小时后再试或联系管理员')
|
||||||
if register_device_once and device_id:
|
if guard.device_once and guard.device_id:
|
||||||
# 设备赠送闸并入同一事务——锁外 device_used 预检有 TOCTOU,
|
# 设备赠送闸并入同一事务——锁外 device_used 预检有 TOCTOU,
|
||||||
# 并发同设备双注册可各得一次赠送;锁内复查命中则本次建号不赠送(账号照常创建)
|
# 并发同设备双注册可各得一次赠送;锁内复查命中则本次建号不赠送(账号照常创建)
|
||||||
n = conn.execute("SELECT COUNT(*) AS n FROM users WHERE trial_device = ? AND role != 'admin'",
|
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:
|
if n > 0:
|
||||||
credits = 0
|
credits = 0
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -58,7 +76,7 @@ def create_user(username: str, password: str, email: str = '', credits: int = 0,
|
|||||||
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||||
(username, _hash_new(password), '', email, role,
|
(username, _hash_new(password), '', email, role,
|
||||||
-1 if role == 'admin' else int(credits),
|
-1 if role == 'admin' else int(credits),
|
||||||
device_id, ip, ua, now, now))
|
guard.device_id, guard.ip, guard.ua, now, now))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
@@ -151,8 +151,9 @@ async def user_create(admin: AdminUser, request: Request):
|
|||||||
return error('用户名已存在')
|
return error('用户名已存在')
|
||||||
if email and await asyncio.to_thread(auth.get_user_by_email, email):
|
if email and await asyncio.to_thread(auth.get_user_by_email, email):
|
||||||
return error('该邮箱已被注册,请更换邮箱')
|
return error('该邮箱已被注册,请更换邮箱')
|
||||||
await asyncio.to_thread(auth.create_user, username, password, email=email,
|
await asyncio.to_thread(
|
||||||
credits=credits, ip='-', ua='admin创建')
|
auth.create_user, username, password, email=email, credits=credits,
|
||||||
|
guard=auth.RegistrationGuard(ip='-', ua='admin创建'))
|
||||||
await _log(admin, '添加用户', f'{username} · {credits} 次')
|
await _log(admin, '添加用户', f'{username} · {credits} 次')
|
||||||
return success(message=f'用户 {username} 创建成功({credits} 次)')
|
return success(message=f'用户 {username} 创建成功({credits} 次)')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
+14
-10
@@ -169,8 +169,12 @@ async def download(user: Annotated[SiteUser | None, Depends(current_user)], requ
|
|||||||
# file 模式:注册后台任务
|
# file 模式:注册后台任务
|
||||||
device_id = str(data.get('device_id') or '').strip()[:64]
|
device_id = str(data.get('device_id') or '').strip()[:64]
|
||||||
await asyncio.to_thread(services_tasks._touch_device, device_id)
|
await asyncio.to_thread(services_tasks._touch_device, device_id)
|
||||||
task = _single_task(data, music_id, quality, filename_format, write_meta,
|
task = _single_task({
|
||||||
meta_fields, lyric_format, need_zip, cookies, ip, device_id)
|
'music_id': music_id, 'quality': quality,
|
||||||
|
'filename_format': filename_format, 'write_meta': write_meta,
|
||||||
|
'meta_fields': meta_fields, 'lyric_format': lyric_format,
|
||||||
|
'need_zip': need_zip, 'cookies': cookies, 'remote_ip': ip,
|
||||||
|
}, device_id)
|
||||||
reg_err = await register_download_task(billing, task, str(data.get('task_id') or '').strip())
|
reg_err = await register_download_task(billing, task, str(data.get('task_id') or '').strip())
|
||||||
if reg_err:
|
if reg_err:
|
||||||
await billing.refund_open('rejected')
|
await billing.refund_open('rejected')
|
||||||
@@ -189,20 +193,20 @@ async def download(user: Annotated[SiteUser | None, Depends(current_user)], requ
|
|||||||
return error('服务暂时不可用,请稍后再试', 500)
|
return error('服务暂时不可用,请稍后再试', 500)
|
||||||
|
|
||||||
|
|
||||||
def _single_task(data, music_id, quality, filename_format, write_meta, meta_fields,
|
def _single_task(params: dict[str, Any], device_id: str) -> dict[str, Any]:
|
||||||
lyric_format, need_zip, cookies, ip, device_id) -> dict[str, Any]:
|
"""组装单曲下载任务记录(params 原样透传给 worker)。
|
||||||
|
|
||||||
|
改成「一个字典 + device_id」而非 11 个位置参数:原先调用点要按固定顺序
|
||||||
|
传 11 个几乎全是字符串的参数,错位不报错却会静默串味(quality 与
|
||||||
|
filename_format 互换这类),而本函数只是把它们原样放进 params。
|
||||||
|
"""
|
||||||
return {
|
return {
|
||||||
'type': 'single',
|
'type': 'single',
|
||||||
'total': 1, 'done': 0, 'failed': 0,
|
'total': 1, 'done': 0, 'failed': 0,
|
||||||
'status': 'queued', 'start': time.time(), 'msg': '排队中...',
|
'status': 'queued', 'start': time.time(), 'msg': '排队中...',
|
||||||
'cancel': threading.Event(), 'pause': threading.Event(),
|
'cancel': threading.Event(), 'pause': threading.Event(),
|
||||||
'device_id': device_id, 'result': None,
|
'device_id': device_id, 'result': None,
|
||||||
'params': {
|
'params': params,
|
||||||
'music_id': music_id, 'quality': quality,
|
|
||||||
'filename_format': filename_format, 'write_meta': write_meta,
|
|
||||||
'meta_fields': meta_fields, 'lyric_format': lyric_format,
|
|
||||||
'need_zip': need_zip, 'cookies': cookies, 'remote_ip': ip,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -186,9 +186,10 @@ async def register(request: Request):
|
|||||||
try:
|
try:
|
||||||
user = await asyncio.to_thread(
|
user = await asyncio.to_thread(
|
||||||
auth.create_user, username, password, email=email, credits=credits,
|
auth.create_user, username, password, email=email, credits=credits,
|
||||||
device_id=device_id, ip=ip,
|
guard=auth.RegistrationGuard(
|
||||||
ua=str(request.headers.get('user-agent') or ''),
|
device_id=device_id, ip=ip,
|
||||||
register_ip_limit=3, register_device_once=True)
|
ua=str(request.headers.get('user-agent') or ''),
|
||||||
|
ip_limit=3, device_once=True))
|
||||||
except sqlite3.IntegrityError:
|
except sqlite3.IntegrityError:
|
||||||
await asyncio.to_thread(auth.restore_verify_code, email, code, code_ttl, 'register')
|
await asyncio.to_thread(auth.restore_verify_code, email, code, code_ttl, 'register')
|
||||||
return error('用户名或邮箱已被注册,请换一个再试')
|
return error('用户名或邮箱已被注册,请换一个再试')
|
||||||
|
|||||||
Reference in new issue
Block a user