"""全局配置:pydantic-settings,全部来自环境变量 / .env。 规则: - 密钥不得硬编码;SECRET_KEY 缺失或仍为模板值时,生产环境拒绝启动。 - DATABASE_URL 支持 MySQL / SQLite。 """ from functools import lru_cache from pathlib import Path from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict _TEMPLATE_MARKERS = ("change-me",) class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") # --- 基础 --- environment: str = "local" # local / production secret_key: str = Field(min_length=16) # Cookie Secure 标记:默认跟随 environment(production→Secure)。 # 站点还是 HTTP 时必须显式设为 false,否则浏览器丢弃 Cookie 导致登录后立即被踢回。 cookie_secure: bool | None = None # --- 数据库 --- database_url: str = "mysql+pymysql://admin:Lzcc6-01@47.109.98.44:33306/amb_rag?charset=utf8mb4" # --- 文件存储 --- storage_root: str = "./data" # --- 套餐 --- default_storage_quota: int = 104_857_600 # 100 MB default_max_file_size: int = 20_971_520 # 20 MB # --- 限流 --- rate_limit_per_token_per_min: int = 60 rate_limit_per_ip_per_min: int = 30 # --- 数据保留 --- # 访问日志保留天数,超过自动清理(0 = 永久保留,不推荐) access_log_retention_days: int = 90 # --- CORS --- frontend_origin: str = "http://localhost:5173" @property def is_production(self) -> bool: return self.environment == "production" @property def use_secure_cookie(self) -> bool: """HTTPS 站点才应启用 Secure Cookie;未显式配置时跟随 environment。""" if self.cookie_secure is not None: return self.cookie_secure return self.is_production @property def is_mysql(self) -> bool: return "mysql" in self.database_url @property def is_sqlite(self) -> bool: return "sqlite" in self.database_url @property def storage_root_path(self) -> Path: return Path(self.storage_root).resolve() def validate_secrets(self) -> None: """生产环境禁止携带模板密钥启动。""" if not self.is_production: return for field_name in ("secret_key",): value = getattr(self, field_name).lower() if any(marker in value for marker in _TEMPLATE_MARKERS): raise RuntimeError( f"配置错误:{field_name} 仍为模板值,生产环境禁止启动。" "请运行: python -c \"import secrets; print(secrets.token_urlsafe(48))\"" ) @lru_cache def get_settings() -> Settings: s = Settings() # type: ignore[call-arg] s.validate_secrets() return s