2026-03-18 17:18:23 +08:00
|
|
|
|
"""
|
2026-05-22 13:44:50 +08:00
|
|
|
|
统一配置:密钥与连接串经 .env / Settings;其余非密钥项见 config/*.toml(AppConfig)。
|
2026-03-23 13:21:07 +08:00
|
|
|
|
|
2026-05-22 13:44:50 +08:00
|
|
|
|
本地开发时由 api/development.sh 在启动前将 .env.development 同步为 .env。
|
2026-03-18 17:18:23 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
2026-05-22 13:44:50 +08:00
|
|
|
|
from __future__ import annotations
|
2026-03-18 17:18:23 +08:00
|
|
|
|
|
2026-05-22 13:44:50 +08:00
|
|
|
|
from pydantic import AliasChoices, Field, model_validator
|
2026-03-18 17:18:23 +08:00
|
|
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
|
2026-05-22 13:44:50 +08:00
|
|
|
|
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())
|
|
|
|
|
|
|
2026-03-18 17:18:23 +08:00
|
|
|
|
|
|
|
|
|
|
class Settings(BaseSettings):
|
2026-05-22 13:44:50 +08:00
|
|
|
|
"""Secrets and bootstrap only — non-secret deploy/product config lives in TOML."""
|
|
|
|
|
|
|
2026-03-18 17:18:23 +08:00
|
|
|
|
model_config = SettingsConfigDict(
|
|
|
|
|
|
env_file=".env",
|
|
|
|
|
|
env_file_encoding="utf-8",
|
|
|
|
|
|
case_sensitive=False,
|
|
|
|
|
|
extra="ignore",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-22 13:44:50 +08:00
|
|
|
|
database_url: str = "postgresql://postgres:postgres@localhost:48291/life_echo"
|
|
|
|
|
|
redis_url: str = "redis://localhost:48307/0"
|
|
|
|
|
|
redis_password: str = ""
|
|
|
|
|
|
celery_redis_url: str = ""
|
2026-04-08 21:36:12 +08:00
|
|
|
|
app_environment: str = Field(
|
|
|
|
|
|
default="development",
|
|
|
|
|
|
validation_alias=AliasChoices("APP_ENV", "APP_ENVIRONMENT"),
|
|
|
|
|
|
)
|
2026-05-22 13:44:50 +08:00
|
|
|
|
|
|
|
|
|
|
secret_key: str = _DEV_SECRET_KEY
|
2026-03-18 17:18:23 +08:00
|
|
|
|
deepseek_api_key: str = ""
|
2026-03-30 13:54:35 +08:00
|
|
|
|
zhipu_api_key: str = ""
|
2026-05-22 13:44:50 +08:00
|
|
|
|
|
2026-03-18 17:18:23 +08:00
|
|
|
|
tencent_secret_id: str = ""
|
|
|
|
|
|
tencent_secret_key: str = ""
|
2026-05-25 11:28:22 +08:00
|
|
|
|
tencent_app_id: str = ""
|
2026-03-18 17:18:23 +08:00
|
|
|
|
|
|
|
|
|
|
wechat_pay_api_v3_key: str = ""
|
2026-05-22 13:44:50 +08:00
|
|
|
|
wechat_pay_private_key: str = ""
|
2026-03-18 17:18:23 +08:00
|
|
|
|
wechat_pay_platform_public_key: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
alipay_private_key: str = ""
|
|
|
|
|
|
alipay_public_key: str = ""
|
2026-05-22 13:44:50 +08:00
|
|
|
|
|
2026-03-18 17:18:23 +08:00
|
|
|
|
liblib_access_key: str = ""
|
|
|
|
|
|
liblib_secret_key: str = ""
|
2026-04-03 14:44:46 +08:00
|
|
|
|
|
2026-05-22 13:44:50 +08:00
|
|
|
|
internal_eval_api_key: str = ""
|
2026-03-18 17:18:23 +08:00
|
|
|
|
|
2026-05-22 13:44:50 +08:00
|
|
|
|
@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())
|