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。
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
# ============================================================
|
||||||
|
# Pear Admin Flask - 环境变量示例
|
||||||
|
# ============================================================
|
||||||
|
# 用法:
|
||||||
|
# 1) 复制本文件:cp .env.example .env
|
||||||
|
# 2) 按需修改 .env 中的值(不要提交 .env 到 git)
|
||||||
|
# 3) 应用启动时由 applications/__init__.py 自动 load_dotenv 读取
|
||||||
|
#
|
||||||
|
# 加载优先级:系统环境变量 > .env > 代码默认值
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# ---- 运行模式 ----
|
||||||
|
# dev: 开发模式(DEBUG=True,自动生成 SECRET_KEY,无需外部传入)
|
||||||
|
# production: 生产模式(DEBUG=False,强制要求 SECRET_KEY)
|
||||||
|
PEAR_ENV=dev
|
||||||
|
|
||||||
|
# ---- 安全 ----
|
||||||
|
# 生产模式必填。可用以下命令生成:
|
||||||
|
# python -c "import secrets; print(secrets.token_urlsafe(48))"
|
||||||
|
SECRET_KEY=
|
||||||
|
|
||||||
|
# ---- 数据库 ----
|
||||||
|
# 默认是项目根目录下的 SQLite pear.db;生产推荐 MySQL。
|
||||||
|
# MySQL 示例:
|
||||||
|
# SQLALCHEMY_DATABASE_URI=mysql+pymysql://user:pass@127.0.0.1:3306/pear?charset=utf8mb4
|
||||||
|
# SQLALCHEMY_DATABASE_URI=
|
||||||
|
|
||||||
|
# ---- 邮件(Flask-Mail)----
|
||||||
|
# QQ 邮箱示例
|
||||||
|
MAIL_SERVER=smtp.qq.com
|
||||||
|
MAIL_USE_TLS=false
|
||||||
|
MAIL_USE_SSL=true
|
||||||
|
MAIL_PORT=465
|
||||||
|
MAIL_USERNAME=your_account@qq.com
|
||||||
|
MAIL_PASSWORD=your_authorization_code_here
|
||||||
|
MAIL_DEFAULT_SENDER=your_account@qq.com
|
||||||
|
|
||||||
|
# ---- Flask ----
|
||||||
|
# FLASK_APP=app.py
|
||||||
|
# FLASK_DEBUG=1
|
||||||
+22
-1
@@ -130,4 +130,25 @@ static/upload/
|
|||||||
flask_session/
|
flask_session/
|
||||||
|
|
||||||
# WorkBuddy 运行时目录(不应纳入版本管理)
|
# WorkBuddy 运行时目录(不应纳入版本管理)
|
||||||
.workbuddy/
|
.workbuddy/
|
||||||
|
|
||||||
|
# ---- Pear Admin Flask 额外忽略 ----
|
||||||
|
# 私有环境变量模板:保留 .env.example 入库;本地 .env 不入
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# 系统监控日志(如果产生)
|
||||||
|
*.pid
|
||||||
|
*.sock
|
||||||
|
|
||||||
|
# 运行日志
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# pycharm / vscode
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# 临时下载/构建产物
|
||||||
|
tmp/
|
||||||
|
build/
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# ============================================================
|
||||||
|
# Pear Admin Flask - Makefile(跨平台,Windows 下需用 Git Bash / WSL)
|
||||||
|
#
|
||||||
|
# 用法(在项目根目录):
|
||||||
|
# make help 查看所有命令
|
||||||
|
# make venv 创建虚拟环境
|
||||||
|
# make install 安装依赖
|
||||||
|
# make migrate 执行 db migrate + upgrade
|
||||||
|
# make seed 导入 admin 基础数据(app_data/init.json)
|
||||||
|
# make init 等价于 migrate + seed
|
||||||
|
# make run 开发模式启动(PEAR_ENV=dev)
|
||||||
|
# make run-prod 生产模式启动(PEAR_ENV=production,要求 .env SECRET_KEY)
|
||||||
|
# make clean 清理缓存与数据库(危险!请先备份)
|
||||||
|
# make health curl /healthz
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
PY ?= python
|
||||||
|
VENV ?= venv
|
||||||
|
PYBIN := $(VENV)/bin/python
|
||||||
|
PIP := $(PYBIN) -m pip
|
||||||
|
FLASK := $(PYBIN) -m flask
|
||||||
|
APP := app.py
|
||||||
|
|
||||||
|
.PHONY: help venv install migrate seed init run run-prod clean health
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "可用命令:"
|
||||||
|
@echo " make venv 创建虚拟环境"
|
||||||
|
@echo " make install 安装依赖"
|
||||||
|
@echo " make migrate db migrate + upgrade"
|
||||||
|
@echo " make seed flask admin init"
|
||||||
|
@echo " make init migrate + seed"
|
||||||
|
@echo " make run 开发模式启动(默认端口 5000)"
|
||||||
|
@echo " make run-prod 生产模式启动(强制要求 .env SECRET_KEY)"
|
||||||
|
@echo " make health curl http://127.0.0.1:5000/healthz"
|
||||||
|
@echo " make clean 清理 pyc / __pycache__ / pear.db / flask_session"
|
||||||
|
|
||||||
|
venv:
|
||||||
|
@test -d "$(VENV)" || $(PY) -m venv $(VENV)
|
||||||
|
@echo "[venv] 已创建 $(VENV)"
|
||||||
|
|
||||||
|
install: venv
|
||||||
|
$(PIP) install --upgrade pip
|
||||||
|
$(PIP) install -r requirements.txt
|
||||||
|
|
||||||
|
migrate:
|
||||||
|
$(FLASK) db migrate
|
||||||
|
$(FLASK) db upgrade
|
||||||
|
|
||||||
|
seed:
|
||||||
|
$(FLASK) admin init
|
||||||
|
|
||||||
|
init: migrate seed
|
||||||
|
|
||||||
|
run:
|
||||||
|
PEAR_ENV=dev $(PYBIN) $(APP)
|
||||||
|
|
||||||
|
run-prod:
|
||||||
|
PEAR_ENV=production $(PYBIN) $(APP)
|
||||||
|
|
||||||
|
health:
|
||||||
|
@curl -sS http://127.0.0.1:5000/healthz | python -m json.tool
|
||||||
|
|
||||||
|
clean:
|
||||||
|
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
find . -type f -name "*.pyc" -delete
|
||||||
|
rm -rf flask_session
|
||||||
|
@echo "[clean] 已清理缓存。注意:pear.db 请手动备份后删除。"
|
||||||
@@ -3,4 +3,12 @@ from applications import create_app
|
|||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run()
|
# 绑定 0.0.0.0 后,同局域网手机可通过本机 IP 访问(例如 http://192.168.1.10:5000/site/)
|
||||||
|
# debug 标志交给配置类决定:PEAR_ENV=dev → DEBUG=True;PEAR_ENV=production → DEBUG=False
|
||||||
|
# 防止重载进程抢占端口;局域网访问保持稳定
|
||||||
|
app.run(
|
||||||
|
host='0.0.0.0',
|
||||||
|
port=5000,
|
||||||
|
debug=app.config.get('DEBUG', False),
|
||||||
|
use_reloader=False,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,24 +1,40 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
from applications.common.script import init_script
|
from applications.common.script import init_script
|
||||||
from applications.config import BaseConfig
|
from applications.config import BaseConfig, get_config_by_name
|
||||||
from applications.extensions import init_plugs
|
from applications.extensions import init_plugs
|
||||||
from applications.view import init_bps
|
from applications.view import init_bps
|
||||||
|
|
||||||
|
# 项目根目录 = 本文件父目录的父目录
|
||||||
|
BASE_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
app = Flask(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
# 1) 先加载 .env(在 BaseConfig 读取 os.environ 之前)
|
||||||
|
# override=False:环境变量优先级高于 .env,便于 CI/CD 在部署时再覆盖
|
||||||
|
load_dotenv(os.path.join(BASE_DIR, ".env"), override=False)
|
||||||
|
|
||||||
# 引入配置
|
app = Flask(BASE_DIR)
|
||||||
app.config.from_object(BaseConfig)
|
|
||||||
|
|
||||||
# 注册flask组件
|
# 2) 根据 PEAR_ENV / FLASK_ENV 选择配置类,默认 dev
|
||||||
|
env_name = os.environ.get("PEAR_ENV") or os.environ.get("FLASK_ENV") or "dev"
|
||||||
|
config_cls = get_config_by_name(env_name)
|
||||||
|
app.config.from_object(config_cls)
|
||||||
|
|
||||||
|
# 3) 把 BaseConfig 默认值里没写、但 .env 可能写了的 SQLALCHEMY_DATABASE_URI 等再覆盖一次
|
||||||
|
if os.environ.get("SQLALCHEMY_DATABASE_URI"):
|
||||||
|
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ["SQLALCHEMY_DATABASE_URI"]
|
||||||
|
|
||||||
|
# 3.5) 让配置类有机会做运行时校验(例如 ProConfig 强制要求 SECRET_KEY)
|
||||||
|
if hasattr(config_cls, "init_app"):
|
||||||
|
config_cls.init_app(app)
|
||||||
|
|
||||||
|
# 4) 注册 flask 组件 / 蓝图 / 命令
|
||||||
init_plugs(app)
|
init_plugs(app)
|
||||||
|
|
||||||
# 注册蓝图
|
|
||||||
init_bps(app)
|
init_bps(app)
|
||||||
|
|
||||||
# 注册命令
|
|
||||||
init_script(app)
|
init_script(app)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
+105
-59
@@ -1,84 +1,130 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
||||||
class BaseConfig:
|
def _env_bool(key: str, default: bool = False) -> bool:
|
||||||
# 超级管理员账号
|
"""读取布尔环境变量,兼容 true/1/yes/on(大小写不敏感)。"""
|
||||||
SUPERADMIN = 'admin'
|
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_NAME = 'Pear Admin'
|
||||||
|
|
||||||
# 主题面板的链接列表配置
|
|
||||||
SYSTEM_PANEL_LINKS = [
|
SYSTEM_PANEL_LINKS = [
|
||||||
{
|
{"icon": "layui-icon layui-icon-auz", "title": "官方网站", "href": "http://www.pearadmin.com"},
|
||||||
"icon": "layui-icon layui-icon-auz",
|
{"icon": "layui-icon layui-icon-auz", "title": "开发文档", "href": "http://www.pearadmin.com"},
|
||||||
"title": "官方网站",
|
{"icon": "layui-icon layui-icon-auz", "title": "开源地址", "href": "https://gitee.com/Jmysy/Pear-Admin-Layui"},
|
||||||
"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_PHOTOS_DEST = 'static/upload'
|
||||||
UPLOADED_FILES_ALLOW = ['gif', 'jpg', 'jpeg', 'png', 'webp']
|
UPLOADED_FILES_ALLOW = ['gif', 'jpg', 'jpeg', 'png', 'webp']
|
||||||
UPLOADS_AUTOSERVE = True
|
UPLOADS_AUTOSERVE = True
|
||||||
|
|
||||||
# JSON 配置
|
# ---- JSON ----
|
||||||
JSON_AS_ASCII = False
|
JSON_AS_ASCII = False
|
||||||
|
|
||||||
# 配置多个数据库连接的连接串写法示例
|
# ---- 数据库 ----
|
||||||
# HOSTNAME: 指数据库的IP地址、USERNAME:指数据库登录的用户名、PASSWORD:指数据库登录密码、PORT:指数据库开放的端口、DATABASE:指需要连接的数据库名称
|
# 默认走项目根目录下的 pear.db(与 instance/ 同一级,Flask 2.x 行为)。
|
||||||
# MSSQL: f"mssql+pymssql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=cp936"
|
# 生产环境推荐通过 SQLALCHEMY_DATABASE_URI 环境变量切换到 MySQL。
|
||||||
# MySQL: f"mysql+pymysql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=utf8mb4"
|
|
||||||
# Oracle: f"oracle+cx_oracle://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}"
|
|
||||||
# SQLite "sqlite:/// database.db"
|
|
||||||
# Postgres f"postgresql+psycopg2://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}"
|
|
||||||
# Oracle的第二种连接方式
|
|
||||||
# dsnStr = cx_Oracle.makedsn({HOSTNAME}, 1521, service_name='orcl')
|
|
||||||
# connect_str = "oracle://%s:%s@%s" % ('{USERNAME}', ' {PASSWORD}', dsnStr)
|
|
||||||
|
|
||||||
# 在SQLALCHEMY_BINDS 中设置:'{数据库连接别名}': '{连接串}'
|
|
||||||
# 最后在models的数据模型class中,在__tablename__前设置 __bind_key__ = '{数据库连接别名}' 即可,表示该数据模型不使用默认的数据库连接,改用“SQLALCHEMY_BINDS”中设置的其他数据库连接
|
|
||||||
# SQLALCHEMY_BINDS = {
|
|
||||||
# 'testMySQL': 'mysql+pymysql://test:123456@192.168.1.1:3306/test?charset=utf8',
|
|
||||||
# 'testMsSQL': 'mssql+pymssql://test:123456@192.168.1.1:1433/test?charset=cp936',
|
|
||||||
# 'testOracle': 'oracle+cx_oracle://test:123456@192.168.1.1:1521/test',
|
|
||||||
# 'testSQLite': 'sqlite:///database.db
|
|
||||||
# }
|
|
||||||
|
|
||||||
# 数据库的配置信息
|
|
||||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///../pear.db'
|
SQLALCHEMY_DATABASE_URI = 'sqlite:///../pear.db'
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
|
||||||
# 默认日志等级
|
# ---- 日志 ----
|
||||||
LOG_LEVEL = logging.WARN
|
LOG_LEVEL = logging.WARN
|
||||||
|
|
||||||
# 发信设置
|
# ---- Session ----
|
||||||
MAIL_SERVER = 'smtp.qq.com'
|
PERMANENT_SESSION_LIFETIME = timedelta(days=7)
|
||||||
MAIL_USE_TLS = False
|
SESSION_TYPE = "filesystem"
|
||||||
MAIL_USE_SSL = True
|
SESSION_PERMANENT = False
|
||||||
MAIL_PORT = 465
|
SESSION_USE_SIGNER = True
|
||||||
MAIL_USERNAME = '123@qq.com'
|
|
||||||
MAIL_PASSWORD = 'XXXXX' # 生成的授权码
|
|
||||||
MAIL_DEFAULT_SENDER = MAIL_USERNAME
|
|
||||||
|
|
||||||
# 插件配置,填写插件的文件名名称,默认不启用插件。
|
# ---- 安全 / 密钥 ----
|
||||||
|
# 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 = []
|
PLUGIN_ENABLE_FOLDERS = []
|
||||||
|
|
||||||
# Session 设置
|
|
||||||
PERMANENT_SESSION_LIFETIME = timedelta(days=7)
|
|
||||||
|
|
||||||
SESSION_TYPE = "filesystem" # 默认使用文件系统来保存会话
|
class DevConfig(BaseConfig):
|
||||||
SESSION_PERMANENT = False # 会话是否持久化
|
"""开发环境配置:DEBUG=True,详细日志。"""
|
||||||
SESSION_USE_SIGNER = True # 是否对发送到浏览器上 session 的 cookie 值进行加密
|
|
||||||
|
|
||||||
SECRET_KEY = "pear-system-flask"
|
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)
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
from applications.view.system import register_system_bps
|
from applications.view.system import register_system_bps
|
||||||
from applications.view.public import bp as public_bp # public 包的入口就是 nav_bp
|
from applications.view.public import bp as public_bp, register_public_bp # public 包的入口就是 nav_bp
|
||||||
|
from applications.view.health import bp as health_bp
|
||||||
from applications.extensions.init_plugins import broadcast_execute
|
from applications.extensions.init_plugins import broadcast_execute
|
||||||
|
|
||||||
|
|
||||||
def init_bps(app):
|
def init_bps(app):
|
||||||
|
# 健康检查端点(无需登录,应在负载均衡/容器探活里使用)
|
||||||
|
app.register_blueprint(health_bp)
|
||||||
|
|
||||||
# 前台公开蓝图(无需登录)
|
# 前台公开蓝图(无需登录)
|
||||||
app.register_blueprint(public_bp)
|
app.register_blueprint(public_bp)
|
||||||
|
register_public_bp(app)
|
||||||
|
|
||||||
# 后台系统蓝图(需要登录 + 权限码)
|
# 后台系统蓝图(需要登录 + 权限码)
|
||||||
register_system_bps(app)
|
register_system_bps(app)
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
"""
|
||||||
|
健康检查端点(无需登录)
|
||||||
|
|
||||||
|
- GET /healthz
|
||||||
|
用于探活/负载均衡/容器 readinessProbe。
|
||||||
|
返回:
|
||||||
|
{
|
||||||
|
"status": "ok" | "degraded",
|
||||||
|
"db_ok": true | false,
|
||||||
|
"env": "dev" | "production",
|
||||||
|
"system": "Pear Admin",
|
||||||
|
"timestamp": "2026-09-05T17:00:00+08:00"
|
||||||
|
}
|
||||||
|
|
||||||
|
HTTP 状态码:
|
||||||
|
200 - 全部正常
|
||||||
|
503 - DB 不可达(用于触发 k8s readinessProbe 失败)
|
||||||
|
"""
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
import os
|
||||||
|
|
||||||
|
from flask import Blueprint, current_app, jsonify
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
from applications.extensions import db
|
||||||
|
|
||||||
|
bp = Blueprint("health", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _beijing_now() -> str:
|
||||||
|
"""返回 +08:00 时区 ISO8601 时间字符串。"""
|
||||||
|
tz = timezone(timedelta(hours=8))
|
||||||
|
return datetime.now(tz).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("/healthz")
|
||||||
|
def healthz():
|
||||||
|
# 优先按 PEAR_ENV/FLASK_ENV 取环境名;兜底按 DEBUG 反推
|
||||||
|
env = (
|
||||||
|
os.environ.get("PEAR_ENV")
|
||||||
|
or os.environ.get("FLASK_ENV")
|
||||||
|
or current_app.config.get("PEAR_ENV")
|
||||||
|
or ("dev" if current_app.config.get("DEBUG") else "production")
|
||||||
|
)
|
||||||
|
system_name = current_app.config.get("SYSTEM_NAME", "Pear Admin")
|
||||||
|
|
||||||
|
# DB 探活:跑一个最轻量的 SELECT 1
|
||||||
|
db_ok = True
|
||||||
|
db_error = None
|
||||||
|
try:
|
||||||
|
db.session.execute(text("SELECT 1"))
|
||||||
|
except Exception as e: # noqa: BLE001 - 这里就是要把异常吃掉,转换成 False
|
||||||
|
db_ok = False
|
||||||
|
db_error = str(e)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
db.session.rollback()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"status": "ok" if db_ok else "degraded",
|
||||||
|
"db_ok": db_ok,
|
||||||
|
"env": env,
|
||||||
|
"system": system_name,
|
||||||
|
"timestamp": _beijing_now(),
|
||||||
|
}
|
||||||
|
if db_error:
|
||||||
|
payload["db_error"] = db_error
|
||||||
|
|
||||||
|
return jsonify(payload), (200 if db_ok else 503)
|
||||||
+164
@@ -0,0 +1,164 @@
|
|||||||
|
# Pear Admin Flask - 部署指南
|
||||||
|
|
||||||
|
本文档面向把本项目部署到生产环境的运维 / 二次开发者。
|
||||||
|
请结合根目录的 `README.md` 与 `docs/SECURITY.md` 一起阅读。
|
||||||
|
|
||||||
|
## 0. 环境要求
|
||||||
|
|
||||||
|
| 项目 | 版本 |
|
||||||
|
| --- | --- |
|
||||||
|
| Python | 3.8(推荐 3.11) |
|
||||||
|
| SQLite | 3.x(仅开发用) |
|
||||||
|
| MySQL | 5.7+ / 8.0(生产推荐) |
|
||||||
|
| 操作系统 | Windows / Linux 均可 |
|
||||||
|
|
||||||
|
## 1. 克隆代码
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.bwhome.top/bwadmin/pear-admin-flask.git
|
||||||
|
cd pear-admin-flask
|
||||||
|
```
|
||||||
|
|
||||||
|
> 如仓库使用自签证书导致 `SEC_E_WRONG_PRINCIPAL` 报错,
|
||||||
|
> 可临时关闭 SSL 校验:`git -c http.sslVerify=false clone <url>`。
|
||||||
|
> 长期建议把根证书导入到系统信任库。
|
||||||
|
|
||||||
|
## 2. 创建虚拟环境 + 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux / macOS
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Windows (cmd)
|
||||||
|
python -m venv venv
|
||||||
|
venv\Scripts\activate.bat
|
||||||
|
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 配置 .env
|
||||||
|
|
||||||
|
复制模板:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env # Linux
|
||||||
|
copy .env.example .env # Windows
|
||||||
|
```
|
||||||
|
|
||||||
|
按需修改(**生产模式 SECRET_KEY 必填**,参见下文):
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
PEAR_ENV=production
|
||||||
|
SECRET_KEY=<用 python -c "import secrets; print(secrets.token_urlsafe(48))" 生成>
|
||||||
|
|
||||||
|
# MySQL 示例
|
||||||
|
SQLALCHEMY_DATABASE_URI=mysql+pymysql://user:pass@127.0.0.1:3306/pear?charset=utf8mb4
|
||||||
|
|
||||||
|
# 邮件(可选)
|
||||||
|
MAIL_SERVER=smtp.qq.com
|
||||||
|
MAIL_USE_SSL=true
|
||||||
|
MAIL_PORT=465
|
||||||
|
MAIL_USERNAME=your_account@qq.com
|
||||||
|
MAIL_PASSWORD=your_authorization_code_here
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ `.env` 文件已在 `.gitignore` 中,**不会**被提交到 git。
|
||||||
|
|
||||||
|
## 4. 数据库初始化
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) 生成迁移脚本
|
||||||
|
flask db migrate
|
||||||
|
# 2) 升级到当前 head
|
||||||
|
flask db upgrade
|
||||||
|
# 3) 导入 admin 基础数据(用户 / 角色 / 菜单)
|
||||||
|
flask admin init
|
||||||
|
```
|
||||||
|
|
||||||
|
MySQL 用户需提前建库:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE DATABASE pear DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
CREATE USER 'pear'@'%' IDENTIFIED BY 'your_strong_password';
|
||||||
|
GRANT ALL PRIVILEGES ON pear.* TO 'pear'@'%';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 启动
|
||||||
|
|
||||||
|
### 5.1 开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux
|
||||||
|
make run
|
||||||
|
# Windows
|
||||||
|
start.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 生产
|
||||||
|
|
||||||
|
推荐用 `gunicorn`(`pip install gunicorn`),或反向代理到 `waitress`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux
|
||||||
|
PEAR_ENV=production gunicorn -w 4 -b 0.0.0.0:5000 app:app
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
pip install waitress
|
||||||
|
PEAR_ENV=production waitress-serve --port=5000 app:app
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 健康检查
|
||||||
|
|
||||||
|
服务起来后:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://127.0.0.1:5000/healthz
|
||||||
|
# 200 OK → {"status":"ok","db_ok":true,...}
|
||||||
|
# 503 → {"status":"degraded","db_ok":false,...}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 反向代理(Nginx 示例)
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name pear.example.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:5000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
client_max_body_size 50m;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /healthz {
|
||||||
|
proxy_pass http://127.0.0.1:5000/healthz;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 升级流程
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
flask db migrate
|
||||||
|
flask db upgrade
|
||||||
|
# 重启进程
|
||||||
|
systemctl restart pear-admin-flask
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 常见问题
|
||||||
|
|
||||||
|
| 现象 | 处理 |
|
||||||
|
| --- | --- |
|
||||||
|
| `[ProConfig] 生产环境必须通过环境变量 SECRET_KEY 传入强随机密钥` | 在 `.env` 或系统环境变量中设置 `SECRET_KEY`。 |
|
||||||
|
| `SEC_E_WRONG_PRINCIPAL` 克隆失败 | `git -c http.sslVerify=false clone ...` 或导入根证书。 |
|
||||||
|
| 启动后 `/` 跳转到登录页 | 正常。后台默认账号 `admin` / 密码 `123456`,**请立即修改**。 |
|
||||||
|
| 数据库表缺失 | `flask db upgrade` 后 `flask admin init`。 |
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Pear Admin Flask - 安全指南
|
||||||
|
|
||||||
|
## 1. 密钥(SECRET_KEY)
|
||||||
|
|
||||||
|
`SECRET_KEY` 用于 Flask `session` 签名、`csrf_token`、Flask-Session
|
||||||
|
等。**一旦泄露,攻击者可伪造任意管理员会话。**
|
||||||
|
|
||||||
|
要求:
|
||||||
|
- 长度 ≥ 32 字节(推荐 48+);
|
||||||
|
- 高熵随机(`secrets.token_urlsafe(48)` / `openssl rand -base64 48`);
|
||||||
|
- **禁止**使用默认值 `pear-system-flask`;
|
||||||
|
- **禁止**把 `.env` / 含密钥的部署脚本提交到 git。
|
||||||
|
|
||||||
|
开发模式自动生成的密钥仅供本地调试,**重启即失效**。
|
||||||
|
|
||||||
|
## 2. 凭据(Mail / DB)
|
||||||
|
|
||||||
|
所有凭据通过 `.env` 注入,代码里只放占位符。强烈建议:
|
||||||
|
- 生产数据库单独建用户,**只授必要的库权限**;
|
||||||
|
- Mail 使用**授权码**而非登录密码(QQ / 163 / Gmail 均提供);
|
||||||
|
- 定期轮换凭据,并在变更后重启服务。
|
||||||
|
|
||||||
|
## 3. 默认管理员账号
|
||||||
|
|
||||||
|
`flask admin init` 会写入默认账号 `admin / 123456`,**首次登录后必须立即修改密码**。
|
||||||
|
|
||||||
|
可执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) 登录后台 → 用户管理 → 修改 admin 密码;
|
||||||
|
# 2) 或直接 SQL:
|
||||||
|
sqlite3 pear.db "UPDATE rbac_user SET password='<pbkdf2:sha256:...>' WHERE username='admin';"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Cookie / Session
|
||||||
|
|
||||||
|
`SESSION_COOKIE_HTTPONLY = True`(默认)与 `SESSION_COOKIE_SAMESITE = 'Lax'`
|
||||||
|
已经在配置中体现。生产部署在 HTTPS 下请额外设置:
|
||||||
|
|
||||||
|
```python
|
||||||
|
SESSION_COOKIE_SECURE = True
|
||||||
|
```
|
||||||
|
|
||||||
|
> 若启用 Flask-Session(filesystem / redis),请把 `flask_session/`
|
||||||
|
> 目录排除在 Web 静态目录之外,并定期清理。
|
||||||
|
|
||||||
|
## 5. CSRF
|
||||||
|
|
||||||
|
本项目基于 `flask_wtf`,所有写操作均有 CSRF 校验。
|
||||||
|
前端表单务必带上 `{{ csrf_token() }}` 或请求头 `X-CSRFToken`。
|
||||||
|
|
||||||
|
## 6. 上传文件
|
||||||
|
|
||||||
|
`UPLOADED_PHOTOS_DEST = static/upload`,后缀白名单已限制为图片。
|
||||||
|
但**用户可上传 SVG / 伪装为图片的 JS** 等——建议:
|
||||||
|
|
||||||
|
- 上传目录关闭脚本执行(Nginx: `location ^~ /static/upload/ { ... }`);
|
||||||
|
- 后端校验真实文件类型(`python-magic`),而非仅看后缀。
|
||||||
|
|
||||||
|
## 7. 限流
|
||||||
|
|
||||||
|
`Flask-Limiter` 已挂载(`applications/extensions/init_limit.py`)。
|
||||||
|
生产模式请按业务需求调整默认速率,避免误伤。
|
||||||
|
|
||||||
|
## 8. 日志
|
||||||
|
|
||||||
|
`LOG_LEVEL` 控制根 logger 输出等级。生产推荐 `WARN` / `ERROR`。
|
||||||
|
**不要把 `SECRET_KEY`、用户密码、token 写入日志。**
|
||||||
|
|
||||||
|
## 9. 依赖漏洞
|
||||||
|
|
||||||
|
建议在 CI 中加入 `pip-audit`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install pip-audit
|
||||||
|
pip-audit -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. 上线前 Checklist
|
||||||
|
|
||||||
|
- [ ] `PEAR_ENV=production`
|
||||||
|
- [ ] `SECRET_KEY` 已替换为强随机值
|
||||||
|
- [ ] 数据库用户仅授必要权限
|
||||||
|
- [ ] 默认 admin 密码已修改
|
||||||
|
- [ ] HTTPS 已上线(`SESSION_COOKIE_SECURE` 同步开启)
|
||||||
|
- [ ] `flask_session/` 目录已加入备份策略
|
||||||
|
- [ ] `static/upload/` 已禁用脚本执行
|
||||||
|
- [ ] `pip-audit` 跑过
|
||||||
|
- [ ] `/healthz` 已接入负载均衡 / 容器探活
|
||||||
+2
-1
@@ -42,4 +42,5 @@ Werkzeug==2.3.6
|
|||||||
zipp==3.16.2
|
zipp==3.16.2
|
||||||
flask_wtf==1.2.1
|
flask_wtf==1.2.1
|
||||||
wtforms~=3.1.1
|
wtforms~=3.1.1
|
||||||
Flask-Session==0.5.0
|
Flask-Session==0.5.0
|
||||||
|
python-dotenv==1.0.1
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
@echo off
|
||||||
|
REM ============================================================
|
||||||
|
REM Pear Admin Flask 一键启动脚本(Windows / cmd)
|
||||||
|
REM
|
||||||
|
REM 用法:
|
||||||
|
REM start.bat 开发模式启动(默认)
|
||||||
|
REM start.bat pro 生产模式启动(要求 .env 已设 SECRET_KEY)
|
||||||
|
REM start.bat init 仅初始化数据库 + 导入 admin 数据
|
||||||
|
REM start.bat migrate 仅做 db migrate / upgrade
|
||||||
|
REM ============================================================
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
set "VENV_PY=venv\Scripts\python.exe"
|
||||||
|
set "APP=app.py"
|
||||||
|
set "MODE=%~1"
|
||||||
|
|
||||||
|
if "%MODE%"=="" set "MODE=dev"
|
||||||
|
|
||||||
|
echo [start.bat] 模式=%MODE%
|
||||||
|
|
||||||
|
REM 1) 准备虚拟环境(首次)
|
||||||
|
if not exist "%VENV_PY%" (
|
||||||
|
echo [start.bat] 创建虚拟环境 venv ...
|
||||||
|
python -m venv venv
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [start.bat][错误] 创建 venv 失败,请确认系统已安装 Python 3.8+
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
REM 2) 安装依赖
|
||||||
|
echo [start.bat] 安装依赖 ...
|
||||||
|
"%VENV_PY%" -m pip install --upgrade pip >nul
|
||||||
|
"%VENV_PY%" -m pip install -r requirements.txt
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [start.bat][错误] 安装依赖失败
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM 3) 数据库迁移 + admin 初始化(仅首次或有变更时执行)
|
||||||
|
if /I "%MODE%"=="init" goto do_init
|
||||||
|
if /I "%MODE%"=="migrate" goto do_migrate
|
||||||
|
|
||||||
|
:do_init
|
||||||
|
echo [start.bat] 初始化数据库(migrate + upgrade) ...
|
||||||
|
"%VENV_PY%" -m flask db migrate
|
||||||
|
"%VENV_PY%" -m flask db upgrade
|
||||||
|
echo [start.bat] 导入 admin 基础数据 ...
|
||||||
|
"%VENV_PY%" -m flask admin init
|
||||||
|
echo [start.bat] 初始化完成。可以执行 start.bat 启动服务。
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
:do_migrate
|
||||||
|
"%VENV_PY%" -m flask db migrate
|
||||||
|
"%VENV_PY%" -m flask db upgrade
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
REM 4) 启动
|
||||||
|
echo [start.bat] 启动 Flask(PEAR_ENV=%MODE%)...
|
||||||
|
set "PEAR_ENV=%MODE%"
|
||||||
|
"%VENV_PY%" "%APP%"
|
||||||
|
|
||||||
|
endlocal
|
||||||
Reference in New Issue
Block a user