""" 统一配置:密钥与连接串经 .env / Settings;其余非密钥项见 config/*.toml(AppConfig)。 本地开发时由 api/development.sh 在启动前将 .env.development 同步为 .env。 """ from __future__ import annotations from pydantic import AliasChoices, Field, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict from app.core.app_config_models import DeployConfig from app.core.redis_urls import resolve_redis_urls _DEV_SECRET_KEY = "dev-only-secret-key-do-not-use-in-production" _DEPLOY_FIELD_NAMES = frozenset(DeployConfig.model_fields.keys()) class Settings(BaseSettings): """Secrets and bootstrap only — non-secret deploy/product config lives in TOML.""" model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore", ) database_url: str = "postgresql://postgres:postgres@localhost:48291/life_echo" redis_url: str = "redis://localhost:48307/0" redis_password: str = "" celery_redis_url: str = "" app_environment: str = Field( default="development", validation_alias=AliasChoices("APP_ENV", "APP_ENVIRONMENT"), ) secret_key: str = _DEV_SECRET_KEY deepseek_api_key: str = "" zhipu_api_key: str = "" tencent_secret_id: str = "" tencent_secret_key: str = "" tencent_app_id: str = "" wechat_pay_api_v3_key: str = "" wechat_pay_private_key: str = "" wechat_pay_platform_public_key: str = "" alipay_private_key: str = "" alipay_public_key: str = "" liblib_access_key: str = "" liblib_secret_key: str = "" internal_eval_api_key: str = "" google_token_verifier_secret: str = "" @model_validator(mode="after") def _validate_secret_key(self) -> "Settings": env = (self.app_environment or "").strip().lower() if env in ("production", "staging") and ( not self.secret_key or self.secret_key == _DEV_SECRET_KEY ): raise ValueError( "SECRET_KEY must be set to a strong random value in production/staging" ) return self @property def is_production(self) -> bool: return (self.app_environment or "").strip().lower() == "production" @property def enable_test_subscription(self) -> bool: return not self.is_production @property def enable_test_plan(self) -> bool: return not self.is_production @property def redis_url_resolved(self) -> str: business, _ = resolve_redis_urls( self.redis_url, redis_password=self.redis_password or None, celery_redis_url_override=None, ) return business @property def celery_redis_url_resolved(self) -> str: override = (self.celery_redis_url or "").strip() or None _, celery = resolve_redis_urls( self.redis_url, redis_password=self.redis_password or None, celery_redis_url_override=override, ) return celery class SettingsFacade: """Backward-compatible facade: secrets from Settings, deploy flags from TOML.""" __slots__ = ("_secrets",) def __init__(self, secrets: Settings) -> None: object.__setattr__(self, "_secrets", secrets) @property def _deploy(self) -> DeployConfig: from app.core.app_config import get_app_config return get_app_config().deploy def __getattr__(self, name: str): secrets = object.__getattribute__(self, "_secrets") if hasattr(secrets, name): return getattr(secrets, name) if name in _DEPLOY_FIELD_NAMES: return getattr(self._deploy, name) raise AttributeError(f"Settings has no attribute {name!r}") def __setattr__(self, name: str, value) -> None: if name == "_secrets": object.__setattr__(self, name, value) return secrets = object.__getattribute__(self, "_secrets") if hasattr(type(secrets), name) and name not in _DEPLOY_FIELD_NAMES: setattr(secrets, name, value) return if name in _DEPLOY_FIELD_NAMES: setattr(self._deploy, name, value) return setattr(secrets, name, value) settings = SettingsFacade(Settings())