Files
beige/backend/app/api/v1/delivery.py
yangqianqian 6a2632da70 baseline: Clover 独立仓库首次基线提交
将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。
当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包),
M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 11:30:22 +08:00

185 lines
6.5 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.
"""
app/api/v1/delivery.py — 交付包路由
POST /tasks/{id}/package — 生成达人素材交付包
GET /delivery-packages/{id}/download — 下载status: pending/ready/downloaded
POST /delivery-packages/{id}/download-token — 签发60s一次性下载令牌C1坑修复
GET /delivery-packages/{id}/download-file?token= — 带令牌下载,前端可用 window.open
"""
import logging
from typing import Annotated
from fastapi import APIRouter, Depends, Header, Query
from fastapi.responses import FileResponse
from pydantic import BaseModel
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.response import ok, raise_not_found
from app.middleware.workspace_guard import CurrentUser, require_write_permission
from app.models.task import DeliveryPackage, GenerationTask
logger = logging.getLogger(__name__)
router = APIRouter(tags=["delivery"])
class PackageRequest(BaseModel):
note_ids: list[int] = [] # 可选,指定要打包的内容
def _fmt_package(pkg: DeliveryPackage) -> dict:
return {
"id": pkg.id,
"status": pkg.status,
"download_url": pkg.download_url,
"expires_at": pkg.expires_at.isoformat() if pkg.expires_at else None,
}
@router.post("/tasks/{task_id}/package")
def create_package(
task_id: int, body: PackageRequest,
current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None,
db: Session = Depends(get_db),
):
"""
生成达人素材交付包。
任务推入 Celery build_delivery_package 队列(只传 package_id
"""
task = db.query(GenerationTask).filter(
GenerationTask.id == task_id,
GenerationTask.workspace_id == current_user.workspace_id,
).first()
if not task:
raise_not_found("任务不存在")
if task.status not in ("approved", "pending_selection"):
from app.core.response import raise_business
raise_business("任务尚未生成完成,无法打包")
pkg = DeliveryPackage(
workspace_id=current_user.workspace_id,
task_id=task_id,
status="pending",
)
db.add(pkg)
db.commit()
db.refresh(pkg)
# 推 Celery 队列,只传 package_id基石B思路不传任何敏感信息
from app.workers.tasks import build_delivery_package
build_delivery_package.delay(pkg.id)
return ok({"delivery_package_id": pkg.id, "status": "pending"})
@router.get("/delivery-packages/{package_id}/download")
def get_package_download(
package_id: int,
current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None,
db: Session = Depends(get_db),
):
"""下载交付包status: pending/ready/downloaded"""
pkg = db.query(DeliveryPackage).filter(
DeliveryPackage.id == package_id,
DeliveryPackage.workspace_id == current_user.workspace_id,
).first()
if not pkg:
raise_not_found("交付包不存在")
if pkg.status == "ready" and pkg.download_url:
# 标记为 downloaded只能下一次防重复公开 URL
pkg.status = "downloaded"
db.commit()
return ok(_fmt_package(pkg))
@router.post("/delivery-packages/{package_id}/download-token")
def create_download_token(
package_id: int,
current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None,
db: Session = Depends(get_db),
):
"""
C1坑修复签发一次性下载令牌60s有效
前端先用 JWT 调此接口,再用 token 直接 window.open/fetch无需在URL里传JWT。
"""
import secrets
import redis as sync_redis
from app.core.config import get_settings
pkg = db.query(DeliveryPackage).filter(
DeliveryPackage.id == package_id,
DeliveryPackage.workspace_id == current_user.workspace_id,
).first()
if not pkg:
raise_not_found("交付包不存在")
if pkg.status not in ("ready", "downloaded"):
from app.core.response import raise_business
raise_business("交付包尚未准备好")
token = secrets.token_hex(32)
r = sync_redis.from_url(get_settings().REDIS_URL, decode_responses=True)
r.setex(f"dl_token:{token}", 60, str(package_id))
return ok({"token": token, "expires_in": 60})
@router.get("/delivery-packages/{package_id}/download-file")
def download_package_file(
package_id: int,
token: str = Query(default=""),
authorization: str = Header(default=""),
db: Session = Depends(get_db),
):
"""
直接下载交付包 zip 文件FileResponse
支持两种认证:
1. ?token=<download-token>(前端 window.open 用C1坑修复
2. Authorization: Bearer <JWT>API 调用用)
"""
import os
import redis as sync_redis
from app.core.config import get_settings
from app.core.security import decode_access_token
import jwt as pyjwt
# token 认证一次性60s有效跳过JWT
if token:
r = sync_redis.from_url(get_settings().REDIS_URL, decode_responses=True)
stored = r.getdel(f"dl_token:{token}")
if not stored or int(stored) != package_id:
from app.core.response import raise_business
raise_business("下载令牌无效或已过期")
pkg = db.query(DeliveryPackage).filter(DeliveryPackage.id == package_id).first()
else:
# JWT 认证
if not authorization or not authorization.startswith("Bearer "):
from app.core.response import raise_unauthorized
raise_unauthorized("缺少认证信息")
try:
payload = decode_access_token(authorization.split(" ", 1)[1])
except (pyjwt.PyJWTError, Exception):
from app.core.response import raise_unauthorized
raise_unauthorized("Token 无效")
workspace_id = int(payload["current_workspace_id"])
pkg = db.query(DeliveryPackage).filter(
DeliveryPackage.id == package_id,
DeliveryPackage.workspace_id == workspace_id,
).first()
if not pkg:
raise_not_found("交付包不存在")
if pkg.status not in ("ready", "downloaded"):
from app.core.response import raise_business
raise_business("交付包尚未准备好,请稍后重试")
if not pkg.package_path or not os.path.exists(pkg.package_path):
from app.core.response import raise_business
raise_business("交付包文件不存在,请重新打包")
filename = os.path.basename(pkg.package_path)
return FileResponse(
path=pkg.package_path,
media_type="application/zip",
filename=filename,
)