setting.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. import os
  2. from functools import lru_cache
  3. from pathlib import Path
  4. from typing import Any, Literal
  5. from urllib.parse import quote_plus
  6. from pydantic_settings import BaseSettings, SettingsConfigDict
  7. from app.common.enums import EnvironmentEnum
  8. from app.config.path_conf import BASE_DIR, ENV_DIR
  9. class Settings(BaseSettings):
  10. """系统配置类"""
  11. model_config = SettingsConfigDict(
  12. env_file=ENV_DIR / f".env.{os.getenv('ENVIRONMENT')}",
  13. env_file_encoding="utf-8",
  14. extra="ignore",
  15. case_sensitive=True, # 区分大小写
  16. )
  17. # ================================================= #
  18. # ******************* 项目环境 ****************** #
  19. # ================================================= #
  20. ENVIRONMENT: EnvironmentEnum = EnvironmentEnum.DEV
  21. # ================================================= #
  22. # ******************* 服务器配置 ****************** #
  23. # ================================================= #
  24. SERVER_HOST: str = "0.0.0.0" # 允许访问的IP地址
  25. SERVER_PORT: int = 8001 # 服务端口
  26. # ================================================= #
  27. # ******************* API文档配置 ****************** #
  28. # ================================================= #
  29. DEBUG: bool = True # 调试模式
  30. TITLE: str = "🎉 Payment Platform 🎉 " # 文档标题
  31. VERSION: str = "1.0.0" # 版本号
  32. DESCRIPTION: str = (
  33. "Payment Platform API文档" # 文档描述
  34. )
  35. SUMMARY: str = "接口汇总" # 文档概述
  36. DOCS_URL: str = "/docs" # Swagger UI路径
  37. REDOC_URL: str = "/redoc" # ReDoc路径
  38. LJDOC_URL: str = "/ljdoc" # LangJin UI路径
  39. ROOT_PATH: str = "/api/v1" # API路由前缀
  40. # ================================================= #
  41. # ******************** 日志配置 ******************** #
  42. # ================================================= #
  43. LOGGER_LEVEL: str = "DEBUG" # 日志级别
  44. # ================================================= #
  45. # ******************** 跨域配置 ******************** #
  46. # ================================================= #
  47. CORS_ORIGIN_ENABLE: bool = True # 是否启用跨域
  48. ALLOW_ORIGINS: list[str] = ["*"] # 允许的域名列表
  49. ALLOW_METHODS: list[str] = ["*"] # 允许的HTTP方法
  50. ALLOW_HEADERS: list[str] = ["*"] # 允许的请求头
  51. ALLOW_CREDENTIALS: bool = True # 是否允许携带cookie
  52. CORS_EXPOSE_HEADERS: list[str] = ["X-Request-ID"]
  53. # ================================================= #
  54. # ******************* 登录认证配置 ****************** #
  55. # ================================================= #
  56. SECRET_KEY: str = "vgb0tnl9d58+6n-6h-ea&u^1#s0ccp!794=krylxcjq75vzps$" # JWT密钥
  57. ALGORITHM: str = "HS256" # JWT算法
  58. ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 60 # access_token过期时间(秒)30 分钟
  59. REFRESH_TOKEN_EXPIRE_MINUTES: int = 60 * 120 # refresh_token过期时间(秒)30 分钟
  60. TOKEN_TYPE: str = "bearer" # token类型
  61. TOKEN_REQUEST_PATH_EXCLUDE: list[str] = ["api/v1/auth/login"] # JWT / RBAC 路由白名单
  62. TOKEN_SLIDING_EXPIRE: bool = True # 是否启用滑动过期(用户操作时自动续期)
  63. # 多租户:通配子域与登录租户一致(默认关闭;生产按 base 解析 {code}.base)
  64. TENANT_HOST_ENFORCE: bool = False
  65. TENANT_HOST_BASE_DOMAIN: str = ""
  66. TENANT_HOST_IGNORE_PREFIXES: list[str] = ["www", "api", "admin"]
  67. # ================================================= #
  68. # ******************** 数据库配置 ******************* #
  69. # ================================================= #
  70. SQL_DB_ENABLE: bool = True # 是否启用数据库
  71. DATABASE_ECHO: bool | Literal["debug"] = False # 是否显示SQL日志
  72. ECHO_POOL: bool | Literal["debug"] = False # 是否显示连接池日志
  73. POOL_SIZE: int = 10 # 连接池大小
  74. MAX_OVERFLOW: int = 20 # 最大溢出连接数
  75. POOL_TIMEOUT: int = 30 # 连接超时时间(秒)
  76. POOL_RECYCLE: int = 1800 # 连接回收时间(秒)
  77. POOL_USE_LIFO: bool = True # 是否使用LIFO连接池
  78. POOL_PRE_PING: bool = True # 是否开启连接预检
  79. FUTURE: bool = True # 是否使用SQLAlchemy 2.0特性
  80. AUTOCOMMIT: bool = False # 是否自动提交
  81. AUTOFETCH: bool = False # 是否自动刷新
  82. EXPIRE_ON_COMMIT: bool = False # 是否在提交时过期
  83. # MySQL/PostgreSQL数据库连接
  84. DATABASE_TYPE: Literal["mysql", "postgres", "sqlite"] = "mysql"
  85. DATABASE_HOST: str = "localhost"
  86. DATABASE_PORT: int = 3306
  87. DATABASE_USER: str = "root"
  88. DATABASE_PASSWORD: str = "ServBay.dev"
  89. DATABASE_NAME: str = "fastapiadmin"
  90. # ================================================= #
  91. # ******************** Redis配置 ******************* #
  92. # ================================================= #
  93. REDIS_ENABLE: bool = True # 是否启用Redis
  94. REDIS_HOST: str = "localhost"
  95. REDIS_PORT: int = 6379
  96. REDIS_DB_NAME: int = 1
  97. REDIS_USER: str = ""
  98. REDIS_PASSWORD: str = ""
  99. # ================================================= #
  100. # ******************** 验证码配置 ******************* #
  101. # ================================================= #
  102. CAPTCHA_ENABLE: bool = True # 是否启用验证码
  103. CAPTCHA_EXPIRE_SECONDS: int = 60 * 1 # 验证码过期时间(秒) 1分钟
  104. CAPTCHA_FONT_SIZE: int = 32 # 字体大小
  105. CAPTCHA_FONT_PATH: str = "static/assets/font/Arial.ttf" # 字体路径
  106. # 是否请求外网解析 IP 归属地(登录发 token、操作日志写 login_location 共用;关闭可明显加快登录)
  107. LOGIN_RESOLVE_IP_LOCATION: bool = False
  108. # ================================================= #
  109. # ******************* 外部 HTTP(httpx)******************* #
  110. # ================================================= #
  111. HTTPX_DEFAULT_TIMEOUT: float = 10.0 # 对外请求默认超时(秒),见 app/common/httpx_defaults.py
  112. # ================================================= #
  113. # ********************* 日志配置 ******************* #
  114. # ================================================= #
  115. # 是否额外写入 JSON Lines(loguru ``serialize=True``,与控制台/info.log 同源,便于日志平台采集)
  116. LOG_JSON_FILE_ENABLE: bool = False
  117. LOG_JSON_FILE_NAME: str = "app.jsonl" # 相对 LOG_DIR
  118. LOG_JSON_RETENTION_DAYS: int = 7 # JSON 文件保留天数(通常比文本日志短)
  119. OPERATION_LOG_RECORD: bool = True # 是否记录操作日志
  120. IGNORE_OPERATION_FUNCTION: list[str] = ["get_captcha_for_login"] # 忽略记录的函数
  121. OPERATION_RECORD_METHOD: list[str] = [
  122. "POST",
  123. "PUT",
  124. "PATCH",
  125. "DELETE",
  126. "HEAD",
  127. "OPTIONS",
  128. ] # 需要记录的请求方法
  129. # ================================================= #
  130. # ******************* Gzip压缩配置 ******************* #
  131. # ================================================= #
  132. GZIP_ENABLE: bool = True # 是否启用Gzip
  133. GZIP_MIN_SIZE: int = 1000 # 最小压缩大小(字节)
  134. GZIP_COMPRESS_LEVEL: int = 9 # 压缩级别(1-9)
  135. # ================================================= #
  136. # ***************** 静态文件配置 ***************** #
  137. # ================================================= #
  138. STATIC_ENABLE: bool = True # 是否启用静态文件
  139. STATIC_URL: str = "/static" # 访问路由
  140. STATIC_DIR: str = "static" # 目录名
  141. STATIC_ROOT: Path = BASE_DIR.joinpath(STATIC_DIR) # 绝对路径
  142. # ================================================= #
  143. # ***************** 动态文件配置 ***************** #
  144. # ================================================= #
  145. UPLOAD_FILE_PATH: Path = Path("static/upload") # 上传目录
  146. UPLOAD_MACHINE: str = "A" # 上传机器标识
  147. ALLOWED_EXTENSIONS: list[str] = [ # 允许的文件类型
  148. ".gif",
  149. ".jpg",
  150. ".jpeg",
  151. ".png",
  152. ".ico",
  153. ".svg",
  154. ".xls",
  155. ".xlsx",
  156. ]
  157. MAX_FILE_SIZE: int = 10 * 1024 * 1024 # 最大文件大小(10MB)
  158. # ================================================= #
  159. # ***************** Swagger配置 ***************** #
  160. # ================================================= #
  161. SWAGGER_CSS_URL: str = "static/swagger/swagger-ui/swagger-ui.css"
  162. SWAGGER_JS_URL: str = "static/swagger/swagger-ui/swagger-ui-bundle.js"
  163. REDOC_JS_URL: str = "static/swagger/redoc/bundles/redoc.standalone.js"
  164. CUSTOM_CSS_URL: str = "static/swagger/custom-ui/styles.css"
  165. CUSTOM_JS_URL: str = "static/swagger/custom-ui/scripts.js"
  166. FAVICON_URL: str = "static/swagger/favicon.png"
  167. # ================================================= #
  168. # ******************* AI大模型配置 ****************** #
  169. # ================================================= #
  170. OPENAI_BASE_URL: str = ""
  171. OPENAI_API_KEY: str = ""
  172. OPENAI_MODEL: str = ""
  173. # ================================================= #
  174. # ******************* ChromaDB配置 ****************** #
  175. # ================================================= #
  176. CHROMA_PERSIST_DIR: str = str(BASE_DIR / "data" / "chroma") # ChromaDB 持久化目录
  177. CHROMA_COLLECTION_NAME: str = "knowledge_base" # ChromaDB 集合名称
  178. # ================================================= #
  179. # ******************* 请求限制配置 ****************** #
  180. # ================================================= #
  181. REQUEST_LIMITER_REDIS_PREFIX: str = "fastapiadmin:request_limiter:"
  182. # ================================================= #
  183. # ******************* 重构配置 ******************* #
  184. # ================================================= #
  185. @property
  186. def MIDDLEWARE_LIST(self) -> list[str | None]:
  187. """
  188. 根据开关组装的中间件类路径列表(未启用的项为 None)。
  189. 返回:
  190. - list[str | None]: 中间件 import 路径或 None。
  191. """
  192. # 中间件列表(注册时逆序叠加:下列第一项在列表中最前,最终位于最外层,优先生效)
  193. MIDDLEWARES: list[str | None] = [
  194. "app.core.middlewares.CustomCORSMiddleware" if self.CORS_ORIGIN_ENABLE else None,
  195. "app.core.middlewares.RequestLogMiddleware" if self.OPERATION_LOG_RECORD else None,
  196. "app.core.middlewares.CustomGZipMiddleware" if self.GZIP_ENABLE else None,
  197. ]
  198. return MIDDLEWARES
  199. @property
  200. def EVENT_LIST(self) -> list[str | None]:
  201. """
  202. 应用启动时加载的全局异步事件模块路径列表。
  203. 返回:
  204. - list[str | None]: 事件模块路径或 None。
  205. """
  206. EVENTS: list[str | None] = [
  207. "app.core.database.redis_connect" if self.REDIS_ENABLE else None,
  208. ]
  209. return EVENTS
  210. @property
  211. def ASYNC_DB_URI(self) -> str:
  212. """
  213. 异步 SQLAlchemy 数据库 URL。
  214. 返回:
  215. - str: 异步驱动连接串。
  216. 异常:
  217. - ValueError: 数据库类型不支持时抛出。
  218. """
  219. if self.DATABASE_TYPE not in ("mysql", "postgres", "sqlite"):
  220. raise ValueError(
  221. f"数据库驱动不支持: {self.DATABASE_TYPE}, 异步数据库请选择 mysql、postgres、sqlite"
  222. )
  223. db_connect: str = ""
  224. if self.DATABASE_TYPE == "mysql":
  225. db_connect = f"mysql+asyncmy://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}?charset=utf8mb4"
  226. elif self.DATABASE_TYPE == "postgres":
  227. db_connect = f"postgresql+asyncpg://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}"
  228. else:
  229. db_connect = f"sqlite+aiosqlite:///{self.DATABASE_NAME}.db"
  230. return db_connect
  231. @property
  232. def DB_URI(self) -> str:
  233. """
  234. 同步 SQLAlchemy 数据库 URL。
  235. 返回:
  236. - str: 同步驱动连接串。
  237. 异常:
  238. - ValueError: 数据库类型不支持时抛出。
  239. """
  240. if self.DATABASE_TYPE not in ("mysql", "postgres", "sqlite"):
  241. raise ValueError(
  242. f"数据库驱动不支持: {self.DATABASE_TYPE}, 同步数据库请选择 mysql、postgres、sqlite"
  243. )
  244. db_connect: str = ""
  245. if self.DATABASE_TYPE == "mysql":
  246. db_connect = f"mysql+pymysql://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}?charset=utf8mb4"
  247. elif self.DATABASE_TYPE == "postgres":
  248. db_connect = f"postgresql+psycopg://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}"
  249. else:
  250. db_connect = f"sqlite:///{self.DATABASE_NAME}.db"
  251. return db_connect
  252. @property
  253. def REDIS_URI(self) -> str:
  254. """
  255. Redis 连接 URL。
  256. 返回:
  257. - str: redis:// 连接串。
  258. """
  259. return f"redis://{self.REDIS_USER}:{quote_plus(self.REDIS_PASSWORD)}@{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB_NAME}"
  260. @property
  261. def FASTAPI_CONFIG(self) -> dict[str, Any]:
  262. """
  263. 创建 FastAPI 应用实例时使用的关键字参数子集。
  264. 返回:
  265. - dict[str, Any]: debug、title、responses 等配置。
  266. """
  267. return {
  268. "debug": self.DEBUG,
  269. "title": self.TITLE,
  270. "version": self.VERSION,
  271. "description": self.DESCRIPTION,
  272. "summary": self.SUMMARY,
  273. "docs_url": None,
  274. "redoc_url": None,
  275. "root_path": self.ROOT_PATH,
  276. "responses": {
  277. 200: {"description": "成功"},
  278. 400: {"description": "请求参数错误"},
  279. 401: {"description": "未认证"},
  280. 403: {"description": "未授权"},
  281. 404: {"description": "资源不存在"},
  282. 422: {"description": "请求参数验证错误"},
  283. 500: {"description": "服务器内部错误"},
  284. },
  285. }
  286. @lru_cache(maxsize=1)
  287. def get_settings() -> Settings:
  288. """
  289. 获取全局 Settings 单例(lru_cache 缓存)。
  290. 返回:
  291. - Settings: 配置实例。
  292. """
  293. return Settings()
  294. settings = get_settings()