Files
xiaoxia-saas/packages/infrastructure/feature_flags.py
CI Bot 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
cleanup: Backend Phase 1 — 5项代码清理任务
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)
2026-07-13 13:23:30 +08:00

189 lines
6.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()