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 # ---- 数据目录(持久化根目录)---- # # 所有「会变、需要留下来」的东西统一放这里: # data/pear.db SQLite 主库(含 -wal / -shm) # data/backup/ 在线热备产物 # data/flask_session/ Flask-Session 文件会话 # # 为什么要单独一个目录: # 项目根目录会被 Git / Docker 镜像整包替换,数据库混在源码里既容易误提交, # 也会在容器重建时随镜像层一起丢掉。独立成 data/ 之后: # - 宿主机/NAS 上只要备份或绑定挂载这一个目录,数据就在; # - .gitignore / .dockerignore 各写一条就能彻底隔开。 # # 默认值:项目根目录下的 data/(applications/ 的上一级)。 # Docker 部署时用环境变量 PEAR_DATA_DIR=/app/data 覆盖, # 再把 NAS 共享文件夹绑到 /app/data 即可。 _PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DATA_DIR = os.path.abspath(os.environ.get("PEAR_DATA_DIR") or os.path.join(_PROJECT_ROOT, "data")) def _sqlite_uri(filename: str) -> str: """拼 SQLite URI。统一换成正斜杠,保证 Linux 容器与 Windows 都能解析。""" return "sqlite:///" + os.path.join(DATA_DIR, filename).replace("\\", "/") def ensure_data_dir() -> str: """确保数据目录(及子目录)存在,返回根目录绝对路径。 在 create_app 里调用;这里再兜一层 try,避免只读环境下 import 就崩。 """ for sub in ("", "backup", "flask_session"): try: os.makedirs(os.path.join(DATA_DIR, sub), exist_ok=True) except OSError: pass return DATA_DIR 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 # ---- 数据库 ---- # 默认走 data/pear.db(Docker 下由 PEAR_DATA_DIR 重定向到挂载点)。 # 想换成 MySQL 就直接设环境变量 SQLALCHEMY_DATABASE_URI,优先级最高。 SQLALCHEMY_DATABASE_URI = os.environ.get( "SQLALCHEMY_DATABASE_URI", _sqlite_uri("pear.db") ) SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_ENGINE_OPTIONS = {"pool_pre_ping": True} if SQLALCHEMY_DATABASE_URI.startswith("sqlite"): # SQLite 写保护:拿不到锁最多等 30s 再放弃,避免 gunicorn 多 worker 下 # 偶发 "database is locked"。WAL 模式由 extensions 里的 connect 事件开启。 # 注意:这组 connect_args 是 sqlite3 专用的,切 MySQL 时不能带上。 SQLALCHEMY_ENGINE_OPTIONS["connect_args"] = {"timeout": 30} # ---- 日志 ---- LOG_LEVEL = logging.WARN # ---- Session ---- PERMANENT_SESSION_LIFETIME = timedelta(days=7) SESSION_TYPE = "filesystem" SESSION_FILE_DIR = os.path.join(DATA_DIR, "flask_session") 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) # ---- 插件 ---- # 站点业务模块(导航 / 友链 / 关于 / 访问统计)均通过 plugins 方式开发,启用即可挂载路由 PLUGIN_ENABLE_FOLDERS = ["navManager", "friendManager", "aboutManager", "siteStats"] 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)