提供基本前后端骨架
This commit is contained in:
202
app/core/config.py
Normal file
202
app/core/config.py
Normal file
@@ -0,0 +1,202 @@
|
||||
"""
|
||||
应用配置管理
|
||||
|
||||
使用 Pydantic Settings 进行类型安全的配置管理,
|
||||
集成 ConfigLoader 支持环境变量和 YAML 配置文件。
|
||||
|
||||
配置优先级(从高到低):
|
||||
1. 环境变量(前缀 SATONANO_)
|
||||
2. config.yaml 文件
|
||||
3. .env 文件
|
||||
4. 默认值
|
||||
"""
|
||||
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import Field, computed_field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
from .config_loader import create_config_loader
|
||||
|
||||
|
||||
def _yaml_settings_source() -> dict[str, Any]:
|
||||
"""
|
||||
YAML 配置源工厂函数
|
||||
|
||||
为 Pydantic Settings 提供 YAML 配置数据
|
||||
"""
|
||||
# 查找配置文件路径
|
||||
config_paths = [
|
||||
Path("config.yaml"),
|
||||
Path("config.yml"),
|
||||
Path(__file__).parent.parent.parent / "config.yaml",
|
||||
Path(__file__).parent.parent.parent / "config.yml",
|
||||
]
|
||||
|
||||
yaml_path = None
|
||||
for path in config_paths:
|
||||
if path.exists():
|
||||
yaml_path = path
|
||||
break
|
||||
|
||||
if yaml_path is None:
|
||||
return {}
|
||||
|
||||
# 使用 ConfigLoader 加载配置(不带环境变量前缀,让 pydantic 处理)
|
||||
loader = create_config_loader(
|
||||
yaml_path=yaml_path,
|
||||
env_prefix="", # 不读取环境变量,由 pydantic-settings 处理
|
||||
yaml_required=False,
|
||||
)
|
||||
return loader.load()
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""应用配置"""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="SATONANO_",
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
case_sensitive=False,
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
# 应用基础配置
|
||||
app_name: str = "SatoNano"
|
||||
app_version: str = "0.1.0"
|
||||
debug: bool = False
|
||||
environment: Literal["development", "staging", "production"] = "development"
|
||||
|
||||
# API 配置
|
||||
api_v1_prefix: str = "/api/v1"
|
||||
|
||||
# 安全配置
|
||||
secret_key: str = Field(
|
||||
default="CHANGE-THIS-SECRET-KEY-IN-PRODUCTION",
|
||||
description="JWT 签名密钥,生产环境必须更改",
|
||||
)
|
||||
algorithm: str = "HS256"
|
||||
access_token_expire_minutes: int = 30
|
||||
refresh_token_expire_days: int = 7
|
||||
|
||||
# 数据库配置
|
||||
database_type: Literal["sqlite", "mysql", "postgresql"] = "sqlite"
|
||||
database_host: str = "localhost"
|
||||
database_port: int = 3306
|
||||
database_name: str = "satonano"
|
||||
database_username: str = ""
|
||||
database_password: str = ""
|
||||
database_sqlite_path: str = "./satonano.db"
|
||||
database_echo: bool = False
|
||||
|
||||
# OAuth2 配置 (Linux.do)
|
||||
oauth2_client_id: str = ""
|
||||
oauth2_client_secret: str = ""
|
||||
oauth2_callback_path: str = "/oauth2/callback"
|
||||
# 首选端点
|
||||
oauth2_authorize_endpoint: str = "https://connect.linux.do/oauth2/authorize"
|
||||
oauth2_token_endpoint: str = "https://connect.linux.do/oauth2/token"
|
||||
oauth2_user_info_endpoint: str = "https://connect.linux.do/api/user"
|
||||
# 备用端点
|
||||
oauth2_authorize_endpoint_reserve: str = "https://connect.linuxdo.org/oauth2/authorize"
|
||||
oauth2_token_endpoint_reserve: str = "https://connect.linuxdo.org/oauth2/token"
|
||||
oauth2_user_info_endpoint_reserve: str = "https://connect.linuxdo.org/api/user"
|
||||
oauth2_request_timeout: int = 10 # 请求超时时间(秒)
|
||||
|
||||
# 密码策略
|
||||
password_min_length: int = 8
|
||||
password_max_length: int = 128
|
||||
password_require_uppercase: bool = True
|
||||
password_require_lowercase: bool = True
|
||||
password_require_digit: bool = True
|
||||
password_require_special: bool = False
|
||||
|
||||
# 用户名策略
|
||||
username_min_length: int = 3
|
||||
username_max_length: int = 32
|
||||
|
||||
# 前端静态文件配置
|
||||
frontend_static_path: str = "./frontend/out"
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
"""
|
||||
动态构建数据库连接 URL
|
||||
|
||||
根据 database_type 自动生成对应的连接字符串
|
||||
"""
|
||||
if self.database_type == "sqlite":
|
||||
return f"sqlite+aiosqlite:///{self.database_sqlite_path}"
|
||||
|
||||
if self.database_type == "mysql":
|
||||
return (
|
||||
f"mysql+aiomysql://{self.database_username}:{self.database_password}"
|
||||
f"@{self.database_host}:{self.database_port}/{self.database_name}"
|
||||
)
|
||||
|
||||
if self.database_type == "postgresql":
|
||||
return (
|
||||
f"postgresql+asyncpg://{self.database_username}:{self.database_password}"
|
||||
f"@{self.database_host}:{self.database_port}/{self.database_name}"
|
||||
)
|
||||
|
||||
return f"sqlite+aiosqlite:///{self.database_sqlite_path}"
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def is_production(self) -> bool:
|
||||
"""是否为生产环境"""
|
||||
return self.environment == "production"
|
||||
|
||||
@classmethod
|
||||
def settings_customise_sources(
|
||||
cls,
|
||||
settings_cls: type[BaseSettings],
|
||||
init_settings: Any,
|
||||
env_settings: Any,
|
||||
dotenv_settings: Any,
|
||||
file_secret_settings: Any,
|
||||
) -> tuple[Any, ...]:
|
||||
"""
|
||||
自定义配置源优先级
|
||||
|
||||
优先级顺序(从高到低):
|
||||
1. 初始化参数
|
||||
2. 环境变量
|
||||
3. YAML 文件
|
||||
4. .env 文件
|
||||
5. 文件密钥
|
||||
"""
|
||||
|
||||
class YamlConfigSettingsSource:
|
||||
"""YAML 配置源适配器"""
|
||||
|
||||
def __init__(self, settings_cls: type[BaseSettings]) -> None:
|
||||
self.settings_cls = settings_cls
|
||||
self._yaml_data: dict[str, Any] | None = None
|
||||
|
||||
def __call__(self) -> dict[str, Any]:
|
||||
if self._yaml_data is None:
|
||||
self._yaml_data = _yaml_settings_source()
|
||||
return self._yaml_data
|
||||
|
||||
return (
|
||||
init_settings,
|
||||
env_settings,
|
||||
YamlConfigSettingsSource(settings_cls),
|
||||
dotenv_settings,
|
||||
file_secret_settings,
|
||||
)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
"""获取配置单例"""
|
||||
return Settings()
|
||||
|
||||
|
||||
settings = get_settings()
|
||||
Reference in New Issue
Block a user