Files
pear-admin-flask/applications/config.py
T
bwstudio 1214bfdf4a feat(ops): 环境变量化配置 + 健康检查 + 启动脚本 + 部署/安全文档
主要变更:

config
- 引入 python-dotenv,从 .env 加载 SECRET_KEY / MAIL_* / SQLALCHEMY_DATABASE_URI
- 拆分 BaseConfig / DevConfig / ProConfig,PEAR_ENV 控制切换
- ProConfig 在 init_app() 校验 SECRET_KEY,禁止使用默认值

ops
- 新增 /healthz 端点:DB 探活 + 环境名 + 时间戳,失败返回 503
- 新增 start.bat (Windows) / Makefile (跨平台) 一键启动 / 初始化
- 新增 .env.example 模板;.gitignore 排除 .env / logs / vscode 等
- 新增 docs/DEPLOY.md(部署指南)和 docs/SECURITY.md(安全清单)
- app.py 不再硬编码 debug=False,交给配置类决定

本提交不包含上一次会话的其它工作树修改(模板/导航相关),后续单独 PR。
2026-09-05 20:46:12 +08:00

131 lines
4.4 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 logging
import os
import secrets
from datetime import timedelta
def _env_bool(key: str, default: bool = False) -> bool:
"""读取布尔环境变量,兼容 true/1/yes/on(大小写不敏感)。"""
val = os.environ.get(key)
if val is None:
return default
return val.strip().lower() in {"1", "true", "yes", "on"}
def _env_str(key: str, default: str = "") -> str:
val = os.environ.get(key)
return val if val is not None and val != "" else default
class BaseConfig:
"""通用配置:所有环境共有的项。子类按需覆盖。"""
# ---- 应用基础 ----
SUPERADMIN = 'admin'
SYSTEM_NAME = 'Pear Admin'
SYSTEM_PANEL_LINKS = [
{"icon": "layui-icon layui-icon-auz", "title": "官方网站", "href": "http://www.pearadmin.com"},
{"icon": "layui-icon layui-icon-auz", "title": "开发文档", "href": "http://www.pearadmin.com"},
{"icon": "layui-icon layui-icon-auz", "title": "开源地址", "href": "https://gitee.com/Jmysy/Pear-Admin-Layui"},
]
# ---- 上传 ----
UPLOADED_PHOTOS_DEST = 'static/upload'
UPLOADED_FILES_ALLOW = ['gif', 'jpg', 'jpeg', 'png', 'webp']
UPLOADS_AUTOSERVE = True
# ---- JSON ----
JSON_AS_ASCII = False
# ---- 数据库 ----
# 默认走项目根目录下的 pear.db(与 instance/ 同一级,Flask 2.x 行为)。
# 生产环境推荐通过 SQLALCHEMY_DATABASE_URI 环境变量切换到 MySQL。
SQLALCHEMY_DATABASE_URI = 'sqlite:///../pear.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
# ---- 日志 ----
LOG_LEVEL = logging.WARN
# ---- Session ----
PERMANENT_SESSION_LIFETIME = timedelta(days=7)
SESSION_TYPE = "filesystem"
SESSION_PERMANENT = False
SESSION_USE_SIGNER = True
# ---- 安全 / 密钥 ----
# SECRET_KEY 的优先级:
# 1. 环境变量 SECRET_KEY
# 2. .env 中的 SECRET_KEY
# 3. 自动生成一个临时随机值(仅用于开发启动;生产模式强制要求外部传入)
SECRET_KEY = os.environ.get("SECRET_KEY") or "pear-system-flask"
# ---- Mail(默认占位;环境变量可覆盖)----
MAIL_SERVER = _env_str("MAIL_SERVER", "smtp.qq.com")
MAIL_USE_TLS = _env_bool("MAIL_USE_TLS", False)
MAIL_USE_SSL = _env_bool("MAIL_USE_SSL", True)
MAIL_PORT = int(_env_str("MAIL_PORT", "465"))
MAIL_USERNAME = _env_str("MAIL_USERNAME", "123@qq.com")
MAIL_PASSWORD = _env_str("MAIL_PASSWORD", "XXXXX") # QQ 邮箱授权码
MAIL_DEFAULT_SENDER = _env_str("MAIL_DEFAULT_SENDER", MAIL_USERNAME)
# ---- 插件 ----
PLUGIN_ENABLE_FOLDERS = []
class DevConfig(BaseConfig):
"""开发环境配置:DEBUG=True,详细日志。"""
DEBUG = True
TESTING = False
LOG_LEVEL = logging.INFO
# 开发环境下 SECRET_KEY 自动生成(每次启动都不同,仅用于本地调试)。
SECRET_KEY = os.environ.get("SECRET_KEY") or secrets.token_urlsafe(48)
class ProConfig(BaseConfig):
"""生产环境配置:DEBUG=False,强制要求外部传入 SECRET_KEY。"""
DEBUG = False
TESTING = False
LOG_LEVEL = logging.WARN
# 生产推荐使用 MySQL,例如:
# SQLALCHEMY_DATABASE_URI=mysql+pymysql://user:pass@host:3306/pear?charset=utf8mb4
SQLALCHEMY_DATABASE_URI = os.environ.get(
"SQLALCHEMY_DATABASE_URI",
BaseConfig.SQLALCHEMY_DATABASE_URI,
)
@classmethod
def init_app(cls, app):
"""Flask 在 app.config.from_object 之后调用此钩子。
生产模式要求:必须从环境变量传入强随机 SECRET_KEY
否则启动失败 —— 防止线上仍用默认密钥。
"""
secret = os.environ.get("SECRET_KEY")
if not secret or secret == "pear-system-flask":
raise RuntimeError(
"[ProConfig] 生产环境必须通过环境变量 SECRET_KEY 传入强随机密钥,"
"禁止使用默认值。可用以下命令生成:\n"
" python -c \"import secrets; print(secrets.token_urlsafe(48))\""
)
app.config["SECRET_KEY"] = secret
# 暴露给外部按名字取
config_map = {
"dev": DevConfig,
"development": DevConfig,
"pro": ProConfig,
"production": ProConfig,
}
def get_config_by_name(name: str):
"""根据名字取配置类,未知名字退回 DevConfig。"""
if not name:
return DevConfig
return config_map.get(name.strip().lower(), DevConfig)