from pydantic_settings import BaseSettings, SettingsConfigDict from typing import Optional import os class AppSettings(BaseSettings): """应用配置""" # 基础配置 app_name: str = "xiaoxia-saas" app_env: str = "development" # development / staging / production app_version: str = "0.1.0" debug: bool = True # API 配置 api_host: str = "0.0.0.0" api_port: int = 8000 api_prefix: str = "/api/v1" # 数据库配置 database_url: str = "postgresql+psycopg://postgres:postgres@localhost:5432/xiaoxia_saas" database_pool_size: int = 20 database_max_overflow: int = 40 database_pool_timeout: int = 30 database_pool_recycle: int = 3600 # Redis 配置 redis_url: str = "redis://localhost:6379/0" redis_max_connections: int = 50 # Celery 配置 celery_broker_url: str = "redis://localhost:6379/0" celery_result_backend: str = "redis://localhost:6379/1" celery_worker_concurrency: int = 4 celery_worker_max_tasks_per_child: int = 1000 # MinIO 配置 minio_endpoint: str = "localhost:9000" minio_access_key: str = "admin" minio_secret_key: str = "xiaoxia2026" minio_bucket: str = "xiaoxia-assets" minio_secure: bool = False minio_public_url: str = "http://localhost:9000" # 日志配置 log_level: str = "INFO" log_format: str = "json" # json / text log_file: Optional[str] = None # CORS 配置 cors_origins: str = "http://localhost:3000,http://localhost:8000" cors_allow_credentials: bool = True # 文件上传限制 max_upload_size_mb: int = 1000 allowed_file_types: str = "video/mp4,video/quicktime,video/x-msvideo,audio/mpeg,audio/wav,image/jpeg,image/png,image/gif" # 安全配置 secret_key: str = "change-me-in-production" access_token_expire_minutes: int = 60 refresh_token_expire_days: int = 7 # 监控配置(可选) sentry_dsn: Optional[str] = None prometheus_port: Optional[int] = None model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore", ) @property def cors_origins_list(self) -> list[str]: """解析 CORS origins 为列表""" return [origin.strip() for origin in self.cors_origins.split(",")] @property def allowed_file_types_list(self) -> list[str]: """解析允许的文件类型为列表""" return [ft.strip() for ft in self.allowed_file_types.split(",")] @property def is_production(self) -> bool: """是否为生产环境""" return self.app_env == "production" @property def is_staging(self) -> bool: """是否为 staging 环境""" return self.app_env == "staging" @property def is_development(self) -> bool: """是否为开发环境""" return self.app_env == "development" # 全局配置实例 _settings: Optional[AppSettings] = None def get_settings() -> AppSettings: """获取配置实例(单例模式)""" global _settings if _settings is None: # 根据环境加载不同的 .env 文件 env = os.getenv("APP_ENV", "development") env_file = f".env.{env}" if env != "development" else ".env" # 如果环境特定的配置文件存在,则使用它 if os.path.exists(env_file): _settings = AppSettings(_env_file=env_file) else: _settings = AppSettings() return _settings def reload_settings(): """重新加载配置(用于测试)""" global _settings _settings = None return get_settings()