""" 付费体系 API:套餐、订阅、订单、用量 """ from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, Field from typing import Optional, List from sqlalchemy.orm import Session from datetime import date, datetime, timedelta from app.core.database import get_db from app.api.auth import get_current_user from app.models.user import User from app.models.billing import BillingPlan, BillingOrder, UserSubscription, UsageRecord router = APIRouter(prefix="/api/v1/billing", tags=["billing"]) # ─── Pydantic Models ─── class PlanResponse(BaseModel): id: str name: str tier: str price_monthly: float price_yearly: float daily_quota: int agent_limit: int knowledge_limit: int file_upload_mb: int = 5 models: Optional[list] = None features: Optional[list] = None is_active: bool = True sort_order: int = 0 class Config: from_attributes = True class SubscriptionResponse(BaseModel): tier: str = "free" status: str = "active" plan_name: Optional[str] = None started_at: Optional[str] = None expires_at: Optional[str] = None auto_renew: bool = False daily_quota: int = 50 agent_limit: int = 3 knowledge_limit: int = 10 file_upload_mb: int = 5 models: Optional[list] = None features: Optional[list] = None class UsageResponse(BaseModel): daily_used: int = 0 daily_limit: int = 50 daily_remaining: int = 50 is_exhausted: bool = False class CreateOrderRequest(BaseModel): plan_id: str = Field(..., description="套餐ID") period: str = Field(default="monthly", description="周期: monthly/yearly") payment_method: str = Field(default="mock", description="支付方式: mock/wechat/alipay") class OrderResponse(BaseModel): id: str plan_id: Optional[str] = None amount: float period: str status: str payment_method: Optional[str] = None payment_ref: Optional[str] = None created_at: Optional[str] = None paid_at: Optional[str] = None class Config: from_attributes = True # ─── Helpers ─── QUOTA_DEFAULTS = { "free": {"daily_quota": 50, "agent_limit": 3, "knowledge_limit": 10, "file_upload_mb": 5}, "pro": {"daily_quota": 500, "agent_limit": 20, "knowledge_limit": 500, "file_upload_mb": 50}, "enterprise": {"daily_quota": 0, "agent_limit": 0, "knowledge_limit": 0, "file_upload_mb": 200}, } def _get_user_subscription_info(db: Session, user: User) -> SubscriptionResponse: """获取用户当前订阅信息(套餐+权益)""" sub = db.query(UserSubscription).filter(UserSubscription.user_id == user.id).first() tier = sub.tier if sub else (user.subscription_tier or "free") plan = db.query(BillingPlan).filter(BillingPlan.id == sub.plan_id).first() if (sub and sub.plan_id) else None quota = QUOTA_DEFAULTS.get(tier, QUOTA_DEFAULTS["free"]) return SubscriptionResponse( tier=tier, status=sub.status if sub else "active", plan_name=plan.name if plan else (sub.tier if sub else "免费版"), started_at=sub.started_at.isoformat() if (sub and sub.started_at) else None, expires_at=sub.expires_at.isoformat() if (sub and sub.expires_at) else None, auto_renew=sub.auto_renew if sub else False, daily_quota=plan.daily_quota if plan else quota["daily_quota"], agent_limit=plan.agent_limit if plan else quota["agent_limit"], knowledge_limit=plan.knowledge_limit if plan else quota["knowledge_limit"], file_upload_mb=plan.file_upload_mb if plan else quota["file_upload_mb"], models=plan.models if plan else None, features=plan.features if plan else None, ) def _get_daily_limit(tier: str) -> int: plan = QUOTA_DEFAULTS.get(tier) return plan["daily_quota"] if plan else 50 # ─── Endpoints ─── @router.get("/plans", response_model=List[PlanResponse]) async def list_plans( db: Session = Depends(get_db), ): """获取所有可用套餐""" return db.query(BillingPlan).filter(BillingPlan.is_active == True).order_by(BillingPlan.sort_order.asc()).all() @router.get("/user/subscription", response_model=SubscriptionResponse) async def get_user_subscription( current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """获取当前用户订阅状态与权益""" return _get_user_subscription_info(db, current_user) @router.get("/user/usage", response_model=UsageResponse) async def get_user_usage( current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """获取当前用户今日用量""" today = date.today() sub = db.query(UserSubscription).filter(UserSubscription.user_id == current_user.id).first() tier = sub.tier if sub else (current_user.subscription_tier or "free") daily_limit = _get_daily_limit(tier) # 如果日期变了,重置计数 if current_user.daily_usage_date and current_user.daily_usage_date < today: current_user.daily_usage_count = 0 current_user.daily_usage_date = today db.commit() used = current_user.daily_usage_count or 0 remaining = max(0, daily_limit - used) if daily_limit > 0 else 999999 return UsageResponse( daily_used=used, daily_limit=daily_limit, daily_remaining=remaining, is_exhausted=daily_limit > 0 and used >= daily_limit, ) @router.post("/orders", response_model=OrderResponse) async def create_order( req: CreateOrderRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """创建订单""" plan = db.query(BillingPlan).filter(BillingPlan.id == req.plan_id, BillingPlan.is_active == True).first() if not plan: raise HTTPException(status_code=404, detail="套餐不存在") if plan.tier == "free": raise HTTPException(status_code=400, detail="免费套餐无需购买") amount = plan.price_yearly if req.period == "yearly" else plan.price_monthly if amount <= 0: raise HTTPException(status_code=400, detail="套餐价格无效") order = BillingOrder( user_id=current_user.id, plan_id=plan.id, amount=amount, period=req.period, status="pending", payment_method=req.payment_method, ) db.add(order) db.commit() db.refresh(order) return OrderResponse( id=order.id, plan_id=order.plan_id, amount=order.amount, period=order.period, status=order.status, payment_method=order.payment_method, payment_ref=order.payment_ref, created_at=order.created_at.isoformat() if order.created_at else None, paid_at=order.paid_at.isoformat() if order.paid_at else None, ) @router.get("/orders/{order_id}", response_model=OrderResponse) async def get_order( order_id: str, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """查询订单状态""" order = db.query(BillingOrder).filter( BillingOrder.id == order_id, BillingOrder.user_id == current_user.id, ).first() if not order: raise HTTPException(status_code=404, detail="订单不存在") return OrderResponse( id=order.id, plan_id=order.plan_id, amount=order.amount, period=order.period, status=order.status, payment_method=order.payment_method, payment_ref=order.payment_ref, created_at=order.created_at.isoformat() if order.created_at else None, paid_at=order.paid_at.isoformat() if order.paid_at else None, ) @router.post("/orders/{order_id}/pay") async def pay_order( order_id: str, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """模拟支付(Phase 4 替换为真实支付)""" order = db.query(BillingOrder).filter( BillingOrder.id == order_id, BillingOrder.user_id == current_user.id, ).first() if not order: raise HTTPException(status_code=404, detail="订单不存在") if order.status != "pending": raise HTTPException(status_code=400, detail="订单状态不允许支付") plan = db.query(BillingPlan).filter(BillingPlan.id == order.plan_id).first() if not plan: raise HTTPException(status_code=404, detail="套餐不存在") # 计算到期时间 days = 365 if order.period == "yearly" else 30 now = datetime.utcnow() expires_at = now + timedelta(days=days) # 更新订单状态 order.status = "paid" order.paid_at = now order.payment_ref = f"mock_{order.id}" # 创建或更新订阅 sub = db.query(UserSubscription).filter(UserSubscription.user_id == current_user.id).first() if not sub: sub = UserSubscription(user_id=current_user.id) db.add(sub) sub.plan_id = plan.id sub.tier = plan.tier sub.status = "active" sub.started_at = now sub.expires_at = expires_at # 更新用户模型 current_user.subscription_tier = plan.tier current_user.subscription_expires_at = expires_at current_user.daily_usage_count = 0 current_user.daily_usage_date = date.today() db.commit() return { "success": True, "tier": plan.tier, "expires_at": expires_at.isoformat(), "message": f"已升级到 {plan.name},有效期至 {expires_at.strftime('%Y-%m-%d')}", } @router.post("/callback/wechat") async def wechat_callback(): """微信支付回调(Phase 4 实现)""" return {"code": "SUCCESS", "message": "not implemented yet"} @router.post("/callback/alipay") async def alipay_callback(): """支付宝回调(Phase 4 实现)""" return {"code": "SUCCESS", "message": "not implemented yet"}