from __future__ import annotations from functools import lru_cache from pathlib import Path from typing import Literal, Optional from pydantic import ValidationError from pydantic_settings import BaseSettings, SettingsConfigDict def _guess_env_file(app_env: str) -> Optional[str]: """ 根据 APP_ENV 选择 env 文件。 说明: - `.env.dev` / `.env.prod` 不进入仓库,通常由运维在部署时放到 `server/` 目录 - 如果文件不存在,则返回 None,让系统从“真实环境变量”读取 """ base_dir = Path(__file__).resolve().parents[2] # .../server/app -> .../server candidates = { "dev": base_dir / ".env.dev", "prod": base_dir / ".env.prod", } p = candidates.get(app_env) if p and p.exists(): return str(p) return None class Settings(BaseSettings): """ 应用配置(统一从环境变量读取)。 注意:请不要把真实账号密码写入仓库;使用 `.env.dev/.env.prod` 或部署系统注入。 """ app_env: Literal["dev", "prod"] = "dev" app_name: str = "mindfulness-server" app_host: str = "0.0.0.0" app_port: int = 8000 database_url: str redis_url: str celery_broker_url: str celery_result_backend: Optional[str] = None model_config = SettingsConfigDict( env_prefix="", case_sensitive=False, extra="ignore", ) @lru_cache def get_settings() -> Settings: """ 获取配置(带缓存)。 加载顺序: - 优先使用 `.env.dev/.env.prod`(若存在) - 否则使用系统环境变量 """ import os env = os.getenv("APP_ENV", "dev").strip() or "dev" env_file = _guess_env_file(env) try: return Settings(_env_file=env_file) except ValidationError as e: # 给出更可执行的错误信息,避免只看到一串校验栈。 missing_fields: list[str] = [] for err in e.errors(): if err.get("type") == "missing": loc = err.get("loc") or () if loc: missing_fields.append(str(loc[0])) # 将字段名映射为常见的环境变量名(默认规则:字段名大写) missing_env_keys = [f.upper() for f in missing_fields] if missing_fields else [] env_file_hint = env_file or f".env.{env}(未找到,已回退到系统环境变量)" required_hint = ( "、".join(missing_env_keys) if missing_env_keys else "DATABASE_URL、REDIS_URL、CELERY_BROKER_URL" ) msg = ( "应用启动失败:缺少必填配置。\n\n" f"- 当前 APP_ENV:{env}\n" f"- 期望读取的 env 文件:{env_file_hint}\n" f"- 缺少的环境变量:{required_hint}\n\n" "修复方式(任选其一):\n" "1) 直接注入环境变量(推荐):\n" " - DATABASE_URL=...\n" " - REDIS_URL=...\n" " - CELERY_BROKER_URL=...\n" "2) 使用 env 文件:在 `server/` 下准备 `.env.dev` 或 `.env.prod`(KEY=value 格式),\n" " 本地可通过 `server/run.sh --env dev|prod` 自动加载;Docker 运行可用 `--env-file` 传入。\n" ) # 不附带原始 ValidationError 的异常上下文,减少日志噪音; # msg 已包含缺失项与修复方式,足够定位问题。 raise RuntimeError(msg) from None