Files
pear-admin-flask/docs/plugins-development.md
T
bwstudio 0ffc103e46 feat(plugins): 4 个业务插件代码 + 前台公开页面改造
plugins/navManager:导航 CRUD + 导航分类 CRUD(独立 blueprint)
plugins/friendManager:友情链接 CRUD
plugins/aboutManager:关于本站 CRUD
plugins/siteStats:访问统计主页 + JSON 接口 + /site/* GET 埋点 + nav 卡片点击埋点

applications/view/public/nav.py:导航聚合页 / 分类详情 / JSON 接口,支持 visit_count 注入
applications/view/public/about.py:关于本站公开页
applications/view/public/friend.py:友情链接公开页

templates/public/base.html:响应式首页(搜索框 / 分类抽屉 / 5 主题切换 / footer 隐私小字)
templates/public/index.html / category.html:卡片右下角访问次数
templates/public/about.html / friend.html:关于 + 友链前台页

docs/plugins-development.md:插件开发完整指南(生命周期 + 4 个示例 + FAQ + framework 迁移)

scripts/:探针与端到端验证脚本(probe_*/verify_*/test_*)
2026-09-06 18:18:06 +08:00

19 KiB
Raw Blame History

Pear Admin Flask 插件开发操作手册

适用版本:Pear Admin Flask master 分支(fork 自 lab.lovepikachu.top 文档站点的最新文档)

文档维护:根据 plugins// 下的实际插件实现同步更新

Pear Admin Flask 的插件机制是其核心竞争力——不修改框架代码即可扩展新页面、新菜单、新数据埋点

本文以仓库中已落地的 navManager / friendManager / aboutManager / siteStats 四个插件为依据,整理出从设计到上线的完整操作步骤。


1. 插件是什么

概念 framework(框架) plugin(插件)
代码位置 applications/templates/system/ 之外的共用代码 plugins/<pluginName>/ 下,与 framework 解耦
启用方式 自动加载 applications/config.pyPLUGIN_ENABLE_FOLDERS 列表中显式声明
修改影响 改 framework 会影响所有升级 改 plugin 不影响 framework 与其他 plugin
典型用途 用户/角色/权限/部门等所有项目共用的基础模块 站点特有业务(导航、友链、关于、统计等)

2. 目录结构最小模型

一个最简插件的目录是这样:

plugins/
└── helloworld/                 # 插件文件夹名(与 PLUGIN_ENABLE_FOLDERS 中写的保持一致)
    ├── __init__.json           # 【可选】插件元信息(名称/版本/描述),删掉也能加载
    ├── __init__.py             # 必须。定义 event_init / event_finish 等事件函数
    └── view/                   # 建议。存放 view.pyBlueprint 定义和路由)
        └── main.py             # 自定义视图

# 复杂插件(仿 navManager/friendManager/aboutManager/siteStats):
plugins/
└── navManager/
    ├── __init__.json
    ├── __init__.py             # 入口
    ├── view/
    │   └── nav.py              # Blueprint 'nav'url_prefix='/system/nav'
    └── templates/
        └── system/
            └── nav/
                ├── main.html
                ├── add.html
                └── edit.html

3. 启用插件

打开 applications/config.py,找到 BaseConfig 类尾部的配置项:

# ---- 插件 ----
# 站点业务模块(导航 / 友链 / 关于 / 访问统计)均通过 plugins 方式开发,启用即可挂载路由
PLUGIN_ENABLE_FOLDERS = ["navManager", "friendManager", "aboutManager", "siteStats"]

顺序就是加载顺序,越靠前越早被 import;按需新增或删除。

修改完成后,重启 Flask 即可生效,启动日志会列出:

 * Plugin: Loaded plugin: 导航管理 .
 * Plugin: Loaded plugin: 友情链接 .
 * Plugin: Loaded plugin: 关于本站 .
 * Plugin: Loaded plugin: 访问统计 .
 * navManager: registered /system/nav/* blueprint
 * friendManager: registered /system/friend/* blueprint
 * aboutManager: registered /system/about/* blueprint
 * siteStats: registered /system/stats/* blueprint
 * siteStats: registered /site/* before_request hook

4. 插件生命周期(4 个事件)

插件入口 __init__.py 中可以暴露 4 个事件函数(任意函数缺省将自动跳过):

函数 时机 典型用途
event_begin(app) 框架功能注册之前(数据库连上之前) 极少数兼容场景;多数插件不需要
event_init(app) 所有 framework 蓝图注册之后 app.register_blueprint(bp)
event_finish(app) 数据库/CLI/脚本初始化完毕之后 注册 @app.before_request 钩子、@app.cli.command
event_context(app) 第一个请求到来之前(自动 app.app_context() 预热缓存、检查/修正数据库初始数据

调用顺序:event_begin → ... framework 注册 ... → event_init → ... 启动余下逻辑 ... → event_finish → (自动)event_context

注意:如果同时定义 event_initevent_finish,前者注册蓝图、后者挂 before_request / CLI不能用一次事件全做——否则会与 framework 自身的广播节奏冲突。


5. 最小可运行示例:Hello World(来自仓库 plugins/helloworld/

5.1 plugins/helloworld/__init__.py

import os
from flask import Flask
from .main import helloworld_blueprint

from applications.models import Dept
from applications.common import curd
from applications.schemas import DeptSchema

dir_path = os.path.dirname(__file__).replace("\\", "/")


def event_begin(app: Flask):
    print("所有功能初始化之前加载")


def event_init(app: Flask):
    """初始化完成时会调用这里"""
    print("初始插件初始化视图")
    app.register_blueprint(helloworld_blueprint)


def event_finish(app: Flask):
    print("所有初始化完毕")


def event_context(app: Flask):
    """Flask 初始化完成,等待第一个请求之前,等同于 with app.app_context():"""
    dept = Dept.query.order_by(Dept.sort).all()
    print(curd.model_to_dicts(schema=DeptSchema, data=dept))

5.2 plugins/helloworld/main.py

from flask import render_template, Blueprint

helloworld_blueprint = Blueprint('hello_world', __name__,
                                 template_folder='templates',
                                 static_folder='static',
                                 url_prefix='/hello_world')


@helloworld_blueprint.route('/')
def index():
    return render_template('helloworld_index.html')

5.3 启用

PLUGIN_ENABLE_FOLDERS = ["helloworld"]

启动后访问 http://127.0.0.1:5000/hello_world/


6. 含数据库的完整插件(实操示例:navManager)

这一节以仓库实际存在的 navManager 为模板,演示创建"导航表 + 后台 CRUD + 模板 + 权限码绑定 + 菜单种子"完整业务插件的全流程。

6.1 设计

  • 数据模型:单表 site_nav(继承自 applications.models.admin_nav.Nav
  • 路由:/system/nav//system/nav/data/system/nav/add/system/nav/save/system/nav/edit/system/nav/update/system/nav/remove/<int:nav_id>/system/nav/enable/system/nav/disable
  • 权限码:system:nav:main / system:nav:add / system:nav:edit / system:nav:remove

6.2 创建插件骨架

mkdir -p plugins/navManager/{view,templates/system/nav}

6.3 写 plugins/navManager/__init__.json

{
  "plugin_name": "导航管理",
  "plugin_version": "1.0.0",
  "plugin_description": "后台导航管理模块。对 nav 表的增删改查、分页查询、关键字 + 分类过滤、启用/禁用切换。"
}

6.4 写 plugins/navManager/__init__.py

from flask import Flask
from .view.nav import bp


def event_init(app: Flask):
    """初始化完成时注册蓝图到 app。"""
    app.register_blueprint(bp)
    print(" * navManager: registered /system/nav/* blueprint")

6.5 写 plugins/navManager/view/nav.py

最关键的一步——要把 Blueprint 的 url_prefix 拼成 /system/nav 并指定独立 template_folder

import os
from flask import Blueprint, render_template, request
from flask_login import current_user

from applications.common import curd
from applications.common.utils.http import table_api, fail_api, success_api
from applications.common.utils.rights import authorize
from applications.common.utils.validate import str_escape
from applications.extensions import db
from applications.extensions.init_limit import limiter
from applications.models import Nav
from applications.schemas import NavManageSchema

dir_path = os.path.dirname(os.path.abspath(__file__))
bp = Blueprint(
    'nav', __name__,
    url_prefix='/system/nav',
    template_folder=os.path.join(dir_path, '..', 'templates'),
)


@bp.get('/')
@authorize("system:nav:main")
def main():
    return render_template('system/nav/main.html')


@bp.get('/data')
@limiter.limit("60 per minute")
@authorize("system:nav:main")
def data():
    keyword = str_escape(request.args.get('keyword', type=str))
    category = str_escape(request.args.get('category', type=str))
    query = Nav.query.filter()
    if keyword:
        query = query.filter(
            db.or_(
                Nav.title.contains(keyword),
                Nav.url.contains(keyword),
                Nav.description.contains(keyword),
            )
        )
    if category:
        query = query.filter(Nav.category == category)
    items = query.order_by(Nav.category.asc(), Nav.sort.asc(), Nav.id.asc()).layui_paginate()
    return table_api(
        msg='请求成功',
        data=curd.model_to_dicts(schema=NavManageSchema, data=items.items),
        count=items.total,
    )
# ... 其余 CRUD 路由基本按 user.py 风格展开

⚠️ 与 framework 视图的关键区别

  • Blueprinturl_prefix 直接是 /system/nav不再注册到 framework 的 system_bp,避免对 framework 文件的依赖
  • template_folder 显式指向 plugin 自带的 templates/,与根 templates/ 解耦

6.6 数据库 Schema

模型仍然需要放在 framework 的 applications/models/admin_nav.py,因为 Flask 在 init_databases(app) 时只 import 这一目录,而且 Alembic 也只看这里做迁移:

# applications/models/admin_nav.py
class Nav(db.Model):
    __tablename__ = 'site_nav'
    id = db.Column(db.Integer, primary_key=True)
    ...

这点是 Pear Admin Flask 的设计局限:模型层不能放到 plugin。Alembic migration 也需要落到 migrations/versions/ 下。

6.7 模板文件

把模板放到 plugins/navManager/templates/system/nav/main.html

<!DOCTYPE html>
<html>
<head>
    <title>导航管理</title>
    {% include 'system/common/header.html' %}
</head>
<body class="pear-container">
<!-- 查询表单 + 表格 -->
</body>
</body>
{% include 'system/common/footer.html' %}
{% raw %}
<script type="text/html" id="col-status">...</script>
{% endraw %}
<script>
layui.use(['table', 'form', 'layer'], function () {
    var table = layui.table;
    table.render({
        elem: '#nav-table',
        url: '/system/nav/data',
        cols: [[...]],
        page: true, limit: 20,
        skin: 'line',
        text: {none: '暂无导航数据'},
    });
});
</script>

注意 footer include 必须放在所有业务 <script> 之前(这是 Pear Admin 2.x 同步加载的特性,错误顺序会 layui is not defined)。

6.8 菜单种子(Power 表)

site_nav 表的菜单项需要 seed 到 RBAC 的 Power 表:

Power(
    id=60, name='导航管理', type='1',
    code='system:nav:main',
    url='/system/nav/', open_type='_iframe',
    parent_id=64, icon='layui-icon layui-icon-link',
    enable=1, sort=1,
),
Power(
    name='导航新增', type='2', code='system:nav:add', ...
),
# 编辑 / 删除 同理

子权限的 parent_id 必须指向主菜单的 idPython seed 时可先 commit 主菜单拿到 id。

6.9 启用

PLUGIN_ENABLE_FOLDERS = ["navManager"]

启动 Flask 后访问 http://127.0.0.1:5000/system/nav/(仍需要登录),可见后台菜单「站点管理 / 导航管理」。


7. 含 before_request 钩子的插件(实操示例:siteStats)

类似访问统计这种需要监听全局请求的插件,用 event_finish 钩子:

# plugins/siteStats/__init__.py
from flask import Flask
from .view.stat import bp, record_visit


def event_init(app: Flask):
    app.register_blueprint(bp)


def event_finish(app: Flask):
    @app.before_request
    def _stat_record():
        record_visit()
# plugins/siteStats/view/stat.py
def record_visit():
    """埋点 /site/* GET 请求。"""
    if not request.path.startswith('/site'):
        return
    if request.method != 'GET':
        return
    # ... 累计 PV/UV 到 PageStat

优势:未来增加新的"统计规则"或"埋点路径"只需要改 plugin 文件,不动 framework 的 app.py


8. 含自定义 CLI 子命令的插件(实操示例:giftManager

若插件需要自定义 flask <your-cmd> 命令:

# plugins/giftManager/cli/__init__.py
from flask.cli import AppGroup

gift_cli = AppGroup('gift')


@gift_cli.command('init')
def init_db():
    """初始化兑换码管理插件的菜单和种子数据"""
    # seed Power / seed Gift data
    pass
# plugins/giftManager/__init__.py
from flask import Flask
from .cli import gift_cli
from .view.gift import bp


def event_init(app: Flask):
    app.register_blueprint(bp)


def event_finish(app: Flask):
    app.cli.add_command(gift_cli)

执行 flask gift init 即可。


9. 插件开发检查清单

下面是一份自检表,开发完成后逐项确认:

  • plugins/<name>/__init__.json 写好
  • plugins/<name>/__init__.py 定义 event_init(注册蓝图)或/和 event_finishbefore_request / CLI
  • 视图文件 view/<name>.py 中 Blueprint 的 name 全项目唯一;url_prefix/system/<xxx> 或自定其它前缀
  • 蓝图 template_folder 显式指向 plugin 内 templates,避免与 framework 模板同名冲突
  • 模型文件保留在 applications/models/admin_<name>.pyPear Admin 限制)
  • Schema 保留在 applications/schemas/admin_<name>.py
  • 权限码 seed 已写入 Power 表(type=1 主菜单 + type=2 子权限)
  • 管理员 role 的 power 列表中已挂载本插件相关权限码
  • applications/config.pyPLUGIN_ENABLE_FOLDERS 加上插件文件夹名
  • framework 中没有与该插件同名的 Blueprint 注册(避免 endpoint 冲突)
  • 模板放在 {% include 'system/common/footer.html' %} 之前 + <script>layui.use(...) 之后 仍然运行正常
  • 用 Playwright/Selenium 验证一次页面渲染(控制台无 layui is not defined
  • alembic migration 已生成(如果模型新增列):flask db migrate -m "..." + flask db upgrade

10. 常见问题(FAQ

Q1:出现 ValueError: The name 'xxx' is already registered for this blueprint

答:framework 里有个同名 Blueprint 已注册 + 当前 plugin 又注册了同名 Blueprint。检查:

  1. applications/view/system/__init__.pyregister_system_bps 是否仍包含迁移前的 bp 注册
  2. extensions/__init__.py:33-34 早期版本同时调用了 event_init / event_finish,后又由 init_bps / init_script 重复触发——必须删除其中一处(建议只在 view/__init__.py 触发 event_init,只在 script/__init__.py 触发 event_finish)。

Q2event_finish 调用了两次,before_request 钩子被注册两次

答:同 Q1,问题来源 + 修复同。

Q3:菜单在前台后台不显示 / 点菜单 403

答:检查 applications/models/admin_power.py 的 Power 表里菜单的 codeparent_id 是否正确;管理员 role 的 power 关联表里是否绑定了相关 code。

Q4:模板渲染找不到文件 TemplateNotFound

答:检查 Blueprint 实例化时是否显式设置 template_folder=os.path.join(dir_path, '..', 'templates'),不要省略。

Q5:插件导入顺序错误(a 插件 import 时触发 b 插件未加载)

答:把"a"放 PLUGIN_ENABLE_FOLDERS 更前面。Pear Admin 的 loader 通过 importlib.import_module 顺序装载。

Q6:插件里如何获取当前请求

答:直接 request 对象或 flask.session;访问数据库用 with app.app_context(): 或在 event_context 函数体内。

Q7:插件改动后必须重启 Flask 吗?

答:是的。所有 __init__.pyevent_* 函数、Blueprint 注册只发生在进程启动时。模板可用 TEMPLATES_AUTO_RELOAD=True 动态生效。


11. 插件模式 vs framework 内置

维度 plugin 内置 framework 内置
升级 pear-admin-framework 后是否受影响
删除即下线的便利性 注释 PLUGIN_ENABLE_FOLDERS 即可 改 framework 才能下
团队协作时移植到新项目 直接复制 plugins/ 整目录 diff 框架代码
适合场景 大多数业务模块 不变的用户/角色/权限/部门等基础模块

结论:除非改动涉及 framework 自身的核心能力(如重写 @authorize 实现),否则一律走 plugin。这正是 Pear Admin Flask 推荐的最佳实践。


12. 当前仓库已实现的插件

插件 文件夹 用途 状态
导航管理 plugins/navManager/ 后台 CRUD nav 表;前台 /site/ 入口 + 类目聚合走 framework 的 public_nav
友情链接 plugins/friendManager/ 后台 CRUD friend 表;前台 /site/friend
关于本站 plugins/aboutManager/ 后台编辑表单 + 保存;前台 /site/about 渲染
访问统计 plugins/siteStats/ 后台 PV/UV 主页 + 3 个 JSON 接口 + 全站 /site/* 埋点
兑换码示例 plugins/giftManager/ 官方示例,需要 flask gift init 初始化 (未启用)
Hello World plugins/helloworld/ 官方示例 (未启用)
Replace Page plugins/replacePage/ framework 页面替换示例 (未启用)
Real IP plugins/realip/ Flask 上下文修改示例 (未启用)

启用列表见 applications/config.pyPLUGIN_ENABLE_FOLDERS


13. 首次启用 nav / friend / about / stat 时的手动操作

这些插件依赖数据库模型和权限码已经在 framework 中存在。首次部署到新环境:

  1. flask db migrate -m "add admin_nav / admin_friend / admin_about / admin_stat tables"(如已存在表跳过)
  2. flask db upgrade
  3. 为 site_nav / site_about / site_friend 注册菜单权限(Power)记录:
    # 用 Flask shell 或在 admin.py 中 seed
    Power(name='导航管理', type='1', code='system:nav:main', url='/system/nav/', open_type='_iframe', parent_id=1, ...)
    # ... 类似地注册 site:friend:main / site:about:main / site:stats:main 与子权限
    
  4. 把上述 code 加到 admin role 的 power 列表
  5. PLUGIN_ENABLE_FOLDERS 写好四项
  6. 重启 Flask,用 admin / 123456 登录后台,可见新增菜单

14. 进阶:把现有 framework 业务迁出到 plugin 的步骤

如果有一天希望把用户管理(applications/view/system/user.py)也从 framework 迁移到 plugin,按以下顺序:

  1. 复制 applications/view/system/user.py 内容到 plugins/userManager/view/user.py
  2. 修改 Blueprinturl_prefix='/system/user'、加 template_folder=...
  3. 复制 templates/system/user/plugins/userManager/templates/system/user/
  4. applications/view/system/__init__.py 删除 user_bp 的 import 和 system_bp.register_blueprint(user_bp) 一行
  5. 删除 applications/view/system/user.pyapplications/templates/system/user/(如果都已迁出)
  6. PLUGIN_ENABLE_FOLDERS = [..., "userManager"]
  7. 重启验证

⚠️ 注意保留模型和 Schema 不动。


文档结束。