diff --git a/applications/config.py b/applications/config.py index b5369f0..6f5b4a2 100644 --- a/applications/config.py +++ b/applications/config.py @@ -72,7 +72,7 @@ class BaseConfig: MAIL_DEFAULT_SENDER = MAIL_USERNAME # 插件配置,填写插件的文件名名称,默认不启用插件。 - PLUGIN_ENABLE_FOLDERS = [] + PLUGIN_ENABLE_FOLDERS = ["giftManager"] # Session 设置 PERMANENT_SESSION_LIFETIME = timedelta(days=7) diff --git a/docs/source/_static/插件目录规划.png b/docs/source/_static/插件目录规划.png new file mode 100644 index 0000000..4a48ba7 Binary files /dev/null and b/docs/source/_static/插件目录规划.png differ diff --git a/docs/source/_static/添加菜单.png b/docs/source/_static/添加菜单.png new file mode 100644 index 0000000..8b001dd Binary files /dev/null and b/docs/source/_static/添加菜单.png differ diff --git a/docs/source/practices/backend.rst b/docs/source/practices/backend.rst index 00bcdcd..b5ac66f 100644 --- a/docs/source/practices/backend.rst +++ b/docs/source/practices/backend.rst @@ -493,3 +493,8 @@ return success_api(msg="编辑成功") except Exception as e: return fail_api(msg="编辑失败") + +.. note:: + + 插件方式接入项目,请查看 :ref:`以插件的方式接入项目` 章节。 + diff --git a/docs/source/practices/frontend.rst b/docs/source/practices/frontend.rst index 791ca48..02fce3e 100644 --- a/docs/source/practices/frontend.rst +++ b/docs/source/practices/frontend.rst @@ -553,5 +553,7 @@ Pear Admin Layui 的控制主题色逻辑是通过设置全局的 css 属性:` 对应的视图函数,可以查看 :ref:`编写修改视图函数` 章节。 +.. note:: + 插件方式接入项目,请查看 :ref:`以插件的方式接入项目` 章节。 diff --git a/docs/source/practices/plugin.rst b/docs/source/practices/plugin.rst index a2590c0..0d17a7f 100644 --- a/docs/source/practices/plugin.rst +++ b/docs/source/practices/plugin.rst @@ -3,7 +3,8 @@ 插件功能旨在最大限度不修改原框架的前提下添加新功能,并可以像程序原有框架一样进行流程注册,而且不需要修改任何程序框架原有代码(仅在配置文件中设置即可)。 -所有插件放置在 `plugins` 文件夹中,项目提供了三个示例插件,分别是 `helloworld` 、 `realip` 和 `replacePage` ,分别用于示例页面的注册、修改 Flask 上下文和页面替换。 +所有插件放置在 `plugins` 文件夹中,项目提供了三个示例插件,分别是 `helloworld` 、 `realip` 、 `replacePage` 和 `giftManager`, +分别用于示例页面的注册、修改 Flask 上下文、页面替换和新功能接入。 像项目自带的用户管理、部门管理等基本功能属于程序自身的“功能插件”,对于大多数衍生项目来说,多的是修改字符串和删除部分不需要的功能, 而插件开发主要可以用于添加自己的视图函数和功能,可以完美于项目融合,增加可拓展性,尤其是用于既想保留原项目功能又想填写新功能的项目开发。 @@ -17,8 +18,8 @@ PLUGIN_ENABLE_FOLDERS = [] -而在目录 `plugins` 中,你会发现存在 文件夹名称 为 `helloworld` 、 `realip` 和 `replacePage` 三个插件。比如我们想要启用 `helloworld` 插件, -仅需要做如下修改: +而在目录 `plugins` 中,你会发现存在 文件夹名称 为 `helloworld` 、 `realip` 、 `replacePage` 和 `giftManager` 三个插件。 +比如我们想要启用 `helloworld` 插件,仅需要做如下修改: .. code-block:: python @@ -48,6 +49,18 @@ | +.. note:: + + 对于 `giftManager` 插件,需要在启用的情况下,先使用 `flask gift init` 来初始化其数据库。假设项目已经搭建完成(已经初始化数据库),需要进行以下步骤: + + .. code-block:: bash + + flask db migrate + flask db upgrade + flask gift init + + 该命令行用于创建新 admin_gift 表并写入数据。 + 插件的目录架构 ------------------- @@ -149,4 +162,216 @@ 在编写插件的前端模板(template)时,请尽量将模板文件放在项目根目录的 `templates` 文件中,这样可以保持良好的项目架构。 当然另一种做法是像 helloworld 插件那样,直接放在插件目录的 templates 中,但是一定要做好模板名称的区分, 因为 flask 默认找模板行为是从根目录开始找的,如果根目录 templates 和插件目录的 templates 中存在的模板重名, - 则会优先使用根目录 templates 的模板文件。 \ No newline at end of file + 则会优先使用根目录 templates 的模板文件。 + +.. _以插件的方式接入项目: + +以插件的方式接入项目 +--------------------------- + +在 :ref:`简单前端页面示例` 和 :ref:`后端页面编写` 章节中,我们编写了新的管理页面。接下来,我们将其改为插件接入,获得更高的拓展性。 + +首先在 `plugins` 新建一个名为 `giftManager` 的文件夹,将兑换码管理页面相关的功能一五一十的照搬到这个文件夹中。 + +我们可以规划如下的目录架构: + +| + +.. image:: ../_static/插件目录规划.png + :align: center + +| + +入口文件 +~~~~~~~~~~~~~~ + +入口文件相对简单, + +.. code-block:: python + + 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) + +在 `event_init` 事件中注册相关蓝图(页面),在 `event_finish` 中,注册了初始化数据库的命令。 + +注册初始化数据库命令 +~~~~~~~~~~~~~~~~~~~~~~~~ + +一个合理的设想是,我已经搭建了 Pear Admin Flask 原项目,而之后我想要加入新的功能,新功能中存在添加权限管理等数据库的操作, +所以我们想使用其他的初始化命令,来新增数据库记录行。 + +.. code-block:: python + + import datetime + + from flask.cli import AppGroup + + from ..models import Gift + from applications.models import Power + from applications.extensions import db + + gift_cli = AppGroup("gift") + + now_time = datetime.datetime.now() + + powerdata = [ + Power( + name='兑换码添加', + type='2', + code='system:gift:add', + url='', + open_type='', + parent_id='60', + icon='', + sort=0, + create_time=now_time, + enable=1 + ), Power( + name='兑换码删除', + type='2', + code='system:gift:remove', + url='', + open_type='', + parent_id='60', + icon='', + sort=0, + create_time=now_time, + enable=1 + ), Power( + name='兑换码编辑', + type='2', + code='system:gift:edit', + url='', + open_type='', + parent_id='60', + icon='', + sort=0, + create_time=now_time, + enable=1 + ) + ] + + giftdata = [ + Gift( + id=0, + key='myTestCode', + content='8折优惠', + enable=1, + used=0, + create_at=now_time + ), + Gift( + id=1, + key='DisableCode', + content='1折优惠', + enable=0, + used=0, + create_at=now_time + ) + ] + + + @gift_cli.command("init") + def init_db(): + print("存入兑换码管理页面数据") + + top_power = Power( + name='兑换码管理', + type='1', + code='system:gift:main', + url='/system/gift/', + open_type='_iframe', + parent_id='1', + icon='layui-icon layui-icon layui-icon layui-icon-diamond', + sort=8, + create_time=now_time, + enable=1 + ) + + db.session.add(top_power) + db.session.commit() # 提交了才有 id + + for i in range(len(powerdata)): + powerdata[i].parent_id = top_power.id + + db.session.add_all(powerdata) + + db.session.add_all(giftdata) + db.session.commit() + + +上述代码中,主要说明一下 `top_power` 的做法,由于兑换码管理需要新的菜单(权限),但是新的菜单在添加之前可能已经存在了其他菜单, +我们并不知道新菜单的 ID 是什么,所以需要让主菜单(top_power)先添加,并获取到其 ID 而后才可以把子菜单添加进去。 + +| + +.. image:: ../_static/添加菜单.png + :align: center + +| + +.. note:: + + 要获取添加菜单的 ID 必须先 commit 一次数据库。 + +随后我们可以使用 `flask gift init` 来初始化兑换码管理的数据库数据。 + +注册蓝图 +~~~~~~~~~~~~ + +注册蓝图部分和设计一般前端页面差不多,只不过要注意正确把模板文件夹的路径提供给蓝图进行初始化。 + +.. code-block:: python + + import os + + from flask import Blueprint, request, render_template + + from ..models import Gift + from ..schemas import GiftSchema + from applications.extensions import db + + from applications.common.helper import ModelFilter + from applications.common.curd import enable_status, disable_status, delete_one_by_id, get_one_by_id + from applications.common.utils.http import table_api, success_api, fail_api + from applications.common.utils.rights import authorize + + # 获取插件所在的目录(结尾没有分割符号) + dir_path = os.path.dirname(__file__).replace("\\", "/") + + bp = Blueprint('gift', __name__, url_prefix='/system/gift', + template_folder=dir_path + '/../templates') + +上述代码中,获取了 `dir_path` 并指定了上一级目录的 `templates` 为该蓝图的模板文件夹。另外,对于 `url_prefix` ,在统一规划下, +我们建议将 gift 路由放在 `system` 下,在 :ref:`后端页面编写` 中,我们似乎并没有指定 `system` 这是因为当时注册蓝图的时候是在 system_bp 的蓝图中注册的: + +.. code-block:: python + + system_bp = Blueprint('system', __name__, url_prefix='/system') # 但是是在这个蓝图下注册的 + + + def register_system_bps(app: Flask): + ... + system_bp.register_blueprint(gift_bp) + app.register_blueprint(index_bp) + app.register_blueprint(system_bp) + +我们不能通过正常方法拿到 system_bp 就只能使用指定路由在 `system` 下了,唯一的缺点就是不能使用 “system.gift.\*” 来指定路由中的函数,而是要使用 “gift.\*” . + +.. code-block:: + + from flask import Flask, url_for + + url_for("system.gift.index") # 不能这样写 + url_for("gift.index") # 要这样写 + diff --git a/plugins/giftManager/__init__.py b/plugins/giftManager/__init__.py new file mode 100644 index 0000000..b0ad05b --- /dev/null +++ b/plugins/giftManager/__init__.py @@ -0,0 +1,12 @@ +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) diff --git a/plugins/giftManager/cli/__init__.py b/plugins/giftManager/cli/__init__.py new file mode 100644 index 0000000..84a847c --- /dev/null +++ b/plugins/giftManager/cli/__init__.py @@ -0,0 +1,96 @@ +import datetime + +from flask.cli import AppGroup + +from ..models import Gift +from applications.models import Power +from applications.extensions import db + +gift_cli = AppGroup("gift") + +now_time = datetime.datetime.now() + +powerdata = [ + Power( + name='兑换码添加', + type='2', + code='system:gift:add', + url='', + open_type='', + parent_id='60', + icon='', + sort=0, + create_time=now_time, + enable=1 + ), Power( + name='兑换码删除', + type='2', + code='system:gift:remove', + url='', + open_type='', + parent_id='60', + icon='', + sort=0, + create_time=now_time, + enable=1 + ), Power( + name='兑换码编辑', + type='2', + code='system:gift:edit', + url='', + open_type='', + parent_id='60', + icon='', + sort=0, + create_time=now_time, + enable=1 + ) +] + +giftdata = [ + Gift( + id=0, + key='myTestCode', + content='8折优惠', + enable=1, + used=0, + create_at=now_time + ), + Gift( + id=1, + key='DisableCode', + content='1折优惠', + enable=0, + used=0, + create_at=now_time + ) +] + + +@gift_cli.command("init") +def init_db(): + print("存入兑换码管理页面数据") + + top_power = Power( + name='兑换码管理', + type='1', + code='system:gift:main', + url='/system/gift/', + open_type='_iframe', + parent_id='1', + icon='layui-icon layui-icon layui-icon layui-icon-diamond', + sort=8, + create_time=now_time, + enable=1 + ) + + db.session.add(top_power) + db.session.commit() # 提交了才有 id + + for i in range(len(powerdata)): + powerdata[i].parent_id = top_power.id + + db.session.add_all(powerdata) + + db.session.add_all(giftdata) + db.session.commit() diff --git a/plugins/giftManager/models/__init__.py b/plugins/giftManager/models/__init__.py new file mode 100644 index 0000000..d6721b1 --- /dev/null +++ b/plugins/giftManager/models/__init__.py @@ -0,0 +1,12 @@ +import datetime +from applications.extensions import db + + +class Gift(db.Model): + __tablename__ = 'admin_gift' + id = db.Column(db.Integer, primary_key=True, comment="唯一ID") + key = db.Column(db.String(50), comment="兑换码") + content = db.Column(db.String(), comment="具体内容") + enable = db.Column(db.Integer, default=0, comment='是否启用') + used = db.Column(db.Integer, default=0, comment='是否已经使用') + create_at = db.Column(db.DateTime, default=datetime.datetime.now, comment='创建时间') \ No newline at end of file diff --git a/plugins/giftManager/schemas/__init__.py b/plugins/giftManager/schemas/__init__.py new file mode 100644 index 0000000..c612575 --- /dev/null +++ b/plugins/giftManager/schemas/__init__.py @@ -0,0 +1,12 @@ +from flask_marshmallow.sqla import SQLAlchemyAutoSchema + +from ..models import Gift + + +class GiftSchema(SQLAlchemyAutoSchema): + class Meta: + model = Gift # table = models.Album.__table__ + # include_relationships = True # 输出模型对象时同时对外键,是否也一并进行处理 + include_fk = True # 序列化阶段是否也一并返回主键 + # fields= ["id","name"] # 启动的字段列表 + # exclude = ["id","name"] # 排除字段列表 diff --git a/plugins/giftManager/templates/system/gift/add.html b/plugins/giftManager/templates/system/gift/add.html new file mode 100644 index 0000000..3609960 --- /dev/null +++ b/plugins/giftManager/templates/system/gift/add.html @@ -0,0 +1,84 @@ + + + + 兑换码添加 + {% include 'system/common/header.html' %} + + +
+
+
+
+ + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+
+
+
+
+ + +
+
+
+{% include 'system/common/footer.html' %} + + + + \ No newline at end of file diff --git a/plugins/giftManager/templates/system/gift/edit.html b/plugins/giftManager/templates/system/gift/edit.html new file mode 100644 index 0000000..30ac3dc --- /dev/null +++ b/plugins/giftManager/templates/system/gift/edit.html @@ -0,0 +1,91 @@ + + + + 兑换码编辑 + {% include 'system/common/header.html' %} + + +
+
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+
+
+
+
+ + +
+
+
+{% include 'system/common/footer.html' %} + + + + \ No newline at end of file diff --git a/plugins/giftManager/templates/system/gift/main.html b/plugins/giftManager/templates/system/gift/main.html new file mode 100644 index 0000000..c654202 --- /dev/null +++ b/plugins/giftManager/templates/system/gift/main.html @@ -0,0 +1,205 @@ + + + + 兑换码管理 + {% include 'system/common/header.html' %} + + + +{# 查询表单 #} +
+
+
+
+ +
+ +
+ + +
+
+
+
+ +{# 用户表格 #} +
+
+
+
+
+
+
+ + + +{% raw %} + + + + + +{% endraw %} + + + + + + + + +{% include 'system/common/footer.html' %} + + + \ No newline at end of file diff --git a/plugins/giftManager/view/gift.py b/plugins/giftManager/view/gift.py new file mode 100644 index 0000000..cc4dfd2 --- /dev/null +++ b/plugins/giftManager/view/gift.py @@ -0,0 +1,142 @@ +import os + +from flask import Blueprint, request, render_template + +from ..models import Gift +from ..schemas import GiftSchema +from applications.extensions import db + +from applications.common.helper import ModelFilter +from applications.common.curd import enable_status, disable_status, delete_one_by_id, get_one_by_id +from applications.common.utils.http import table_api, success_api, fail_api +from applications.common.utils.rights import authorize + +# 获取插件所在的目录(结尾没有分割符号) +dir_path = os.path.dirname(__file__).replace("\\", "/") + +bp = Blueprint('gift', __name__, url_prefix='/system/gift', + template_folder=dir_path + '/../templates') + + +@bp.get('/') +@authorize("system:gift:main") +def index(): + return render_template('system/gift/main.html') + + +@bp.get('/add') +@authorize("system:gift:main") +def add(): + return render_template('system/gift/add.html') + + +@bp.get('/data') +@authorize("system:gift:main") +def data(): + key = request.args.get('key', type=str) + + mf = ModelFilter() + if key: + mf.vague('key', key) # 模糊查询 + + data, total, page, limit = db.session.query(Gift).filter(mf.get_filter(Gift)).layui_paginate_json(GiftSchema) + + return table_api( + data=data, + count=total, + limit=limit + ) + + +@bp.put('/enable') +@authorize("system:gift:edit") +def enable_api(): + data = request.get_json(force=True) + + if enable_status(Gift, data.get('id')): + return success_api(msg="启用成功") + + return success_api(msg="启用失败") + + +@bp.put('/disable') +@authorize("system:gift:edit") +def disable_api(): + req_json = request.get_json(force=True) + + if disable_status(Gift, req_json.get('id')): + return success_api(msg="禁用成功") + + return success_api(msg="禁用失败") + + +@bp.delete('/remove/') +@authorize("system:gift:remove") +def remove_api(_id): + if delete_one_by_id(Gift, _id): + return success_api(msg="删除成功") + + return success_api(msg="删除失败") + + +@bp.post('/save') +@authorize("system:gift:add", log=True) +def save(): + req_json = request.get_json(force=True) + + data = { + 'key': req_json.get('key'), + 'content': req_json.get('content'), + 'enable': req_json.get('enable'), + 'used': 0 + } + + # 效验参数 + if not all(list(data.keys())): + return fail_api(msg="参数不全") + + if not data['enable'].isdigit(): + return fail_api(msg="参数 enable 错误") + + try: + db.session.add(Gift(**data)) + db.session.commit() + return success_api(msg="添加成功") + except Exception as e: + return fail_api(msg="添加失败") + + +@bp.get('/edit/') +@authorize("system:gift:edit", log=True) +def edit(_id): + gift = get_one_by_id(Gift, _id) + return render_template('system/gift/edit.html', gift=gift) + + +@bp.post('/update') +@authorize("system:gift:edit", log=True) +def update(): + req_json = request.get_json(force=True) + + _id = req_json.get('id') + + data = { + 'key': req_json.get('key'), + 'content': req_json.get('content'), + 'enable': req_json.get('enable'), + 'used': 0 + } + + # 效验参数 + if not all(list(data.keys())): + return fail_api(msg="参数不全") + + if not data['enable'].isdigit(): + return fail_api(msg="参数 enable 错误") + + try: + db.session.query(Gift).filter(Gift.id == _id).update(data) + db.session.commit() + return success_api(msg="编辑成功") + except Exception as e: + return fail_api(msg="编辑失败")