1
This commit is contained in:
@@ -0,0 +1,602 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, UploadFile, File, status
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.dependencies import get_db, get_current_user
|
||||
from app.models.user import User
|
||||
from app.models.project import Project
|
||||
from app.models.generation_record import GenerationRecord
|
||||
from app.schemas.generation import (
|
||||
OptimizeParams,
|
||||
GenerateParams,
|
||||
GenerationRecordOut,
|
||||
OptimizeResult,
|
||||
UpdatePromptRequest,
|
||||
GenerationType,
|
||||
DURATIONS,
|
||||
ASPECT_RATIOS,
|
||||
RESOLUTIONS,
|
||||
IMAGE_SIZES,
|
||||
)
|
||||
from app.services.credits import deduct_credits, calc_text_credits, calc_video_credits, calc_image_credits
|
||||
from app.services.llm import optimize_prompt
|
||||
from app.services.video_url import generate_temp_url, validate_and_get_record_id, get_video_stream_url
|
||||
from app.utils.id_gen import generate_id
|
||||
from app.utils.exceptions import InsufficientCreditsError, RecordNotFoundError, InvalidStatusError
|
||||
|
||||
router = APIRouter(prefix="/generation-records", tags=["generation"])
|
||||
logger = logging.getLogger("videogen")
|
||||
|
||||
|
||||
def _record_to_out(record: GenerationRecord, project_name: str) -> GenerationRecordOut:
|
||||
refs = None
|
||||
if record.media_references:
|
||||
try:
|
||||
refs = json.loads(record.media_references)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
refs = None
|
||||
|
||||
error_message = record.error_message
|
||||
if error_message:
|
||||
from app.services.error_codes import ARK_ERRORS
|
||||
import re
|
||||
match = re.search(r"code='([^']+)'", error_message)
|
||||
if match:
|
||||
code = match.group(1)
|
||||
if code in ARK_ERRORS:
|
||||
error_message = ARK_ERRORS[code]
|
||||
else:
|
||||
parts = error_message.split(":")
|
||||
if len(parts) >= 2 and parts[1].strip() in ARK_ERRORS:
|
||||
error_message = ARK_ERRORS[parts[1].strip()]
|
||||
|
||||
return GenerationRecordOut(
|
||||
id=record.id,
|
||||
project_id=record.project_id,
|
||||
project_name=project_name,
|
||||
original_prompt=record.original_prompt,
|
||||
optimized_prompt=record.optimized_prompt,
|
||||
gen_type=record.gen_type,
|
||||
duration=record.duration,
|
||||
aspect_ratio=record.aspect_ratio,
|
||||
resolution=record.resolution,
|
||||
image_size=record.image_size,
|
||||
image_proportion=record.image_proportion,
|
||||
image_px=record.image_px,
|
||||
status=record.status,
|
||||
video_url=record.video_url,
|
||||
image_url=record.image_url,
|
||||
references=refs,
|
||||
text_credits_cost=round(record.text_credits_cost or 0.00, 2),
|
||||
# text_tokens_used=record.text_tokens_used or 0,
|
||||
credits_cost=round(record.credits_cost or 0.00, 2),
|
||||
# video_tokens_used=record.video_tokens_used or 0,
|
||||
# image_tokens_used=record.image_tokens_used or 0,
|
||||
error_message=error_message,
|
||||
created_at=record.created_at,
|
||||
generated_at=record.generated_at,
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=list[GenerationRecordOut])
|
||||
async def list_records(
|
||||
project_id: str | None = Query(None, alias="project_id"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
query = (
|
||||
select(GenerationRecord, Project.name)
|
||||
.join(Project, GenerationRecord.project_id == Project.id)
|
||||
.where(GenerationRecord.user_id == current_user.id)
|
||||
.order_by(GenerationRecord.created_at.desc())
|
||||
)
|
||||
if project_id:
|
||||
query = query.where(GenerationRecord.project_id == project_id)
|
||||
|
||||
result = await db.execute(query)
|
||||
rows = result.all()
|
||||
return [
|
||||
_record_to_out(record, project_name)
|
||||
for record, project_name in rows
|
||||
]
|
||||
|
||||
|
||||
@router.post("/optimize", response_model=OptimizeResult)
|
||||
async def optimize(
|
||||
req: OptimizeParams,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
# Validate parameters based on generation type
|
||||
if req.gen_type == GenerationType.video:
|
||||
if req.duration not in DURATIONS:
|
||||
raise HTTPException(status_code=400, detail=f"视频时长必须为{DURATIONS}秒之一")
|
||||
if not req.duration:
|
||||
raise HTTPException(status_code=400, detail="视频生成需要指定时长")
|
||||
elif req.gen_type == GenerationType.image:
|
||||
if req.image_size not in IMAGE_SIZES:
|
||||
raise HTTPException(status_code=400, detail=f"图片分辨率必须为{IMAGE_SIZES}之一")
|
||||
if not req.image_size:
|
||||
raise HTTPException(status_code=400, detail="图片生成需要指定画面分辨率")
|
||||
|
||||
# Idempotency check: if key provided, return existing record if found
|
||||
if req.idempotency_key:
|
||||
existing = await db.execute(
|
||||
select(GenerationRecord, Project.name)
|
||||
.join(Project, GenerationRecord.project_id == Project.id)
|
||||
.where(
|
||||
GenerationRecord.user_id == current_user.id,
|
||||
GenerationRecord.idempotency_key == req.idempotency_key,
|
||||
GenerationRecord.gen_type == req.gen_type,
|
||||
GenerationRecord.status == "prompt_optimized",
|
||||
)
|
||||
.order_by(GenerationRecord.created_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
row = existing.first()
|
||||
if row:
|
||||
record, project_name = row
|
||||
return OptimizeResult(
|
||||
optimized_prompt=record.optimized_prompt or "",
|
||||
text_credits_cost=record.text_credits_cost or 0.00,
|
||||
text_tokens_used=record.text_tokens_used or 0,
|
||||
record=_record_to_out(record, project_name),
|
||||
)
|
||||
|
||||
# Check project exists and belongs to user
|
||||
proj_result = await db.execute(
|
||||
select(Project).where(
|
||||
Project.id == req.project_id,
|
||||
Project.user_id == current_user.id,
|
||||
)
|
||||
)
|
||||
project = proj_result.scalar_one_or_none()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
# Optimize prompt via LLM with type-specific context
|
||||
try:
|
||||
optimized, token_usage = await optimize_prompt(
|
||||
db, req.prompt,
|
||||
user_id=current_user.id,
|
||||
industry_key=project.industry,
|
||||
duration=req.duration if req.gen_type == GenerationType.video else None,
|
||||
image_size=req.image_size if req.gen_type == GenerationType.image else None,
|
||||
image_proportion=req.image_proportion if req.gen_type == GenerationType.image else None,
|
||||
image_px=req.image_px if req.gen_type == GenerationType.image else None,
|
||||
references=req.references,
|
||||
gen_type=req.gen_type,
|
||||
)
|
||||
# Create record BEFORE LLM call so it's visible if user refreshes
|
||||
record = GenerationRecord(
|
||||
id=generate_id(),
|
||||
user_id=current_user.id,
|
||||
project_id=req.project_id,
|
||||
original_prompt=req.prompt,
|
||||
gen_type=req.gen_type,
|
||||
duration=req.duration,
|
||||
image_size=req.image_size,
|
||||
image_proportion=req.image_proportion,
|
||||
image_px=req.image_px,
|
||||
status="optimizing",
|
||||
credits_cost=0,
|
||||
text_credits_cost=0,
|
||||
text_tokens_used=0,
|
||||
media_references=json.dumps(req.references) if req.references else None,
|
||||
idempotency_key=req.idempotency_key,
|
||||
)
|
||||
db.add(record)
|
||||
await db.flush()
|
||||
await db.commit()
|
||||
except Exception as e:
|
||||
from app.services.error_codes import extract_error_message
|
||||
# record.status = "failed"
|
||||
# record.error_message = extract_error_message(e, "提示词")
|
||||
# await db.flush()
|
||||
# await db.commit()
|
||||
raise HTTPException(status_code=502, detail=f"AI模型调用失败: {extract_error_message(e, "提示词")}")
|
||||
|
||||
text_credits = await calc_text_credits(
|
||||
db, token_usage["input_tokens"], token_usage["output_tokens"],
|
||||
)
|
||||
|
||||
await deduct_credits(
|
||||
db, current_user.id, text_credits,
|
||||
f"提示词优化 - {project.name}",
|
||||
)
|
||||
|
||||
record.optimized_prompt = optimized
|
||||
record.status = "prompt_optimized"
|
||||
record.text_credits_cost = round(text_credits, 2)
|
||||
record.text_tokens_used = token_usage["total_tokens"]
|
||||
await db.flush()
|
||||
|
||||
return OptimizeResult(
|
||||
optimized_prompt=optimized,
|
||||
text_credits_cost=round(text_credits, 2),
|
||||
# text_tokens_used=token_usage["total_tokens"],
|
||||
record=_record_to_out(record, project.name),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{record_id}/generate")
|
||||
async def generate(
|
||||
record_id: str,
|
||||
req: GenerateParams,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(GenerationRecord, Project.name)
|
||||
.join(Project, GenerationRecord.project_id == Project.id)
|
||||
.where(
|
||||
GenerationRecord.id == record_id,
|
||||
GenerationRecord.user_id == current_user.id,
|
||||
)
|
||||
)
|
||||
row = result.first()
|
||||
if not row:
|
||||
raise RecordNotFoundError()
|
||||
|
||||
record, project_name = row
|
||||
if record.status not in ("prompt_optimized", "failed"):
|
||||
raise InvalidStatusError("当前状态不允许生成")
|
||||
|
||||
if record.gen_type == GenerationType.video:
|
||||
# Video generation
|
||||
if req.aspect_ratio not in ASPECT_RATIOS:
|
||||
raise HTTPException(status_code=400, detail="不支持的画面比例")
|
||||
if req.resolution not in RESOLUTIONS:
|
||||
raise HTTPException(status_code=400, detail="不支持的分辨率")
|
||||
|
||||
duration = record.duration or 5
|
||||
video_credits = await calc_video_credits(db, duration, req.resolution)
|
||||
await deduct_credits(
|
||||
db, current_user.id, video_credits,
|
||||
f"视频生成 - {project_name}",
|
||||
related_id=record_id,
|
||||
)
|
||||
|
||||
record.aspect_ratio = req.aspect_ratio
|
||||
record.resolution = req.resolution
|
||||
record.credits_cost = round(video_credits, 2)
|
||||
record.status = "generating"
|
||||
record.error_message = None
|
||||
await db.flush()
|
||||
|
||||
try:
|
||||
from app.services.video_gen import get_active_engine, submit_video_task
|
||||
from app.services.error_codes import extract_error_message
|
||||
from app.services.video_queue import task_queue
|
||||
|
||||
engine = await get_active_engine(db)
|
||||
task_id = await submit_video_task(db, engine, record)
|
||||
record.seedance_task_id = task_id
|
||||
await db.flush()
|
||||
await task_queue.enqueue(record_id)
|
||||
except Exception as e:
|
||||
record.status = "failed"
|
||||
record.error_message = extract_error_message(e, "视频")
|
||||
await db.flush()
|
||||
|
||||
elif record.gen_type == GenerationType.image:
|
||||
# Image generation
|
||||
|
||||
image_credits = await calc_image_credits(db, req.image_size or record.image_size or "2K")
|
||||
await deduct_credits(
|
||||
db, current_user.id, image_credits,
|
||||
f"图片生成 - {project_name}",
|
||||
related_id=record_id,
|
||||
)
|
||||
|
||||
record.image_size = req.image_size or record.image_size or "2K"
|
||||
record.credits_cost = round(image_credits, 2)
|
||||
record.status = "generating"
|
||||
record.error_message = None
|
||||
await db.commit()
|
||||
|
||||
from app.services.video_queue import task_queue
|
||||
await task_queue.enqueue(record_id)
|
||||
|
||||
return _record_to_out(record, project_name)
|
||||
|
||||
|
||||
|
||||
|
||||
@router.post("/{record_id}/retry")
|
||||
async def retry_generation(
|
||||
record_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(GenerationRecord, Project.name)
|
||||
.join(Project, GenerationRecord.project_id == Project.id)
|
||||
.where(
|
||||
GenerationRecord.id == record_id,
|
||||
GenerationRecord.user_id == current_user.id,
|
||||
)
|
||||
)
|
||||
row = result.first()
|
||||
if not row:
|
||||
raise RecordNotFoundError()
|
||||
|
||||
record, project_name = row
|
||||
if record.status != "failed":
|
||||
raise InvalidStatusError("只有失败的记录可以重试")
|
||||
|
||||
# Re-deduct video credits for retry
|
||||
if record.duration and record.resolution:
|
||||
video_credits = await calc_video_credits(db, record.duration, record.resolution)
|
||||
await deduct_credits(
|
||||
db, current_user.id, video_credits,
|
||||
f"视频重试 - {project_name}",
|
||||
related_id=record_id,
|
||||
)
|
||||
record.credits_cost = round((record.credits_cost or 0) + video_credits, 2)
|
||||
|
||||
record.status = "generating"
|
||||
record.error_message = None
|
||||
await db.flush()
|
||||
|
||||
try:
|
||||
from app.services.video_gen import get_active_engine, submit_video_task, extract_error_message
|
||||
from app.services.video_queue import task_queue
|
||||
|
||||
engine = await get_active_engine(db)
|
||||
task_id = await submit_video_task(db, engine, record)
|
||||
record.seedance_task_id = task_id
|
||||
await db.flush()
|
||||
await task_queue.enqueue(record_id)
|
||||
except Exception as e:
|
||||
record.status = "failed"
|
||||
record.error_message = extract_error_message(e)
|
||||
await db.flush()
|
||||
|
||||
return _record_to_out(record, project_name)
|
||||
|
||||
|
||||
@router.put("/{record_id}/prompt")
|
||||
async def update_prompt(
|
||||
record_id: str,
|
||||
req: UpdatePromptRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(GenerationRecord).where(
|
||||
GenerationRecord.id == record_id,
|
||||
GenerationRecord.user_id == current_user.id,
|
||||
)
|
||||
)
|
||||
record = result.scalar_one_or_none()
|
||||
if not record:
|
||||
raise RecordNotFoundError()
|
||||
if record.status != "prompt_optimized":
|
||||
raise InvalidStatusError("只有待生成状态可以修改提示词")
|
||||
|
||||
record.optimized_prompt = req.optimized_prompt
|
||||
await db.flush()
|
||||
return {"message": "ok"}
|
||||
|
||||
|
||||
@router.get("/{record_id}/video")
|
||||
async def get_video(
|
||||
record_id: str,
|
||||
token: str = Query(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Validate temp token and redirect to video URL."""
|
||||
validated_id = await validate_and_get_record_id(token)
|
||||
if validated_id != record_id:
|
||||
raise HTTPException(status_code=403, detail="无效的视频链接")
|
||||
|
||||
video_url = await get_video_stream_url(db, record_id)
|
||||
if not video_url:
|
||||
raise HTTPException(status_code=404, detail="视频不存在")
|
||||
|
||||
return RedirectResponse(url=video_url)
|
||||
|
||||
|
||||
@router.get("/{record_id}/queue-status")
|
||||
async def get_queue_status(
|
||||
record_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Get queue position, estimated wait time, and current status for a generation record."""
|
||||
from sqlalchemy import func
|
||||
|
||||
result = await db.execute(
|
||||
select(GenerationRecord).where(
|
||||
GenerationRecord.id == record_id,
|
||||
GenerationRecord.user_id == current_user.id,
|
||||
)
|
||||
)
|
||||
record = result.scalar_one_or_none()
|
||||
if not record:
|
||||
raise RecordNotFoundError()
|
||||
|
||||
queue_position = None
|
||||
estimated_wait_seconds = None
|
||||
|
||||
if record.status == "generating":
|
||||
ahead_result = await db.execute(
|
||||
select(func.count(GenerationRecord.id)).where(
|
||||
GenerationRecord.status == "generating",
|
||||
GenerationRecord.created_at < record.created_at,
|
||||
)
|
||||
)
|
||||
ahead = ahead_result.scalar() or 0
|
||||
queue_position = ahead + 1
|
||||
estimated_wait_seconds = ahead * 60
|
||||
|
||||
return {
|
||||
"record_id": record.id,
|
||||
"status": record.status,
|
||||
"queue_position": queue_position,
|
||||
"estimated_wait_seconds": estimated_wait_seconds,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/callbacks/seedance")
|
||||
async def seedance_callback(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
"""Receive async callback from Seedance API."""
|
||||
data = await request.json()
|
||||
task_id = data.get("id")
|
||||
task_status = data.get("status")
|
||||
|
||||
if not task_id:
|
||||
return {"message": "ignored"}
|
||||
|
||||
result = await db.execute(
|
||||
select(GenerationRecord).where(GenerationRecord.seedance_task_id == task_id)
|
||||
)
|
||||
record = result.scalar_one_or_none()
|
||||
if not record:
|
||||
return {"message": "record not found"}
|
||||
|
||||
if task_status == "succeeded":
|
||||
remote_url = data.get("content", {}).get("video_url", "")
|
||||
record.status = "completed"
|
||||
# Download video to local storage
|
||||
if settings.STORAGE_TYPE == "local" and remote_url:
|
||||
try:
|
||||
from app.services.video_gen import download_video
|
||||
dest = os.path.join(settings.STORAGE_LOCAL_PATH, f"{record.id}.mp4")
|
||||
await download_video(remote_url, dest)
|
||||
record.video_url = f"/videos/{record.id}.mp4"
|
||||
except Exception as e:
|
||||
logger.warning(f"Callback download failed, using remote URL: {e}")
|
||||
record.video_url = remote_url
|
||||
else:
|
||||
record.video_url = remote_url
|
||||
record.generated_at = datetime.now()
|
||||
# Extract video token usage from callback
|
||||
usage = data.get("usage", {})
|
||||
if usage:
|
||||
record.video_tokens_used = usage.get("total_tokens", 0)
|
||||
# Log callback response
|
||||
from app.services.video_gen import _log_video_response
|
||||
_log_video_response(record.id, data)
|
||||
# Notify user
|
||||
from app.services.notification import create_notification
|
||||
from app.api.v1.notifications import push_notification_to_user
|
||||
notif = await create_notification(
|
||||
db, record.user_id, "视频生成完成",
|
||||
"您的视频已生成完成,可以查看了。", "video", record.id,
|
||||
)
|
||||
await push_notification_to_user(record.user_id, notif)
|
||||
elif task_status == "failed":
|
||||
record.status = "failed"
|
||||
record.error_message = data.get("error", "视频生成失败")
|
||||
# Log callback response
|
||||
from app.services.video_gen import _log_video_response
|
||||
_log_video_response(record.id, data, error=record.error_message)
|
||||
# Notify user
|
||||
from app.services.notification import create_notification
|
||||
from app.api.v1.notifications import push_notification_to_user
|
||||
notif = await create_notification(
|
||||
db, record.user_id, "视频生成失败",
|
||||
f"视频生成失败:{record.error_message}", "video", record.id,
|
||||
)
|
||||
await push_notification_to_user(record.user_id, notif)
|
||||
|
||||
await db.flush()
|
||||
return {"message": "ok"}
|
||||
|
||||
|
||||
@router.post("/upload-image")
|
||||
async def upload_image(
|
||||
file: UploadFile = File(...),
|
||||
current_user: User = Depends(get_current_user),
|
||||
gen_type: str = Query("video", description="生成类型:video-视频,image-图片"),
|
||||
):
|
||||
"""Upload an image for generation reference."""
|
||||
import os
|
||||
import uuid
|
||||
from app.config import settings
|
||||
from datetime import datetime
|
||||
|
||||
if not file.content_type or not file.content_type.startswith("image/"):
|
||||
raise HTTPException(status_code=400, detail="仅支持图片文件")
|
||||
|
||||
ext = os.path.splitext(file.filename or ".png")[1] or ".png"
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
safe_name = f"{gen_type}_img_{current_user.id}_{timestamp}_{uuid.uuid4().hex[:8]}{ext}"
|
||||
date_dir = datetime.now().strftime("%Y/%m/%d")
|
||||
dir_path = os.path.join(settings.UPLOAD_LOCAL_PATH, "images", date_dir)
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
file_path = os.path.join(dir_path, safe_name)
|
||||
|
||||
content = await file.read()
|
||||
if len(content) > 10 * 1024 * 1024:
|
||||
raise HTTPException(status_code=400, detail="图片大小不能超过10MB")
|
||||
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(content)
|
||||
|
||||
url = f"/uploads/images/{date_dir}/{safe_name}"
|
||||
return {"url": url, "filename": file.filename or safe_name, "type": "image", "gen_type": gen_type}
|
||||
|
||||
|
||||
@router.post("/upload-video")
|
||||
async def upload_video(
|
||||
file: UploadFile = File(...),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Upload a video for generation reference."""
|
||||
import os
|
||||
import uuid
|
||||
from app.config import settings
|
||||
from datetime import datetime
|
||||
|
||||
if not file.content_type or not file.content_type.startswith("video/"):
|
||||
raise HTTPException(status_code=400, detail="仅支持视频文件")
|
||||
|
||||
ext = os.path.splitext(file.filename or ".mp4")[1] or ".mp4"
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
safe_name = f"video_ref_{current_user.id}_{timestamp}_{uuid.uuid4().hex[:8]}{ext}"
|
||||
date_dir = datetime.now().strftime("%Y/%m/%d")
|
||||
dir_path = os.path.join(settings.UPLOAD_LOCAL_PATH, "videos", date_dir)
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
file_path = os.path.join(dir_path, safe_name)
|
||||
|
||||
content = await file.read()
|
||||
if len(content) > 100 * 1024 * 1024:
|
||||
raise HTTPException(status_code=400, detail="视频大小不能超过100MB")
|
||||
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(content)
|
||||
|
||||
url = f"/uploads/videos/{date_dir}/{safe_name}"
|
||||
return {"url": url, "filename": file.filename or safe_name, "type": "video"}
|
||||
|
||||
|
||||
@router.post("/delete-file")
|
||||
async def delete_upload(
|
||||
url: str = Query(..., description="文件URL,如 /uploads/images/2024/01/01/video_img_xxx.png"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Delete an uploaded file by URL."""
|
||||
import os
|
||||
from app.config import settings
|
||||
|
||||
if not url.startswith("/uploads/"):
|
||||
raise HTTPException(status_code=400, detail="无效的文件路径")
|
||||
|
||||
if current_user.id not in url:
|
||||
raise HTTPException(status_code=403, detail="无权删除此文件")
|
||||
|
||||
file_path = os.path.join(settings.UPLOAD_LOCAL_PATH, url.replace("/uploads/", ""))
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
return {"message": "ok"}
|
||||
Reference in New Issue
Block a user