from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from sqlalchemy import and_, desc, asc, func, extract from pydantic import BaseModel, Field from typing import Optional import uuid from datetime import datetime, date, timedelta from ..database import get_db from ..models import ( User, TokenUsageLog, TokenBalance, AIModel, SmsConfig, gen_uuid ) from ..auth import get_current_user from ..middleware.log_service import LogService from ..sms_client import send_sms_text router = APIRouter() class RechargeRequest(BaseModel): amount: int = Field(ge=1) channel: str = Field(default="simulation") @router.get("/overview") def get_overview( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): # 总消耗 total_used = db.query(func.coalesce(func.sum(TokenUsageLog.total_tokens), 0)).filter( TokenUsageLog.user_id == current_user.id ).scalar() # 今日使用中应用数 today_start = datetime.combine(date.today(), datetime.min.time()) today_apps = db.query(func.count(func.distinct(TokenUsageLog.model_id))).filter( and_( TokenUsageLog.user_id == current_user.id, TokenUsageLog.created_at >= today_start, ) ).scalar() or 0 # 余额 balance = db.query(TokenBalance).filter( TokenBalance.user_id == current_user.id ).first() from datetime import timezone, timedelta shanghai_tz = timezone(timedelta(hours=8)) server_time = datetime.now(shanghai_tz).isoformat() return { "code": 0, "data": { "total_used": total_used or 0, "balance": balance.balance if balance else 0, "today_apps": today_apps, "server_time": server_time, }, "message": "ok", } @router.get("/rankings") def get_rankings( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): # Token消耗最多的模型 model_rankings = ( db.query( TokenUsageLog.model_id, func.sum(TokenUsageLog.total_tokens).label("total"), func.count(TokenUsageLog.id).label("count"), ) .filter(TokenUsageLog.user_id == current_user.id) .group_by(TokenUsageLog.model_id) .order_by(desc("total")) .all() ) models_list = [] for r in model_rankings: model = db.query(AIModel).filter(AIModel.id == r.model_id).first() models_list.append({ "model_id": r.model_id, "model_name": model.name if model else "unknown", "total_tokens": int(r.total), "usage_count": int(r.count), }) # TOP3应用 app_rankings = ( db.query( TokenUsageLog.conversation_id, func.sum(TokenUsageLog.total_tokens).label("total"), ) .filter(TokenUsageLog.user_id == current_user.id) .group_by(TokenUsageLog.conversation_id) .order_by(desc("total")) .limit(3) .all() ) apps_list = [] for r in app_rankings: apps_list.append({ "conversation_id": r.conversation_id, "total_tokens": int(r.total), }) return { "code": 0, "data": { "model_rankings": models_list, "top_apps": apps_list, }, "message": "ok", } @router.get("/logs") def get_token_logs( start_date: Optional[str] = Query(None, description="开始日期 YYYY-MM-DD"), end_date: Optional[str] = Query(None, description="结束日期 YYYY-MM-DD"), model_id: Optional[str] = Query(None), app_type: Optional[str] = Query(None), page: int = Query(default=1, ge=1), page_size: int = Query(default=20, ge=1, le=100), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): query = db.query(TokenUsageLog).filter(TokenUsageLog.user_id == current_user.id) if start_date: try: start_dt = datetime.strptime(start_date, "%Y-%m-%d") query = query.filter(TokenUsageLog.created_at >= start_dt) except ValueError: raise HTTPException(status_code=400, detail="开始日期格式错误,请使用YYYY-MM-DD") if end_date: try: end_dt = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1) query = query.filter(TokenUsageLog.created_at < end_dt) except ValueError: raise HTTPException(status_code=400, detail="结束日期格式错误,请使用YYYY-MM-DD") if model_id: query = query.filter(TokenUsageLog.model_id == model_id) total = query.count() logs = ( query.order_by(desc(TokenUsageLog.created_at)) .offset((page - 1) * page_size) .limit(page_size) .all() ) result = [] for log_item in logs: model = db.query(AIModel).filter(AIModel.id == log_item.model_id).first() result.append({ "id": log_item.id, "model_id": log_item.model_id, "model_name": model.name if model else "unknown", "prompt_tokens": log_item.prompt_tokens or 0, "completion_tokens": log_item.completion_tokens or 0, "total_tokens": log_item.total_tokens or 0, "conversation_id": log_item.conversation_id, "created_at": log_item.created_at.isoformat() if log_item.created_at else None, }) return { "code": 0, "data": { "total": total, "page": page, "page_size": page_size, "items": result, }, "message": "ok", } @router.get("/balance") def get_balance( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): balance = db.query(TokenBalance).filter( TokenBalance.user_id == current_user.id ).first() return { "code": 0, "data": { "balance": balance.balance if balance else 0, "updated_at": str(balance.updated_at) if balance and balance.updated_at else None, }, "message": "ok", } @router.post("/recharge") def recharge( req: RechargeRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): # 获取或创建余额记录 balance = db.query(TokenBalance).filter( TokenBalance.user_id == current_user.id ).first() if not balance: balance = TokenBalance( id=gen_uuid(), user_id=current_user.id, balance=0, updated_at=datetime.now(), ) db.add(balance) db.flush() old_balance = balance.balance or 0 balance.balance = old_balance + req.amount balance.updated_at = datetime.now() # 记录操作日志 LogService.log_operation( user_id=current_user.id, username=current_user.username, action="recharge", target="token_balance", target_id=balance.id, detail=f"充值 {req.amount} tokens,余额从 {old_balance} 变为 {balance.balance}", ) db.commit() # 充值成功后发送短信通知(若已配置短信且用户绑定手机号) try: sms_cfg = db.query(SmsConfig).first() if sms_cfg and sms_cfg.provider and current_user.phone: send_sms_text( sms_cfg, current_user.phone, f"您已成功充值 {req.amount} tokens,当前余额 {balance.balance} tokens。", ) except Exception as sms_err: import logging logging.getLogger("aiflow").warning(f"充值短信通知失败(不影响充值结果): {sms_err}") return { "code": 0, "data": { "balance": balance.balance, "recharged": req.amount, }, "message": "充值成功", }