This repository has been archived on 2026-10-02. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
NeteaseUrl/web/routers/delivery.py
T
snowy-yang cd0f1f78a3 refactor: 迁移收尾(管理员2FA)+ 抽取公共工具层,消除 router 间重复
补齐最后缺口:
- POST /api/v1/auth/admin_login/email、/auth/admin_login/verify(管理员 2FA Step2/3),
  复用 auth_routes 的票据表与尝试上限(跨栈共享),签发会话 Cookie

重构(消除约 200 行重复,11 个 router 受益):
- web/request_utils.py:request_data / safe_int / extract_id / cookies / dedup_ids
  原在各 router 各复制一份,现集中
- web/serializers.py:format_credits / user_public / record_req_log
- 各 router 改为导入别名,行为逐字不变

覆盖率:Flask 90 条业务路由全部有 FastAPI 等价端点(含 5 个向后兼容别名)。
验证:pytest 69/69;ruff All checks passed;pyright 0 errors;奇偶 36/36;冒烟 10/10。
2026-09-26 10:20:46 +08:00

112 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""结果交付端点(FastAPI 栈)——与 Flask 侧同名端点响应契约逐字段对齐。
已迁移:
- POST/GET /api/v1/download/result ↔ Flask /download/result(单曲)
- POST/GET /api/v1/download/batch_zip/result ↔ Flask /download/batch_zip/result(批量 ZIP)
语义对齐要点:
- 任务归属:仅任务本人的站点账号可领取(非本人 403)
- 状态语义:进行中 202 / 暂停 200+提示 / 取消 200+原因 / 失败 500+原因 /
文件缺失 404(与 Flask 分支逐一对应)
- 交付物:FileResponse 流式发送;Cache-Control: no-store;
X-Download-Filename(URL 编码,供前端取回原始文件名)
- 文件清理仍由监控线程按 35 分钟保留期统一回收(交付请求不删文件)
"""
import asyncio
import os
from typing import Annotated, Any
from urllib.parse import quote
from fastapi import APIRouter, Depends, Request
from fastapi.responses import FileResponse
from core.state import DOWNLOAD_TASKS, DOWNLOAD_TASKS_LOCK
from web.deps import SiteUser, require_login
from web.request_utils import (
request_data as _request_data,
)
from web.response import error
router = APIRouter()
LoginUser = Annotated[SiteUser, Depends(require_login)]
def _owned_by(task: dict[str, Any], user: SiteUser) -> bool:
return int(task.get('user_id') or 0) == int(user.id)
def _file_response(target: str, filename: str, mimetype: str) -> FileResponse:
resp = FileResponse(target, media_type=mimetype, filename=filename)
resp.headers['Cache-Control'] = 'no-store'
resp.headers['X-Download-Filename'] = quote(filename, safe='')
return resp
@router.api_route('/download/result', methods=['GET', 'POST'])
async def download_result(user: LoginUser, request: Request):
"""单曲下载结果:任务完成后流式返回文件。"""
data = await _request_data(request)
task_id = str(data.get('task_id') or '').strip()
if not task_id:
return error('缺少 task_id 参数')
with DOWNLOAD_TASKS_LOCK:
task = DOWNLOAD_TASKS.get(task_id)
if not task or task.get('type') != 'single':
return error('任务不存在或已过期', 404)
if not _owned_by(task, user):
return error('无权下载该任务', 403)
status = task.get('status')
if status in ('queued', 'running'):
return error('任务还在进行中,请稍后再试', 202)
if status == 'paused':
return error('任务已暂停,请重新打开页面后恢复', 200)
result = task.get('result') or {}
target = result.get('zip_path') or result.get('file_path')
target_exists = bool(target) and await asyncio.to_thread(os.path.exists, target)
if status in ('canceled', 'error') and not target_exists:
return error(task.get('msg') or ('任务已取消' if status == 'canceled' else '任务失败'),
200 if status == 'canceled' else 500)
if not target_exists:
return error('文件不存在或已被清理', 404)
filename = result.get('filename') or 'download'
assert target is not None
return _file_response(target, filename, result.get('mimetype') or 'application/octet-stream')
@router.api_route('/download/batch_zip/result', methods=['GET', 'POST'])
async def download_batch_zip_result(user: LoginUser, request: Request):
"""批量打包结果:任务完成后流式返回 ZIP。"""
data = await _request_data(request)
task_id = str(data.get('task_id') or '').strip()
if not task_id:
return error('缺少 task_id 参数')
with DOWNLOAD_TASKS_LOCK:
task = DOWNLOAD_TASKS.get(task_id)
if not task or task.get('type') != 'batch':
return error('任务不存在或已过期', 404)
if not _owned_by(task, user):
return error('无权下载该任务', 403)
status = task.get('status')
result = task.get('result') or {}
zip_path = result.get('zip_path')
has_deliverable = bool(zip_path) and await asyncio.to_thread(os.path.exists, zip_path)
if status in ('queued', 'running'):
return error('任务还在进行中,请稍后再试', 202)
if status == 'paused':
return error('任务已暂停,请重新打开页面后恢复', 200)
# 取消/失败但已有部分成果的任务,成果必须可领取
if status in ('canceled', 'error') and not has_deliverable:
return error(task.get('msg') or ('任务已取消' if status == 'canceled' else '任务失败'),
200 if status == 'canceled' else 500)
if not has_deliverable:
return error('打包文件不存在或已被清理', 404)
zip_name = result.get('filename') or '批量下载.zip'
assert zip_path is not None
return _file_response(zip_path, zip_name, 'application/zip')