feat(points): 积分消耗系统全链路集成

后端:
- 简化积分服务: 删除 freeze/settle/refund, 保留 consume/recharge/expire
- 计费配置化: config/points-config.yaml 驱动 fixed/duration/free 三种模式
- TTS 时长探测: app/utils/audio_utils.py (httpx + mutagen 纯 Python)
- Python 层扣费: script(5)/polish(1)/title(1)/voice_clone(200)/tts(按秒)/video(按秒)
- 字幕 free_services: caption/auto_align 不扣费
- 新增 POST /points/consume 端点(402余额预检)
- 新增 check_balance + /points/cost 返回 sufficient/balance/required
- 新增 expire_batches 定时回收, 接入 scheduler main(每5分钟)
- 删除废弃 tts_handler.py
- Alembic 迁移: 删除 frozen/total_refunded 字段
- 同步 requirements.lock 添加 mutagen

前端:
- Rust/IPC 层扣费: compose(5)/subtitle_burn(2)/cover_design(2)
- 字幕打轴改异步: 走 scheduler subtitle handler
- 对口型传 duration: VideoGeneration 传 actualDuration
- 创建 pointStore: 全局余额 + fetchBalance + 充值弹窗控制
- 402 欠费弹 RechargeModal: VideoGeneration/SubtitleBurning/CoverDesign
- 修复 VoiceDubbing.tsx 类型错误 (alignResult never)
- 同步 PointBalance 类型(删除 frozen/available/totalRefunded)

Refs: 积分消耗集成收尾
This commit is contained in:
小鱼开发
2026-05-09 15:42:54 +08:00
parent 8f55093457
commit c6eba97b43
28 changed files with 790 additions and 652 deletions
+38 -3
View File
@@ -10,8 +10,11 @@ from __future__ import annotations
import asyncio
import logging
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user
from app.db.session import get_db
from app.ai.model_router import get_model_router
from app.ai.prompts import list_categories, load_prompt, render_template
from app.schemas.common import ApiResponse, success_response
@@ -25,6 +28,8 @@ from app.schemas.script import (
TestModelResponse,
)
from app.services.script_service import get_script_service
from app.services import point_service as ps
from app.models.user import User
router = APIRouter()
logger = logging.getLogger(__name__)
@@ -45,7 +50,11 @@ async def get_categories():
@router.post("/polish", response_model=ApiResponse[str])
async def polish_content(request: PolishRequest):
async def polish_content(
request: PolishRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""
AI 润色文案/画面描述
@@ -65,6 +74,17 @@ async def polish_content(request: PolishRequest):
shot_type=request.shot_type or "segment",
)
# 扣费
points = ps._calculate_cost("polish")
await ps.consume(
db,
user_id=current_user.id,
points=points,
source_type="polish",
source_id=f"polish_{current_user.id}_{asyncio.get_event_loop().time()}",
description=f"润色 {request.polish_type}",
)
return success_response(
data=polished,
message=f"{type_name}润色完成",
@@ -118,7 +138,11 @@ async def test_model(request: TestModelRequest):
@router.post("/generate-title", response_model=ApiResponse[GenerateTitleResponse])
async def generate_title(request: GenerateTitleRequest):
async def generate_title(
request: GenerateTitleRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""
根据脚本内容智能生成标题
@@ -178,6 +202,17 @@ async def generate_title(request: GenerateTitleRequest):
if len(title) > request.max_length:
title = title[:request.max_length]
# 扣费
points = ps._calculate_cost("title")
await ps.consume(
db,
user_id=current_user.id,
points=points,
source_type="title",
source_id=f"title_{current_user.id}_{asyncio.get_event_loop().time()}",
description=f"生成{request.title_type}标题",
)
return success_response(
data=GenerateTitleResponse(title=title),
message="标题生成成功",