276342520f
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 8s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1562h1m1s
CI/CD Pipeline / Build Production Runtime Images (pull_request) Failing after 1562h1m5s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1562h1m3s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Failing after 1562h1m5s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1562h1m3s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1562h32m38s
1. 修复密码重置接口路径不一致 Bug (auth.py) - forgot_password 硬编码 localhost → 使用 APP_BASE_URL 配置 2. 删除 8 处死代码(未使用 import/变量) - 清理多个文件中的未使用导入和变量 3. 删除 8 个空文件/空模块 - 删除无内容的 __init__.py 文件 4. 合并 3 对 100% 完全重复的函数 - 提取 check_project_access/get_user_plan/require_project_and_library - 新建 apps/api/app/api/routes/_helpers.py 作为共享模块 - 6 个路由文件改为从 _helpers 导入 5. 对齐 6 个废弃/异常环境变量 - 修复 DATABASE_POOL_RECYLE 拼写错误 → DATABASE_POOL_RECYCLE - 添加 JWT_ALGORITHM/JWT_ACCESS_TOKEN_EXPIRE_MINUTES/JWT_REFRESH_TOKEN_EXPIRE_DAYS 到 Settings - 修复 jwt_service.py hasattr 字段名匹配 - .env.example: CORS_ORIGINS → CORS_ORIGINS_RAW(逗号分隔格式) - .env.example: 启用 APP_ENV - 修复 OSS_ENDPOINT 默认值拼写错误 (aliiyuncs.com → aliyuncs.com) - 添加 COSYVOICE_* 变量来源注释 修改文件: 52 个(新增 1,删除 8,修改 43)
189 lines
6.3 KiB
Python
189 lines
6.3 KiB
Python
"""Feature Flags - 功能开关系统
|
||
|
||
支持三级控制:
|
||
1. 全局开关:全局启用/禁用某个功能
|
||
2. 套餐级别:按订阅套餐(free/basic/premium)控制
|
||
3. 用户白名单:针对特定用户单独启用
|
||
|
||
优先级:用户白名单 > 套餐级别 > 全局开关
|
||
|
||
Usage:
|
||
from packages.infrastructure.feature_flags import feature_flags
|
||
|
||
# 检查功能是否启用
|
||
if feature_flags.is_enabled("ai_voice_generation", user_plan="basic", user_id="u123"):
|
||
...
|
||
|
||
# 管理员动态更新
|
||
feature_flags.set_global("batch_export", enabled=False)
|
||
feature_flags.set_plan_override("multi_platform_output", "premium", True)
|
||
feature_flags.set_user_override("deduplication_report", "user_42", True)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from dataclasses import dataclass, field
|
||
from typing import Dict, Optional
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
@dataclass
|
||
class FeatureFlag:
|
||
"""单个 Feature Flag 的定义"""
|
||
|
||
name: str
|
||
description: str = ""
|
||
# 全局开关,默认 True(启用)
|
||
global_enabled: bool = True
|
||
# 套餐级别覆盖:{plan_name: bool}
|
||
plan_overrides: Dict[str, bool] = field(default_factory=dict)
|
||
# 用户白名单:user_id -> bool(True=强制启用,False=强制禁用)
|
||
user_overrides: Dict[str, bool] = field(default_factory=dict)
|
||
|
||
def is_enabled(
|
||
self,
|
||
user_plan: Optional[str] = None,
|
||
user_id: Optional[str] = None,
|
||
) -> bool:
|
||
"""判断功能是否对指定用户启用
|
||
|
||
优先级:用户白名单 > 套餐级别 > 全局开关
|
||
"""
|
||
# 1. 用户白名单优先
|
||
if user_id and user_id in self.user_overrides:
|
||
return self.user_overrides[user_id]
|
||
|
||
# 2. 套餐级别
|
||
if user_plan and user_plan in self.plan_overrides:
|
||
return self.plan_overrides[user_plan]
|
||
|
||
# 3. 全局开关
|
||
return self.global_enabled
|
||
|
||
|
||
class FeatureScope:
|
||
"""Feature Flag 名称常量,避免硬编码字符串"""
|
||
|
||
AI_VOICE_GENERATION = "ai_voice_generation"
|
||
DEDUPLICATION_REPORT = "deduplication_report"
|
||
BATCH_EXPORT = "batch_export"
|
||
MULTI_PLATFORM_OUTPUT = "multi_platform_output"
|
||
RECIPE_REUSE = "recipe_reuse"
|
||
|
||
|
||
class FeatureFlags:
|
||
"""Feature Flags 管理器
|
||
|
||
单例模式,全局唯一实例(feature_flags)。
|
||
"""
|
||
|
||
def __init__(self) -> None:
|
||
self._flags: Dict[str, FeatureFlag] = {}
|
||
self._init_defaults()
|
||
|
||
def _init_defaults(self) -> None:
|
||
"""初始化默认 Feature Flags"""
|
||
defaults = [
|
||
FeatureFlag(
|
||
name=FeatureScope.AI_VOICE_GENERATION,
|
||
description="AI 配音生成功能",
|
||
global_enabled=True,
|
||
plan_overrides={"free": False}, # free 套餐不可用
|
||
),
|
||
FeatureFlag(
|
||
name=FeatureScope.DEDUPLICATION_REPORT,
|
||
description="素材去重检测报告",
|
||
global_enabled=True,
|
||
plan_overrides={"free": False, "basic": False}, # 仅 premium
|
||
),
|
||
FeatureFlag(
|
||
name=FeatureScope.BATCH_EXPORT,
|
||
description="批量导出功能",
|
||
global_enabled=True,
|
||
plan_overrides={"free": False},
|
||
),
|
||
FeatureFlag(
|
||
name=FeatureScope.MULTI_PLATFORM_OUTPUT,
|
||
description="多平台发布输出",
|
||
global_enabled=True,
|
||
plan_overrides={"free": False, "basic": False}, # 仅 premium
|
||
),
|
||
FeatureFlag(
|
||
name=FeatureScope.RECIPE_REUSE,
|
||
description="配方复用功能",
|
||
global_enabled=True,
|
||
plan_overrides={"free": False}, # 仅基础版和高级版可用
|
||
),
|
||
]
|
||
for flag in defaults:
|
||
self._flags[flag.name] = flag
|
||
|
||
def register(self, flag: FeatureFlag) -> None:
|
||
"""注册一个新的 Feature Flag"""
|
||
self._flags[flag.name] = flag
|
||
logger.info(f"Feature flag '{flag.name}' registered")
|
||
|
||
def get(self, name: str) -> Optional[FeatureFlag]:
|
||
"""获取 Feature Flag 定义"""
|
||
return self._flags.get(name)
|
||
|
||
def is_enabled(
|
||
self,
|
||
name: str,
|
||
user_plan: Optional[str] = None,
|
||
user_id: Optional[str] = None,
|
||
) -> bool:
|
||
"""检查功能是否启用
|
||
|
||
Args:
|
||
name: Feature flag 名称
|
||
user_plan: 用户订阅套餐 (free/basic/premium)
|
||
user_id: 用户 ID
|
||
|
||
Returns:
|
||
True 如果功能启用,False 否则
|
||
"""
|
||
flag = self._flags.get(name)
|
||
if flag is None:
|
||
logger.warning(f"Feature flag '{name}' not found, defaulting to disabled")
|
||
return False
|
||
return flag.is_enabled(user_plan=user_plan, user_id=user_id)
|
||
|
||
def set_global(self, name: str, enabled: bool) -> None:
|
||
"""设置全局开关"""
|
||
flag = self._flags.get(name)
|
||
if flag is None:
|
||
raise KeyError(f"Feature flag '{name}' not found")
|
||
flag.global_enabled = enabled
|
||
logger.info(f"Feature flag '{name}' global set to {enabled}")
|
||
|
||
def set_plan_override(self, name: str, plan: str, enabled: bool) -> None:
|
||
"""设置套餐级别覆盖"""
|
||
flag = self._flags.get(name)
|
||
if flag is None:
|
||
raise KeyError(f"Feature flag '{name}' not found")
|
||
flag.plan_overrides[plan] = enabled
|
||
logger.info(f"Feature flag '{name}' plan '{plan}' set to {enabled}")
|
||
|
||
def set_user_override(self, name: str, user_id: str, enabled: bool) -> None:
|
||
"""设置用户白名单覆盖"""
|
||
flag = self._flags.get(name)
|
||
if flag is None:
|
||
raise KeyError(f"Feature flag '{name}' not found")
|
||
flag.user_overrides[user_id] = enabled
|
||
logger.info(f"Feature flag '{name}' user '{user_id}' set to {enabled}")
|
||
|
||
def list_flags(self) -> Dict[str, FeatureFlag]:
|
||
"""列出所有 Feature Flags"""
|
||
return dict(self._flags)
|
||
|
||
def get_enabled_for_plan(self, plan: str) -> list[str]:
|
||
"""获取指定套餐下所有启用的功能名称"""
|
||
return [name for name, flag in self._flags.items() if flag.is_enabled(user_plan=plan)]
|
||
|
||
|
||
# 全局单例
|
||
feature_flags = FeatureFlags()
|