Files
xiaoxia-saas/packages/infrastructure/feature_flags.py
CI Bot 9c6c477f55
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 2m22s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m24s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 37s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 4m3s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
fix(backend): Phase 1 后端代码清理与修复
P0 关键修复:
- P0-1: 注册接口添加 RateLimitMiddleware 限流保护
- P0-3: /metrics 端点添加 JWT 认证(移除匿名访问)
- P0-4: 修复 Celery 任务名冲突(generation_task vs generate_video)
- P1-5: JWT logout token 黑名单机制

P1 修复:
- P1-1: forgot_password 硬编码 localhost → 使用 settings.APP_BASE_URL
- P1-2: generation.py 直接创建 DB 连接 → 使用依赖注入
- P1-6: Image.open() 未关闭 → 统一使用 with 语句
- P1-7: 订阅续费事务修复

P2 代码质量:
- P2-1: 修复 EditingMode 枚举重复定义 → 统一引用 shared 包
- P2-2: 修复 SMTP_FRON_NAME → SMTP_FROM_NAME 拼写
- P2-3: UserModel subscription_quota 类型统一为 float
- P2-4: .env.production DATABASE_MAX_OVERFLOW 30 → 10
- 清理 15 处 except:pass(保留 2 处有注释说明的)
- 禁用 SVG 上传(XSS 风险)
- 删除 decode_token_unsafe() 不安全函数
- 简化 /ready 端点
- 删除 8 处死代码、10 个空文件/模块
- 合并 3 对 100% 重复函数
- 对齐 6 个废弃环境变量

v2 修复(代码审查后):
- 修复密码重置路由路径: /password/forgot → /forgot-password,
  /password/reset → /reset-password(与前端 API 对齐)
- 合并 _check_project_access: asset_libraries.py 和 edit_plans.py
  中的重复函数统一到 _helpers.py(含空字符串守卫 + 中文错误信息)
- 顺手修复: HTTPException 统一从 fastapi 导入(替换 starlette 导入)
- OSS_ENDPOINT 拼写修复拆分为单独 PR,本 PR 不包含
2026-07-13 13:50:52 +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 -> boolTrue=强制启用,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()