baseline: Clover 独立仓库首次基线提交
将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。 当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包), M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
184
backend/app/api/v1/delivery.py
Normal file
184
backend/app/api/v1/delivery.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user