前台页面/conversation修改积分计算逻辑

1、后台页面/credit-ratios,需要选择视频,上边是模型价格,下边增加传入视频价格,也要输入对应的倍率、基础积分、每秒积分
2、前台/conversation获取积分逻辑的接口也要增加对应的传入视频的比例
3、然后前台根据用户上传视频自动获取对应的比例加上对应模型选择的积分计算总积分
4、后台提交也要验证对应的积分是否正确
5、多个视频总时长需限制15s,最低视频时长2s
This commit is contained in:
2026-07-01 19:37:51 +08:00
parent 5e70064751
commit 5302d7f531
18 changed files with 657 additions and 1662 deletions
@@ -0,0 +1,37 @@
"""6idufv2q1c_add_credits_ratio_增加上传视频积分规则
Revision ID: a1b2c3d4e5f7
Revises: 6idufv2q1c
Create Date: 2026-07-01 00:00:00.000000
该文件包含 2026-07-01 的数据库迁移内容:
1. 积分规则表增加传入视频计费字段(input_video_ratio, input_video_base_credits, input_video_per_second_credits
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = 'a1b2c3d4e5f7'
down_revision: Union[str, None] = '6idufv2q1c'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ========================================
# 2026-07-01 - 积分规则表增加传入视频计费字段
# ========================================
op.add_column('credit_ratios', sa.Column('input_video_ratio', sa.Float(), server_default='1.0', nullable=False))
op.add_column('credit_ratios', sa.Column('input_video_base_credits', sa.Float(), server_default='0.0', nullable=False))
op.add_column('credit_ratios', sa.Column('input_video_per_second_credits', sa.Float(), server_default='0.5', nullable=False))
def downgrade() -> None:
# ========================================
# 2026-07-01 - 积分规则表增加传入视频计费字段(回滚)
# ========================================
op.drop_column('credit_ratios', 'input_video_per_second_credits')
op.drop_column('credit_ratios', 'input_video_base_credits')
op.drop_column('credit_ratios', 'input_video_ratio')
+3
View File
@@ -22,3 +22,6 @@ class CreditRatio(Base, TimestampMixin):
ratio: Mapped[float] = mapped_column(Float, nullable=False)
base_credits: Mapped[float] = mapped_column(Float, default=80.0)
per_second_credits: Mapped[float] = mapped_column(Float, default=2.0)
input_video_ratio: Mapped[float] = mapped_column(Float, default=1.0)
input_video_base_credits: Mapped[float] = mapped_column(Float, default=0.0)
input_video_per_second_credits: Mapped[float] = mapped_column(Float, default=0.5)
+18
View File
@@ -43,6 +43,24 @@ class CreditRatioCreate(BaseModel):
description="视频每秒积分。图片规则通常为 0",
examples=[2.0],
)
input_video_ratio: float = Field(
default=1.0,
ge=0,
description="传入视频积分倍率。视频生成时,用户上传参考视频的额外积分倍率",
examples=[1.0],
)
input_video_base_credits: float = Field(
default=0.0,
ge=0,
description="传入视频基础积分。视频生成时,用户上传参考视频的基础积分",
examples=[0.0],
)
input_video_per_second_credits: float = Field(
default=0.5,
ge=0,
description="传入视频每秒积分。视频生成时,用户上传参考视频每秒消耗的积分",
examples=[0.5],
)
class CreditRatioOut(CreditRatioCreate):
@@ -14,6 +14,7 @@ class GenerationAIReference(BaseModel):
"url": "https://example.com/reference.png",
"type": "image",
"name": "参考图.png",
"duration": 5.0,
}
}
)
@@ -33,6 +34,12 @@ class GenerationAIReference(BaseModel):
description="参考素材名称,前端展示用,可为空",
examples=["参考图.png"],
)
duration: float | None = Field(
None,
ge=0,
description="视频素材时长(秒)。type=video 时使用,用于视频素材计费和时长校验",
examples=[5.0],
)
class GenerationAITaskCreate(BaseModel):
+14 -4
View File
@@ -65,6 +65,7 @@ async def calc_video_credits(
duration: int,
resolution: str,
engine_id: str | None = None,
input_video_duration: float | None = None,
) -> float:
"""Calculate video credits using CreditRatio table, with fallback to hardcoded.
@@ -72,8 +73,9 @@ async def calc_video_credits(
1. gen_type=video + engine_id + resolution 精确规则;
2. gen_type=video + resolution 下 base_credits/per_second_credits 最高规则;
3. 原硬编码默认算法。
input_video_duration: 用户上传的参考视频总时长(秒),不为空时额外计费
"""
# 如果engine_id为空,默认查询权重最高的视频引擎积分规则
if not engine_id:
video_engines_result = await db.execute(
select(VideoEngine.id)
@@ -89,13 +91,21 @@ async def calc_video_credits(
engine_id=engine_id,
)
if ratio:
return round((ratio.base_credits + ratio.per_second_credits * duration) * ratio.ratio, 2)
base_cost = (ratio.base_credits + ratio.per_second_credits * duration) * ratio.ratio
if input_video_duration and input_video_duration > 0:
input_video_cost = (
ratio.input_video_base_credits + ratio.input_video_per_second_credits * input_video_duration
) * ratio.input_video_ratio
base_cost += input_video_cost
return round(base_cost, 2)
# Fallback
base = 60.0
duration_cost = duration * 2.0
multiplier = {"480p": 1, "1080p": 2, "720p": 1.5}.get(resolution, 1.0)
return round((base + duration_cost) * multiplier, 2)
total = (base + duration_cost) * multiplier
if input_video_duration and input_video_duration > 0:
total += input_video_duration * 0.5 * multiplier
return round(total, 2)
def calc_credits(duration: int, resolution: str) -> float:
@@ -301,6 +301,18 @@ async def create_async_generation_task(db: AsyncSession, current_user: User, req
raise HTTPException(status_code=400, detail=f"视频时长不支持: {duration}")
if engine.max_duration and duration > engine.max_duration:
raise HTTPException(status_code=400, detail=f"视频时长不能超过 {engine.max_duration}")
input_video_duration = 0.0
if refs:
video_refs = [r for r in refs if r.get("type") == "video"]
for ref in video_refs:
ref_duration = float(ref.get("duration") or 0)
if ref_duration < 2:
raise HTTPException(status_code=400, detail=f"视频素材最短不能少于 2 秒")
input_video_duration += ref_duration
if input_video_duration > 15:
raise HTTPException(status_code=400, detail=f"所有视频素材总时长不能超过 15 秒,当前 {input_video_duration:.1f}")
media_billing = await charge_generation_media_by_params(
db,
user_id=current_user.id,
@@ -309,6 +321,7 @@ async def create_async_generation_task(db: AsyncSession, current_user: User, req
duration=duration,
resolution=resolution,
engine_id=engine.id,
input_video_duration=input_video_duration if input_video_duration > 0 else None,
project_name="AI生成任务",
description_prefix="AI创作-",
owner_type=OWNER_CHAT_GENERATION_TASK,
@@ -460,6 +460,7 @@ async def charge_generation_media_by_params(
duration: int | None = None,
resolution: str | None = None,
engine_id: str | None = None,
input_video_duration: float | None = None,
project_name: str | None = None,
description_prefix: str = "AI创作-",
owner_type: str = OWNER_CHAT_GENERATION_TASK,
@@ -522,7 +523,11 @@ async def charge_generation_media_by_params(
)
)
elif gen_type == "video":
amount = await calc_video_credits(db, duration or 5, resolution or "720p", engine_id=engine_id)
amount = await calc_video_credits(
db, duration or 5, resolution or "720p",
engine_id=engine_id,
input_video_duration=input_video_duration,
)
items.append(
await deduct_credits_locked_once(
db,