Files
FastapiAdmin/backend/app/config/setting.py
T
zhangtao 398f5cb8f8 refactor(页面): 移除多个页面中的导航栏和返回逻辑
feat(轮播图): 更新轮播图图片资源并简化点击事件
style(主题): 简化导航栏颜色设置逻辑
chore(配置): 更新开发环境API基础URL配置
chore(数据库): 删除mysql配置文件
docs(组件): 更新组件类型声明文件
2025-08-26 23:00:36 +08:00

439 lines
20 KiB
Python
Executable File

# -*- coding: utf-8 -*-
import os
from functools import lru_cache
from pathlib import Path
from typing import Any, ClassVar, Dict, List, Optional, Union, Literal
from pydantic import MongoDsn, PostgresDsn, RedisDsn, MySQLDsn
from pydantic_settings import BaseSettings, SettingsConfigDict
from uvicorn.config import LifespanType
from app.common.enums import EnvironmentEnum
class Settings(BaseSettings):
"""系统配置类"""
model_config = SettingsConfigDict(
env_file='.env',
env_file_encoding="utf-8",
case_sensitive=True,
)
ENVIRONMENT: EnvironmentEnum
BANNER: ClassVar[str] = f"""
______ _ _
| ____| | | /\ (_)
| |__ __ _ ___| |_ / \ _ __ _
| __/ _` / __| __| / /\ \ | '_ \| |
| | | (_| \__ \ |_ / ____ \| |_) | |
|_| \__,_|___/\__/_/ \_\ .__/|_|
| |
|_|
"""
# ================================================= #
# ******************* 项目配置 ****************** #
# ================================================= #
# 项目根目录
BASE_DIR: Path = Path(__file__).parent.parent.parent
# ================================================= #
# ******************* 服务器配置 ****************** #
# ================================================= #
SERVER_HOST: str # 允许访问的IP地址
SERVER_PORT: int # 服务端口
RELOAD: bool # 是否自动重启
FACTORY: bool # 是否使用异步模式
LIFESPAN: LifespanType # 生命周期模式
WORKERS: int # 启动进程数
LIMIT_CONCURRENCY: int # 最大并发连接数
BACKLOG: int # 等待队列最大连接数
LIMIT_MAX_REQUESTS: int # HTTP最大请求数
TIMEOUT_KEEP_ALIVE: int # 保持连接时间(秒)
# ================================================= #
# ******************* API文档配置 ****************** #
# ================================================= #
DEBUG: bool # 调试模式
TITLE: str # 文档标题
VERSION: str # 版本号
DESCRIPTION: str # 文档描述
SUMMARY: str | None # 文档概述
DOCS_URL: str | None # Swagger UI路径
REDOC_URL: str | None # ReDoc路径
ROOT_PATH: str # API路由前缀
# ================================================= #
# ******************** 跨域配置 ******************** #
# ================================================= #
CORS_ORIGIN_ENABLE: bool = True # 是否启用跨域
ALLOW_ORIGINS: List[str] = ["*"] # 允许的域名列表
# ALLOW_ORIGINS: List[str] = ["http://localhost:5180", "http://127.0.0.1:5180"] # 允许的域名列表
ALLOW_METHODS: List[str] = ["*"] # 允许的HTTP方法
ALLOW_HEADERS: List[str] = ["*"] # 允许的请求头
ALLOW_CREDENTIALS: bool = True # 是否允许携带cookie
# ================================================= #
# ******************* 登录认证配置 ****************** #
# ================================================= #
SECRET_KEY: str = "vgb0tnl9d58+6n-6h-ea&u^1#s0ccp!794=krylxcjq75vzps$" # JWT密钥
ALGORITHM: str = "HS256" # JWT算法
ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440 # access_token过期时间(分钟)
REFRESH_TOKEN_EXPIRE_MINUTES: int = 10080 # refresh_token过期时间(分钟)
TOKEN_TYPE: str = "bearer" # token类型
# ================================================= #
# ******************** 数据库配置 ******************* #
# ================================================= #
SQL_DB_ENABLE: bool = True # 是否启用数据库
DATABASE_ECHO: bool = False # 是否显示SQL日志
ECHO_POOL: bool = False # 是否显示连接池日志
POOL_SIZE: int = 20 # 连接池大小
MAX_OVERFLOW: int = 10 # 最大溢出连接数
POOL_TIMEOUT: int = 30 # 连接超时时间(秒)
POOL_RECYCLE: int = 1800 # 连接回收时间(秒)
POOL_PRE_PING: bool = True # 是否开启连接预检
FUTURE: bool = True # 是否使用SQLAlchemy 2.0特性
AUTOCOMMIT: bool = False # 是否自动提交
AUTOFETCH: bool = False # 是否自动获取
EXPIRE_ON_COMMIT: bool = False # 是否在提交时过期
# SQLite数据库连接
DB_DRIVER: Literal['sqlite','mysql', 'postgresql'] = 'sqlite'
SQLITE_DB_NAME: str
# MySQL数据库连接
MYSQL_USER: str
MYSQL_PASSWORD: str
MYSQL_HOST: str
MYSQL_PORT: int
MYSQL_DB_NAME: str
# PostgreSQL数据库连接
POSTGRESQL_USER: str
POSTGRESQL_PASSWORD: str
POSTGRESQL_HOST: str
POSTGRESQL_PORT: int
POSTGRESQL_DB_NAME: str
# ================================================= #
# ******************** MongoDB配置 ******************* #
# ================================================= #
MONGO_DB_ENABLE: bool # 是否启用MongoDB
MONGO_DB_USER: str
MONGO_DB_PASSWORD: str
MONGO_DB_HOST: str
MONGO_DB_PORT: int
MONGO_DB_NAME: str
# ================================================= #
# ******************** Redis配置 ******************* #
# ================================================= #
REDIS_ENABLE: bool # 是否启用Redis
REDIS_HOST: str
REDIS_PORT: int
REDIS_DB_NAME: int
REDIS_USER: str
REDIS_PASSWORD: str
# ================================================= #
# ******************** 验证码配置 ******************* #
# ================================================= #
CAPTCHA_ENABLE: bool = True # 是否启用验证码
CAPTCHA_EXPIRE_SECONDS: int = 60 # 验证码过期时间(秒)
CAPTCHA_FONT_SIZE: int = 40 # 字体大小
CAPTCHA_FONT_PATH: Path = 'static/assets/font/Arial.ttf' # 字体路径
# ================================================= #
# ********************* 日志配置 ******************* #
# ================================================= #
LOGGER_LEVEL: str # 日志级别 (DEBUG, INFO, WARNING, ERROR, CRITICAL)
LOGGER_DIR: Path = BASE_DIR.joinpath('logs')
if not LOGGER_DIR.exists():
LOGGER_DIR.mkdir(parents=True, exist_ok=True)
BACKUPCOUNT: int = 10 # 日志文件备份数
WHEN: str = 'MIDNIGHT' # 日志分割时间 (MIDNIGHT, H, D, W0-W6)
INTERVAL: int = 1 # 日志分割间隔
ENCODING: str = 'utf-8' # 日志编码
LOGGER_FORMAT: str = '%(asctime)s - %(levelname)8s - [%(name)s:%(filename)s:%(funcName)s:%(lineno)d] %(message)s' # 日志格式
OPERATION_LOG_RECORD: bool = True # 是否记录操作日志
OPERATION_RECORD_METHOD: List[str] = ["POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] # 需要记录的请求方法
IGNORE_OPERATION_FUNCTION: List[str] = ["get_captcha_for_login"] # 忽略记录的函数
LOG_RETENTION_DAYS: int = 30 # 日志保留天数,超过此天数的日志文件将被自动清理
# 日志文件说明:
# all.log - 包含所有级别日志(应用日志 + uvicorn日志 + uvicorn访问日志)
# error.log - 包含ERROR及以上级别日志(应用错误 + uvicorn错误)
# 轮转后文件名: info_日期.log, error_日期.log
# ================================================= #
# ******************* Gzip压缩配置 ******************* #
# ================================================= #
GZIP_ENABLE: bool = True # 是否启用Gzip
GZIP_MIN_SIZE: int = 1000 # 最小压缩大小(字节)
GZIP_COMPRESS_LEVEL: int = 9 # 压缩级别(1-9)
# ================================================= #
# ***************** 演示模型配置 ***************** #
# ================================================= #
DEMO_ENABLE: bool # 是否开启演示模式
DEMO_WHITE_LIST_PATH: List[str] = [ # 演示白名单
"/api/v1/system/auth/login",
"/api/v1/system/auth/token/refresh",
"/api/v1/system/auth/captcha/get",
"/api/v1/system/auth/logout",
"/api/v1/system/config/info",
"/api/v1/system/user/current/info",
"/api/v1/system/notice/available",
]
DEMO_BLACK_LIST_PATH: List[str] = [ # 演示黑名单
"/auth/login"
]
DEMO_IP_WHITE_LIST: List[str] = [ # 演示白名单IP
"127.0.0.1"
]
DEMO_USER_WHITE_LIST: List[str] = [ # 演示白名单用户
"superadmin",
]
# ================================================= #
# ***************** 静态文件配置 ***************** #
# ================================================= #
STATIC_ENABLE: bool = True # 是否启用静态文件
STATIC_URL: str = "/static" # 访问路由
STATIC_DIR: str = "static" # 目录名
STATIC_ROOT: Path = BASE_DIR.joinpath(STATIC_DIR) # 绝对路径
# ================================================= #
# ***************** 动态文件配置 ***************** #
# ================================================= #
UPLOAD_FILE_PATH: Path = 'static/upload' # 上传目录
UPLOAD_MACHINE: str = 'A' # 上传机器标识
ALLOWED_EXTENSIONS: list[str] = [ # 允许的文件类型
# 图片
'.bmp', '.gif', '.jpg', '.jpeg', '.png', '.ico', '.svg',
# 文档
'.csv', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.html', '.htm', '.txt', '.pdf',
# 压缩包
'.rar', '.zip', '.gz', '.bz2',
# 视频
'.mp4', '.avi', '.rmvb'
]
MAX_FILE_SIZE: int = 10 * 1024 * 1024 # 最大文件大小(10MB)
# ================================================= #
# ***************** Swagger配置 ***************** #
# ================================================= #
SWAGGER_CSS_URL: Path = "static/swagger/swagger-ui/swagger-ui.css"
SWAGGER_JS_URL: Path = "static/swagger/swagger-ui/swagger-ui-bundle.js"
REDOC_JS_URL: Path = "static/swagger/redoc/bundles/redoc.standalone.js"
FAVICON_URL: Path = "static/swagger/favicon.png"
# ================================================= #
# ******************* 初始化数据 ****************** #
# ================================================= #
SCRIPT_DIR: Path = BASE_DIR.joinpath('app/scripts/data') # 管理员路由目录
# ================================================= #
# ******************* 其他配置 ******************* #
# ================================================= #
@property
def MIDDLEWARE_LIST(self) -> List[Optional[str]]:
"""获取项目根目录"""
# 中间件列表
MIDDLEWARES: List[Optional[str]] = [
"app.core.middlewares.CustomCORSMiddleware" if self.CORS_ORIGIN_ENABLE else None,
"app.core.middlewares.RequestLogMiddleware" if self.OPERATION_LOG_RECORD else None,
"app.core.middlewares.CustomGZipMiddleware" if self.GZIP_ENABLE else None,
"app.core.middlewares.DemoEnvMiddleware" if self.DEMO_ENABLE else None,
]
return MIDDLEWARES
@property
def EVENT_LIST(self) -> List[Optional[str]]:
# 事件列表
EVENTS: List[Optional[str]] = [
"app.core.database.mongodb_connect" if self.MONGO_DB_ENABLE else None,
"app.core.database.redis_connect" if self.REDIS_ENABLE else None,
]
return EVENTS
@property
def DB_URI(self) -> str:
"""获取数据库连接"""
supported_db_drivers = ("sqlite", "mysql", "postgresql")
if settings.DB_DRIVER not in supported_db_drivers:
raise ValueError(f"数据库驱动不支持: {settings.DB_DRIVER}, 请选择 {supported_db_drivers}")
if settings.DB_DRIVER == "mysql":
MYSQL_URI: MySQLDsn = f"mysql+asyncmy://{settings.MYSQL_USER}:{settings.MYSQL_PASSWORD}@{settings.MYSQL_HOST}:{settings.MYSQL_PORT}/{settings.MYSQL_DB_NAME}?charset=utf8mb4"
return MYSQL_URI
elif settings.DB_DRIVER == "sqlite":
SQLITE_URI: str = f"sqlite+aiosqlite:///{settings.BASE_DIR.joinpath(settings.SQLITE_DB_NAME)}?characterEncoding=UTF-8"
return SQLITE_URI
elif settings.DB_DRIVER == "postgresql":
POSRGRES_URI: PostgresDsn = f"postgresql+asyncpg://{settings.POSTGRESQL_USER}:{settings.POSTGRESQL_PASSWORD}@{settings.POSTGRESQL_HOST}:{settings.POSTGRESQL_PORT}/{settings.POSTGRESQL_DB_NAME}"
return POSRGRES_URI
else:
raise ValueError(f"数据库驱动不支持: {settings.DB_DRIVER}, 请选择 {supported_db_drivers}")
@property
def DATABASES_URI(self) -> str:
"""获取数据库连接"""
supported_db_drivers = ("sqlite", "mysql", "postgresql")
if settings.DB_DRIVER not in supported_db_drivers:
raise ValueError(f"数据库驱动不支持: {settings.DB_DRIVER}, 请选择 {supported_db_drivers}")
if settings.DB_DRIVER == "mysql":
MYSQL_URI: MySQLDsn = f"mysql+pymysql://{settings.MYSQL_USER}:{settings.MYSQL_PASSWORD}@{settings.MYSQL_HOST}:{settings.MYSQL_PORT}/{settings.MYSQL_DB_NAME}?charset=utf8mb4"
return MYSQL_URI
elif settings.DB_DRIVER == "postgresql":
POSRGRES_URI: PostgresDsn = f"postgresql://{settings.POSTGRESQL_USER}:{settings.POSTGRESQL_PASSWORD}@{settings.POSTGRESQL_HOST}:{settings.POSTGRESQL_PORT}/{settings.POSTGRESQL_DB_NAME}"
return POSRGRES_URI
elif settings.DB_DRIVER == "sqlite":
SQLITE_URI: str = f"sqlite:///{settings.BASE_DIR.joinpath(settings.SQLITE_DB_NAME)}?characterEncoding=UTF-8"
return SQLITE_URI
else:
raise ValueError(f"数据库驱动不支持: {settings.DB_DRIVER}, 请选择 {supported_db_drivers}")
@property
def MONGO_DB_URI(self) -> MongoDsn:
"""获取MongoDB连接"""
if settings.MONGO_DB_USER and settings.MONGO_DB_PASSWORD:
return f"mongodb://{settings.MONGO_DB_USER}:{settings.MONGO_DB_PASSWORD}@{settings.MONGO_DB_HOST}:{settings.MONGO_DB_PORT}/{settings.MONGO_DB_NAME}"
else:
return f"mongodb://{settings.MONGO_DB_HOST}:{settings.MONGO_DB_PORT}/{settings.MONGO_DB_NAME}"
@property
def REDIS_URI(self) -> RedisDsn:
"""获取Redis连接"""
return f"redis://{settings.REDIS_USER}:{self.REDIS_PASSWORD}@{settings.REDIS_HOST}:{settings.REDIS_PORT}/{settings.REDIS_DB_NAME}"
@property
def FASTAPI_CONFIG(self) -> Dict[str, Union[str, bool, None]]:
"""获取FastAPI应用属性"""
return {
"debug": self.DEBUG,
"title": self.TITLE,
"version": self.VERSION,
"description": self.DESCRIPTION,
"summary": self.SUMMARY,
"docs_url": None,
"redoc_url": None,
"root_path": self.ROOT_PATH
}
@property
def UVICORN_CONFIG(self) -> Dict[str, Any]:
"""获取Uvicorn配置"""
return {
"host": self.SERVER_HOST,
"port": self.SERVER_PORT,
"reload": self.RELOAD,
"log_config": {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"()": "uvicorn.logging.DefaultFormatter",
"fmt": self.LOGGER_FORMAT,
"use_colors": None,
},
"access": {
"()": "uvicorn.logging.AccessFormatter",
"fmt": self.LOGGER_FORMAT,
},
},
"handlers": {
"console": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stderr",
},
"access_console": {
"formatter": "access",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
},
"file": {
"formatter": "default",
"class": "app.core.logger.CustomTimedRotatingFileHandler",
"filename": self.LOGGER_DIR.joinpath("all.log"),
"when": self.WHEN,
"backupCount": self.BACKUPCOUNT,
"encoding": self.ENCODING,
},
},
"loggers": {
"uvicorn": {"handlers": ["file", "console"], "level": self.LOGGER_LEVEL, "propagate": False},
"uvicorn.error": {"handlers": ["file", "console"], "level": self.LOGGER_LEVEL, "propagate": False},
"uvicorn.access": {"handlers": ["file", "access_console"], "level": self.LOGGER_LEVEL, "propagate": False},
},
},
"workers": self.WORKERS,
"limit_concurrency": self.LIMIT_CONCURRENCY,
"backlog": self.BACKLOG,
"limit_max_requests": self.LIMIT_MAX_REQUESTS,
"timeout_keep_alive": self.TIMEOUT_KEEP_ALIVE,
"lifespan": self.LIFESPAN,
"factory": self.FACTORY,
}
class WxMiniConfig(BaseSettings):
WX_MINI_APPID: str = "xxxx"
WX_MINI_APPID_SECRET: str = "xxxx"
WX_MINI_TOKEN_URL: str = ("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}"
.format(WX_MINI_APPID, WX_MINI_APPID_SECRET))
WX_MINI_GET_PHONE_URL: str = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={}"
WX_MINI_LOGIN: str = ("https://api.weixin.qq.com/sns/jscode2session?grant_type=authorization_code&appid={}"
"&secret={}&js_code={}")
# 获取无限小程序码
WX_MINI_GEN_PATH_CODE: str = 'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={}'
# 微信小程序发送订阅消息
WX_MINI_SEND_SUBSCRIBE_MSG: str = 'https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={}'
# 微信商户ID,后续支付使用
WXPAY_MCHID: str= "xxxx"
# 微信支付 API v3秘钥
WXPAY_APIV3_KEY: str = "xxxx"
# 微信支付结果回调接口
WXPAY_NOTIFYURL: str = "微信支付结果回调接口"
# 商户证书序列号
WXPAY_SERIALNO: str = "xxxx"
# 商户私钥
WXPAY_CLIENT_PRIKEY: str = "wxmini/wepay_cert/apiclient_key.pem"
# 商品描述(统一下单接口用到)
WXPAY_PAY_DESC: str = "商品描述(统一下单接口用到)"
# 回调地址,也可以在调用接口的时候覆盖
NOTIFY_URL: str = 'https://xxxx.xxxxx.cn/api/v1/wepay/notify'
# 微信支付平台证书缓存目录,减少证书下载调用次数,首次使用确保此目录为空目录.
# 初始调试时可不设置,调试通过后再设置,示例值:'./cert'
CERT_DIR: str = './cert'
SubscribeTemplate: Any = {
# 微信小程序通知模板ID
"rest_minute": "xxxxx"
}
pass
@lru_cache(maxsize=1)
def get_settings() -> Settings:
"""获取配置实例"""
env = os.getenv('ENVIRONMENT', EnvironmentEnum.DEV.value)
if env not in [e.value for e in EnvironmentEnum]:
raise ValueError(f"无效的环境配置: {env}")
env_file = Path(__file__).parent.parent.parent / "env" / f".env.{env}"
if not env_file.exists():
raise FileNotFoundError(f"环境配置文件不存在: {env_file}")
return Settings(_env_file=env_file)
settings = get_settings()