import os from typing import Optional from pydantic import AliasChoices, Field, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): APP_NAME: str = "xiaoxia-saas" APP_VERSION: str = "0.1.61" ENVIRONMENT: str = "development" DEBUG: bool = True # 应用基础 URL,用于生成认证邮件中的链接 # 开发环境默认 http://localhost:3000 # 生产环境应通过环境变量 APP_BASE_URL 设置 APP_BASE_URL: str = "http://localhost:3000" # Container bind address; external expose is controlled by Docker/Nginx. API_HOST: str = "0.0.0.0" # nosec: B104 API_PORT: int = 8000 DATABASE_URL: str = "postgresql+psycopg://postgres:postgres@localhost:5432/xiaoxia_saas" DATABASE_POOL_SIZE: int = 20 DATABASE_MAX_OVERFLOW: int = 10 # 调整为合理值:pool_size(20) + max_overflow(10) = 最大30连接 DATABASE_POOL_TIMEOUT: int = 30 DATABASE_POOL_RECYCLE: int = 3600 USE_IN_MEMORY_DB: bool = False AUTO_CREATE_SCHEMA: bool = False REDIS_URL: str = "redis://localhost:6379/0" ENABLE_REDIS_SESSIONS: bool = False # JWT secret key - MUST be set via environment variable, no default allowed JWT_SECRET_KEY: Optional[str] = None # JWT 算法与过期时间(与 .env.example 对齐) JWT_ALGORITHM: str = "HS256" JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = 30 @field_validator("JWT_SECRET_KEY", mode="before") @classmethod def validate_jwt_secret_key(cls, v): if v is None or v == "": raise ValueError( "JWT_SECRET_KEY must be set via environment variable. " "Do not use default value in production!" ) # Block known insecure default values insecure_defaults = [ "your-secret-key-change-in-production", "your-secret-key", "secret", "changeme", "password", ] if v.lower() in [d.lower() for d in insecure_defaults]: raise ValueError( f"JWT_SECRET_KEY '{v}' is insecure. " "Please set a strong random secret via environment variable." ) return v ENABLE_EMAIL_DELIVERY: bool = False SMTP_HOST: str = "smtp.gmail.com" SMTP_PORT: int = 587 SMTP_USER: str = "" SMTP_PASSWORD: str = "" SMTP_FROM_EMAIL: str = "" SMTP_FROM_NAME: str = "小虾 SaaS" SMTP_USE_TLS: bool = True CELERY_BROKER_URL: str = "redis://localhost:6379/0" CELERY_RESULT_BACKEND: str = "redis://localhost:6379/1" # OSS 七牛云相关 OSS_ENDPOINT: str = "oss-cn-hangzhou.aliyuncs.com" OSS_ACCESS_KEY_ID: str = "" OSS_ACCESS_KEY_SECRET: str = "" OSS_BUCKET_NAME: str = "xiaoxia-autocut" @field_validator("OSS_ACCESS_KEY_ID", mode="before") @classmethod def validate_oss_access_key_id(cls, v): if (v is None or v == "") and os.getenv("APP_ENV", "development") != "development": raise ValueError( "OSS_ACCESS_KEY_ID must be set via environment variable in non-development environments. " "Check the server .env file (e.g. /var/lib/xiaoxia-saas-staging/.env)." ) return v or "" @field_validator("OSS_ACCESS_KEY_SECRET", mode="before") @classmethod def validate_oss_access_key_secret(cls, v): if (v is None or v == "") and os.getenv("APP_ENV", "development") != "development": raise ValueError( "OSS_ACCESS_KEY_SECRET must be set via environment variable in non-development environments. " "Check the server .env file (e.g. /var/lib/xiaoxia-saas-staging/.env)." ) return v or "" OSS_DIRECT_UPLOAD_MAX_MB: int = Field( default=2000, validation_alias=AliasChoices("OSS_DIRECT_UPLOAD_MAX_MB", "MAX_UPLOAD_SIZE_MB"), ) OSS_DIRECT_UPLOAD_EXPIRE_SECONDS: int = 900 CORS_ORIGINS_RAW: str = "http://localhost:3000,http://localhost:5173,http://localhost:8000" # 渲染引擎选择:legacy=旧VideoComposeService,unified=新UnifiedRenderService RENDER_ENGINE: str = "legacy" model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore", ) @property def CORS_ORIGINS(self) -> list[str]: return [origin.strip() for origin in self.CORS_ORIGINS_RAW.split(",") if origin.strip()] @property def database_url(self) -> str: return self.DATABASE_URL @property def redis_url(self) -> str: return self.REDIS_URL @property def celery_broker_url(self) -> str: return self.CELERY_BROKER_URL @property def celery_result_backend(self) -> str: return self.CELERY_RESULT_BACKEND @property def oss_endpoint(self) -> str: return self.OSS_ENDPOINT @property def oss_access_key_id(self) -> str: return self.OSS_ACCESS_KEY_ID @property def oss_access_key_secret(self) -> str: return self.OSS_ACCESS_KEY_SECRET @property def oss_bucket_name(self) -> str: return self.OSS_BUCKET_NAME _settings: Optional[Settings] = None def get_settings() -> Settings: global _settings if _settings is None: env = os.getenv("APP_ENV", "development") env_file = f".env.{env}" if env != "development" else ".env" if os.path.exists(env_file): _settings = Settings(_env_file=env_file) else: _settings = Settings() return _settings settings = get_settings()