Files
dpb/backend/app/config/setting.py
T
34047007@qq.com 86126ea16f checkpoint: 架构治理四件套——统一 async / 测试真 PG / Redis 降级可选 / DPB 扶正
- A1 业务层统一 async:gencode 表内省改 run_sync 异步(不再阻塞事件循环)、
  check_db 改 async_engine、dict_util 启动预热异步载入;psycopg 降为
  APScheduler SQLAlchemyJobStore 专用同步孤岛(注释标注)
- A2 测试真 PG 化:conftest 弃 SQLite + mock Redis,改连真实 PG16(dpb_test)+
  Redis(number_gen advisory lock 路径真被测);新增 docker/test-compose.yaml
  测试依赖栈(postgres:16:5433 + redis:7:6380);run_ci 前置探测 + 容器兜底,
  pytest -m pg 10 套正式 tc 全过(真实 PG16 + Redis)
- B1 Redis 启动不强依赖:redis_connect 失败降级启动,缓存类回源 DB、存储类
  (会话/AI 配置/调度)经 require_redis 守卫返回 503;真机 3 阶段降级测试
  PASS=11 FAIL=0(独立 Redis 6390 + 后端 8091,不动共享 dev 服务)
- C 扶正 DPB:后端横幅/日志(dpb.log)/README/pyproject/ai_factory agent 名、
  前端 package.json/署名注释/链接文案/deploy.sh/docker 注释全部去 fastapiadmin;
  grep backend/app + frontend/web/src 零残留
- 验证:全量回归 pass=72 fail=0
2026-08-07 09:06:19 +08:00

374 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
from functools import lru_cache
from pathlib import Path
from typing import Any, Literal
from urllib.parse import quote_plus
from pydantic import model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from app.common.enums import EnvironmentEnum
from app.config.path_conf import ENV_DIR, STATIC_DIR
# JWT 开发默认密钥:仅允许 DEV 环境使用,生产命中即拒绝启动
_DEV_DEFAULT_SECRET_KEY = "dpb-dev-secret-key-do-not-use-in-production"
class Settings(BaseSettings):
"""系统配置类"""
model_config = SettingsConfigDict(
env_file=ENV_DIR / f".env.{os.getenv('ENVIRONMENT')}",
env_file_encoding="utf-8",
extra="ignore",
case_sensitive=True, # 区分大小写
)
# ================================================= #
# ******************* 项目环境 ****************** #
# ================================================= #
ENVIRONMENT: EnvironmentEnum = EnvironmentEnum.DEV
# ================================================= #
# ******************* 服务器配置 ****************** #
# ================================================= #
SERVER_HOST: str = "0.0.0.0" # 允许访问的IP地址
SERVER_PORT: int = 8001 # 服务端口
# 是否信任反向代理透传的客户端 IP 头(X-Forwarded-For / X-Real-IP)。
# 仅当后端只被可信代理(如 nginx)访问时设置为 true,否则攻击者可伪造该头绕过 IP 限流。
# DEV 环境默认 false(直连取 socket IP);生产经 nginx 代理时在 .env.prod 中显式开启。
IP_TRUST_PROXY_HEADERS: bool = False
# 育种审计切面总开关:module_bre 业务表(bre_*)写操作是否落 bre_audit_log。
# 置 False 可整域关闭审计(不含恢复选项,建议保持 True)。
AUDIT_BRE_ENABLED: bool = True
# ================================================= #
# ******************* API文档配置 ****************** #
# ================================================= #
DEBUG: bool = True # 调试模式
TITLE: str = "DPB 桃育种系统" # 文档标题
VERSION: str = "3.0.0" # 版本号
DESCRIPTION: str = "DPB 桃树育种管理系统:基于 FastAPI、SQLAlchemy、PostgreSQL、Redis 与 numpy 统计引擎的育种数据管理与遗传评估平台" # 文档描述
SUMMARY: str = "接口汇总" # 文档概述
DOCS_URL: str = "/docs" # Swagger UI路径
REDOC_URL: str = "/redoc" # ReDoc路径
WEB_URL: str = "/web" # 前端路径
ROOT_PATH: str = "/api/v1" # API路由前缀
# ================================================= #
# ******************** 日志配置 ******************** #
# ================================================= #
LOGGER_LEVEL: str = "DEBUG" # 日志级别
# ================================================= #
# ******************** 跨域配置 ******************** #
# ================================================= #
# DEV 环境: ALLOW_ORIGINS=["*"] + ALLOW_CREDENTIALS=True
# 注意: 根据 W3C 规范,allow_origins=["*"] 时浏览器会忽略 allow_credentials
# 实际表现为 credentials 不生效。但在开发场景下不影响使用。
# PROD 环境: 通过 PROD_CORS_ORIGINS 环境变量配置具体域名列表(逗号分隔),allow_credentials=True 正常生效。
PROD_CORS_ORIGINS: str = "" # 生产环境允许的域名列表,逗号分隔,如 "https://admin.example.com,https://www.example.com"
ALLOW_METHODS: list[str] = ["*"] # 允许的HTTP方法
ALLOW_HEADERS: list[str] = ["*"] # 允许的请求头
ALLOW_CREDENTIALS: bool = True # 是否允许携带cookie
CORS_EXPOSE_HEADERS: list[str] = ["X-Request-ID"]
# ================================================= #
# ******************* 登录认证配置 ****************** #
# ================================================= #
# JWT 签名密钥。默认值仅供 DEV 环境使用;生产环境必须通过环境变量 SECRET_KEY 注入,
# 见下方 _validate_production_secretprod 下命中默认值会直接拒绝启动。
SECRET_KEY: str = _DEV_DEFAULT_SECRET_KEY # JWT密钥(生产必须通过环境变量 SECRET_KEY 设置)
ALGORITHM: str = "HS256" # JWT算法
# 登录暴力破解防护:连续失败达到阈值后锁定该用户名+IP 一段时间(Redis 计数)
LOGIN_MAX_FAILURES: int = 5 # 连续失败次数阈值
LOGIN_FAILURE_WINDOW_SECONDS: int = 900 # 失败计数窗口(秒)
LOGIN_LOCKOUT_SECONDS: int = 900 # 锁定时长(秒)
ACCESS_TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 12 # access_token过期时间(秒)12 小时
REFRESH_TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 12 # refresh_token过期时间(秒)12 小时
TOKEN_TYPE: str = "Bearer" # token类型(RFC 6750 标准大小写)
TOKEN_SLIDING_EXPIRE: bool = True # 是否启用滑动过期(用户操作时自动续期)
# ================================================= #
# ******************** 数据库配置 ******************* #
# ================================================= #
DATABASE_ECHO: bool | Literal["debug"] = False # 是否显示SQL日志
ECHO_POOL: bool | Literal["debug"] = False # 是否显示连接池日志
POOL_SIZE: int = 10 # 连接池大小
MAX_OVERFLOW: int = 20 # 最大溢出连接数
POOL_TIMEOUT: int = 30 # 连接超时时间(秒)
POOL_RECYCLE: int = 1800 # 连接回收时间(秒)
POOL_USE_LIFO: bool = True # 是否使用LIFO连接池
POOL_PRE_PING: bool = True # 是否开启连接预检
FUTURE: bool = True # 是否使用SQLAlchemy 2.0特性
AUTOCOMMIT: bool = False # 是否自动提交(映射 SQLAlchemy sessionmaker(autocommit=...)
AUTOFLUSH: bool = False # 是否自动刷新(映射 SQLAlchemy sessionmaker(autoflush=...)
AUTOFETCH: bool | None = None # AUTOFLUSH 别名(优先级高于 AUTOFLUSH,兼容旧环境变量名)
EXPIRE_ON_COMMIT: bool = False # 是否在提交时过期
# 数据库连接(生产 PostgreSQL;测试套件用 SQLite
DATABASE_TYPE: Literal["postgres", "sqlite"] = "postgres"
DATABASE_HOST: str = "localhost"
DATABASE_PORT: int = 5432
DATABASE_USER: str = "dpb"
DATABASE_PASSWORD: str = ""
DATABASE_NAME: str = "dpb"
# ================================================= #
# ******************** Redis配置 ******************* #
# ================================================= #
REDIS_HOST: str = "localhost"
REDIS_PORT: int = 6379
REDIS_DB_NAME: int = 1
REDIS_USER: str = ""
REDIS_PASSWORD: str = ""
REDIS_HEALTH_CHECK_INTERVAL: int = 20 # Redis 健康检查间隔(秒,对应 async_pool 的 health_check_interval
REDIS_DEFAULT_CACHE_TTL: int = 86400 # RedisCURD.set() 默认 TTL(秒,24 小时)
# ================================================= #
# ******************** 验证码配置 ******************* #
# ================================================= #
CAPTCHA_ENABLE: bool = True # 是否启用验证码
CAPTCHA_EXPIRE_SECONDS: int = 60 * 1 # 验证码过期时间(秒) 1分钟
# ================================================= #
# ******************* 邮件(SMTP)配置 ****************** #
# ================================================= #
# 腾讯云 SES 邮件推送,465 端口为 SSL
SMTP_HOST: str = ""
SMTP_PORT: int = 465 # 465=SSL, 587=STARTTLS
SMTP_USER: str = "" # 发件账号(邮箱地址)
SMTP_PASSWORD: str = "" # 授权码
SMTP_FROM: str = "" # 发件人邮箱地址(默认同 SMTP_USER)
SMTP_FROM_NAME: str = "" # 发件人显示名(如 "客户服务")
SMTP_TLS: bool = True # 是否启用 SSL/TLS(465 端口必须为 True)
# 找回密码邮箱验证码有效期(秒)
EMAIL_CODE_EXPIRE_SECONDS: int = 60 * 5 # 5 分钟
# ================================================= #
# ************* 开发环境登录默认值(仅DEV) ********** #
# ================================================= #
# 仅用于开发阶段方便登录:填入真实密码后,前端登录页自动预填(用户名固定 super)。
# 留空(默认)则前端不预填,需手动输入。
# ⚠ 生产环境(PROD)必须保持为空,否则等同于把密码明文下发到浏览器,存在泄露风险。
DEV_DEFAULT_PASSWORD: str = ""
# ================================================= #
# ***************** 第三方 OAuth 登录(可选)********* #
# ================================================= #
# 自动注册用户的默认角色 ID 列表(须与库中角色主键一致)
OAUTH_DEFAULT_ROLE_IDS: list[int] = [2]
OAUTH_FRONTEND_FALLBACK: str = "http://127.0.0.1:5173/login"
OAUTH_GITHUB_CLIENT_ID: str = ""
OAUTH_GITHUB_CLIENT_SECRET: str = ""
OAUTH_GITEE_CLIENT_ID: str = ""
OAUTH_GITEE_CLIENT_SECRET: str = ""
OAUTH_WECHAT_OPEN_APP_ID: str = ""
OAUTH_WECHAT_OPEN_APP_SECRET: str = ""
OAUTH_QQ_APP_ID: str = ""
OAUTH_QQ_APP_SECRET: str = ""
OAUTH_STATE_TTL: int = 600 # OAuth state 参数过期时间(秒)
# OAuth 回调域名白名单(["*"] 表示不限制,生产环境请设置为具体域名列表,如 ["example.com"]
OAUTH_ALLOWED_HOSTS: list[str] = ["*"]
# ================================================= #
# ******************* 外部 HTTPhttpx******************* #
# ================================================= #
HTTPX_DEFAULT_TIMEOUT: float = 10.0 # 对外 HTTP 请求默认超时(秒)
IP_LOCATION_ENABLE: bool = True # 是否启用 IP 归属地查询(登录时对外发起 HTTP 请求)
IP_LOCATION_CACHE_TTL: int = 604800 # IP 归属地缓存时间(秒,默认 7 天)
IP_LOCATION_QUERY_TIMEOUT: float = 3.0 # IP 归属地查询单次 HTTP 超时(秒)
# ================================================= #
# ********************* 日志配置 ******************* #
# ================================================= #
OPERATION_RECORD_METHOD: list[str] = [
"POST",
"PUT",
"PATCH",
"DELETE",
"HEAD",
"OPTIONS",
] # 需要记录的请求方法
# ================================================= #
# ******************* Gzip压缩配置 ******************* #
# ================================================= #
GZIP_MIN_SIZE: int = 1000 # 最小压缩大小(字节)
GZIP_COMPRESS_LEVEL: int = 9 # 压缩级别(1-9)
# ================================================= #
# ******************* 安全中间件配置 ****************** #
# ================================================= #
# 允许访问的主机名列表(仅 PROD 经 TrustedHostMiddleware 生效;内网部署请按实际访问域名补充)
ALLOWED_HOSTS: list[str] = ["localhost", "127.0.0.1", "0.0.0.0"]
# 操作日志保留天数(调度器按此天数定期清理过期日志)
OPERATION_LOG_RETENTION_DAYS: int = 90
# 接口白名单(无需认证即可访问的接口路径,支持 * 开头表示前缀匹配)
WHITE_API_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/captcha/slider/complete",
"/api/v1/system/auth/logout",
"/api/v1/system/param/info",
"/api/v1/system/dict/info",
"/api/v1/system/user/current/info",
"/api/v1/system/notice/available",
"/api/v1/common/health",
"/api/v1/common/health/ready",
"/api/v1/common/health/live",
"/metrics",
]
# ================================================= #
# ***************** 静态文件配置 ***************** #
# ================================================= #
STATIC_URL: str = "/static" # 访问路由
# ================================================= #
# ***************** 动态文件配置 ***************** #
# ================================================= #
UPLOAD_FILE_PATH: Path = STATIC_DIR / "upload" # 上传目录(绑定 STATIC_DIR,避免受启动 cwd 影响导致与 /static 挂载目录不一致)
UPLOAD_MACHINE: str = "A" # 上传机器标识
ALLOWED_EXTENSIONS: list[str] = [ # 允许的文件类型
".gif",
".jpg",
".jpeg",
".png",
".ico",
".svg",
".xls",
".xlsx",
]
MAX_FILE_SIZE: int = 10 * 1024 * 1024 # 最大文件大小(10MB)
# ================================================= #
# ***************** Swagger配置 ***************** #
# ================================================= #
SWAGGER_CSS_URL: str = "static/swagger/swagger-ui/swagger-ui.css"
SWAGGER_JS_URL: str = "static/swagger/swagger-ui/swagger-ui-bundle.js"
REDOC_JS_URL: str = "static/swagger/redoc/bundles/redoc.standalone.js"
FAVICON_URL: str = "static/image/favicon.ico"
# ================================================= #
# ******************* AI大模型配置 ****************** #
# ================================================= #
OPENAI_API_KEY: str = ""
OPENAI_MODEL: str = ""
OPENAI_BASE_URL: str = "" # API Base URL,如 https://api.minimax.chat/v1
# ================================================= #
# ******************* 动态配置 ******************* #
# ================================================= #
@property
def ALLOW_ORIGINS(self) -> list[str]:
"""根据环境动态返回 CORS 允许的域名列表。"""
if self.ENVIRONMENT == EnvironmentEnum.PROD and self.PROD_CORS_ORIGINS:
return [origin.strip() for origin in self.PROD_CORS_ORIGINS.split(",") if origin.strip()]
return ["*"]
# ================================================= #
@property
def REDIS_URI(self) -> str:
"""构建 Redis 连接 URI(供 slowapi / 其他模块复用)。"""
auth_part = ""
if self.REDIS_USER and self.REDIS_PASSWORD:
auth_part = f"{self.REDIS_USER}:{self.REDIS_PASSWORD}@"
elif self.REDIS_PASSWORD:
auth_part = f":{self.REDIS_PASSWORD}@"
return f"redis://{auth_part}{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB_NAME}"
@property
def MIDDLEWARE_LIST(self) -> list[str | None]:
# 中间件列表(注册时逆序叠加:下列第一项在列表中最前,最终位于最外层,优先生效)
# 中间件执行顺序(从外到内):
# HTTPSRedirect → TrustedHost → CORS → RequestLog → GZip → CorrelationId → 业务路由
# 安全响应头(X-Content-Type-Options / Referrer-Policy / Permissions-Policy / HSTS
# 由前置 Nginx / 反向代理通过 add_header 设置,避免应用层 BaseHTTPMiddleware 开销。
MIDDLEWARES: list[str | None] = [
"app.core.middlewares.CustomHTTPSRedirectMiddleware" if self.ENVIRONMENT == EnvironmentEnum.PROD else None,
"app.core.middlewares.CustomTrustedHostMiddleware" if self.ENVIRONMENT == EnvironmentEnum.PROD else None,
"app.core.middlewares.CustomCORSMiddleware",
"app.core.middlewares.RequestLogMiddleware",
"app.core.middlewares.CustomGZipMiddleware",
"app.core.middlewares.CorrelationIdMiddleware", # 请求上下文
]
return MIDDLEWARES
@property
def ASYNC_DB_URI(self) -> str:
if self.DATABASE_TYPE == "postgres":
db_connect = f"postgresql+asyncpg://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}"
else:
db_connect = f"sqlite+aiosqlite:///{self.DATABASE_NAME}.db"
return db_connect
@property
def DB_URI(self) -> str:
if self.DATABASE_TYPE == "postgres":
db_connect = f"postgresql+psycopg://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}"
else:
db_connect = f"sqlite:///{self.DATABASE_NAME}.db"
return db_connect
@property
def FASTAPI_CONFIG(self) -> dict[str, Any]:
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,
"responses": {
200: {"description": "成功"},
400: {"description": "请求参数错误"},
401: {"description": "未认证"},
403: {"description": "未授权"},
404: {"description": "资源不存在"},
422: {"description": "请求参数验证错误"},
500: {"description": "服务器内部错误"},
}
}
@model_validator(mode="after")
def _validate_production_security(self) -> "Settings":
"""生产环境安全强校验:关键安全项缺失即拒绝启动,避免"看似生产、实为开发默认值"。
- SECRET_KEY:禁止使用公开的开发默认值(可伪造任意用户 JWT,完全接管系统)。
- DEBUG:生产必须关闭(开启会暴露堆栈/调试信息)。
- PROD_CORS_ORIGINS:必须配置具体域名白名单(否则 CORS 回退为 "*" + credentials 全放行)。
"""
if self.ENVIRONMENT != EnvironmentEnum.PROD:
return self
problems: list[str] = []
if not self.SECRET_KEY or self.SECRET_KEY == _DEV_DEFAULT_SECRET_KEY:
problems.append("SECRET_KEY 必须通过环境变量配置独立随机密钥,禁止使用开发默认值")
elif len(self.SECRET_KEY) < 32:
problems.append("SECRET_KEY 长度不足 32 位(如 openssl rand -hex 32 生成)")
if self.DEBUG:
problems.append("DEBUG 必须为 false")
if not (self.PROD_CORS_ORIGINS or "").strip():
problems.append("PROD_CORS_ORIGINS 必须配置具体域名白名单(逗号分隔),不能为空")
if problems:
raise ValueError("生产环境配置不安全:" + "".join(problems))
return self
@lru_cache(maxsize=1)
def get_settings() -> Settings:
return Settings()
settings = get_settings()