127 lines
4.4 KiB
Python
127 lines
4.4 KiB
Python
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
|
||
|
||
# 法务协议链接(由后端统一托管并下发给客户端)
|
||
# 说明:
|
||
# - 当前仅支持 EN / TC 两种语言(与客户端现状一致)
|
||
# - 若未显式配置 LEGAL_*,后端会使用“内置协议页面”(/v1/legal/privacy、/v1/legal/terms)
|
||
# - 线上建议配置为你们的官网/托管页/静态站点 HTTPS 链接,避免与 API 域名强绑定
|
||
#
|
||
# 这里用一个内部哨兵值表示“走内置页面”,避免默认值误导为可用的公网链接。
|
||
legal_privacy_url_en: str = "__internal__"
|
||
legal_terms_url_en: str = "__internal__"
|
||
legal_privacy_url_tc: Optional[str] = None
|
||
legal_terms_url_tc: Optional[str] = None
|
||
|
||
# Expo Push(可选)
|
||
# 说明:
|
||
# - 不填也能调用 Expo Push API,但会受更严格的速率限制
|
||
# - 建议生产环境配置 EXPO_ACCESS_TOKEN,便于稳定性与配额
|
||
expo_access_token: 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
|
||
|