from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from typing import Optional, List from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.database import get_db from app.api.v1.admin import require_admin from app.services.credit import CreditService from app.models.credit_package import CreditPackage, SubscriptionPlan from app.models.user_credit import UserCredit from app.models.user import User import uuid router = APIRouter() class PackageForm(BaseModel): name: str name_en: str credits: int price: float price_usd: Optional[float] = None original_price: Optional[float] = None is_active: bool = True sort_order: int = 0 class PlanForm(BaseModel): name: str name_en: str credits_per_month: int price: float price_usd: Optional[float] = None duration_days: int = 30 is_active: bool = True sort_order: int = 0 class AdjustCreditsForm(BaseModel): user_id: str credits: float reason: str = "" @router.get("/credit-packages") async def list_packages( _: dict = Depends(require_admin), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(CreditPackage).order_by(CreditPackage.sort_order) ) return [{ "id": str(p.id), "name": p.name, "name_en": p.name_en, "credits": p.credits, "price": p.price, "price_usd": p.price_usd, "original_price": p.original_price, "is_active": p.is_active, "sort_order": p.sort_order, } for p in result.scalars().all()] @router.post("/credit-packages") async def create_package( data: PackageForm, _: dict = Depends(require_admin), db: AsyncSession = Depends(get_db), ): pkg = CreditPackage(**data.model_dump()) db.add(pkg) await db.flush() return {"id": str(pkg.id), "status": "ok"} @router.put("/credit-packages/{pkg_id}") async def update_package( pkg_id: str, data: PackageForm, _: dict = Depends(require_admin), db: AsyncSession = Depends(get_db), ): try: uid = uuid.UUID(pkg_id) except ValueError: raise HTTPException(status_code=400, detail="无效ID") result = await db.execute(select(CreditPackage).where(CreditPackage.id == uid)) pkg = result.scalar_one_or_none() if not pkg: raise HTTPException(status_code=404, detail="次数包不存在") for k, v in data.model_dump().items(): setattr(pkg, k, v) await db.flush() return {"status": "ok"} @router.delete("/credit-packages/{pkg_id}") async def delete_package( pkg_id: str, _: dict = Depends(require_admin), db: AsyncSession = Depends(get_db), ): try: uid = uuid.UUID(pkg_id) except ValueError: raise HTTPException(status_code=400, detail="无效ID") result = await db.execute(select(CreditPackage).where(CreditPackage.id == uid)) pkg = result.scalar_one_or_none() if not pkg: raise HTTPException(status_code=404, detail="次数包不存在") await db.delete(pkg) await db.flush() return {"status": "ok"} @router.get("/subscription-plans") async def list_plans( _: dict = Depends(require_admin), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(SubscriptionPlan).order_by(SubscriptionPlan.sort_order) ) return [{ "id": str(p.id), "name": p.name, "name_en": p.name_en, "credits_per_month": p.credits_per_month, "price": p.price, "price_usd": p.price_usd, "duration_days": p.duration_days, "is_active": p.is_active, "sort_order": p.sort_order, } for p in result.scalars().all()] @router.post("/subscription-plans") async def create_plan( data: PlanForm, _: dict = Depends(require_admin), db: AsyncSession = Depends(get_db), ): plan = SubscriptionPlan(**data.model_dump()) db.add(plan) await db.flush() return {"id": str(plan.id), "status": "ok"} @router.put("/subscription-plans/{plan_id}") async def update_plan( plan_id: str, data: PlanForm, _: dict = Depends(require_admin), db: AsyncSession = Depends(get_db), ): try: uid = uuid.UUID(plan_id) except ValueError: raise HTTPException(status_code=400, detail="无效ID") result = await db.execute(select(SubscriptionPlan).where(SubscriptionPlan.id == uid)) plan = result.scalar_one_or_none() if not plan: raise HTTPException(status_code=404, detail="订阅套餐不存在") for k, v in data.model_dump().items(): setattr(plan, k, v) await db.flush() return {"status": "ok"} @router.delete("/subscription-plans/{plan_id}") async def delete_plan( plan_id: str, _: dict = Depends(require_admin), db: AsyncSession = Depends(get_db), ): try: uid = uuid.UUID(plan_id) except ValueError: raise HTTPException(status_code=400, detail="无效ID") result = await db.execute(select(SubscriptionPlan).where(SubscriptionPlan.id == uid)) plan = result.scalar_one_or_none() if not plan: raise HTTPException(status_code=404, detail="订阅套餐不存在") await db.delete(plan) await db.flush() return {"status": "ok"} @router.get("/user-credits") async def list_user_credits( page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100), _: dict = Depends(require_admin), db: AsyncSession = Depends(get_db), ): offset = (page - 1) * size result = await db.execute( select(UserCredit).order_by(UserCredit.updated_at.desc()).offset(offset).limit(size) ) items = result.scalars().all() from sqlalchemy import func count_result = await db.execute(select(func.count(UserCredit.id))) total = count_result.scalar() or 0 enriched = [] for uc in items: user_result = await db.execute(select(User).where(User.id == uc.user_id)) user = user_result.scalar_one_or_none() enriched.append({ "id": str(uc.id), "user_id": str(uc.user_id), "username": user.username if user else "N/A", "balance": uc.balance, "total_purchased": uc.total_purchased, "total_used": uc.total_used, "subscription_plan_id": str(uc.subscription_plan_id) if uc.subscription_plan_id else None, "subscription_expires_at": uc.subscription_expires_at.isoformat() if uc.subscription_expires_at else None, "free_trial_used": uc.free_trial_used, "updated_at": uc.updated_at.isoformat() if uc.updated_at else None, }) return {"items": enriched, "total": total, "page": page, "size": size} class AdjustForm(BaseModel): user_id: str credits: float reason: str = "" @router.post("/user-credits/adjust") async def adjust_credits( data: AdjustForm, _: dict = Depends(require_admin), db: AsyncSession = Depends(get_db), ): svc = CreditService(db) try: uid = uuid.UUID(data.user_id) except ValueError: raise HTTPException(status_code=400, detail="无效用户ID") balance = await svc.add_credits( user_id=uid, credits=data.credits, source="admin_grant", description=data.reason or f"管理员调整: {data.credits:+.1f} 次", ) return {"status": "ok", "balance": balance} @router.get("/credit-consumptions") async def list_consumptions( page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=200), user_id: str = Query(None), result_type: str = Query(None), _: dict = Depends(require_admin), db: AsyncSession = Depends(get_db), ): from app.models.credit_consumption import CreditConsumption from sqlalchemy import select, func, desc conditions = [] if user_id: try: conditions.append(CreditConsumption.user_id == uuid.UUID(user_id)) except ValueError: pass if result_type: conditions.append(CreditConsumption.result_type == result_type) stmt = select(CreditConsumption).where(*conditions).order_by( desc(CreditConsumption.created_at) ).offset((page - 1) * size).limit(size) result = await db.execute(stmt) items = result.scalars().all() count_stmt = select(func.count(CreditConsumption.id)).where(*conditions) count_result = await db.execute(count_stmt) total = count_result.scalar() or 0 return { "items": [{ "id": str(c.id), "user_id": str(c.user_id), "result_type": c.result_type, "credits_change": c.credits_change, "balance_after": c.balance_after, "source": c.source, "description": c.description, "created_at": c.created_at.isoformat() if c.created_at else None, } for c in items], "total": total, "page": page, "size": size, } @router.get("/credit-stats") async def credit_stats( _: dict = Depends(require_admin), db: AsyncSession = Depends(get_db), ): svc = CreditService(db) return await svc.get_stats()