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

525 lines
19 KiB
Markdown
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.
# Pear Admin Flask 插件开发操作手册
> 适用版本:Pear Admin Flask master 分支(fork 自 lab.lovepikachu.top 文档站点的最新文档)
>
> 文档维护:根据 plugins/<name>/ 下的实际插件实现同步更新
Pear Admin Flask 的插件机制是其核心竞争力——**不修改框架代码即可扩展新页面、新菜单、新数据埋点**。
本文以仓库中已落地的 `navManager` / `friendManager` / `aboutManager` / `siteStats` 四个插件为依据,整理出从设计到上线的完整操作步骤。
---
## 1. 插件是什么
| 概念 | framework(框架) | plugin(插件) |
|---|---|---|
| 代码位置 | `applications/``templates/system/` 之外的共用代码 | `plugins/<pluginName>/` 下,与 framework 解耦 |
| 启用方式 | 自动加载 | 在 `applications/config.py``PLUGIN_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` 类尾部的配置项:
```python
# ---- 插件 ----
# 站点业务模块(导航 / 友链 / 关于 / 访问统计)均通过 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_init``event_finish`,前者注册蓝图、后者挂 before_request / CLI**不能**用一次事件全做——否则会与 framework 自身的广播节奏冲突。
---
## 5. 最小可运行示例:Hello World(来自仓库 plugins/helloworld/
### 5.1 `plugins/helloworld/__init__.py`
```python
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`
```python
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 启用
```python
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 创建插件骨架
```bash
mkdir -p plugins/navManager/{view,templates/system/nav}
```
### 6.3 写 `plugins/navManager/__init__.json`
```json
{
"plugin_name": "导航管理",
"plugin_version": "1.0.0",
"plugin_description": "后台导航管理模块。对 nav 表的增删改查、分页查询、关键字 + 分类过滤、启用/禁用切换。"
}
```
### 6.4 写 `plugins/navManager/__init__.py`
```python
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`
```python
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 视图的关键区别**
- `Blueprint``url_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 也只看这里做迁移:
```python
# 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`
```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` 表:
```python
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 启用
```python
PLUGIN_ENABLE_FOLDERS = ["navManager"]
```
启动 Flask 后访问 `http://127.0.0.1:5000/system/nav/`(仍需要登录),可见后台菜单「站点管理 / 导航管理」。
---
## 7. 含 before_request 钩子的插件(实操示例:siteStats)
类似访问统计这种需要监听全局请求的插件,用 `event_finish` 钩子:
```python
# 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()
```
```python
# 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>` 命令:
```python
# 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
```
```python
# 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_finish`before_request / CLI
- [ ] 视图文件 `view/<name>.py` 中 Blueprint 的 `name` 全项目唯一;`url_prefix``/system/<xxx>` 或自定其它前缀
- [ ] 蓝图 `template_folder` 显式指向 plugin 内 templates,避免与 framework 模板同名冲突
- [ ] 模型文件保留在 `applications/models/admin_<name>.py`Pear Admin 限制)
- [ ] Schema 保留在 `applications/schemas/admin_<name>.py`
- [ ] 权限码 seed 已写入 Power 表(type=1 主菜单 + type=2 子权限)
- [ ] 管理员 role 的 power 列表中已挂载本插件相关权限码
- [ ] `applications/config.py``PLUGIN_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__.py``register_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`)。
### Q2`event_finish` 调用了两次,before_request 钩子被注册两次
答:同 Q1,问题来源 + 修复同。
### Q3:菜单在前台后台不显示 / 点菜单 403
答:检查 `applications/models/admin_power.py` 的 Power 表里菜单的 `code``parent_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__.py``event_*` 函数、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.py``PLUGIN_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)记录:
```python
# 用 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. 修改 Blueprint`url_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.py` 与 `applications/templates/system/user/`(如果都已迁出)
6. `PLUGIN_ENABLE_FOLDERS = [..., "userManager"]`
7. 重启验证
> ⚠️ 注意保留模型和 Schema 不动。
---
文档结束。