会员积分改版V1

This commit is contained in:
2026-08-11 09:24:18 +08:00
parent fe24e51b97
commit b9fd07f293
111 changed files with 9355 additions and 3900 deletions
@@ -12,7 +12,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.enums.common import ModuleEventTypeEnum, ModuleProjectStatusEnum, ModulePromptTypeEnum, ModuleStepStatusEnum
from app.enums.credit_record import CreditRecordBillingScene, CreditRecordChargeKind, CreditRecordOwnerType
from app.enums.llm_billing import LlmBillingConfigKey
from app.enums.hot_opening_replicate import HotOpeningGenerationModeEnum, HotOpeningStepCodeEnum, ModuleCodeEnum
from app.models.chat_generation_task import ChatGenerationTask
from app.models.module_generation_project import ModuleGenerationProject
@@ -55,13 +54,15 @@ from app.services.module_generation_log_service import log_module_error, log_mod
from app.services.llm import optimize_prompt
from app.services.llm_billing import (
LlmBillingContext,
ensure_hold_exists,
ensure_pre_deducted,
log_provider_failure,
record_provider_exception,
log_provider_start,
log_provider_success,
release_on_failure,
settle_success,
start_hold,
refund_on_final_failure,
finalize_llm_business_failure,
mark_business_success,
pre_deduct,
)
from app.services.module_generation_flow_base_service import (
assert_project_has_no_active_chat_tasks as _base_assert_project_has_no_active_chat_tasks,
@@ -892,7 +893,7 @@ async def submit_image_prompt_optimize(
project.status = ModuleProjectStatusEnum.PROCESSING.value
project.current_step_code = HotOpeningStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value
project.error_message = None
await start_hold(
await pre_deduct(
db,
LlmBillingContext(
user_id=str(project.user_id),
@@ -906,9 +907,8 @@ async def submit_image_prompt_optimize(
source_step_id=str(step.id),
source_step_code=HotOpeningStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value,
related_id=str(step.id),
hold_config_key=LlmBillingConfigKey.HOLD_MODULE_IMAGE_PROMPT.value,
description_prefix="爆款开头复刻图片AI提词优化",
trace_id=f"llm-submit-hold:{step.id}",
trace_id=f"llm-submit-pre-deduct:{step.id}",
),
)
await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.IMAGE_PROMPT_SUBMITTED.value, message="图片 AI 提词任务已提交")
@@ -1005,14 +1005,13 @@ async def run_image_prompt_optimize(
source_step_id=step_id_value,
source_step_code=HotOpeningStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value,
related_id=step_id_value,
hold_config_key=LlmBillingConfigKey.HOLD_MODULE_IMAGE_PROMPT.value,
description_prefix="爆款开头复刻图片AI提词优化",
description_prefix="爆款开头复刻图片AI提词优化",
trace_id=f"hot-opening-image-prompt:{step_id_value}",
)
hold_validation = await ensure_hold_exists(db, llm_billing_context)
if not hold_validation.can_execute:
pre_deduct_validation = await ensure_pre_deducted(db, llm_billing_context)
if not pre_deduct_validation.can_execute:
step.status = ModuleStepStatusEnum.FAILED.value
step.error_message = f"LLM账务状态异常({hold_validation.state.value}),已终止任务"
step.error_message = f"LLM账务状态异常({pre_deduct_validation.state.value}),已终止任务"
step.completed_at = _now()
project.status = ModuleProjectStatusEnum.FAILED.value
project.error_message = step.error_message
@@ -1023,7 +1022,7 @@ async def run_image_prompt_optimize(
provider_succeeded = False
token_usage: dict[str, Any] = {}
log_provider_start(llm_billing_context, detail={"prompt_type": "image"})
await log_provider_start(db, llm_billing_context, detail={"prompt_type": "image"})
try:
request_log = {"original_prompt": prompt_text, "references": references, "gen_type": "image"}
log_module_prompt_event(
@@ -1048,9 +1047,11 @@ async def run_image_prompt_optimize(
log_owner_type="module_generation_step",
log_owner_id=step_id_value,
generation_attempt_no=expected_step_version,
fixed_model_config_id=llm_billing_context.model_config_id,
fixed_model_snapshot=llm_billing_context.model_parameters_snapshot,
)
provider_succeeded = True
log_provider_success(llm_billing_context, usage=token_usage)
await log_provider_success(db, llm_billing_context, usage=token_usage)
if execution_guard is not None:
await execution_guard()
project, step = await _reload_prompt_context_for_update(
@@ -1064,16 +1065,10 @@ async def run_image_prompt_optimize(
expected_version=expected_step_version,
expected_input_json=expected_input_json,
):
# Provider 已成功,旧步骤即使失效也必须按真实 usage 结算,不能免费释放。
await settle_success(
db,
llm_billing_context,
usage=token_usage,
description="爆款开头复刻-图片AI提词优化(失效结果结算)",
)
await db.commit()
await log_provider_failure(db, llm_billing_context, error="当前步骤已失效,业务结果未采用")
await finalize_llm_business_failure(llm_billing_context, error="当前步骤已失效,业务结果未采用")
return None
billing = await settle_success(
billing = await mark_business_success(
db,
llm_billing_context,
usage=token_usage,
@@ -1121,21 +1116,19 @@ async def run_image_prompt_optimize(
except DatabaseRowLockBusy:
await db.rollback()
if provider_succeeded:
# Provider 已完成后不再重复调用模型;先按真实 usage 结算,本次结果因本地行锁冲突丢弃。
await settle_success(
db,
llm_billing_context,
usage=token_usage,
description="爆款开头复刻-图片AI提词优化(行锁失败结算)",
)
await db.commit()
await log_provider_failure(db, llm_billing_context, error="本地行锁冲突,业务结果未落库")
await finalize_llm_business_failure(llm_billing_context, error="本地行锁冲突,业务结果未落库")
return None
# Provider 尚未成功才允许同一 attempt 做系统自动重试。
raise
except Exception as exc:
await db.rollback()
if not provider_succeeded:
log_provider_failure(llm_billing_context, error=str(exc))
if provider_succeeded:
await log_provider_failure(db, llm_billing_context, error=str(exc))
else:
provider_succeeded, recovered_usage = await record_provider_exception(db, llm_billing_context, exc)
if recovered_usage:
token_usage = recovered_usage
if execution_guard is not None:
await execution_guard()
project, step = await _reload_prompt_context_for_update(
@@ -1150,16 +1143,7 @@ async def run_image_prompt_optimize(
expected_input_json=expected_input_json,
):
await db.rollback()
if provider_succeeded:
await settle_success(
db,
llm_billing_context,
usage=token_usage,
description="爆款开头复刻-图片AI提词优化(异常失效结算)",
)
else:
await release_on_failure(db, llm_billing_context, error="当前步骤已失效,释放LLM预扣积分")
await db.commit()
await finalize_llm_business_failure(llm_billing_context, error="当前步骤已失效,业务最终失败")
return None
step.status = ModuleStepStatusEnum.FAILED.value
step.error_message = str(exc) if str(exc) else type(exc).__name__
@@ -1178,16 +1162,8 @@ async def run_image_prompt_optimize(
)
_log_project_error(project=project, step=step, event_type="IMAGE_PROMPT_FAILED", message=project.error_message, exc=exc)
await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.IMAGE_PROMPT_FAILED.value, message=project.error_message)
if provider_succeeded:
await settle_success(
db,
llm_billing_context,
usage=token_usage,
description="爆款开头复刻-图片AI提词优化(本地失败结算)",
)
else:
await release_on_failure(db, llm_billing_context, error=str(exc))
await db.commit()
await finalize_llm_business_failure(llm_billing_context, error=str(exc))
return step
@@ -1351,7 +1327,7 @@ async def submit_video_prompt_optimize(
project.status = ModuleProjectStatusEnum.PROCESSING.value
project.current_step_code = HotOpeningStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value
project.error_message = None
await start_hold(
await pre_deduct(
db,
LlmBillingContext(
user_id=str(project.user_id),
@@ -1365,9 +1341,8 @@ async def submit_video_prompt_optimize(
source_step_id=str(step.id),
source_step_code=HotOpeningStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value,
related_id=str(step.id),
hold_config_key=LlmBillingConfigKey.HOLD_MODULE_VIDEO_PROMPT.value,
description_prefix="爆款开头复刻视频AI提词优化",
trace_id=f"llm-submit-hold:{step.id}",
trace_id=f"llm-submit-pre-deduct:{step.id}",
),
)
await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.VIDEO_PROMPT_SUBMITTED.value, message="视频 AI 提词任务已提交")
@@ -1462,14 +1437,13 @@ async def run_video_prompt_optimize(
source_step_id=step_id_value,
source_step_code=HotOpeningStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value,
related_id=step_id_value,
hold_config_key=LlmBillingConfigKey.HOLD_MODULE_VIDEO_PROMPT.value,
description_prefix="爆款开头复刻视频AI提词优化",
description_prefix="爆款开头复刻视频AI提词优化",
trace_id=f"hot-opening-video-prompt:{step_id_value}",
)
hold_validation = await ensure_hold_exists(db, llm_billing_context)
if not hold_validation.can_execute:
pre_deduct_validation = await ensure_pre_deducted(db, llm_billing_context)
if not pre_deduct_validation.can_execute:
step.status = ModuleStepStatusEnum.FAILED.value
step.error_message = f"LLM账务状态异常({hold_validation.state.value}),已终止任务"
step.error_message = f"LLM账务状态异常({pre_deduct_validation.state.value}),已终止任务"
step.completed_at = _now()
project.status = ModuleProjectStatusEnum.FAILED.value
project.error_message = step.error_message
@@ -1480,7 +1454,7 @@ async def run_video_prompt_optimize(
provider_succeeded = False
token_usage: dict[str, Any] = {}
log_provider_start(llm_billing_context, detail={"prompt_type": "video"})
await log_provider_start(db, llm_billing_context, detail={"prompt_type": "video"})
try:
request_log = {
"source_project_name": material.get("source_project_name") or "",
@@ -1517,9 +1491,11 @@ async def run_video_prompt_optimize(
project_id=project_id_value,
step_id=step_id_value,
trace_id=f"hot-video-prompt:{step_id_value}",
fixed_model_config_id=llm_billing_context.model_config_id,
fixed_model_snapshot=llm_billing_context.model_parameters_snapshot,
)
provider_succeeded = True
log_provider_success(llm_billing_context, usage=token_usage)
await log_provider_success(db, llm_billing_context, usage=token_usage)
if execution_guard is not None:
await execution_guard()
project, step = await _reload_prompt_context_for_update(
@@ -1533,15 +1509,10 @@ async def run_video_prompt_optimize(
expected_version=expected_step_version,
expected_input_json=expected_input_json,
):
await settle_success(
db,
llm_billing_context,
usage=token_usage,
description="爆款开头复刻-视频AI提词优化(失效结果结算)",
)
await db.commit()
await log_provider_failure(db, llm_billing_context, error="当前步骤已失效,业务结果未采用")
await finalize_llm_business_failure(llm_billing_context, error="当前步骤已失效,业务结果未采用")
return None
billing = await settle_success(
billing = await mark_business_success(
db,
llm_billing_context,
usage=token_usage,
@@ -1592,21 +1563,19 @@ async def run_video_prompt_optimize(
except DatabaseRowLockBusy:
await db.rollback()
if provider_succeeded:
# Provider 已完成后不再重复调用模型;先按真实 usage 结算,本次结果因本地行锁冲突丢弃。
await settle_success(
db,
llm_billing_context,
usage=token_usage,
description="爆款开头复刻-视频AI提词优化(行锁失败结算)",
)
await db.commit()
await log_provider_failure(db, llm_billing_context, error="本地行锁冲突,业务结果未落库")
await finalize_llm_business_failure(llm_billing_context, error="本地行锁冲突,业务结果未落库")
return None
# Provider 尚未成功才允许同一 attempt 做系统自动重试。
raise
except Exception as exc:
await db.rollback()
if not provider_succeeded:
log_provider_failure(llm_billing_context, error=str(exc))
if provider_succeeded:
await log_provider_failure(db, llm_billing_context, error=str(exc))
else:
provider_succeeded, recovered_usage = await record_provider_exception(db, llm_billing_context, exc)
if recovered_usage:
token_usage = recovered_usage
if execution_guard is not None:
await execution_guard()
project, step = await _reload_prompt_context_for_update(
@@ -1621,16 +1590,7 @@ async def run_video_prompt_optimize(
expected_input_json=expected_input_json,
):
await db.rollback()
if provider_succeeded:
await settle_success(
db,
llm_billing_context,
usage=token_usage,
description="爆款开头复刻-视频AI提词优化(异常失效结算)",
)
else:
await release_on_failure(db, llm_billing_context, error="当前步骤已失效,释放LLM预扣积分")
await db.commit()
await finalize_llm_business_failure(llm_billing_context, error="当前步骤已失效,业务最终失败")
return None
step.status = ModuleStepStatusEnum.FAILED.value
step.error_message = str(exc) if str(exc) else type(exc).__name__
@@ -1649,16 +1609,8 @@ async def run_video_prompt_optimize(
)
_log_project_error(project=project, step=step, event_type="VIDEO_PROMPT_FAILED", message=project.error_message, exc=exc)
await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.VIDEO_PROMPT_FAILED.value, message=project.error_message)
if provider_succeeded:
await settle_success(
db,
llm_billing_context,
usage=token_usage,
description="爆款开头复刻-视频AI提词优化(本地失败结算)",
)
else:
await release_on_failure(db, llm_billing_context, error=str(exc))
await db.commit()
await finalize_llm_business_failure(llm_billing_context, error=str(exc))
return step
@@ -1946,7 +1898,7 @@ async def mark_hot_opening_step_dispatch_failed(
project.status = ModuleProjectStatusEnum.FAILED.value
project.error_message = error_message
if step.step_code in (HotOpeningStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value, HotOpeningStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value):
await release_on_failure(
await refund_on_final_failure(
db,
LlmBillingContext(
user_id=str(project.user_id),
@@ -1964,11 +1916,6 @@ async def mark_hot_opening_step_dispatch_failed(
source_step_id=str(step.id),
source_step_code=str(step.step_code),
related_id=str(step.id),
hold_config_key=(
LlmBillingConfigKey.HOLD_MODULE_IMAGE_PROMPT.value
if step.step_code == HotOpeningStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value
else LlmBillingConfigKey.HOLD_MODULE_VIDEO_PROMPT.value
),
description_prefix=(
"爆款开头复刻图片AI提词优化"
if step.step_code == HotOpeningStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value