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 = ( '{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {request_id} | {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()