fix: 修复重构遗留缺陷,补齐 pyright 检查盲区

上一次重构(8446166)声称「pyright 0 错误」,但 pyproject 里 pyright 的 include
只覆盖 core/web/services —— 根目录 6 个应用模块与整个 tests/ 从未被检查,而
requests→sync_http 迁移的遗留恰好全部藏在那里。补齐 include 后立刻暴露
7 errors + 9 warnings,其中数处在运行时静默失效。

修复:
- services/runtime.py:删除 _podcast_billing_count/_billing_ids_count 两个
  无调用方的死函数。它们仍调用 api_service._safe_get_request_data(),而该方法
  在剥离 Flask 时已被删除,异常又被 except 吞掉 —— 一旦被调用,播客/歌单计费
  恒为 1 次且不报任何错。路由层已各自实现同口径版本,故整体删除。
- web/routers/admin_site.py:备份上传的类型判定必须用 starlette 的 UploadFile。
  fastapi.UploadFile 是它的子类,而 request.form() 返回基类实例,用错会让
  isinstance 恒为 False,把每一次真实上传都判成「请选择数据库文件」。
  已补 4 例真实 multipart 回归测试,并用变异测试确认能抓住该缺陷。
- auth_system.py:修正 create_user(实返 sqlite3.Row|None)与 list_cards
  (实返 (cards,total) 元组)与实现不符的返回注解;注解一改正,立刻暴露出被它
  掩盖的 register.py 中 int(user['id']) 未做 None 检查的 500 崩溃路径,已补守卫。
- music_downloader.py:修正注解引用的 core.sync_http.Response(正确名应为
  SyncResponse);封面缓存淘汰不再读类型为 Optional 的 deque.maxlen,改用命名常量。
- services/runtime.py:zip worker 的 result.music_info 可能为 None,原样使用会
  炸掉整批任务,改为按单曲失败处理。
- music_api.py:三处 isinstance 三元表达式收敛为 _as_dict() 帮助函数。
- web/main.py:文档串移除已删除的 Flask 兜底描述,补充 data/ 相对 CWD 解析的提醒。

验证:ruff check 通过;pyright 0 errors / 0 warnings(filesAnalyzed=67);
pytest 144 passed;真实 uvicorn 启动冒烟通过(/health、/、静态资源 200,
未鉴权管理端点 403)。生产库仅做只读 integrity_check,未被写入。
This commit is contained in:
snowy-yang committed 2026-09-26 11:52:06 +08:00
1 parent 8446166120
commit 838d635bc5
14 files changed
+152 -78

No files matched your search

+7 -4
View File
@@ -288,7 +288,7 @@ def _verify_password(stored: str, password: str, salt: str) -> tuple[bool, bool]
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) -> dict[str, Any]:
register_device_once: bool = False) -> sqlite3.Row | None:
# F77:余额只接受 -1(不限)或 >= 0;非法负值一律按 0 处理,禁止出现 -2 这类"假不限"
try:
credits = int(credits)
@@ -994,7 +994,9 @@ def send_email(to: str, subject: str, body: str) -> None:
raise ValueError('收件邮箱格式不正确')
cfg = get_smtp_config()
msg = MIMEText(body, 'plain', 'utf-8')
msg['Subject'] = Header(subject, 'utf-8')
# email 库推荐写法:Header 对象由 Generator._fold 的 Header 分支做 RFC 2047 编码
# (typeshed 把 Message.__setitem__ 的值参数收窄成 str,属桩过窄;运行时已实测正确)
msg['Subject'] = Header(subject, 'utf-8') # type: ignore[assignment]
msg['From'] = formataddr(('云村小助手', cfg['sender']))
msg['To'] = to
with _smtp_connect() as server:
@@ -1183,8 +1185,9 @@ def generate_cards(count: int, credits: int, created_by: str = '',
return codes
def list_cards(limit: int = 100, offset: int = 0) -> list[dict[str, Any]]:
"""卡密列表(F85:支持分页 limit/offset,管理路由透传)"""
def list_cards(limit: int = 100, offset: int = 0) -> tuple[list[dict[str, Any]], int]:
"""卡密列表(F85:支持分页 limit/offset,管理路由透传)
返回 (当页卡密列表, 卡密总数)——总数供前端分页控件用,调用方按元组解包"""
limit = max(1, min(int(limit), 500))
offset = max(0, int(offset))
rows = _q('SELECT * FROM cards ORDER BY id DESC LIMIT ? OFFSET ?', (limit, offset))