"""Subscription management API routes.""" from __future__ import annotations from dataclasses import replace from datetime import datetime, timezone from typing import List from app.auth import AuthenticatedUser, get_current_user from app.dependencies import get_user_repository from app.schemas.subscription import ( BillingRecord, ChangePlanRequest, ChangePlanResponse, SimpleResponse, SubscriptionInfo, ToggleAutoRenewRequest, ) from fastapi import APIRouter, Depends, HTTPException, status from packages.ports.user_repository import UserRepository router = APIRouter() # ============ 配额定义(硬编码,后续可迁移到配置中心) ============ PLAN_QUOTAS = { "free": {"max_projects": 3, "max_storage_gb": 10}, "standard": {"max_projects": 10, "max_storage_gb": 50}, "pro": {"max_projects": -1, "max_storage_gb": 100}, "enterprise": {"max_projects": -1, "max_storage_gb": 1000}, } # ============ Helper Functions ============ def _get_plan_name(plan_id: str) -> str: """获取套餐显示名称""" plan_names = { "free": "体验版", "standard": "标准版", "pro": "专业版", "enterprise": "企业版", } return plan_names.get(plan_id, "未知套餐") def _get_plan_price(plan_id: str, billing_cycle: str) -> float: """获取套餐价格""" prices = { ("free", "monthly"): 0, ("free", "yearly"): 0, ("standard", "monthly"): 99, ("standard", "yearly"): 999, ("pro", "monthly"): 299, ("pro", "yearly"): 2999, ("enterprise", "monthly"): 999, ("enterprise", "yearly"): 9999, } return prices.get((plan_id, billing_cycle), 0) def _build_subscription_info(user: AuthenticatedUser) -> SubscriptionInfo: """构建订阅信息响应""" now = datetime.now(timezone.utc) if user.user.subscription_expires_at: period_end = user.user.subscription_expires_at.isoformat() period_start = now.isoformat() else: period_start = now.isoformat() period_end = now.isoformat() return SubscriptionInfo( id=f"sub-{user.user.id[:8]}", plan_id=user.user.subscription_plan or "free", plan_name=_get_plan_name(user.user.subscription_plan or "free"), status=user.user.subscription_status or "active", billing_cycle="monthly", current_period_start=period_start, current_period_end=period_end, amount=_get_plan_price(user.user.subscription_plan or "free", "monthly"), auto_renew=True, created_at=user.user.created_at.isoformat() if user.user.created_at else now.isoformat(), ) # ============ API Endpoints ============ @router.get("/current", response_model=SubscriptionInfo) async def get_current_subscription( current_user: AuthenticatedUser = Depends(get_current_user), ): """获取当前订阅信息""" return _build_subscription_info(current_user) @router.get("/billing-records", response_model=List[BillingRecord]) async def get_billing_records( current_user: AuthenticatedUser = Depends(get_current_user), ): """获取账单记录列表""" # TODO: 从数据库查询账单记录 return [] @router.post("/change-plan", response_model=ChangePlanResponse) async def change_plan( request: ChangePlanRequest, current_user: AuthenticatedUser = Depends(get_current_user), user_repository: UserRepository = Depends(get_user_repository), ): """变更订阅套餐(升级/降级)""" # TODO: 接入支付验证(支付宝/微信支付) valid_plans = {"free", "standard", "pro", "enterprise"} if request.target_plan_id not in valid_plans: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"无效的套餐ID。支持的套餐: {', '.join(valid_plans)}", ) valid_cycles = {"monthly", "yearly"} if request.billing_cycle not in valid_cycles: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="无效的计费周期。支持: monthly, yearly", ) user = current_user.user current_plan = user.subscription_plan or "free" target_plan = request.target_plan_id if current_plan == target_plan: return ChangePlanResponse( success=False, message=f"您已经是 {_get_plan_name(target_plan)}", ) # 通过 dataclasses.replace 创建新实例(不直接修改 dataclass) quotas = PLAN_QUOTAS.get(target_plan, PLAN_QUOTAS["free"]) updated_user = replace( user, subscription_plan=target_plan, subscription_status="active", max_projects=quotas["max_projects"], max_storage_gb=quotas["max_storage_gb"], ) user_repository.save(updated_user) # 用更新后的用户构造响应 refreshed_auth_user = AuthenticatedUser(user=updated_user) return ChangePlanResponse( success=True, message=f"套餐已成功变更为 {_get_plan_name(target_plan)}", new_subscription=_build_subscription_info(refreshed_auth_user), ) @router.post("/cancel", response_model=SimpleResponse) async def cancel_subscription( current_user: AuthenticatedUser = Depends(get_current_user), user_repository: UserRepository = Depends(get_user_repository), ): """取消订阅""" user = current_user.user if user.subscription_plan == "free": raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="体验版无需取消", ) updated_user = replace(user, subscription_status="cancelled") user_repository.save(updated_user) return SimpleResponse( success=True, message="订阅已取消,当前周期结束后停止服务", ) @router.post("/toggle-auto-renew", response_model=SimpleResponse) async def toggle_auto_renew( request: ToggleAutoRenewRequest, current_user: AuthenticatedUser = Depends(get_current_user), ): """切换自动续费""" # TODO: 实际需要在数据库中存储 auto_renew 字段 status_text = "已开启自动续费" if request.enabled else "已关闭自动续费" return SimpleResponse( success=True, message=status_text, )