mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-10-14 05:00:08 +00:00
224 lines
7.2 KiB
Python
224 lines
7.2 KiB
Python
import shutil
|
|
|
|
from functools import cache
|
|
from re import Pattern
|
|
from typing import Any, Literal
|
|
|
|
from pydantic import Field, model_validator
|
|
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict
|
|
|
|
from backend.core.path_conf import ENV_EXAMPLE_FILE_PATH, ENV_FILE_PATH
|
|
from backend.plugin.settings_source import PluginSettingsSource
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
"""全局配置"""
|
|
|
|
model_config = SettingsConfigDict(
|
|
env_file=ENV_FILE_PATH,
|
|
env_file_encoding='utf-8',
|
|
extra='allow',
|
|
case_sensitive=True,
|
|
)
|
|
|
|
@classmethod
|
|
def settings_customise_sources(
|
|
cls,
|
|
settings_cls: type[BaseSettings],
|
|
init_settings: PydanticBaseSettingsSource,
|
|
env_settings: PydanticBaseSettingsSource,
|
|
dotenv_settings: PydanticBaseSettingsSource,
|
|
file_secret_settings: PydanticBaseSettingsSource,
|
|
) -> tuple[PydanticBaseSettingsSource, ...]:
|
|
"""自定义配置源优先级"""
|
|
return env_settings, dotenv_settings, PluginSettingsSource(settings_cls)
|
|
|
|
# .env 当前环境
|
|
ENVIRONMENT: Literal['dev', 'prod']
|
|
|
|
# FastAPI
|
|
FASTAPI_API_V1_PATH: str = '/api/v1'
|
|
FASTAPI_TITLE: str = 'fba'
|
|
FASTAPI_DESCRIPTION: str = 'FastAPI Best Architecture'
|
|
FASTAPI_DOCS_URL: str = '/docs'
|
|
FASTAPI_REDOC_URL: str = '/redoc'
|
|
FASTAPI_OPENAPI_URL: str | None = '/openapi'
|
|
FASTAPI_STATIC_FILES: bool = True
|
|
|
|
# .env 数据库
|
|
DATABASE_TYPE: Literal['mysql', 'postgresql']
|
|
DATABASE_HOST: str
|
|
DATABASE_PORT: int
|
|
DATABASE_USER: str
|
|
DATABASE_PASSWORD: str
|
|
DATABASE_SOURCES: dict[str, str] = Field(default_factory=dict)
|
|
|
|
# 数据库
|
|
DATABASE_ECHO: bool | Literal['debug'] = False
|
|
DATABASE_POOL_ECHO: bool | Literal['debug'] = False
|
|
DATABASE_SCHEMA: str = 'fba'
|
|
DATABASE_CHARSET: str = 'utf8mb4'
|
|
DATABASE_PK_MODE: Literal['autoincrement', 'snowflake'] = 'autoincrement'
|
|
|
|
# .env Redis
|
|
REDIS_HOST: str
|
|
REDIS_PORT: int
|
|
REDIS_PASSWORD: str
|
|
REDIS_DATABASE: int
|
|
|
|
# Redis
|
|
REDIS_TIMEOUT: int = 5
|
|
|
|
# 缓存
|
|
CACHE_LOCAL_ENABLED: bool = True
|
|
CACHE_LOCAL_MAXSIZE: int = 100000
|
|
CACHE_LOCAL_TTL: int = 60 * 60 * 2 # 2 小时
|
|
CACHE_REDIS_TTL: int = 60 * 60 * 2 # 2 小时
|
|
CACHE_CONFIG_REDIS_PREFIX: str = 'fba:cache:config'
|
|
CACHE_PUBSUB_CHANNEL: str = 'fba:cache:invalidate'
|
|
CACHE_PUBSUB_RECONNECT_DELAY: int = 5 # 重连延迟(秒)
|
|
CACHE_PUBSUB_MAX_RECONNECT_ATTEMPTS: int = 10 # 最大重连次数
|
|
|
|
# .env Snowflake
|
|
SNOWFLAKE_ENABLED: bool = False
|
|
SNOWFLAKE_DATACENTER_ID: int | None = None
|
|
SNOWFLAKE_WORKER_ID: int | None = None
|
|
|
|
# Snowflake
|
|
SNOWFLAKE_REDIS_PREFIX: str = 'fba:snowflake'
|
|
SNOWFLAKE_HEARTBEAT_INTERVAL_SECONDS: int = 30
|
|
SNOWFLAKE_NODE_TTL_SECONDS: int = 60
|
|
|
|
# .env Token
|
|
TOKEN_SECRET_KEY: str # 密钥 secrets.token_urlsafe(32)
|
|
|
|
# Token
|
|
TOKEN_ALGORITHM: str = 'HS256'
|
|
TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 24 # 1 天
|
|
TOKEN_REFRESH_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 # 7 天
|
|
TOKEN_REDIS_PREFIX: str = 'fba:token'
|
|
TOKEN_EXTRA_INFO_REDIS_PREFIX: str = 'fba:token_extra_info'
|
|
TOKEN_ONLINE_REDIS_PREFIX: str = 'fba:token_online'
|
|
TOKEN_REFRESH_REDIS_PREFIX: str = 'fba:refresh_token'
|
|
TOKEN_REQUEST_UNDERLYING_SECURITY: bool = True
|
|
TOKEN_REQUEST_PATH_EXCLUDE: list[str] = [ # JWT 路由白名单
|
|
f'{FASTAPI_API_V1_PATH}/auth/login',
|
|
]
|
|
TOKEN_REQUEST_PATH_EXCLUDE_PATTERN: list[Pattern[str]] = [] # JWT 路由白名单(正则)
|
|
|
|
# 用户安全
|
|
USER_LOCK_REDIS_PREFIX: str = 'fba:user:lock'
|
|
USER_LOCK_THRESHOLD: int = 5 # 用户密码错误锁定阈值,0 表示禁用锁定
|
|
USER_LOCK_SECONDS: int = 60 * 5 # 5 分钟
|
|
USER_PASSWORD_EXPIRY_DAYS: int = 365 # 用户密码有效期,0 表示永不过期
|
|
USER_PASSWORD_REMINDER_DAYS: int = 7 # 用户密码到期提醒,0 表示不提醒
|
|
USER_PASSWORD_HISTORY_CHECK_COUNT: int = 3
|
|
USER_PASSWORD_MIN_LENGTH: int = 6
|
|
USER_PASSWORD_MAX_LENGTH: int = 32
|
|
USER_PASSWORD_REQUIRE_SPECIAL_CHAR: bool = False
|
|
|
|
# 登录
|
|
LOGIN_CAPTCHA_ENABLED: bool = True
|
|
LOGIN_CAPTCHA_REDIS_PREFIX: str = 'fba:login:captcha'
|
|
LOGIN_CAPTCHA_EXPIRE_SECONDS: int = 60 * 5 # 5 分钟
|
|
LOGIN_FAILURE_PREFIX: str = 'fba:login:failure'
|
|
|
|
# JWT
|
|
JWT_USER_REDIS_PREFIX: str = 'fba:user'
|
|
|
|
# Cookie
|
|
COOKIE_REFRESH_TOKEN_KEY: str = 'fba_refresh_token'
|
|
COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 # 7 天
|
|
|
|
# CORS
|
|
CORS_ALLOWED_ORIGINS: list[str] = [ # 末尾不带斜杠
|
|
'http://127.0.0.1',
|
|
'http://localhost:5173',
|
|
]
|
|
CORS_EXPOSE_HEADERS: list[str] = [
|
|
'X-Request-ID',
|
|
]
|
|
|
|
# 中间件配置
|
|
MIDDLEWARE_CORS: bool = True
|
|
|
|
# 请求限制配置
|
|
REQUEST_LIMITER_REDIS_PREFIX: str = 'fba:limiter'
|
|
|
|
# 时间配置
|
|
DATETIME_TIMEZONE: str = 'Asia/Shanghai'
|
|
DATETIME_FORMAT: str = '%Y-%m-%d %H:%M:%S'
|
|
|
|
# 文件上传
|
|
UPLOAD_READ_SIZE: int = 1024
|
|
UPLOAD_IMAGE_EXT_INCLUDE: list[str] = ['jpg', 'jpeg', 'png', 'gif', 'webp']
|
|
UPLOAD_IMAGE_SIZE_MAX: int = 5 * 1024 * 1024 # 5 MB
|
|
UPLOAD_VIDEO_EXT_INCLUDE: list[str] = ['mp4', 'mov', 'avi', 'flv']
|
|
UPLOAD_VIDEO_SIZE_MAX: int = 20 * 1024 * 1024 # 20 MB
|
|
|
|
# 演示模式配置
|
|
DEMO_MODE: bool = False
|
|
DEMO_MODE_EXCLUDE: set[tuple[str, str]] = {
|
|
('POST', f'{FASTAPI_API_V1_PATH}/auth/login'),
|
|
('POST', f'{FASTAPI_API_V1_PATH}/auth/logout'),
|
|
('GET', f'{FASTAPI_API_V1_PATH}/auth/captcha'),
|
|
('POST', f'{FASTAPI_API_V1_PATH}/auth/refresh'),
|
|
}
|
|
|
|
# IP 定位配置
|
|
IP_LOCATION_PARSE: Literal['online', 'offline', 'false'] = 'offline'
|
|
IP_LOCATION_REDIS_PREFIX: str = 'fba:ip:location'
|
|
IP_LOCATION_EXPIRE_SECONDS: int = 60 * 60 * 24 # 1 天
|
|
|
|
# Trace ID
|
|
TRACE_ID_REQUEST_HEADER_KEY: str = 'X-Request-ID'
|
|
TRACE_ID_LOG_LENGTH: int = 32 # UUID 长度,必须小于等于 32
|
|
TRACE_ID_LOG_DEFAULT_VALUE: str = '-'
|
|
|
|
# 日志
|
|
LOG_FORMAT: str = (
|
|
'<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</> | <lvl>{level: <8}</> | <cyan>{request_id}</> | <lvl>{message}</>'
|
|
)
|
|
|
|
# 日志(控制台)
|
|
LOG_STD_LEVEL: str = 'INFO'
|
|
|
|
# 日志(文件)
|
|
LOG_FILE_ACCESS_LEVEL: str = 'INFO'
|
|
LOG_FILE_ERROR_LEVEL: str = 'ERROR'
|
|
LOG_ACCESS_FILENAME: str = 'fba_access.log'
|
|
LOG_ERROR_FILENAME: str = 'fba_error.log'
|
|
|
|
# Plugin 配置
|
|
PLUGIN_REQUIRED: list[str] = ['config']
|
|
PLUGIN_PIP_CHINA: bool = True
|
|
PLUGIN_PIP_INDEX_URL: str = 'https://mirrors.aliyun.com/pypi/simple/'
|
|
PLUGIN_PIP_MAX_RETRY: int = 3
|
|
PLUGIN_REDIS_PREFIX: str = 'fba:plugin'
|
|
|
|
# I18n 配置
|
|
I18N_DEFAULT_LANGUAGE: str = 'zh-CN'
|
|
|
|
@model_validator(mode='before')
|
|
@classmethod
|
|
def check_env(cls, values: Any) -> Any:
|
|
"""检查环境变量"""
|
|
if values.get('ENVIRONMENT') == 'prod':
|
|
# FastAPI
|
|
values['FASTAPI_OPENAPI_URL'] = None
|
|
values['FASTAPI_STATIC_FILES'] = False
|
|
|
|
return values
|
|
|
|
|
|
@cache
|
|
def get_settings() -> Settings:
|
|
"""获取全局配置单例"""
|
|
if not ENV_FILE_PATH.exists():
|
|
shutil.copy(ENV_EXAMPLE_FILE_PATH, ENV_FILE_PATH)
|
|
return Settings()
|
|
|
|
|
|
# 创建全局配置实例
|
|
settings = get_settings()
|