积分冻结释放
This commit is contained in:
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, File, HTTPException, Path, Query, UploadFile
|
||||
from sqlalchemy import inspect as sa_inspect
|
||||
@@ -11,6 +12,12 @@ from app.dependencies import get_current_user, get_db
|
||||
from app.models.user import User
|
||||
from app.enums.common import ModuleProjectStatusEnum, ModuleEventTypeEnum
|
||||
from app.enums.generation_task import GenerationOwnerType
|
||||
from app.enums.credit_record import (
|
||||
CreditRecordBillingScene,
|
||||
CreditRecordChargeKind,
|
||||
CreditRecordOwnerType,
|
||||
)
|
||||
from app.enums.llm_billing import LlmBillingConfigKey
|
||||
from app.enums.hot_opening_replicate import HotOpeningLogEventEnum, HotOpeningStepCodeEnum, ModuleCodeEnum
|
||||
from app.schemas.hot_opening_replicate import (
|
||||
HotOpeningActionOut,
|
||||
@@ -43,10 +50,20 @@ from app.services.hot_opening_replicate_service import (
|
||||
update_hot_opening_video_prompt_schema,
|
||||
)
|
||||
from app.services.module_generation_log_service import log_module_error, log_module_event_file
|
||||
from app.services.llm_billing import (
|
||||
LlmBillingContext,
|
||||
log_celery_dispatch_compensated,
|
||||
log_celery_dispatch_failure,
|
||||
log_celery_dispatch_start,
|
||||
log_celery_dispatch_success,
|
||||
)
|
||||
from app.services.module_async_recovery_service import (
|
||||
OBJECT_MODULE_STEP,
|
||||
TASK_HOT_IMAGE_PROMPT,
|
||||
TASK_HOT_VIDEO_PROMPT,
|
||||
has_live_object_lock,
|
||||
register_module_step_task,
|
||||
remove_active_task,
|
||||
)
|
||||
from app.tasks.celery_app import celery_app
|
||||
from app.enums.upload_resource import UploadResourceEventEnum, UploadResourceModuleEnum, UploadResourceSourceModelEnum, UploadResourceTypeEnum
|
||||
@@ -138,6 +155,47 @@ def _log_api_exception_from_locals(exc: BaseException, local_values: dict, messa
|
||||
exc=exc,
|
||||
)
|
||||
|
||||
def _prompt_dispatch_billing_context(
|
||||
*,
|
||||
user_id: str,
|
||||
project_id: str,
|
||||
step_id: str,
|
||||
step_code: str,
|
||||
attempt_no: int,
|
||||
celery_task_id: str,
|
||||
) -> LlmBillingContext:
|
||||
is_image = step_code == HotOpeningStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value
|
||||
return LlmBillingContext(
|
||||
user_id=user_id,
|
||||
owner_type=CreditRecordOwnerType.MODULE_GENERATION_STEP.value,
|
||||
owner_id=step_id,
|
||||
attempt_no=attempt_no,
|
||||
charge_kind=CreditRecordChargeKind.TEXT_PROMPT.value,
|
||||
billing_scene=(
|
||||
CreditRecordBillingScene.HOT_OPENING_IMAGE_PROMPT_OPTIMIZE.value
|
||||
if is_image
|
||||
else CreditRecordBillingScene.HOT_OPENING_VIDEO_PROMPT_OPTIMIZE.value
|
||||
),
|
||||
source_module=MODULE,
|
||||
source_project_id=project_id,
|
||||
source_step_id=step_id,
|
||||
source_step_code=step_code,
|
||||
related_id=step_id,
|
||||
hold_config_key=(
|
||||
LlmBillingConfigKey.HOLD_MODULE_IMAGE_PROMPT.value
|
||||
if is_image
|
||||
else LlmBillingConfigKey.HOLD_MODULE_VIDEO_PROMPT.value
|
||||
),
|
||||
description_prefix=(
|
||||
"爆款开头复刻图片AI提词优化"
|
||||
if is_image
|
||||
else "爆款开头复刻视频提词优化"
|
||||
),
|
||||
trace_id=f"hot-opening-prompt:{step_id}:attempt:{attempt_no}",
|
||||
celery_task_id=celery_task_id,
|
||||
)
|
||||
|
||||
|
||||
async def _reload_project_detail(
|
||||
db: AsyncSession,
|
||||
current_user: User,
|
||||
@@ -161,10 +219,26 @@ async def _mark_dispatch_failed_and_raise(
|
||||
project_id: str,
|
||||
step_id: str | None,
|
||||
message: str,
|
||||
billing_context: LlmBillingContext | None = None,
|
||||
) -> None:
|
||||
"""Celery 投递失败后,数据库事务已提交,单独标记步骤失败,避免一直 processing。"""
|
||||
"""Celery 投递失败后补偿步骤和冻结积分,避免一直 processing。"""
|
||||
if billing_context is not None:
|
||||
log_celery_dispatch_failure(billing_context, error=message)
|
||||
compensated = False
|
||||
if step_id:
|
||||
try:
|
||||
if await has_live_object_lock(object_type=OBJECT_MODULE_STEP, object_id=step_id):
|
||||
log_module_error(
|
||||
module=MODULE,
|
||||
event_type=HotOpeningLogEventEnum.CELERY_DISPATCH_FAILED.value,
|
||||
project_id=project_id,
|
||||
step_id=step_id,
|
||||
user_id=_safe_user_id(current_user),
|
||||
message="Celery 投递返回异常,但 worker 已领取任务,跳过失败补偿",
|
||||
detail={"reason": "uncertain_dispatch_worker_started", "dispatch_error": message},
|
||||
error=message,
|
||||
)
|
||||
raise HTTPException(status_code=503, detail=f"{message};任务可能已被 worker 接收,请勿重复提交")
|
||||
await mark_hot_opening_step_dispatch_failed(
|
||||
db,
|
||||
current_user=_user_context(current_user),
|
||||
@@ -173,6 +247,23 @@ async def _mark_dispatch_failed_and_raise(
|
||||
error_message=message,
|
||||
)
|
||||
await db.commit()
|
||||
compensated = True
|
||||
if billing_context is not None:
|
||||
log_celery_dispatch_compensated(billing_context, error=message)
|
||||
try:
|
||||
await remove_active_task(object_type=OBJECT_MODULE_STEP, object_id=step_id)
|
||||
except Exception as cleanup_exc:
|
||||
_log_api_error(
|
||||
event_type=HotOpeningLogEventEnum.CELERY_DISPATCH_MARK_FAILED.value,
|
||||
current_user=current_user,
|
||||
project_id=project_id,
|
||||
step_id=step_id,
|
||||
message="Celery 投递补偿完成,但清理 active registry 失败",
|
||||
detail={"dispatch_error": message},
|
||||
exc=cleanup_exc,
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
await db.rollback()
|
||||
_log_api_error(
|
||||
@@ -191,12 +282,89 @@ async def _mark_dispatch_failed_and_raise(
|
||||
step_id=step_id,
|
||||
user_id=_safe_user_id(current_user),
|
||||
message=message,
|
||||
detail={"reason": "celery_dispatch_failed"},
|
||||
detail={"reason": "celery_dispatch_failed", "compensated": compensated},
|
||||
error=message,
|
||||
)
|
||||
raise HTTPException(status_code=503, detail=message)
|
||||
|
||||
|
||||
async def _dispatch_prompt_task(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
current_user: User,
|
||||
project_id: str,
|
||||
step_id: str,
|
||||
step_code: str,
|
||||
task_name: str,
|
||||
celery_task: Any,
|
||||
celery_task_id: str,
|
||||
billing_context: LlmBillingContext,
|
||||
error_prefix: str,
|
||||
) -> None:
|
||||
"""Redis 注册与 Celery 直投任一成功即视为可恢复投递。"""
|
||||
registry_error: Exception | None = None
|
||||
try:
|
||||
await register_module_step_task(
|
||||
module=MODULE,
|
||||
project_id=project_id,
|
||||
step_id=step_id,
|
||||
step_code=step_code,
|
||||
task_name=task_name,
|
||||
)
|
||||
except Exception as exc:
|
||||
registry_error = exc
|
||||
log_module_error(
|
||||
module=MODULE,
|
||||
event_type=HotOpeningLogEventEnum.CELERY_DISPATCH_FAILED.value,
|
||||
project_id=project_id,
|
||||
step_id=step_id,
|
||||
user_id=_safe_user_id(current_user),
|
||||
message="提词任务 Redis 活跃注册失败,将继续尝试 Celery 直投",
|
||||
detail={"channel": "active_registry"},
|
||||
exc=exc,
|
||||
)
|
||||
|
||||
celery_error: Exception | None = None
|
||||
try:
|
||||
celery_task.apply_async(
|
||||
args=[project_id, step_id],
|
||||
queue="gen_chatapi_create",
|
||||
countdown=0,
|
||||
task_id=celery_task_id,
|
||||
)
|
||||
except Exception as exc:
|
||||
celery_error = exc
|
||||
|
||||
if celery_error is None:
|
||||
log_celery_dispatch_success(billing_context)
|
||||
return
|
||||
if registry_error is None:
|
||||
log_celery_dispatch_failure(
|
||||
billing_context,
|
||||
error=f"Celery 直投失败,已保留 active registry 等待恢复:{celery_error}",
|
||||
)
|
||||
log_module_event_file(
|
||||
module=MODULE,
|
||||
event_type=HotOpeningLogEventEnum.CELERY_DISPATCH_FAILED.value,
|
||||
project_id=project_id,
|
||||
step_id=step_id,
|
||||
user_id=_safe_user_id(current_user),
|
||||
message="Celery 直投失败,任务将由 active registry 恢复投递",
|
||||
detail={"recoverable": True, "celery_task_id": celery_task_id},
|
||||
error=str(celery_error),
|
||||
)
|
||||
return
|
||||
|
||||
await _mark_dispatch_failed_and_raise(
|
||||
db,
|
||||
current_user=current_user,
|
||||
project_id=project_id,
|
||||
step_id=step_id,
|
||||
message=f"{error_prefix}: Redis 注册失败({registry_error});Celery 投递失败({celery_error})",
|
||||
billing_context=billing_context,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/spec",
|
||||
response_model=HotOpeningSpecOut,
|
||||
@@ -514,6 +682,17 @@ async def generate_image_prompt(
|
||||
project, step = await submit_image_prompt_optimize(db, current_user=current_user, project_id=project_id, material_step_id=step_id)
|
||||
project_id_value = str(project.id)
|
||||
step_id_value = str(step.id)
|
||||
user_id_value = str(project.user_id)
|
||||
attempt_no_value = int(step.version or 1)
|
||||
celery_task_id = f"hot-opening:image-prompt:{step_id_value}"
|
||||
billing_context = _prompt_dispatch_billing_context(
|
||||
user_id=user_id_value,
|
||||
project_id=project_id_value,
|
||||
step_id=step_id_value,
|
||||
step_code=HotOpeningStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value,
|
||||
attempt_no=attempt_no_value,
|
||||
celery_task_id=celery_task_id,
|
||||
)
|
||||
await db.commit()
|
||||
except HTTPException:
|
||||
await db.rollback()
|
||||
@@ -525,23 +704,19 @@ async def generate_image_prompt(
|
||||
|
||||
from app.tasks.hot_opening_replicate_tasks import start_image_prompt_optimize
|
||||
|
||||
await register_module_step_task(
|
||||
module=MODULE,
|
||||
log_celery_dispatch_start(billing_context)
|
||||
await _dispatch_prompt_task(
|
||||
db,
|
||||
current_user=current_user,
|
||||
project_id=project_id_value,
|
||||
step_id=step_id_value,
|
||||
step_code=HotOpeningStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value,
|
||||
task_name=TASK_HOT_IMAGE_PROMPT,
|
||||
celery_task=start_image_prompt_optimize,
|
||||
celery_task_id=celery_task_id,
|
||||
billing_context=billing_context,
|
||||
error_prefix="图片提词任务投递失败",
|
||||
)
|
||||
try:
|
||||
start_image_prompt_optimize.apply_async(args=[project_id_value, step_id_value], queue="gen_chatapi_create", countdown=0)
|
||||
except Exception as exc:
|
||||
await _mark_dispatch_failed_and_raise(
|
||||
db,
|
||||
current_user=current_user,
|
||||
project_id=project_id_value,
|
||||
step_id=step_id_value,
|
||||
message=f"图片提词任务投递失败: {exc}",
|
||||
)
|
||||
|
||||
return HotOpeningActionOut(
|
||||
message="图片 AI 提词任务已提交",
|
||||
@@ -657,6 +832,17 @@ async def generate_video_prompt(
|
||||
project, step = await submit_video_prompt_optimize(db, current_user=current_user, project_id=project_id, image_step_id=step_id, req=req)
|
||||
project_id_value = str(project.id)
|
||||
step_id_value = str(step.id)
|
||||
user_id_value = str(project.user_id)
|
||||
attempt_no_value = int(step.version or 1)
|
||||
celery_task_id = f"hot-opening:video-prompt:{step_id_value}"
|
||||
billing_context = _prompt_dispatch_billing_context(
|
||||
user_id=user_id_value,
|
||||
project_id=project_id_value,
|
||||
step_id=step_id_value,
|
||||
step_code=HotOpeningStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value,
|
||||
attempt_no=attempt_no_value,
|
||||
celery_task_id=celery_task_id,
|
||||
)
|
||||
await db.commit()
|
||||
except HTTPException:
|
||||
await db.rollback()
|
||||
@@ -668,23 +854,19 @@ async def generate_video_prompt(
|
||||
|
||||
from app.tasks.hot_opening_replicate_tasks import start_video_prompt_optimize
|
||||
|
||||
await register_module_step_task(
|
||||
module=MODULE,
|
||||
log_celery_dispatch_start(billing_context)
|
||||
await _dispatch_prompt_task(
|
||||
db,
|
||||
current_user=current_user,
|
||||
project_id=project_id_value,
|
||||
step_id=step_id_value,
|
||||
step_code=HotOpeningStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value,
|
||||
task_name=TASK_HOT_VIDEO_PROMPT,
|
||||
celery_task=start_video_prompt_optimize,
|
||||
celery_task_id=celery_task_id,
|
||||
billing_context=billing_context,
|
||||
error_prefix="视频提词任务投递失败",
|
||||
)
|
||||
try:
|
||||
start_video_prompt_optimize.apply_async(args=[project_id_value, step_id_value], queue="gen_chatapi_create", countdown=0)
|
||||
except Exception as exc:
|
||||
await _mark_dispatch_failed_and_raise(
|
||||
db,
|
||||
current_user=current_user,
|
||||
project_id=project_id_value,
|
||||
step_id=step_id_value,
|
||||
message=f"视频提词任务投递失败: {exc}",
|
||||
)
|
||||
|
||||
return HotOpeningActionOut(
|
||||
message="视频 AI 提词任务已提交",
|
||||
|
||||
Reference in New Issue
Block a user