积分冻结释放
This commit is contained in:
@@ -17,7 +17,9 @@ from app.enums.common import (
|
||||
ModuleProjectStatusEnum,
|
||||
ModuleStepStatusEnum,
|
||||
)
|
||||
from app.enums.credit_record import CreditRecordBillingScene, CreditRecordChargeKind, CreditRecordOwnerType
|
||||
from app.enums.generation_task import ChatGenerationTaskStatus
|
||||
from app.enums.llm_billing import LlmBillingConfigKey
|
||||
from app.enums.shot_replicate import (
|
||||
ShotSegmentReplicateStatusEnum,
|
||||
ShotSplitStatusEnum,
|
||||
@@ -42,12 +44,21 @@ from app.services.generation.ai.engine_service import (
|
||||
get_video_engine,
|
||||
parse_json_list,
|
||||
)
|
||||
from app.services.generation.billing_service import charge_module_prompt_usage
|
||||
from app.services.generation.pipeline.db_lock_service import (
|
||||
DatabaseRowLockBusy,
|
||||
execute_with_lock_timeout,
|
||||
)
|
||||
from app.services.generation.task_factory_service import create_chat_generation_task_for_module
|
||||
from app.services.llm_billing import (
|
||||
LlmBillingContext,
|
||||
ensure_hold_exists,
|
||||
log_provider_failure,
|
||||
log_provider_start,
|
||||
log_provider_success,
|
||||
release_on_failure,
|
||||
settle_success,
|
||||
start_hold,
|
||||
)
|
||||
from app.services.hot_opening_video_prompt_service import (
|
||||
build_final_video_prompt,
|
||||
optimize_hot_opening_video_prompt,
|
||||
@@ -331,6 +342,39 @@ async def _create_material_and_prompt_steps(
|
||||
return material_step, prompt_step
|
||||
|
||||
|
||||
|
||||
|
||||
def build_v2_video_prompt_billing_context(
|
||||
*,
|
||||
user_id: str,
|
||||
project_id: str,
|
||||
step_id: str,
|
||||
step_version: int,
|
||||
module: str,
|
||||
display_name: str,
|
||||
) -> LlmBillingContext:
|
||||
return LlmBillingContext(
|
||||
user_id=str(user_id),
|
||||
owner_type=CreditRecordOwnerType.MODULE_GENERATION_STEP.value,
|
||||
owner_id=str(step_id),
|
||||
attempt_no=int(step_version or 1),
|
||||
charge_kind=CreditRecordChargeKind.TEXT_PROMPT.value,
|
||||
billing_scene=(
|
||||
CreditRecordBillingScene.HOT_OPENING_VIDEO_PROMPT_OPTIMIZE.value
|
||||
if module == "hot_opening_replicate"
|
||||
else CreditRecordBillingScene.SHOT_VIDEO_PROMPT_OPTIMIZE.value
|
||||
),
|
||||
source_module=str(module),
|
||||
source_project_id=str(project_id),
|
||||
source_step_id=str(step_id),
|
||||
source_step_code=VIDEO_PROMPT_OPTIMIZE,
|
||||
related_id=str(step_id),
|
||||
hold_config_key=LlmBillingConfigKey.HOLD_MODULE_VIDEO_PROMPT.value,
|
||||
description_prefix=f"{display_name}视频提词优化",
|
||||
trace_id=f"module-v2-video-prompt:{step_id}",
|
||||
)
|
||||
|
||||
|
||||
async def create_hot_opening_project_v2(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
@@ -389,6 +433,17 @@ async def create_hot_opening_project_v2(
|
||||
video_config=video_config,
|
||||
target_platform=req.target_platform or "抖音",
|
||||
)
|
||||
await start_hold(
|
||||
db,
|
||||
build_v2_video_prompt_billing_context(
|
||||
user_id=str(current_user.id),
|
||||
project_id=str(project.id),
|
||||
step_id=str(prompt_step.id),
|
||||
step_version=int(prompt_step.version or 1),
|
||||
module=HOT_OPENING_V2.module,
|
||||
display_name=HOT_OPENING_V2.display_name,
|
||||
),
|
||||
)
|
||||
await bind_upload_resources(
|
||||
db,
|
||||
user_id=current_user.id,
|
||||
@@ -545,6 +600,17 @@ async def create_shot_replicate_project_v2(
|
||||
urls=[req.material_image_url],
|
||||
allow_common_migrate=True,
|
||||
)
|
||||
await start_hold(
|
||||
db,
|
||||
build_v2_video_prompt_billing_context(
|
||||
user_id=str(current_user.id),
|
||||
project_id=str(project.id),
|
||||
step_id=str(prompt_step.id),
|
||||
step_version=int(prompt_step.version or 1),
|
||||
module=SHOT_REPLICATE_V2.module,
|
||||
display_name=SHOT_REPLICATE_V2.display_name,
|
||||
),
|
||||
)
|
||||
segment.module_project_id = project.id
|
||||
segment.replicate_status = ShotSegmentReplicateStatusEnum.PROJECT_CREATED.value
|
||||
await log_v2_event(
|
||||
@@ -658,6 +724,17 @@ async def rebuild_video_prompt_step_v2(
|
||||
project.final_video_cover_url = None
|
||||
project.completed_at = None
|
||||
project.error_message = None
|
||||
await start_hold(
|
||||
db,
|
||||
build_v2_video_prompt_billing_context(
|
||||
user_id=str(project.user_id),
|
||||
project_id=str(project.id),
|
||||
step_id=str(step.id),
|
||||
step_version=int(step.version or 1),
|
||||
module=config.module,
|
||||
display_name=config.display_name,
|
||||
),
|
||||
)
|
||||
await log_v2_event(
|
||||
db,
|
||||
project=project,
|
||||
@@ -722,6 +799,18 @@ async def mark_video_prompt_dispatch_failed_v2(
|
||||
message=error_message,
|
||||
detail={"dispatch_compensated": True},
|
||||
)
|
||||
await release_on_failure(
|
||||
db,
|
||||
build_v2_video_prompt_billing_context(
|
||||
user_id=str(project.user_id),
|
||||
project_id=str(project.id),
|
||||
step_id=str(step.id),
|
||||
step_version=int(step.version or 1),
|
||||
module=config.module,
|
||||
display_name=config.display_name,
|
||||
),
|
||||
error=error_message,
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
@@ -783,8 +872,41 @@ async def run_video_prompt_optimize_v2(
|
||||
if not project_snapshot["material_video_url"]:
|
||||
raise RuntimeError("V2 素材步骤缺少参考视频")
|
||||
schema_config_snapshot = await get_runtime_schema_snapshot(db)
|
||||
llm_billing_context = LlmBillingContext(
|
||||
user_id=project_snapshot["user_id"],
|
||||
owner_type=CreditRecordOwnerType.MODULE_GENERATION_STEP.value,
|
||||
owner_id=project_snapshot["step_id"],
|
||||
attempt_no=expected_version,
|
||||
charge_kind=CreditRecordChargeKind.TEXT_PROMPT.value,
|
||||
billing_scene=(
|
||||
CreditRecordBillingScene.HOT_OPENING_VIDEO_PROMPT_OPTIMIZE.value
|
||||
if project_snapshot["module"] == "hot_opening_replicate"
|
||||
else CreditRecordBillingScene.SHOT_VIDEO_PROMPT_OPTIMIZE.value
|
||||
),
|
||||
source_module=project_snapshot["module"],
|
||||
source_project_id=project_snapshot["project_id"],
|
||||
source_step_id=project_snapshot["step_id"],
|
||||
source_step_code=VIDEO_PROMPT_OPTIMIZE,
|
||||
related_id=project_snapshot["step_id"],
|
||||
hold_config_key=LlmBillingConfigKey.HOLD_MODULE_VIDEO_PROMPT.value,
|
||||
description_prefix=f"{config.display_name}视频提词优化",
|
||||
trace_id=f"module-v2-video-prompt:{project_snapshot['step_id']}",
|
||||
)
|
||||
hold_validation = await ensure_hold_exists(db, llm_billing_context)
|
||||
if not hold_validation.can_execute:
|
||||
step.status = ModuleStepStatusEnum.FAILED.value
|
||||
step.error_message = f"LLM账务状态异常({hold_validation.state.value}),已终止任务"
|
||||
step.completed_at = utc_now()
|
||||
project.status = ModuleProjectStatusEnum.FAILED.value
|
||||
project.error_message = step.error_message
|
||||
await log_v2_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.VIDEO_PROMPT_FAILED.value, message=step.error_message)
|
||||
await db.commit()
|
||||
return step
|
||||
await db.commit()
|
||||
|
||||
provider_succeeded = False
|
||||
usage: dict[str, Any] = {}
|
||||
log_provider_start(llm_billing_context, detail={"prompt_type": "video", "flow_version": "v2"})
|
||||
prompt_schema, final_prompt, usage = await optimize_hot_opening_video_prompt(
|
||||
db,
|
||||
user_id=project_snapshot["user_id"],
|
||||
@@ -800,6 +922,8 @@ async def run_video_prompt_optimize_v2(
|
||||
project_id=project_snapshot["project_id"],
|
||||
step_id=project_snapshot["step_id"],
|
||||
)
|
||||
provider_succeeded = True
|
||||
log_provider_success(llm_billing_context, usage=usage)
|
||||
|
||||
if execution_guard is not None:
|
||||
await execution_guard()
|
||||
@@ -821,10 +945,24 @@ async def run_video_prompt_optimize_v2(
|
||||
row = locked.first()
|
||||
if not row:
|
||||
await db.rollback()
|
||||
# Provider 已成功,即使业务对象被异常移除,也必须按真实 usage 完成幂等结算。
|
||||
await settle_success(
|
||||
db,
|
||||
llm_billing_context,
|
||||
usage=usage,
|
||||
description=f"{config.display_name}-视频提词优化(业务对象失效结算)",
|
||||
)
|
||||
await db.commit()
|
||||
return None
|
||||
project, step = row
|
||||
if int(step.version) != expected_version or step.input_json != expected_input or step.status != ModuleStepStatusEnum.PROCESSING.value:
|
||||
await db.rollback()
|
||||
await settle_success(
|
||||
db,
|
||||
llm_billing_context,
|
||||
usage=usage,
|
||||
description=f"{config.display_name}-视频提词优化(失效结果结算)",
|
||||
)
|
||||
await db.commit()
|
||||
log_module_event_file(
|
||||
module=project_snapshot["module"],
|
||||
event_type=ModuleEventTypeEnum.STALE_STEP_RESULT_DISCARDED.value,
|
||||
@@ -835,13 +973,11 @@ async def run_video_prompt_optimize_v2(
|
||||
detail={"expected_version": expected_version},
|
||||
)
|
||||
return None
|
||||
billing = await charge_module_prompt_usage(
|
||||
billing = await settle_success(
|
||||
db,
|
||||
user_id=project.user_id,
|
||||
step_id=step.id,
|
||||
llm_billing_context,
|
||||
usage=usage,
|
||||
description=f"{config.display_name}-视频提词优化",
|
||||
attempt_no=1,
|
||||
)
|
||||
output_payload = {
|
||||
"prompt_schema": prompt_schema,
|
||||
@@ -861,7 +997,7 @@ async def run_video_prompt_optimize_v2(
|
||||
payload=output_payload,
|
||||
usage={
|
||||
**dict(usage or {}),
|
||||
"text_credits_cost": round(billing.total_charged, 2),
|
||||
"text_credits_cost": billing.get_amount(CreditRecordChargeKind.TEXT_PROMPT.value),
|
||||
},
|
||||
schema_version=config.io_schema_version,
|
||||
),
|
||||
@@ -884,10 +1020,31 @@ async def run_video_prompt_optimize_v2(
|
||||
return step
|
||||
except DatabaseRowLockBusy:
|
||||
await db.rollback()
|
||||
if locals().get("provider_succeeded", False):
|
||||
await settle_success(
|
||||
db,
|
||||
llm_billing_context,
|
||||
usage=locals().get("usage") or {},
|
||||
description=f"{config.display_name}-视频提词优化(行锁失败结算)",
|
||||
)
|
||||
await db.commit()
|
||||
return None
|
||||
raise
|
||||
except Exception as exc:
|
||||
await db.rollback()
|
||||
if "llm_billing_context" in locals() and not locals().get("provider_succeeded", False):
|
||||
log_provider_failure(llm_billing_context, error=str(exc))
|
||||
try:
|
||||
if "llm_billing_context" in locals():
|
||||
if locals().get("provider_succeeded", False):
|
||||
await settle_success(
|
||||
db,
|
||||
llm_billing_context,
|
||||
usage=locals().get("usage") or {},
|
||||
description=f"{config.display_name}-视频提词优化(本地失败结算)",
|
||||
)
|
||||
else:
|
||||
await release_on_failure(db, llm_billing_context, error=str(exc))
|
||||
if execution_guard is not None:
|
||||
await execution_guard()
|
||||
result = await execute_with_lock_timeout(
|
||||
@@ -913,7 +1070,9 @@ async def run_video_prompt_optimize_v2(
|
||||
step.completed_at = utc_now()
|
||||
project.status = ModuleProjectStatusEnum.FAILED.value
|
||||
project.error_message = str(exc)
|
||||
await db.commit()
|
||||
# provider 已成功时 settle_success 已在当前事务写入 RELEASE/CHARGE;
|
||||
# 即使业务步骤已不存在或已不是 processing,也必须提交账务结算。
|
||||
await db.commit()
|
||||
log_module_error(
|
||||
module=row[0].module if row else "module_generation_v2",
|
||||
event_type=ModuleEventTypeEnum.VIDEO_PROMPT_FAILED.value,
|
||||
@@ -930,9 +1089,11 @@ async def run_video_prompt_optimize_v2(
|
||||
project_id=project_id,
|
||||
step_id=step_id,
|
||||
message="V2 视频提词失败状态落库失败",
|
||||
detail={"origin_error": str(exc)},
|
||||
detail={"origin_error": str(exc), "provider_succeeded": locals().get("provider_succeeded", False)},
|
||||
exc=mark_exc,
|
||||
)
|
||||
if locals().get("provider_succeeded", False):
|
||||
raise
|
||||
return None
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user