1、图片生成同步接口超时风险

2、图片返回的 URL 是本地路径
3、视频生成中媒体文件重复下载
4、幂等性检查无数据库唯一约束
5、幂等键冲突返回 409 改为返回已有任务信息
6、虚拟素材库配额校验 TOCTOU
7、项目级联删除与独立素材删除任务并发冲突
This commit is contained in:
2026-08-06 18:57:14 +08:00
parent 44b865e1ca
commit f5d7cfadb4
5 changed files with 131 additions and 33 deletions
@@ -1,6 +1,7 @@
import asyncio
import json
import logging
import os
import time
from datetime import datetime, timezone
from sqlalchemy.ext.asyncio import AsyncSession
@@ -16,6 +17,26 @@ from app.services.api_v3.logging_service import log_model_request
logger = logging.getLogger("videogen")
def _make_image_url(local_path: str) -> str:
"""将本地图片路径转为完整可访问 URL。"""
if not local_path:
return local_path
# 如果已经是完整 URL,直接返回
if local_path.startswith(("http://", "https://")):
return local_path
from app.config import settings
from app.services.resource_signed_url_service import build_resource_signed_url
# 生成签名 URL
signed = build_resource_signed_url(local_path)
if signed and not signed.startswith(("http://", "https://")):
base = settings.BASE_URL.rstrip("/")
if signed.startswith("/"):
signed = f"{base}{signed}"
else:
signed = f"{base}/{signed}"
return signed or local_path
async def submit_video_generation(
db: AsyncSession,
key: ApiKey,
@@ -379,10 +400,12 @@ async def generate_image_sync(
logger.error("Failed to record usage on image submit: %s", log_exc)
try:
# 设置总体超时(120秒,防止同步请求长时间挂起)
_IMAGE_GEN_TIMEOUT = 120
# 3. 调用 Volcano Ark SDK(同步函数,在线程中执行)
from app.services.image_gen import submit_image_task, download_image
from app.config import settings
import os
# 构建 media_references,下载图片到本地
from app.services.api_v3.file_service import process_media_url
@@ -402,14 +425,17 @@ async def generate_image_sync(
task.image_size = req.size or "2K"
await db.flush()
# 在线程中执行同步 SDK 调用
result = await asyncio.to_thread(
submit_image_task,
db,
engine,
task,
True, # include_media_references
req.generation_count or 1,
# 在线程中执行同步 SDK 调用(带超时保护)
result = await asyncio.wait_for(
asyncio.to_thread(
submit_image_task,
db,
engine,
task,
True, # include_media_references
req.generation_count or 1,
),
timeout=_IMAGE_GEN_TIMEOUT,
)
# 4. 下载图片
@@ -425,12 +451,19 @@ async def generate_image_sync(
os.makedirs(dest_dir, exist_ok=True)
dest_path = os.path.join(dest_dir, f"{task.id}_{item.get('generation_index', 1)}.png")
try:
await download_image(url, dest_path)
await asyncio.wait_for(
download_image(url, dest_path),
timeout=30,
)
except asyncio.TimeoutError:
logger.warning("Image download timeout: %s", url[:80])
except Exception as dl_err:
logger.warning("Image download failed: %s", dl_err)
# 将本地路径转为完整 URL
image_url = _make_image_url(dest_path)
downloaded_items.append(ApiImageGenerateDataItem(
url=dest_path, # 使用本地路径
url=image_url,
size=item.get("size"),
output_format=item.get("output_format"),
))
@@ -459,6 +492,21 @@ async def generate_image_sync(
model=result.get("model", req.model),
)
except asyncio.TimeoutError:
from fastapi import HTTPException, status
logger.exception("API image generation timed out (task_id=%s)", task.id)
# 超时:退回预扣配额
if estimated_price > 0:
key.quota_used = round(max(0, (key.quota_used or 0.0) - estimated_price), 2)
task.status = "failed"
task.error_message = "图片生成超时(超过120秒)"
task.credits_cost = 0
await db.commit()
raise HTTPException(
status_code=504,
detail="图片生成超时,请稍后重试",
)
except Exception as exc:
# 失败:退回预扣配额
if estimated_price > 0: