生成任务失败积分回退
This commit is contained in:
@@ -43,6 +43,13 @@ from app.services.notification import create_notification
|
||||
from app.services.auth import hash_password, verify_password
|
||||
from app.services.operation_log import log_operation
|
||||
from app.services.resource_signed_url_service import build_resource_signed_url
|
||||
|
||||
from app.services.generation_billing_service import (
|
||||
OWNER_GENERATION_RECORD,
|
||||
charge_generation_media_by_params,
|
||||
get_next_credit_attempt_no,
|
||||
)
|
||||
from app.services.generation_refund_service import mark_generation_record_failed_and_refund_once
|
||||
from app.utils.id_gen import generate_id
|
||||
from app.schemas.generation import GenerationType, ASPECT_RATIOS, RESOLUTIONS
|
||||
|
||||
@@ -1054,6 +1061,7 @@ async def admin_update_generation_status(
|
||||
GenerationRecord.id == record_id,
|
||||
GenerationRecord.deleted_at.is_(None),
|
||||
)
|
||||
.with_for_update()
|
||||
.limit(1)
|
||||
)
|
||||
record = result.scalar_one_or_none()
|
||||
@@ -1064,11 +1072,21 @@ async def admin_update_generation_status(
|
||||
if new_status not in ("prompt_optimized", "generating", "completed", "failed"):
|
||||
raise HTTPException(status_code=400, detail="无效状态")
|
||||
|
||||
record.status = new_status
|
||||
if new_status == "failed":
|
||||
await mark_generation_record_failed_and_refund_once(
|
||||
db,
|
||||
record=record,
|
||||
error_message=body.get("error_message") or record.error_message or "管理员设置为失败",
|
||||
)
|
||||
else:
|
||||
record.status = new_status
|
||||
|
||||
if body.get("video_url"):
|
||||
record.video_url = body["video_url"]
|
||||
if body.get("video_cover_url"):
|
||||
record.video_cover_url = body["video_cover_url"]
|
||||
if body.get("image_url"):
|
||||
record.image_url = body["image_url"]
|
||||
if new_status == "completed":
|
||||
record.generated_at = datetime.now()
|
||||
await db.flush()
|
||||
@@ -1082,9 +1100,8 @@ async def admin_generate_video(
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Admin trigger video generation for a record with specified params."""
|
||||
"""Admin trigger video/image generation for a record with specified params."""
|
||||
from app.models.project import Project
|
||||
from app.services.credits import calc_video_credits, deduct_credits, calc_image_credits
|
||||
from app.services.video_queue import task_queue
|
||||
|
||||
result = await db.execute(
|
||||
@@ -1095,6 +1112,7 @@ async def admin_generate_video(
|
||||
GenerationRecord.deleted_at.is_(None),
|
||||
Project.deleted_at.is_(None),
|
||||
)
|
||||
.with_for_update()
|
||||
)
|
||||
row = result.first()
|
||||
if not row:
|
||||
@@ -1106,6 +1124,12 @@ async def admin_generate_video(
|
||||
if record.status not in ("prompt_optimized", "failed"):
|
||||
raise HTTPException(status_code=400, detail=f"当前状态不允许生成{type_str}")
|
||||
|
||||
attempt_no = await get_next_credit_attempt_no(
|
||||
db,
|
||||
owner_type=OWNER_GENERATION_RECORD,
|
||||
owner_id=record.id,
|
||||
)
|
||||
|
||||
if record.gen_type == GenerationType.video:
|
||||
# Video Generation
|
||||
aspect_ratio = body.get("aspect_ratio", "16:9")
|
||||
@@ -1116,55 +1140,80 @@ async def admin_generate_video(
|
||||
raise HTTPException(status_code=400, detail="不支持的分辨率")
|
||||
|
||||
duration = record.duration or 5
|
||||
video_credits = await calc_video_credits(db, duration, resolution)
|
||||
await deduct_credits(
|
||||
db, record.user_id, video_credits,
|
||||
f"视频生成(管理后台) - {project_name}",
|
||||
related_id=record_id,
|
||||
media_billing = await charge_generation_media_by_params(
|
||||
db,
|
||||
user_id=record.user_id,
|
||||
record_id=record.id,
|
||||
gen_type="video",
|
||||
duration=duration,
|
||||
resolution=resolution,
|
||||
project_name=project_name,
|
||||
description_prefix="视频生成(管理后台)",
|
||||
owner_type=OWNER_GENERATION_RECORD,
|
||||
attempt_no=attempt_no,
|
||||
)
|
||||
|
||||
record.aspect_ratio = aspect_ratio
|
||||
record.resolution = resolution
|
||||
record.credits_cost = (record.credits_cost or 0) + video_credits
|
||||
record.credits_cost = round(float(record.credits_cost or 0) + media_billing.total_charged, 2)
|
||||
record.status = "generating"
|
||||
record.error_message = None
|
||||
record.video_url = None
|
||||
record.video_cover_url = None
|
||||
record.image_url = None
|
||||
record.seedance_task_id = None
|
||||
await db.flush()
|
||||
|
||||
try:
|
||||
from app.services.video_gen import get_active_engine, submit_video_task
|
||||
|
||||
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 = str(e)
|
||||
await mark_generation_record_failed_and_refund_once(
|
||||
db,
|
||||
record=record,
|
||||
error_message=str(e),
|
||||
)
|
||||
await db.flush()
|
||||
|
||||
elif record.gen_type == GenerationType.image:
|
||||
# Image generation
|
||||
|
||||
post_image_size = body.get("image_size", "")
|
||||
image_credits = await calc_image_credits(db, post_image_size or record.image_size or "2K")
|
||||
await deduct_credits(
|
||||
db, record.user_id, image_credits,
|
||||
f"图片生成 - {project_name}",
|
||||
related_id=record_id,
|
||||
image_size = post_image_size or record.image_size or "2K"
|
||||
media_billing = await charge_generation_media_by_params(
|
||||
db,
|
||||
user_id=record.user_id,
|
||||
record_id=record.id,
|
||||
gen_type="image",
|
||||
image_size=image_size,
|
||||
project_name=project_name,
|
||||
description_prefix="图片生成(管理后台)",
|
||||
owner_type=OWNER_GENERATION_RECORD,
|
||||
attempt_no=attempt_no,
|
||||
)
|
||||
|
||||
record.image_size = image_size
|
||||
record.credits_cost = round(float(record.credits_cost or 0) + media_billing.total_charged, 2)
|
||||
record.status = "generating"
|
||||
record.error_message = None
|
||||
record.image_url = None
|
||||
record.video_url = None
|
||||
record.video_cover_url = None
|
||||
record.seedance_task_id = None
|
||||
await db.flush()
|
||||
|
||||
try:
|
||||
record.image_size = post_image_size or record.image_size or "2K"
|
||||
record.credits_cost = round(image_credits, 2)
|
||||
record.status = "generating"
|
||||
record.error_message = None
|
||||
await db.flush()
|
||||
await task_queue.enqueue(record_id)
|
||||
except Exception as e:
|
||||
record.status = "failed"
|
||||
record.error_message = str(e)
|
||||
await mark_generation_record_failed_and_refund_once(
|
||||
db,
|
||||
record=record,
|
||||
error_message=str(e),
|
||||
)
|
||||
await db.flush()
|
||||
|
||||
return {"message": "ok", "record_id": record_id}
|
||||
|
||||
@@ -26,7 +26,7 @@ from app.schemas.generation import (
|
||||
RESOLUTIONS,
|
||||
IMAGE_SIZES,
|
||||
)
|
||||
from app.services.credits import deduct_credits, calc_text_credits, calc_video_credits, calc_image_credits
|
||||
from app.services.credits import deduct_credits, calc_text_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.services.resource_accounting_service import (
|
||||
@@ -34,6 +34,13 @@ from app.services.resource_accounting_service import (
|
||||
safe_file_size,
|
||||
)
|
||||
from app.services.resource_signed_url_service import build_resource_signed_url
|
||||
from app.services.generation_billing_service import (
|
||||
OWNER_GENERATION_RECORD,
|
||||
charge_generation_media_by_params,
|
||||
charge_generation_media_for_record,
|
||||
get_next_credit_attempt_no,
|
||||
)
|
||||
from app.services.generation_refund_service import mark_generation_record_failed_and_refund_once
|
||||
from app.services.video_cover_service import async_create_video_cover_for_local_video
|
||||
from app.utils.id_gen import generate_id
|
||||
from app.utils.exceptions import InsufficientCreditsError, RecordNotFoundError, InvalidStatusError
|
||||
@@ -319,6 +326,7 @@ async def generate(
|
||||
GenerationRecord.deleted_at.is_(None),
|
||||
Project.deleted_at.is_(None),
|
||||
)
|
||||
.with_for_update()
|
||||
)
|
||||
row = result.first()
|
||||
if not row:
|
||||
@@ -328,6 +336,12 @@ async def generate(
|
||||
if record.status not in ("prompt_optimized", "failed"):
|
||||
raise InvalidStatusError("当前状态不允许生成")
|
||||
|
||||
attempt_no = await get_next_credit_attempt_no(
|
||||
db,
|
||||
owner_type=OWNER_GENERATION_RECORD,
|
||||
owner_id=record.id,
|
||||
)
|
||||
|
||||
if record.gen_type == GenerationType.video:
|
||||
# Video generation
|
||||
if req.aspect_ratio not in ASPECT_RATIOS:
|
||||
@@ -336,20 +350,28 @@ async def generate(
|
||||
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,
|
||||
media_billing = await charge_generation_media_by_params(
|
||||
db,
|
||||
user_id=current_user.id,
|
||||
record_id=record.id,
|
||||
gen_type="video",
|
||||
duration=duration,
|
||||
resolution=req.resolution,
|
||||
project_name=project_name,
|
||||
description_prefix="视频生成",
|
||||
owner_type=OWNER_GENERATION_RECORD,
|
||||
attempt_no=attempt_no,
|
||||
)
|
||||
|
||||
record.aspect_ratio = req.aspect_ratio
|
||||
record.resolution = req.resolution
|
||||
record.credits_cost = round(video_credits, 2)
|
||||
record.credits_cost = round(float(record.credits_cost or 0) + media_billing.total_charged, 2)
|
||||
record.status = "generating"
|
||||
record.error_message = None
|
||||
record.video_url = None
|
||||
record.video_cover_url = None
|
||||
record.image_url = None
|
||||
record.seedance_task_id = None
|
||||
await db.flush()
|
||||
|
||||
try:
|
||||
@@ -363,34 +385,51 @@ async def generate(
|
||||
await db.flush()
|
||||
await task_queue.enqueue(record_id)
|
||||
except Exception as e:
|
||||
record.status = "failed"
|
||||
record.error_message = extract_error_message(e, "视频")
|
||||
await mark_generation_record_failed_and_refund_once(
|
||||
db,
|
||||
record=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,
|
||||
image_size = req.image_size or record.image_size or "2K"
|
||||
media_billing = await charge_generation_media_by_params(
|
||||
db,
|
||||
user_id=current_user.id,
|
||||
record_id=record.id,
|
||||
gen_type="image",
|
||||
image_size=image_size,
|
||||
project_name=project_name,
|
||||
description_prefix="图片生成",
|
||||
owner_type=OWNER_GENERATION_RECORD,
|
||||
attempt_no=attempt_no,
|
||||
)
|
||||
|
||||
record.image_size = req.image_size or record.image_size or "2K"
|
||||
record.credits_cost = round(image_credits, 2)
|
||||
record.image_size = image_size
|
||||
record.credits_cost = round(float(record.credits_cost or 0) + media_billing.total_charged, 2)
|
||||
record.status = "generating"
|
||||
record.error_message = None
|
||||
await db.commit()
|
||||
record.image_url = None
|
||||
record.video_url = None
|
||||
record.video_cover_url = None
|
||||
record.seedance_task_id = None
|
||||
await db.flush()
|
||||
|
||||
from app.services.video_queue import task_queue
|
||||
await task_queue.enqueue(record_id)
|
||||
try:
|
||||
from app.services.video_queue import task_queue
|
||||
await task_queue.enqueue(record_id)
|
||||
except Exception as e:
|
||||
await mark_generation_record_failed_and_refund_once(
|
||||
db,
|
||||
record=record,
|
||||
error_message=f"图片任务队列投递失败: {e}",
|
||||
)
|
||||
await db.flush()
|
||||
|
||||
return _record_to_out(record, project_name)
|
||||
|
||||
|
||||
|
||||
|
||||
@router.post("/{record_id}/retry")
|
||||
async def retry_generation(
|
||||
record_id: str,
|
||||
@@ -406,6 +445,7 @@ async def retry_generation(
|
||||
GenerationRecord.deleted_at.is_(None),
|
||||
Project.deleted_at.is_(None),
|
||||
)
|
||||
.with_for_update()
|
||||
)
|
||||
row = result.first()
|
||||
if not row:
|
||||
@@ -415,34 +455,45 @@ async def retry_generation(
|
||||
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)
|
||||
attempt_no = await get_next_credit_attempt_no(
|
||||
db,
|
||||
owner_type=OWNER_GENERATION_RECORD,
|
||||
owner_id=record.id,
|
||||
)
|
||||
media_billing = await charge_generation_media_for_record(
|
||||
db,
|
||||
record=record,
|
||||
project_name=project_name,
|
||||
description_prefix="视频重试",
|
||||
attempt_no=attempt_no,
|
||||
)
|
||||
|
||||
record.status = "generating"
|
||||
record.error_message = None
|
||||
record.video_url = None
|
||||
record.video_cover_url = None
|
||||
record.image_url = None
|
||||
record.seedance_task_id = None
|
||||
record.generated_at = None
|
||||
record.credits_cost = round(float(record.credits_cost or 0) + media_billing.total_charged, 2)
|
||||
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()
|
||||
if record.gen_type == GenerationType.video:
|
||||
from app.services.video_gen import get_active_engine, submit_video_task, extract_error_message
|
||||
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)
|
||||
from app.services.error_codes import extract_error_message
|
||||
await mark_generation_record_failed_and_refund_once(
|
||||
db,
|
||||
record=record,
|
||||
error_message=extract_error_message(e, "重试"),
|
||||
)
|
||||
await db.flush()
|
||||
|
||||
return _record_to_out(record, project_name)
|
||||
@@ -551,6 +602,7 @@ async def seedance_callback(request: Request, db: AsyncSession = Depends(get_db)
|
||||
GenerationRecord.seedance_task_id == task_id,
|
||||
GenerationRecord.deleted_at.is_(None),
|
||||
)
|
||||
.with_for_update()
|
||||
.limit(1)
|
||||
)
|
||||
record = result.scalar_one_or_none()
|
||||
@@ -613,8 +665,12 @@ async def seedance_callback(request: Request, db: AsyncSession = Depends(get_db)
|
||||
)
|
||||
await push_notification_to_user(record.user_id, notif)
|
||||
elif task_status == "failed":
|
||||
record.status = "failed"
|
||||
record.error_message = data.get("error", "视频生成失败")
|
||||
error_message = data.get("error", "视频生成失败")
|
||||
await mark_generation_record_failed_and_refund_once(
|
||||
db,
|
||||
record=record,
|
||||
error_message=error_message,
|
||||
)
|
||||
# Log callback response
|
||||
from app.services.video_gen import _log_video_response
|
||||
_log_video_response(record.id, data, error=record.error_message)
|
||||
|
||||
@@ -26,7 +26,13 @@ from app.services.generation_ai_service import (
|
||||
record_to_out,
|
||||
soft_delete_chat_generation_task,
|
||||
)
|
||||
from app.services.generation_billing_service import (
|
||||
OWNER_CHAT_GENERATION_TASK,
|
||||
charge_generation_media_by_params,
|
||||
get_next_credit_attempt_no,
|
||||
)
|
||||
from app.services.generation_log_service import log_task_event
|
||||
from app.services.generation_refund_service import mark_chat_generation_task_failed_and_refund_once
|
||||
from app.tasks.celery_app import celery_app
|
||||
|
||||
router = APIRouter(
|
||||
@@ -149,7 +155,18 @@ async def create_task(
|
||||
|
||||
from app.tasks.generation_create_tasks import chatapi_create_generation_task
|
||||
|
||||
chatapi_create_generation_task.delay(task.id)
|
||||
try:
|
||||
chatapi_create_generation_task.delay(task.id)
|
||||
except Exception as exc:
|
||||
await mark_chat_generation_task_failed_and_refund_once(
|
||||
db,
|
||||
task_id=task.id,
|
||||
error_message=f"任务队列投递失败: {exc}",
|
||||
pipeline_stage="failed",
|
||||
)
|
||||
await db.commit()
|
||||
raise HTTPException(status_code=503, detail="任务队列投递失败,请稍后重试")
|
||||
|
||||
return record_to_out(task)
|
||||
|
||||
|
||||
@@ -538,6 +555,7 @@ async def retry_task(
|
||||
ChatGenerationTask.generation_mode == "chatapi_async",
|
||||
ChatGenerationTask.deleted_at.is_(None),
|
||||
)
|
||||
.with_for_update()
|
||||
.limit(1)
|
||||
)
|
||||
task = result.scalar_one_or_none()
|
||||
@@ -546,41 +564,60 @@ async def retry_task(
|
||||
if task.status != "failed":
|
||||
raise HTTPException(status_code=400, detail="只有失败任务可以重试")
|
||||
|
||||
attempt_no = await get_next_credit_attempt_no(
|
||||
db,
|
||||
owner_type=OWNER_CHAT_GENERATION_TASK,
|
||||
owner_id=task.id,
|
||||
)
|
||||
media_billing = await charge_generation_media_by_params(
|
||||
db,
|
||||
user_id=task.user_id,
|
||||
record_id=task.id,
|
||||
gen_type=task.gen_type,
|
||||
image_size=task.image_size,
|
||||
duration=task.duration,
|
||||
resolution=task.resolution,
|
||||
engine_id=task.engine_id,
|
||||
project_name="AI生成任务",
|
||||
description_prefix="Chat任务重试",
|
||||
owner_type=OWNER_CHAT_GENERATION_TASK,
|
||||
attempt_no=attempt_no,
|
||||
)
|
||||
|
||||
task.status = "generating"
|
||||
task.pipeline_stage = "queued"
|
||||
task.error_message = None
|
||||
task.retry_count = (task.retry_count or 0) + 1
|
||||
if task.retry_count > 3:
|
||||
raise HTTPException(status_code=400, detail="任务已超过最大重试次数")
|
||||
|
||||
# Resume from the earliest missing stage.
|
||||
if not task.optimized_prompt:
|
||||
task.pipeline_stage = "queued"
|
||||
from app.tasks.generation_create_tasks import chatapi_create_generation_task
|
||||
|
||||
chatapi_create_generation_task.delay(task.id)
|
||||
elif not task.seedance_task_id and not task.remote_result_url:
|
||||
task.pipeline_stage = "creating_provider_task"
|
||||
from app.tasks.generation_create_tasks import chatapi_create_generation_task
|
||||
|
||||
chatapi_create_generation_task.delay(task.id)
|
||||
elif task.seedance_task_id and not task.remote_result_url:
|
||||
task.pipeline_stage = "waiting_remote"
|
||||
from app.tasks.generation_poll_tasks import poll_generation_task
|
||||
|
||||
poll_generation_task.delay(task.id)
|
||||
elif task.remote_result_url and not (task.image_url or task.video_url):
|
||||
task.pipeline_stage = "result_ready"
|
||||
from app.tasks.generation_download_tasks import download_generation_result_task
|
||||
|
||||
download_generation_result_task.delay(task.id)
|
||||
else:
|
||||
task.status = "completed"
|
||||
task.pipeline_stage = "done"
|
||||
task.poll_count = 0
|
||||
task.last_poll_at = None
|
||||
task.provider_task_id = None
|
||||
task.seedance_task_id = None
|
||||
task.remote_result_url = None
|
||||
task.provider_response_json = None
|
||||
task.image_url = None
|
||||
task.video_url = None
|
||||
task.video_cover_url = None
|
||||
task.generated_at = None
|
||||
task.credits_cost = round(float(task.credits_cost or 0) + media_billing.total_charged, 2)
|
||||
|
||||
await db.commit()
|
||||
|
||||
from app.tasks.generation_create_tasks import chatapi_create_generation_task
|
||||
|
||||
try:
|
||||
chatapi_create_generation_task.delay(task.id)
|
||||
except Exception as exc:
|
||||
await mark_chat_generation_task_failed_and_refund_once(
|
||||
db,
|
||||
task_id=task.id,
|
||||
error_message=f"任务队列投递失败: {exc}",
|
||||
pipeline_stage="failed",
|
||||
)
|
||||
await db.commit()
|
||||
raise HTTPException(status_code=503, detail="任务队列投递失败,请稍后重试")
|
||||
|
||||
return GenerationAIRetryOut(
|
||||
id=task.id,
|
||||
status=task.status,
|
||||
pipeline_stage=task.pipeline_stage,
|
||||
message="任务已重新投递",
|
||||
)
|
||||
message="任务已重新扣费并重新投递",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user