52ff2f80ad
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
Deploy / Deploy Staging (push) Has been cancelled
Deploy / Build Production Runtime Images (push) Has been cancelled
Deploy / Deploy Production (push) Has been cancelled
Deploy / Production Browser E2E (push) Has been cancelled
271 lines
8.8 KiB
Python
271 lines
8.8 KiB
Python
"""Quota system with registry pattern.
|
|
|
|
Three subscription tiers with different limits:
|
|
- free: 2GB storage, 5 videos/month, 3 concurrent, 3 templates, 50 titles, 10 voiceovers, no AI voice
|
|
- basic: 20GB storage, 30 videos/month, 10 concurrent, 15 templates, 500 titles, 100 voiceovers, AI voice
|
|
- premium: 100GB storage, 100 videos/month, 20 concurrent, unlimited templates, 500 titles, 100 voiceovers, AI voice
|
|
|
|
Quota dimensions are registered by modules via the ModuleRegistry,
|
|
and checked against the user's subscription plan.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from typing import Dict, List, Optional
|
|
|
|
|
|
class QuotaDimension(str, Enum):
|
|
"""配额维度 - 所有可量化的资源限制"""
|
|
|
|
STORAGE_GB = "storage_gb" # 存储空间 (GB)
|
|
VIDEOS_PER_MONTH = "videos_per_month" # 每月生成视频数
|
|
MAX_CONCURRENT = "max_concurrent" # 最大并发任务数
|
|
MAX_TEMPLATES = "max_templates" # 最大模板数
|
|
MAX_TITLES = "max_titles" # 最大标题库条目数
|
|
MAX_VOICEOVERS = "max_voiceovers" # 最大配音库条目数
|
|
AI_VOICE_ENABLED = "ai_voice_enabled" # AI 配音是否可用 (0/1)
|
|
# 以下维度由扩展模块注册,初始配额为 0(由模块注册时填充)
|
|
AI_VOICE_CREDITS = "ai_voice_credits" # AI 配音积分(每月)
|
|
BATCH_EXPORT_ENABLED = "batch_export_enabled" # 批量导出
|
|
MULTI_PLATFORM_ENABLED = "multi_platform_enabled" # 多平台发布
|
|
DEDUP_REPORT_ENABLED = "dedup_report_enabled" # 去重检测报告
|
|
|
|
|
|
@dataclass
|
|
class QuotaTier:
|
|
"""一个套餐等级的配额定义"""
|
|
|
|
name: str
|
|
limits: Dict[str, float] = field(default_factory=dict)
|
|
|
|
def get_limit(self, dimension: str) -> float:
|
|
"""获取指定维度的配额限制,未定义返回 0"""
|
|
return self.limits.get(dimension, 0)
|
|
|
|
def is_unlimited(self, dimension: str) -> bool:
|
|
"""指定维度是否不限量"""
|
|
return self.limits.get(dimension, float("inf")) == float("inf")
|
|
|
|
|
|
# 三个套餐等级的配额定义
|
|
QUOTA_TIERS: Dict[str, QuotaTier] = {
|
|
"free": QuotaTier(
|
|
name="free",
|
|
limits={
|
|
QuotaDimension.STORAGE_GB: 2,
|
|
QuotaDimension.VIDEOS_PER_MONTH: 5,
|
|
QuotaDimension.MAX_CONCURRENT: 3,
|
|
QuotaDimension.MAX_TEMPLATES: 3,
|
|
QuotaDimension.MAX_TITLES: 50,
|
|
QuotaDimension.MAX_VOICEOVERS: 10,
|
|
QuotaDimension.AI_VOICE_ENABLED: 0,
|
|
QuotaDimension.AI_VOICE_CREDITS: 0,
|
|
QuotaDimension.BATCH_EXPORT_ENABLED: 0,
|
|
QuotaDimension.MULTI_PLATFORM_ENABLED: 0,
|
|
QuotaDimension.DEDUP_REPORT_ENABLED: 0,
|
|
},
|
|
),
|
|
"basic": QuotaTier(
|
|
name="basic",
|
|
limits={
|
|
QuotaDimension.STORAGE_GB: 20,
|
|
QuotaDimension.VIDEOS_PER_MONTH: 30,
|
|
QuotaDimension.MAX_CONCURRENT: 10,
|
|
QuotaDimension.MAX_TEMPLATES: 15,
|
|
QuotaDimension.MAX_TITLES: 500,
|
|
QuotaDimension.MAX_VOICEOVERS: 100,
|
|
QuotaDimension.AI_VOICE_ENABLED: 1,
|
|
QuotaDimension.AI_VOICE_CREDITS: 100,
|
|
QuotaDimension.BATCH_EXPORT_ENABLED: 1,
|
|
QuotaDimension.MULTI_PLATFORM_ENABLED: 0,
|
|
QuotaDimension.DEDUP_REPORT_ENABLED: 0,
|
|
},
|
|
),
|
|
"premium": QuotaTier(
|
|
name="premium",
|
|
limits={
|
|
QuotaDimension.STORAGE_GB: 100,
|
|
QuotaDimension.VIDEOS_PER_MONTH: 100,
|
|
QuotaDimension.MAX_CONCURRENT: 20,
|
|
QuotaDimension.MAX_TEMPLATES: float("inf"), # 不限量
|
|
QuotaDimension.MAX_TITLES: 500,
|
|
QuotaDimension.MAX_VOICEOVERS: 100,
|
|
QuotaDimension.AI_VOICE_ENABLED: 1,
|
|
QuotaDimension.AI_VOICE_CREDITS: 500,
|
|
QuotaDimension.BATCH_EXPORT_ENABLED: 1,
|
|
QuotaDimension.MULTI_PLATFORM_ENABLED: 1,
|
|
QuotaDimension.DEDUP_REPORT_ENABLED: 1,
|
|
},
|
|
),
|
|
}
|
|
|
|
|
|
class QuotaWarningLevel:
|
|
"""配额告警级别"""
|
|
|
|
NORMAL = "normal" # 使用量 < 80%
|
|
WARNING = "warning" # 80% <= 使用量 < 100%
|
|
CRITICAL = "critical" # 95% <= 使用量 < 100%
|
|
EXCEEDED = "exceeded" # 使用量 >= 100%
|
|
|
|
|
|
@dataclass
|
|
class QuotaCheckResult:
|
|
"""配额检查结果"""
|
|
|
|
allowed: bool
|
|
dimension: str
|
|
limit: float
|
|
used: float
|
|
remaining: float
|
|
warning_level: str
|
|
|
|
@property
|
|
def usage_percent(self) -> float:
|
|
if self.limit <= 0:
|
|
return 100.0 if self.used > 0 else 0.0
|
|
if self.limit == float("inf"):
|
|
return 0.0
|
|
return min(100.0, (self.used / self.limit) * 100)
|
|
|
|
|
|
class QuotaRegistry:
|
|
"""配额注册表
|
|
|
|
管理所有配额维度的定义和套餐限制。
|
|
扩展模块可通过 register_dimension() 注册新的配额维度。
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self._dimensions: Dict[str, str] = {} # dimension_name -> description
|
|
self._tiers: Dict[str, QuotaTier] = dict(QUOTA_TIERS)
|
|
|
|
# 初始化内置维度
|
|
for dim in QuotaDimension:
|
|
self._dimensions[dim.value] = dim.name
|
|
|
|
def register_dimension(
|
|
self,
|
|
dimension: str,
|
|
description: str,
|
|
default_limits: Optional[Dict[str, float]] = None,
|
|
) -> None:
|
|
"""注册新的配额维度
|
|
|
|
Args:
|
|
dimension: 维度名称
|
|
description: 人类可读描述
|
|
default_limits: 各套餐的默认限制 {plan_name: limit}
|
|
"""
|
|
if dimension in self._dimensions:
|
|
return # 幂等
|
|
|
|
self._dimensions[dimension] = description
|
|
|
|
# 为各套餐设置默认限制
|
|
if default_limits:
|
|
for plan_name, limit in default_limits.items():
|
|
if plan_name in self._tiers:
|
|
self._tiers[plan_name].limits[dimension] = limit
|
|
else:
|
|
# 默认所有套餐该维度为 0
|
|
for tier in self._tiers.values():
|
|
tier.limits.setdefault(dimension, 0)
|
|
|
|
def get_tier(self, plan_name: str) -> Optional[QuotaTier]:
|
|
"""获取套餐配额定义"""
|
|
return self._tiers.get(plan_name)
|
|
|
|
def get_limit(self, plan_name: str, dimension: str) -> float:
|
|
"""获取指定套餐在指定维度的限制"""
|
|
tier = self._tiers.get(plan_name)
|
|
if tier is None:
|
|
return 0
|
|
return tier.get_limit(dimension)
|
|
|
|
def list_dimensions(self) -> Dict[str, str]:
|
|
"""列出所有已注册的配额维度"""
|
|
return dict(self._dimensions)
|
|
|
|
def list_tiers(self) -> List[str]:
|
|
"""列出所有套餐等级"""
|
|
return list(self._tiers.keys())
|
|
|
|
|
|
class QuotaChecker:
|
|
"""配额检查器
|
|
|
|
检查用户在指定维度的使用量是否超出限制。
|
|
需要调用方提供当前使用量(used),本类不直接访问数据库。
|
|
"""
|
|
|
|
def __init__(self, registry: Optional[QuotaRegistry] = None) -> None:
|
|
self._registry = registry or QuotaRegistry()
|
|
|
|
def check(
|
|
self,
|
|
plan_name: str,
|
|
dimension: str,
|
|
used: float,
|
|
) -> QuotaCheckResult:
|
|
"""检查指定维度的配额使用情况
|
|
|
|
Args:
|
|
plan_name: 用户套餐等级 (free/basic/premium)
|
|
dimension: 配额维度
|
|
used: 当前已使用量
|
|
|
|
Returns:
|
|
QuotaCheckResult
|
|
"""
|
|
limit = self._registry.get_limit(plan_name, dimension)
|
|
remaining = max(0, limit - used) if limit != float("inf") else float("inf")
|
|
allowed = used < limit if limit != float("inf") else True
|
|
warning_level = self._compute_warning_level(used, limit)
|
|
|
|
return QuotaCheckResult(
|
|
allowed=allowed,
|
|
dimension=dimension,
|
|
limit=limit,
|
|
used=used,
|
|
remaining=remaining,
|
|
warning_level=warning_level,
|
|
)
|
|
|
|
def check_multiple(
|
|
self,
|
|
plan_name: str,
|
|
usage: Dict[str, float],
|
|
) -> List[QuotaCheckResult]:
|
|
"""批量检查多个维度的配额"""
|
|
return [self.check(plan_name, dim, used) for dim, used in usage.items()]
|
|
|
|
@staticmethod
|
|
def _compute_warning_level(used: float, limit: float) -> str:
|
|
"""计算告警级别"""
|
|
if limit <= 0:
|
|
return QuotaWarningLevel.EXCEEDED if used > 0 else QuotaWarningLevel.NORMAL
|
|
if limit == float("inf"):
|
|
return QuotaWarningLevel.NORMAL
|
|
|
|
ratio = used / limit
|
|
if ratio >= 1.0:
|
|
return QuotaWarningLevel.EXCEEDED
|
|
if ratio >= 0.95:
|
|
return QuotaWarningLevel.CRITICAL
|
|
if ratio >= 0.80:
|
|
return QuotaWarningLevel.WARNING
|
|
return QuotaWarningLevel.NORMAL
|
|
|
|
|
|
def get_warning_level(used: float, limit: float) -> str:
|
|
"""便捷函数:计算配额告警级别"""
|
|
return QuotaChecker._compute_warning_level(used, limit)
|
|
|
|
|
|
# 全局单例
|
|
quota_registry = QuotaRegistry()
|
|
quota_checker = QuotaChecker(quota_registry)
|