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_*)
This commit is contained in:
bwstudio
2026-09-06 18:18:06 +08:00
parent 1ea69c2c34
commit 0ffc103e46
53 changed files with 4705 additions and 89 deletions
+524
View File
@@ -0,0 +1,524 @@
# 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 不动。
---
文档结束。