from fastapi import APIRouter, Depends, Query from sqlalchemy import case, func, select from sqlalchemy.ext.asyncio import AsyncSession from app.dependencies import get_current_user, get_db from app.models.credit.balance import UserCreditBalance from app.models.credit_record import CreditRecord from app.models.credit_ratio import CreditRatio from app.models.image_engine import ImageEngine from app.models.user import User from app.models.video_engine import VideoEngine from app.schemas.credit import CreditRecordOut from app.schemas.credit_balance import CreditBalanceItemOut from app.schemas.credit_ratio import CreditRatioOut from app.services.credit.query_service import ( apply_balance_status_filter, effective_balance_status, get_balance_summary, ) from app.services.credit.utils import utc_now from app.services.credit.time_policy import last_usable_at from app.services.credit_ratio_service import list_all_credit_ratios from app.services.credits import get_records router = APIRouter(prefix="/credits", tags=["credits"]) def _balance_to_out(item: UserCreditBalance, *, checked_at) -> CreditBalanceItemOut: return CreditBalanceItemOut( id=item.id, credit_level=item.credit_level, source_type=item.source_type, source_id=item.source_id, product_id=item.product_id, payment_order_id=item.payment_order_id, subscription_id=item.subscription_id, subscription_period_id=item.subscription_period_id, grant_amount=float(item.grant_amount), unspent_amount=float(item.unspent_amount), consumed_amount=float(item.consumed_amount), expired_amount=float(item.expired_amount), revoked_amount=float(item.revoked_amount), valid_from=item.valid_from, expires_at=item.expires_at, last_usable_at=last_usable_at(item.expires_at), status=effective_balance_status(item, request_time=checked_at), created_at=item.created_at, ) @router.get("") async def get_credits( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): records, total = await get_records(db, current_user.id, page, page_size) summary = await get_balance_summary(db, current_user.id) totals_result = await db.execute( select( func.coalesce(func.sum(case((CreditRecord.type == "recharge", CreditRecord.amount), else_=0)), 0), func.coalesce(func.sum(case((CreditRecord.type == "consume", func.abs(CreditRecord.amount)), else_=0)), 0), func.coalesce(func.sum(case((CreditRecord.type == "refund", CreditRecord.amount), else_=0)), 0), func.coalesce(func.sum(case((CreditRecord.type == "expire", CreditRecord.expired_amount), else_=0)), 0), ).where(CreditRecord.user_id == current_user.id) ) total_granted, total_consumed, total_refunded, total_expired = totals_result.one() return { **summary.to_dict(), "records": [CreditRecordOut.model_validate(r) for r in records], "total": total, "total_granted": float(total_granted or 0), "total_consumed": float(total_consumed or 0), "total_refunded": float(total_refunded or 0), "total_expired": float(total_expired or 0), } @router.get("/balances") async def list_credit_balances( status: str | None = Query(default=None), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): checked_at = utc_now() stmt = select(UserCreditBalance).where(UserCreditBalance.user_id == current_user.id) stmt = apply_balance_status_filter(stmt, status, request_time=checked_at) stmt = stmt.order_by(UserCreditBalance.expires_at.asc(), UserCreditBalance.id.asc()) result = await db.execute(stmt.offset((page - 1) * page_size).limit(page_size)) return [_balance_to_out(item, checked_at=checked_at) for item in result.scalars().all()] @router.get( "/credit-ratios", response_model=list[CreditRatioOut], summary="获取积分比例列表", ) async def list_client_credit_ratios( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): return await list_all_credit_ratios(db) @router.get("/ratios", response_model=dict) async def get_credit_ratios( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): async def get_ratios_for_engine_type(gen_type: str, engine_ids: list): for engine_id in engine_ids: result = await db.execute( select(CreditRatio) .where(CreditRatio.gen_type == gen_type) .where(CreditRatio.model_config_id == engine_id) ) ratios = result.scalars().all() if ratios: return [CreditRatioOut.model_validate(r) for r in ratios] return [] video_result = await db.execute( select(VideoEngine.id) .where(VideoEngine.is_active.is_(True), VideoEngine.deleted_at.is_(None)) .order_by(VideoEngine.priority.desc()) ) image_result = await db.execute( select(ImageEngine.id) .where(ImageEngine.is_active.is_(True), ImageEngine.deleted_at.is_(None)) .order_by(ImageEngine.priority.desc()) ) grouped = {} video_ratios = await get_ratios_for_engine_type("video", list(video_result.scalars().all())) image_ratios = await get_ratios_for_engine_type("image", list(image_result.scalars().all())) if video_ratios: grouped["video"] = video_ratios if image_ratios: grouped["image"] = image_ratios return grouped