129 lines
4.1 KiB
Python
129 lines
4.1 KiB
Python
import math
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.user import User
|
|
from app.models.credit_record import CreditRecord
|
|
from app.models.system_config import SystemConfig
|
|
from app.models.credit_ratio import CreditRatio
|
|
from app.utils.id_gen import generate_id
|
|
from app.utils.exceptions import InsufficientCreditsError
|
|
|
|
|
|
async def calc_text_credits(db: AsyncSession, input_tokens: int, output_tokens: int) -> float:
|
|
"""Calculate text credits based on actual token usage and configurable rate."""
|
|
result = await db.execute(
|
|
select(SystemConfig).where(SystemConfig.key == "text_credits_per_1000_tokens")
|
|
)
|
|
config = result.scalar_one_or_none()
|
|
rate = float(config.value) if config else 1.0
|
|
total_tokens = input_tokens + output_tokens
|
|
return round(total_tokens * rate / 1000, 2)
|
|
|
|
|
|
async def calc_video_credits(db: AsyncSession, duration: int, resolution: str) -> float:
|
|
"""Calculate video credits using CreditRatio table, with fallback to hardcoded."""
|
|
result = await db.execute(
|
|
select(CreditRatio).where(CreditRatio.resolution == resolution).limit(1)
|
|
)
|
|
ratio = result.scalar_one_or_none()
|
|
if ratio:
|
|
return round((ratio.base_credits + ratio.per_second_credits * duration) * ratio.ratio, 2)
|
|
# Fallback
|
|
base = 60.0
|
|
duration_cost = duration * 2.0
|
|
multiplier = {"4K": 2.5, "1080p": 1.5, "720p": 1.0}.get(resolution, 1.0)
|
|
return round((base + duration_cost) * multiplier, 2)
|
|
|
|
|
|
def calc_credits(duration: int, resolution: str) -> float:
|
|
"""Legacy: hardcoded credit calculation. Prefer calc_video_credits for new code."""
|
|
base = 60.0
|
|
duration_cost = duration * 2.0
|
|
multiplier = {"4K": 2.5, "1080p": 1.5, "720p": 1.0}.get(resolution, 1.0)
|
|
return round((base + duration_cost) * multiplier, 2)
|
|
|
|
|
|
async def calc_image_credits(db: AsyncSession, image_size: str) -> float:
|
|
"""Calculate image credits using CreditRatio table, with fallback to hardcoded."""
|
|
result = await db.execute(
|
|
select(CreditRatio).where(CreditRatio.gen_type == "image").where(CreditRatio.resolution == image_size).limit(1)
|
|
)
|
|
ratio = result.scalar_one_or_none()
|
|
if ratio:
|
|
return round(ratio.base_credits * ratio.ratio, 2)
|
|
# Fallback
|
|
multiplier = {"4K": 2.0, "2K": 1.0}.get(image_size, 1.0)
|
|
base_cost = 4.0
|
|
return round(base_cost * multiplier, 2)
|
|
|
|
|
|
async def deduct_credits(
|
|
db: AsyncSession,
|
|
user_id: str,
|
|
amount: float,
|
|
description: str,
|
|
related_id: str | None = None,
|
|
) -> User:
|
|
"""Atomically deduct credits from user. Raises InsufficientCreditsError."""
|
|
result = await db.execute(
|
|
select(User).where(User.id == user_id)
|
|
)
|
|
user = result.scalar_one_or_none()
|
|
if not user or user.credits < amount:
|
|
raise InsufficientCreditsError()
|
|
|
|
user.credits = round(user.credits - amount, 2)
|
|
record = CreditRecord(
|
|
id=generate_id(),
|
|
user_id=user_id,
|
|
type="consume",
|
|
amount=-round(amount, 2),
|
|
balance_after=user.credits,
|
|
description=description,
|
|
related_id=related_id,
|
|
)
|
|
db.add(record)
|
|
await db.flush()
|
|
return user
|
|
|
|
|
|
async def add_credits(
|
|
db: AsyncSession,
|
|
user_id: str,
|
|
amount: float,
|
|
description: str,
|
|
related_id: str | None = None,
|
|
) -> User:
|
|
"""Add credits to user."""
|
|
result = await db.execute(
|
|
select(User).where(User.id == user_id)
|
|
)
|
|
user = result.scalar_one_or_none()
|
|
if not user:
|
|
raise ValueError("User not found")
|
|
|
|
user.credits = round(user.credits + amount, 2)
|
|
record = CreditRecord(
|
|
id=generate_id(),
|
|
user_id=user_id,
|
|
type="recharge",
|
|
amount=round(amount, 2),
|
|
balance_after=user.credits,
|
|
description=description,
|
|
related_id=related_id,
|
|
)
|
|
db.add(record)
|
|
await db.flush()
|
|
return user
|
|
|
|
|
|
async def get_records(db: AsyncSession, user_id: str) -> list[CreditRecord]:
|
|
result = await db.execute(
|
|
select(CreditRecord)
|
|
.where(CreditRecord.user_id == user_id)
|
|
.order_by(CreditRecord.created_at.desc())
|
|
)
|
|
return list(result.scalars().all())
|