From 5153daf7da8ca6e5b939aa07cd503105e366e6d3 Mon Sep 17 00:00:00 2001 From: wojiaoyishang Date: Mon, 27 Jan 2025 19:48:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 +- applications/common/curd.py | 87 ++++-- applications/common/helper.py | 103 ++++--- applications/extensions/init_sqlalchemy.py | 10 +- docs/function.md | 279 ------------------ docs/model.md | 50 ---- docs/plugin.md | 47 --- docs/{assets => source/_static}/1.jpg | Bin docs/{assets => source/_static}/2.jpg | Bin docs/{assets => source/_static}/3.jpg | Bin docs/{assets => source/_static}/4.jpg | Bin docs/{assets => source/_static}/5.jpg | Bin docs/{assets => source/_static}/6.jpg | Bin docs/source/_static/helloworld.png | Bin 0 -> 23768 bytes docs/source/_static/plugin_run.png | Bin 0 -> 11529 bytes docs/{assets => source/_static}/qqgroup.jpg | Bin docs/{assets => source/_static}/官网地址.jpg | Bin docs/{assets => source/_static}/源码仓库.jpg | Bin docs/{assets => source/_static}/界面演示.jpeg | Bin docs/source/function/admin.rst | 28 +- docs/source/function/curd.rst | 90 ++++++ docs/source/function/helper.rst | 121 ++++++++ docs/source/function/index.rst | 2 + docs/source/function/utils/cache.rst | 2 +- docs/source/function/utils/captcha.rst | 2 +- docs/source/function/utils/http.rst | 22 +- docs/source/function/utils/index.rst | 2 +- docs/source/function/utils/mail.rst | 27 +- docs/source/function/utils/rights.rst | 6 +- docs/source/function/utils/upload.rst | 2 +- docs/source/function/utils/validate.rst | 2 +- docs/source/index.rst | 6 + docs/source/practices/index.rst | 9 + docs/source/practices/plugin.rst | 129 ++++++++ docs/source/practices/trick.rst | 199 +++++++++++++ plugins/helloworld/__init__.py | 3 +- plugins/helloworld/main.py | 8 +- plugins/realip/__init__.py | 27 +- plugins/realip/console.py | 89 ------ 39 files changed, 767 insertions(+), 593 deletions(-) delete mode 100644 docs/function.md delete mode 100644 docs/model.md delete mode 100644 docs/plugin.md rename docs/{assets => source/_static}/1.jpg (100%) rename docs/{assets => source/_static}/2.jpg (100%) rename docs/{assets => source/_static}/3.jpg (100%) rename docs/{assets => source/_static}/4.jpg (100%) rename docs/{assets => source/_static}/5.jpg (100%) rename docs/{assets => source/_static}/6.jpg (100%) create mode 100644 docs/source/_static/helloworld.png create mode 100644 docs/source/_static/plugin_run.png rename docs/{assets => source/_static}/qqgroup.jpg (100%) rename docs/{assets => source/_static}/官网地址.jpg (100%) rename docs/{assets => source/_static}/源码仓库.jpg (100%) rename docs/{assets => source/_static}/界面演示.jpeg (100%) create mode 100644 docs/source/function/curd.rst create mode 100644 docs/source/function/helper.rst create mode 100644 docs/source/practices/index.rst create mode 100644 docs/source/practices/plugin.rst create mode 100644 docs/source/practices/trick.rst delete mode 100644 plugins/realip/console.py diff --git a/README.md b/README.md index 9b3953c..79e85b7 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ 开 箱 即 用 的 Flask 快 速 开 发 平 台 - [预览](https://pear.lovepikachu.top/) | [官网](http://www.pearadmin.com/) | [群聊](docs/assets/qqgroup.jpg) | [文档](docs/detail.md) + [预览](https://pear.lovepikachu.top/) | [官网](http://www.pearadmin.com/) | [群聊](docs/source/_static/qqgroup.jpg) | [文档](docs/detail.md)

@@ -221,9 +221,9 @@ docker-compose -f dockercompose.yaml down | | | | ---------------------- | ---------------------- | -| ![](docs/assets/1.jpg) | ![](docs/assets/2.jpg) | -| ![](docs/assets/3.jpg) | ![](docs/assets/4.jpg) | -| ![](docs/assets/5.jpg) | ![](docs/assets/6.jpg) | +| ![](docs/source/_static/1.jpg) | ![](docs/source/_static/2.jpg) | +| ![](docs/source/_static/3.jpg) | ![](docs/assets/4.jpg) | +| ![](ddocs/source/_static/5.jpg) | ![](docs/source/_static/6.jpg) | # 其他说明 diff --git a/applications/common/curd.py b/applications/common/curd.py index 9a4f16c..95a8909 100644 --- a/applications/common/curd.py +++ b/applications/common/curd.py @@ -1,31 +1,39 @@ import datetime - from marshmallow import Schema from marshmallow_sqlalchemy import SQLAlchemyAutoSchema - from applications.extensions import db, ma class LogicalDeleteMixin(object): """ - class Test(db.Model,LogicalDeleteMixin): - __tablename__ = 'admin_test' - id = db.Column(db.Integer, primary_key=True, comment='角色ID') + 逻辑删除混入类,为模型提供软删除功能。 - Test.query.filter_by(id=1).soft_delete() - Test.query.logic_all() + 示例: + class Test(db.Model, LogicalDeleteMixin): + __tablename__ = 'admin_test' + id = db.Column(db.Integer, primary_key=True, comment='角色ID') + + # 软删除 + Test.query.filter_by(id=1).soft_delete() + + # 查询所有未删除的记录 + Test.query.logic_all() """ create_at = db.Column(db.DateTime, default=datetime.datetime.now, comment='创建时间') - update_at = db.Column(db.DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment='创建时间') + update_at = db.Column(db.DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment='更新时间') delete_at = db.Column(db.DateTime, comment='删除时间') def auto_model_jsonify(data, model: db.Model): """ - 不需要建立schemas,直接使用orm的定义模型进行序列化 - 基本功能,待完善 - 示例 - power_data = curd.auto_model_jsonify(model=Dept, data=dept) + 自动序列化模型数据为 JSON 格式,无需手动定义 Schema。 + + 示例: + power_data = curd.auto_model_jsonify(model=Dept, data=dept) + + :param data: 需要序列化的 SQLAlchemy 查询结果。 + :param model: SQLAlchemy 模型类。 + :return: 返回序列化后的 JSON 数据。 """ def get_model(): return model @@ -33,49 +41,60 @@ def auto_model_jsonify(data, model: db.Model): class AutoSchema(SQLAlchemyAutoSchema): class Meta(Schema): model = get_model() - include_fk = True - include_relationships = True - load_instance = True + include_fk = True # 包含外键 + include_relationships = True # 包含关联关系 + load_instance = True # 反序列化时加载为模型实例 - common_schema = AutoSchema(many=True) # 用已继承ma.ModelSchema类的自定制类生成序列化类 + common_schema = AutoSchema(many=True) # 支持序列化多个对象 output = common_schema.dump(data) return output def model_to_dicts(schema: ma.Schema, data): """ - :param schema: schema类 - :param model: sqlalchemy查询结果 - :return: 返回单个查询结果 + 使用指定的 Schema 序列化 SQLAlchemy 查询结果。 + + :param schema: Marshmallow Schema 类。 + :param data: SQLAlchemy 查询结果。 + :return: 返回序列化后的数据,返回字典。 """ - # 如果是分页器返回,需要传入model.items - common_schema = schema(many=True) # 用已继承ma.ModelSchema类的自定制类生成序列化类 - output = common_schema.dump(data) # 生成可序列化对象 + common_schema = schema(many=True) # 支持序列化多个对象 + output = common_schema.dump(data) return output def get_one_by_id(model: db.Model, id): """ - :param model: 模型类 - :param id: id - :return: 返回单个查询结果 + 根据 ID 查询单个记录。 + + :param model: SQLAlchemy 模型类。 + :param id: 记录的主键 ID。 + :return: 返回查询到的记录,如果未找到则返回 None。 """ return model.query.filter_by(id=id).first() def delete_one_by_id(model: db.Model, id): """ - :param model: 模型类 - :param id: id - :return: 返回单个查询结果 + 根据 ID 删除单个记录。 + + :param model: SQLAlchemy 模型类。 + :param id: 记录的主键 ID。 + :return: 返回删除操作影响的行数。 """ r = model.query.filter_by(id=id).delete() db.session.commit() return r -# 启动状态 def enable_status(model: db.Model, id): + """ + 启用指定 ID 的记录。 + + :param model: SQLAlchemy 模型类。 + :param id: 记录的主键 ID。 + :return: 如果操作成功返回 True,否则返回 False。 + """ enable = 1 role = model.query.filter_by(id=id).update({"enable": enable}) if role: @@ -84,11 +103,17 @@ def enable_status(model: db.Model, id): return False -# 停用状态 def disable_status(model: db.Model, id): + """ + 停用指定 ID 的记录。 + + :param model: SQLAlchemy 模型类。 + :param id: 记录的主键 ID。 + :return: 如果操作成功返回 True,否则返回 False。 + """ enable = 0 role = model.query.filter_by(id=id).update({"enable": enable}) if role: db.session.commit() return True - return False + return False \ No newline at end of file diff --git a/applications/common/helper.py b/applications/common/helper.py index da69071..6b82cb8 100644 --- a/applications/common/helper.py +++ b/applications/common/helper.py @@ -1,112 +1,131 @@ from sqlalchemy import and_ - from applications.extensions import db class ModelFilter: """ - orm多参数构造器 - """ - filter_field = {} - filter_list = [] + ORM 多条件查询构造器,支持多种查询条件组合。 - type_exact = "exact" - type_neq = "neq" - type_greater = "greater" - type_less = "less" - type_vague = "vague" - type_contains = "contains" - type_between = "between" + 示例: + mf = ModelFilter() + mf.exact('name', 'John') + mf.vague('email', 'example.com') + query = User.query.filter(mf.get_filter(User)) + """ + filter_field = {} # 存储字段过滤条件 + filter_list = [] # 存储最终的过滤条件列表 + + # 查询类型常量 + type_exact = "exact" # 精确匹配 + type_neq = "neq" # 不等于 + type_greater = "greater" # 大于 + type_less = "less" # 小于 + type_vague = "vague" # 模糊匹配 + type_contains = "contains" # 包含 + type_between = "between" # 范围查询 def __init__(self): + """初始化过滤条件存储字典和列表。""" self.filter_field = {} self.filter_list = [] def exact(self, field_name, value): """ - 准确查询字段 - :param field_name: 模型字段名称 - :param value: 值 + 添加精确匹配条件。 + + :param field_name: 模型字段名称。 + :param value: 匹配的值。 """ if value and value != '': self.filter_field[field_name] = {"data": value, "type": self.type_exact} def neq(self, field_name, value): """ - 不等于查询字段 - :param field_name: 模型字段名称 - :param value: 值 + 添加不等于条件。 + + :param field_name: 模型字段名称。 + :param value: 不匹配的值。 """ if value and value != '': self.filter_field[field_name] = {"data": value, "type": self.type_neq} def greater(self, field_name, value): """ - 大于查询字段 - :param field_name: 模型字段名称 - :param value: 值 + 添加大于条件。 + + :param field_name: 模型字段名称。 + :param value: 大于的值。 """ if value and value != '': self.filter_field[field_name] = {"data": value, "type": self.type_greater} def less(self, field_name, value): """ - 小于查询字段 - :param field_name: 模型字段名称 - :param value: 值 + 添加小于条件。 + + :param field_name: 模型字段名称。 + :param value: 小于的值。 """ if value and value != '': self.filter_field[field_name] = {"data": value, "type": self.type_less} def vague(self, field_name, value: str): """ - 模糊查询字段 - :param field_name: 模型字段名称 - :param value: 值 + 添加模糊匹配条件(左右模糊)。 + + :param field_name: 模型字段名称。 + :param value: 模糊匹配的值。 """ if value and value != '': self.filter_field[field_name] = {"data": ('%' + value + '%'), "type": self.type_vague} def left_vague(self, field_name, value: str): """ - 左模糊查询字段 - :param field_name: 模型字段名称 - :param value: 值 + 添加左模糊匹配条件。 + + :param field_name: 模型字段名称。 + :param value: 左模糊匹配的值。 """ if value and value != '': self.filter_field[field_name] = {"data": ('%' + value), "type": self.type_vague} def right_vague(self, field_name, value: str): """ - 左模糊查询字段 - :param field_name: 模型字段名称 - :param value: 值 + 添加右模糊匹配条件。 + + :param field_name: 模型字段名称。 + :param value: 右模糊匹配的值。 """ if value and value != '': self.filter_field[field_name] = {"data": (value + '%'), "type": self.type_vague} def contains(self, field_name, value: str): """ - 包含查询字段 - :param field_name: 模型字段名称 - :param value: 值 + 添加包含条件。 + + :param field_name: 模型字段名称。 + :param value: 包含的值。 """ if value and value != '': self.filter_field[field_name] = {"data": value, "type": self.type_contains} def between(self, field_name, value1, value2): """ - 范围查询字段 - :param field_name: 模型字段名称 - :param value: 值 + 添加范围查询条件。 + + :param field_name: 模型字段名称。 + :param value1: 范围起始值。 + :param value2: 范围结束值。 """ if value1 and value2 and value1 != '' and value2 != '': self.filter_field[field_name] = {"data": [value1, value2], "type": self.type_between} def get_filter(self, model: db.Model): """ - 获取过滤条件 - :param model: 模型字段名称 + 获取最终的 SQLAlchemy 过滤条件。 + + :param model: SQLAlchemy 模型类。 + :return: 返回组合后的过滤条件。 """ for k, v in self.filter_field.items(): if v.get("type") == self.type_vague: @@ -123,4 +142,4 @@ class ModelFilter: self.filter_list.append(getattr(model, k) < v.get("data")) if v.get("type") == self.type_between: self.filter_list.append(getattr(model, k).between(v.get("data")[0], v.get("data")[1])) - return and_(*self.filter_list) + return and_(*self.filter_list) \ No newline at end of file diff --git a/applications/extensions/init_sqlalchemy.py b/applications/extensions/init_sqlalchemy.py index d128c22..729ce6e 100644 --- a/applications/extensions/init_sqlalchemy.py +++ b/applications/extensions/init_sqlalchemy.py @@ -60,9 +60,13 @@ class Query(BaseQuery): def all_json(self, schema: Marshmallow().Schema): return schema(many=True).dump(self.all()) - def layui_paginate(self): - return self.paginate(page=request.args.get('page', type=int), - per_page=request.args.get('limit', type=int), + def layui_paginate(self, page=None, limit=None): + if page is None: + page = request.args.get('page', type=int) + if limit is None: + limit = request.args.get('limit', type=int) + return self.paginate(page=page, + per_page=limit, error_out=False) def layui_paginate_json(self, schema: Marshmallow().Schema): diff --git a/docs/function.md b/docs/function.md deleted file mode 100644 index f419276..0000000 --- a/docs/function.md +++ /dev/null @@ -1,279 +0,0 @@ -## 用户权限判断 - -Pear Admin Flask 项目中集成很多实用的功能,为了便于二次开发,同样也提供了许多便于开发的自定义函数。 - -Pear Admin Flask 项目支持多用户,不同用户有不同的权限,此处将介绍 Pear Admin Flask 中的权限管理函数的用法。 - -### 函数原型 - -函数调用位于项目代码 ```applications/common/utils/rights.py``` 中,函数原型如下: - -```python -def authorize(power: str, log: bool = False): - """ - 用户权限判断,用于判断目前会话用户是否拥有访问权限 - - :param power: 权限标识 - :type power: str - :param log: 是否记录日志, defaults to False - :type log: bool, optional - """ - ... -``` - -### 基本用法 - -+ 后端用法 - -```python -from applications.common.utils.rights import authorize - -@app.route("/test") -@authorize("system:power:remove", log=True) -def test_index(): - return 'You are allowed.' -``` - -> 使用装饰器 @authorize时需要注意,该装饰器需要写在 @app.route之后 - -+ 前端用法 - -在前端中,例如增加,删除按钮,对于没有编辑权限的用户不显示的话,可以使用 - - `{% **if** authorize("admin:user:edit") %}` - - `{% endif %}` - -例如 - -```python - {% if authorize("system:user:edit") %} - - {% endif %} - {% if authorize("system:user:remove") %} - - {% endif %} -``` - -## Schema 序列化 - -项目中时常会涉及到数据库的读写,在读入数据时可以采用SQLalchemy,将模型查询的数据对象转化为字典。 - -> Schema 是序列化类,我们把他放在了models文件里,因为觉得没有必要新建一个文件夹叫 Schema ,也方便看着模型写序列化类。 - -```python -# 例如 -class DeptSchema(ma.Schema): # 序列化类 - deptId = fields.Integer(attribute="id") - parentId = fields.Integer(attribute="parent_id") - deptName = fields.Str(attribute="dept_name") - leader = fields.Str() - phone = fields.Str() - email = fields.Str() - address = fields.Str() - status = fields.Str() - sort = fields.Str() -``` - -> 这一部分有问题的话请看 marshmallow 文档 - -### 模型到字典 - -#### 函数原型 - -函数调用位于项目代码 ```applications/common/curd.py``` 中,函数原型如下: - -``` -def model_to_dicts(schema: ma.Schema, data): - """ - 将模型查询的数据对象转化为字典 - - :param schema: schema类 - :param model: sqlalchemy查询结果 - :return: 返回单个查询结果 - """ - ... -``` - -#### 基本用法 - -+ model写的是查询后的对象 - -```python -from applications.common import curd -from applications.models import Dept -from applications.schemas import DeptOutSchema - -def test(): # 某函数内 - dept = Dept.query.order_by(Dept.sort).all() - res = curd.model_to_dicts(Schema=DeptOutSchema, model=dept) -``` - -## 查询多字段构造器 - -```python -# 准确查询字段 -# 不等于查询字段 -# 大于查询字段 -# 小于查询字段 -# 模糊查询字段(%+xxx+%) -# 左模糊 (% + xxx) -# 右模糊查询字段(xxx+ %) -# 包含查询字段 -# 范围查询字段 -# 查询 -``` - -## xss过滤 - -### 函数原型 - -函数调用位于项目代码 ```applications/common/utils/validate.py``` 中,函数原型如下: - -``` -def str_escape(s: str) -> str: - """ - xss过滤,内部采用flask自带的过滤函数。 - 与原过滤函数不同的是此过滤函数将在 s 为 None 时返回 None。 - - :param s: 要过滤的字符串 - :type s: str - :return: s 为 None 时返回 None,否则过滤字符串后返回。 - :rtype: str - """ - ... -``` - -### 使用方法 - -```python -from applications.common.utils.validate import str_escape -real_name = xss_escape(request.args.get('realName', type=str)) -``` - - -## 邮件发送 - -+ 原邮件发送函数 - -### 函数原型 - -函数调用位于项目代码 ```applications/common/utils/mail.py``` 中,函数原型如下: - -``` -def send_mail(subject, recipients, content): - """原发送邮件函数,不会记录邮件发送记录 - - 失败报错,请注意使用 try 拦截。 - - :param subject: 主题 - :param recipients: 接收者 多个用英文分号隔开 - :param content: 邮件 html - """ - ... -``` - -### 示例代码 - -```python -#在.flaskenv中配置邮箱 -from applications.common.utils import mail - -mail.send_mail("subject", "test@test.com", "

Hello

") -``` - -+ 基于二次开发的邮件发送函数 - -### 函数原型 - -函数调用位于项目代码 ```applications/common/utils/mail.py``` 中,函数原型如下: - -``` -def add(receiver, subject, content, user_id): - """ - 发送一封邮件,若发送成功立刻提交数据库。 - - :param receiver: 接收者 多个用英文逗号隔开 - :param subject: 邮件主题 - :param content: 邮件 html - :param user_id: 发送用户ID(谁发送的?) 可以用 from flask_login import current_user ; current_user.id 来表示当前登录用户 - :return: 成功与否 - """ - ... -``` - -### 示例代码 - -```python -#在.flaskenv中配置邮箱 -from applications.common.utils import mail - -mail.add("test@test.com", "subject", "

Hello

", current_user) -``` - - - -## 返回格式 - -> 后端响应时我们推荐使用规定的API响应格式。 - -### 函数原型 - -函数调用位于项目代码 ```applications/common/utils/http.py``` 中,函数原型如下: - -``` -def success_api(msg: str = "成功"): - """ 成功响应 默认值“成功” """ - return jsonify(success=True, msg=msg) - - -def fail_api(msg: str = "失败"): - """ 失败响应 默认值“失败” """ - return jsonify(success=False, msg=msg) - - -def table_api(msg: str = "", count=0, data=None, limit=10): - """ 动态表格渲染响应 """ - res = { - 'msg': msg, - 'code': 0, - 'data': data, - 'count': count, - 'limit': limit - - } - return jsonify(res) -``` - -### 示例代码 - -```python -from applications.common.utils.http import success_api, fail_api, table_api - -@admin_log.get('/operateLog') -@authorize("system:log:main") -def operate_log(): - # orm查询 - # 使用分页获取data需要.items - log = AdminLog.query.filter( - AdminLog.url != '/passport/login').order_by( - desc(AdminLog.create_time)).layui_paginate() - count = log.total - return table_api(data=model_to_dicts(schema=LogOutSchema, data=log.items), count=count) -``` - -```python -from applications.common.utils.http import success_api, fail_api, table_api - -@admin_power.post('/save') -@authorize("system:power:add", log=True) -def save(): - ... # 若干操作 - if success: - return success_api(msg="成功") - return fail_api(msg="成功") -``` diff --git a/docs/model.md b/docs/model.md deleted file mode 100644 index 52bf177..0000000 --- a/docs/model.md +++ /dev/null @@ -1,50 +0,0 @@ -## 模型/数据库和序列化 -### 数据库连接 - -项目采用flask-sqlalchemy,支持多数据库连接,默认sqlite - -HOSTNAME: 指数据库的IP地址 -USERNAME:指数据库登录的用户名 -PASSWORD:指数据库登录密码 -PORT:指数据库开放的端口 -DATABASE:指需要连接的数据库名称 -#### mssql -``` -MSSQL: f"mssql+pymssql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=cp936" -``` -#### msyql -``` -$ pip install pymysql - -# 手动在mysql中创建数据库,并将配置文件中的url配置如下示例 - -SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=utf8mb4" -``` -#### Oracle -``` -Oracle: f"oracle+cx_oracle://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}" -``` -#### SQLite -``` -SQLite "sqlite:/// database.db" -``` -#### Postgres -``` -Postgres f"postgresql+psycopg2://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}" -``` - - -### 序列化 - -推荐使用这种自动类型的 -```python -from flask_marshmallow.sqla import SQLAlchemyAutoSchema -from applications.models import 你的模型类 -class RoleOutSchema(SQLAlchemyAutoSchema): - class Meta: - model = 你的模型类 # table = models.Album.__table__ - # include_relationships = True # 输出模型对象时同时对外键,是否也一并进行处理 - include_fk = True # 序列化阶段是否也一并返回主键 - # fields= ["id","name"] # 启动的字段列表 - # exclude = ["id","name"] # 排除字段列表 -``` diff --git a/docs/plugin.md b/docs/plugin.md deleted file mode 100644 index 9ccb1d4..0000000 --- a/docs/plugin.md +++ /dev/null @@ -1,47 +0,0 @@ -### 说明 - -插件功能旨在最大限度不修改原框架的前提下添加新功能,我们提供了三个示例插件。 - - -### 插件配置 - -将插件文件夹放置在 ```applications/config.py``` 文件夹中,并且在 .flaskenv 中配置。再配置项中填入插件的文件夹名,已 json 格式写入其中。 - -```python -# 插件配置 -PLUGIN_ENABLE_FOLDERS = ["helloworld"] -``` - -### 插件目录 - -``` -Plugin -│ __init__.json -└─ __init__.py -``` - -这是一个非常简单的插件。 - -### 插件信息 - -插件信息保存在 ```__init__.py``` 中,以测试插件“helloword”为例。插件的数据应该不少于下面三项: - -```json -{ - "plugin_name": "Hello World", - "plugin_version": "1.0.0.1", - "plugin_description": "一个测试的插件。" -} -``` - -### 插件格式 - -插件的入口点为 ```__init__.py``` 文件,在插件被启用后,程序启动时此 Python 文件中的 ```event_init``` 函数。代码如下: - -```python -from flask import Flask - -def event_init(app: Flask): - """初始化完成时会调用这里""" - print("加载完毕后,我会输出一句话。") -``` \ No newline at end of file diff --git a/docs/assets/1.jpg b/docs/source/_static/1.jpg similarity index 100% rename from docs/assets/1.jpg rename to docs/source/_static/1.jpg diff --git a/docs/assets/2.jpg b/docs/source/_static/2.jpg similarity index 100% rename from docs/assets/2.jpg rename to docs/source/_static/2.jpg diff --git a/docs/assets/3.jpg b/docs/source/_static/3.jpg similarity index 100% rename from docs/assets/3.jpg rename to docs/source/_static/3.jpg diff --git a/docs/assets/4.jpg b/docs/source/_static/4.jpg similarity index 100% rename from docs/assets/4.jpg rename to docs/source/_static/4.jpg diff --git a/docs/assets/5.jpg b/docs/source/_static/5.jpg similarity index 100% rename from docs/assets/5.jpg rename to docs/source/_static/5.jpg diff --git a/docs/assets/6.jpg b/docs/source/_static/6.jpg similarity index 100% rename from docs/assets/6.jpg rename to docs/source/_static/6.jpg diff --git a/docs/source/_static/helloworld.png b/docs/source/_static/helloworld.png new file mode 100644 index 0000000000000000000000000000000000000000..d9e6805581f8af7c03ac73fecb93a29ebf81df19 GIT binary patch literal 23768 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 Zx+xXq6`1x)w$Ir6?&F2=tUGb~#y@QS$s_;( literal 0 HcmV?d00001 diff --git a/docs/source/_static/plugin_run.png b/docs/source/_static/plugin_run.png new file mode 100644 index 0000000000000000000000000000000000000000..20bfca072ae60ee9c5687d2d070f5e37c6845215 GIT binary patch literal 11529 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 EKQ=HudjJ3c literal 0 HcmV?d00001 diff --git a/docs/assets/qqgroup.jpg b/docs/source/_static/qqgroup.jpg similarity index 100% rename from docs/assets/qqgroup.jpg rename to docs/source/_static/qqgroup.jpg diff --git a/docs/assets/官网地址.jpg b/docs/source/_static/官网地址.jpg similarity index 100% rename from docs/assets/官网地址.jpg rename to docs/source/_static/官网地址.jpg diff --git a/docs/assets/源码仓库.jpg b/docs/source/_static/源码仓库.jpg similarity index 100% rename from docs/assets/源码仓库.jpg rename to docs/source/_static/源码仓库.jpg diff --git a/docs/assets/界面演示.jpeg b/docs/source/_static/界面演示.jpeg similarity index 100% rename from docs/assets/界面演示.jpeg rename to docs/source/_static/界面演示.jpeg diff --git a/docs/source/function/admin.rst b/docs/source/function/admin.rst index b23d1a9..8e74ddb 100644 --- a/docs/source/function/admin.rst +++ b/docs/source/function/admin.rst @@ -1,7 +1,7 @@ :mod:`admin` -- 后台函数模块 ======================================= -:mod:`admin` 模块源代码在文件夹 `applications/common/admin.py` 下,主要集结了一些常用的后台需要频繁调用的函数。 +:mod:`admin` 模块源代码在文件 `applications/common/admin.py` 下,主要集结了一些常用的后台需要频繁调用的函数。 .. module:: admin @@ -10,9 +10,21 @@ .. function:: get_captcha() - 生成验证码图片及其对应的验证码字符串。 + 生成验证码图片及其对应的验证码字符串。 - :return: 返回验证码图片的响应对象和验证码字符串。 + :return: 返回验证码图片的响应对象和验证码字符串。 + + **示例:** + + .. code-block:: python + + from applications.common.admin import get_captcha + + @bp.get('/getCaptcha') + def captcha(): + resp, code = get_captcha() + session["code"] = code + return resp .. function:: normal_log(method, url, ip, user_agent, desc, uid, is_access) @@ -31,12 +43,12 @@ .. function:: login_log(request, uid, is_access) - 记录用户登录日志。 + 记录用户登录日志。 - :param request: Flask 请求对象。 - :param uid: 用户 ID。 - :param is_access: 是否成功登录(True 或 False)。 - :return: 返回日志记录的 ID。 + :param request: Flask 请求对象。 + :param uid: 用户 ID。 + :param is_access: 是否成功登录(True 或 False)。 + :return: 返回日志记录的 ID。 .. function:: admin_log(request, is_access, desc=None) diff --git a/docs/source/function/curd.rst b/docs/source/function/curd.rst new file mode 100644 index 0000000..2e6e8a6 --- /dev/null +++ b/docs/source/function/curd.rst @@ -0,0 +1,90 @@ +:mod:`curd` -- 简单增删改查模块 +======================================= + +:mod:`curd` 模块源代码在文件 `applications/common/curd.py` 下,主要集结了一些简单实用的增删改查。 + +.. module:: curd + +类 +------- + +.. class:: LogicalDeleteMixin + + 逻辑删除混入类,为模型提供软删除功能。 + + **示例:** + + .. code-block:: python + + class Test(db.Model, LogicalDeleteMixin): + __tablename__ = 'admin_test' + id = db.Column(db.Integer, primary_key=True, comment='角色ID') + + # 软删除 + Test.query.filter_by(id=1).soft_delete() + + # 查询所有未删除的记录 + Test.query.logic_all() + + +函数 +-------------- + +.. function:: auto_model_jsonify(data, model: db.Model) + + 自动序列化模型数据为 JSON 格式,无需手动定义 Schema。 + + **示例:** + + .. code-block:: python + + power_data = curd.auto_model_jsonify(model=Dept, data=dept) + + :param data: 需要序列化的 SQLAlchemy 查询结果。 + :param model: SQLAlchemy 模型类。 + :return: 返回序列化后的 JSON 数据。 + + +.. function:: model_to_dicts(schema: ma.Schema, data) + + 使用指定的 Schema 序列化 SQLAlchemy 查询结果。 + + :param schema: Marshmallow Schema 类。 + :param data: SQLAlchemy 查询结果。 + :return: 返回序列化后的数据,返回字典。 + + +.. function:: get_one_by_id(model: db.Model, id) + + 根据 ID 查询单个记录。 + + :param model: SQLAlchemy 模型类。 + :param id: 记录的主键 ID。 + :return: 返回查询到的记录,如果未找到则返回 None。 + + +.. function:: delete_one_by_id(model: db.Model, id) + + 根据 ID 删除单个记录。 + + :param model: SQLAlchemy 模型类。 + :param id: 记录的主键 ID。 + :return: 返回删除操作影响的行数。 + + +.. function:: enable_status(model: db.Model, id) + + 启用指定 ID 的记录。 + + :param model: SQLAlchemy 模型类。 + :param id: 记录的主键 ID。 + :return: 如果操作成功返回 True,否则返回 False。 + + +.. function:: disable_status(model: db.Model, id) + + 停用指定 ID 的记录。 + + :param model: SQLAlchemy 模型类。 + :param id: 记录的主键 ID。 + :return: 如果操作成功返回 True,否则返回 False。 \ No newline at end of file diff --git a/docs/source/function/helper.rst b/docs/source/function/helper.rst new file mode 100644 index 0000000..9795748 --- /dev/null +++ b/docs/source/function/helper.rst @@ -0,0 +1,121 @@ +.. _字段构造模块: + +:mod:`helper` -- 字段构造模块 +======================================= + +:mod:`helper` 模块源代码在文件 `applications/common/helper.py` 下,主要集结了一些常用的字段构造方法。 + +.. module:: helper + +类 +-------- + +.. class:: ModelFilter + + ORM 多条件查询构造器,支持多种查询条件组合。 + + **示例:** + + .. code-block:: python + + from applications.common.helper import ModelFilter + mf = ModelFilter() + mf.exact('name', 'John') # 添加精确匹配条件 + mf.vague('email', 'example.com') # 添加模糊匹配条件 + query = User.query.filter(mf.get_filter(User)) + + + .. attribute:: filter_field + + 存储字段过滤条件的字典。 + + + .. attribute:: filter_list + + 存储最终的过滤条件列表。 + + + .. method:: __init__() + + 初始化过滤条件存储字典和列表。 + + + .. method:: exact(field_name, value) + + 添加精确匹配条件。 + + :param field_name: 模型字段名称。 + :param value: 匹配的值。 + + + .. method:: neq(field_name, value) + + 添加不等于条件。 + + :param field_name: 模型字段名称。 + :param value: 不匹配的值。 + + + .. method:: greater(field_name, value) + + 添加大于条件。 + + :param field_name: 模型字段名称。 + :param value: 大于的值。 + + + .. method:: less(field_name, value) + + 添加小于条件。 + + :param field_name: 模型字段名称。 + :param value: 小于的值。 + + + .. method:: vague(field_name, value: str) + + 添加模糊匹配条件(左右模糊)。 + + :param field_name: 模型字段名称。 + :param value: 模糊匹配的值。 + + + .. method:: left_vague(field_name, value: str) + + 添加左模糊匹配条件。 + + :param field_name: 模型字段名称。 + :param value: 左模糊匹配的值。 + + + .. method:: right_vague(field_name, value: str) + + 添加右模糊匹配条件。 + + :param field_name: 模型字段名称。 + :param value: 右模糊匹配的值。 + + + .. method:: contains(field_name, value: str) + + 添加包含条件。 + + :param field_name: 模型字段名称。 + :param value: 包含的值。 + + + .. method:: between(field_name, value1, value2) + + 添加范围查询条件。 + + :param field_name: 模型字段名称。 + :param value1: 范围起始值。 + :param value2: 范围结束值。 + + + .. method:: get_filter(model: db.Model) + + 获取最终的 SQLAlchemy 过滤条件。 + + :param model: SQLAlchemy 模型类。 + :return: 返回组合后的过滤条件。 \ No newline at end of file diff --git a/docs/source/function/index.rst b/docs/source/function/index.rst index b567811..ac13421 100644 --- a/docs/source/function/index.rst +++ b/docs/source/function/index.rst @@ -9,6 +9,8 @@ :maxdepth: 1 admin + curd + helper 辅助函数 ------------ diff --git a/docs/source/function/utils/cache.rst b/docs/source/function/utils/cache.rst index 5d6d087..044f876 100644 --- a/docs/source/function/utils/cache.rst +++ b/docs/source/function/utils/cache.rst @@ -1,7 +1,7 @@ :mod:`cache` -- 应用缓存模块 ================================ -:mod:`cache` 模块源代码在文件夹 `applications/common/utils/cache.py` 下,主要用于简单的程序数据缓存。 +:mod:`cache` 模块源代码在文件 `applications/common/utils/cache.py` 下,主要用于简单的程序数据缓存。 目前此模块仅启用应用程序缓存,暂时没有联动 Redis 等数据库缓存的功能,后续有意向添加。您可以在自己的项目中添加相关的函数,当然也非常欢迎提交 PR ,一起完善项目。 diff --git a/docs/source/function/utils/captcha.rst b/docs/source/function/utils/captcha.rst index 0838a6d..85722d3 100644 --- a/docs/source/function/utils/captcha.rst +++ b/docs/source/function/utils/captcha.rst @@ -1,7 +1,7 @@ :mod:`captcha` -- 验证码生成模块 ================================== -:mod:`captcha` 模块源代码在文件夹 `applications/common/utils/captcha.py` 下,主要用于生成验证码图片。 +:mod:`captcha` 模块源代码在文件 `applications/common/utils/captcha.py` 下,主要用于生成验证码图片。 .. module:: captcha diff --git a/docs/source/function/utils/http.rst b/docs/source/function/utils/http.rst index 7d89bfc..858b029 100644 --- a/docs/source/function/utils/http.rst +++ b/docs/source/function/utils/http.rst @@ -1,7 +1,9 @@ +.. _JSON 响应正文生成模块: + :mod:`http` -- JSON 响应正文生成模块 ======================================= -:mod:`http` 模块源代码在文件夹 `applications/common/utils/http.py` 下,主要用于生成 JSON 格式的响应正文。 +:mod:`http` 模块源代码在文件 `applications/common/utils/http.py` 下,主要用于生成 JSON 格式的响应正文。 对于大部分 JSON 格式响应的数据,请尽量遵循响应格式规范。如此方便后续前后端的分离和项目的构建。 @@ -36,4 +38,22 @@ :param limit: 每页数据条数,默认为 10。 :return: 返回 JSON 格式的响应,包含 `msg`、`code`、`data`、`count` 和 `limit` 字段。 +**示例:** +.. code-block:: python + + from applications.common.utils.http import success_api, fail_api + + @bp.get('/init') + def init(): + if ...: + return success_api(msg="初始化成功") + return fail_api(msg="初始化失败") + +.. code-block:: python + + from applications.common.utils.http import table_api + + @bp.get('/data') + def data(): + return table_api(data=[], total=0) diff --git a/docs/source/function/utils/index.rst b/docs/source/function/utils/index.rst index 17cbaf5..36decf2 100644 --- a/docs/source/function/utils/index.rst +++ b/docs/source/function/utils/index.rst @@ -3,7 +3,7 @@ 目录索引 .. toctree:: - :maxdepth: 1 + :maxdepth: 2 cache captcha diff --git a/docs/source/function/utils/mail.rst b/docs/source/function/utils/mail.rst index 78a09bd..6814d79 100644 --- a/docs/source/function/utils/mail.rst +++ b/docs/source/function/utils/mail.rst @@ -1,7 +1,11 @@ +.. _邮件模块: + :mod:`mail` -- 邮件模块 ================================== -:mod:`mail` 模块源代码在文件夹 `applications/common/utils/mail.py` 下,主要用于邮件的发送。 +:mod:`mail` 模块源代码在文件 `applications/common/utils/mail.py` 下,主要用于邮件的发送。 + +使用前,需要正确在 `applications/config.py` 中配置 SMTP 服务器。 .. module:: mail @@ -31,14 +35,23 @@ .. function:: add(receiver, subject, content, user_id) - 发送一封邮件,并将发送记录保存到数据库。 **该方法被邮件发送的视图函数调用。** + 发送一封邮件,并将发送记录保存到数据库。 **该方法被邮件发送的视图函数调用。** - :param receiver: 接收者邮箱地址,多个邮箱用英文分号隔开。 - :param subject: 邮件主题。 - :param content: 邮件内容(HTML 格式)。 - :param user_id: 发送者用户ID,表示谁发送了这封邮件。 + :param receiver: 接收者邮箱地址,多个邮箱用英文分号隔开。 + :param subject: 邮件主题。 + :param content: 邮件内容(HTML 格式)。 + :param user_id: 发送者用户ID,表示谁发送了这封邮件。 可以使用 `from flask_login import current_user; current_user.id` 获取当前登录用户的ID。 - :return: 发送成功返回 True,失败报错。 + :return: 发送成功返回 True,失败报错。 + + **示例** + + .. code-block:: python + + from flask_login import current_user + from applications.common.utils import mail + + mail.add("test@test.com", "subject", "

Hello

", current_user.id) .. function:: delete(id) diff --git a/docs/source/function/utils/rights.rst b/docs/source/function/utils/rights.rst index 7dd2019..8c9f77e 100644 --- a/docs/source/function/utils/rights.rst +++ b/docs/source/function/utils/rights.rst @@ -1,7 +1,9 @@ +.. _权限验证模块: + :mod:`rights` -- 权限验证模块 ================================== -:mod:`rights` 模块源代码在文件夹 `applications/common/utils/rights.py` 下,主要用于权限验证。 +:mod:`rights` 模块源代码在文件 `applications/common/utils/rights.py` 下,主要用于权限验证。 .. module:: rights @@ -23,6 +25,8 @@ .. code-block:: python + from applications.common.utils.rights import authorize + @app.route("/test") @authorize("system:power:remove", log=True) def test_index(): diff --git a/docs/source/function/utils/upload.rst b/docs/source/function/utils/upload.rst index 4f3b989..522a069 100644 --- a/docs/source/function/utils/upload.rst +++ b/docs/source/function/utils/upload.rst @@ -1,7 +1,7 @@ :mod:`upload` -- 文件上传模块 ================================== -:mod:`upload` 模块源代码在文件夹 `applications/common/utils/upload.py` 下,主要用于文件上传,目前主要用于图片上传。 +:mod:`upload` 模块源代码在文件 `applications/common/utils/upload.py` 下,主要用于文件上传,目前主要用于图片上传。 .. module:: upload diff --git a/docs/source/function/utils/validate.rst b/docs/source/function/utils/validate.rst index b6048bd..b4647cc 100644 --- a/docs/source/function/utils/validate.rst +++ b/docs/source/function/utils/validate.rst @@ -1,7 +1,7 @@ :mod:`validate` -- 效验模块 ================================== -:mod:`validate` 模块源代码在文件夹 `applications/common/utils/validate.py` 下,主要用于数据效验与过滤。 +:mod:`validate` 模块源代码在文件 `applications/common/utils/validate.py` 下,主要用于数据效验与过滤。 .. module:: validate diff --git a/docs/source/index.rst b/docs/source/index.rst index 1a19ccb..81855f9 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -21,3 +21,9 @@ function/index +.. toctree:: + :maxdepth: 1 + :caption: 最佳实践 + + practices/index + diff --git a/docs/source/practices/index.rst b/docs/source/practices/index.rst new file mode 100644 index 0000000..7b9d61f --- /dev/null +++ b/docs/source/practices/index.rst @@ -0,0 +1,9 @@ +.. title:: 最佳实践 + +目录索引 + +.. toctree:: + :maxdepth: 1 + + plugin + trick \ No newline at end of file diff --git a/docs/source/practices/plugin.rst b/docs/source/practices/plugin.rst new file mode 100644 index 0000000..d74d15f --- /dev/null +++ b/docs/source/practices/plugin.rst @@ -0,0 +1,129 @@ +插件开发 +================= + +插件功能旨在最大限度不修改原框架的前提下添加新功能,并可以像程序原有框架一样进行流程注册,而且不需要修改任何程序框架原有代码(仅在配置文件中设置即可)。 + +所有插件放置在 `plugins` 文件夹中,项目提供了三个示例插件,分别是 `helloworld` 、 `realip` 和 `replacePage` ,分别用于示例页面的注册、修改 Flask 上下文和页面替换。 + +像项目自带的用户管理、部门管理等基本功能属于程序自身的“功能插件”,对于大多数衍生项目来说,多的是修改字符串和删除部分不需要的功能, +而插件开发主要可以用于添加自己的视图函数和功能,可以完美于项目融合,增加可拓展性。 + +插件的启用 +----------------- + +插件需要在 `applications/config.py` 中配置,你会找到如下的内容: + +.. code-block:: python + + PLUGIN_ENABLE_FOLDERS = [] + +而在目录 `plugins` 中,你会发现存在 文件夹名称 为 `helloworld` 、 `realip` 和 `replacePage` 三个插件。比如我们想要启用 `helloworld` 插件, +仅需要做如下修改: + +.. code-block:: python + + PLUGIN_ENABLE_FOLDERS = ["helloworld"] + +假设有多个插件,只要依次在列表 `PLUGIN_ENABLE_FOLDERS` 中填入插件的文件夹名称即可。**注意:填写的先后顺序会影响插件加载的前后顺序,越前面的插件越早被加载。** + +假设插件启用成功,你将会在控制台收到如下的提示: + +.. code-block:: bash + + * Plugin: Loaded plugin: Hello World . + +| + +.. image:: ../_static/plugin_run.png + :align: center + +| + +`helloworld` 插件启用之后,你可以访问 `http://127.0.0.1:5000/hello_world/` 来请求到新添加的页面。你会发现添加页面变的简单,仅需要修改一下设置项就行了。 + +| + +.. image:: ../_static/helloworld.png + :align: center + +| + +插件的目录架构 +------------------- + +插件的目录架构如下: + +.. code-block:: bash + + Plugin + │ __init__.json + └─ __init__.py + +这是一个插件基本的目录架构,插件信息保存在 `__init__.json` 中,其本质是一个包含如下 JSON 字符串的文本文件: + +.. code-block:: json + + { + "plugin_name": "Hello World", + "plugin_version": "1.0.0.1", + "plugin_description": "一个测试的插件。" + } + +这个 JSON 文件中,记录了基本的插件名称与插件版本,以及插件的介绍,请确保一个插件至少包含上述的三个字段,因为这三个字段会被项目所读取并在加载成功之后展示在控制台。 + +编写插件入口 +------------------- + +插件入口位于 `__init__.py` 中,请确保 `__init__.py` 文件一定包含 `event_init(app: Flask)` 函数,如下: + +.. code-block:: python + + def event_init(app: Flask): + pass + +这个函数将会在插件加载时被调用,并传入项目的 `Flask` 对象,此后你可以像一般使用 Flask 一样添加视图函数。例如: + +.. code-block:: python + + def event_init(app: Flask): + @app.get('/test') + def test(): + return "这是测试页面" + +**当然,不推荐这样直接使用 Flask 对象创建视图函数,更妥当的做法是通过注册蓝图的方式来添加视图函数。您可以这样做:** + +在您编写的插件目录下建立一个 `main.py` 文件,并在该文件中添加蓝图: + +.. code-block:: 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") + +而后在 `__init__.py` 中注册该蓝图: + +.. code-block:: python + + from flask import Flask + from .main import helloworld_blueprint + + + def event_init(app: Flask): + """初始化完成时会调用这里""" + app.register_blueprint(helloworld_blueprint) + +这样可以使目录架构更加清晰。 + +.. important:: + + 注意不要直接在 `__init__.py` 的 `event_init` 函数外直接写存在阻塞的代码,不然项目 Flask 将不能初始化完成。 + + diff --git a/docs/source/practices/trick.rst b/docs/source/practices/trick.rst new file mode 100644 index 0000000..f3a9510 --- /dev/null +++ b/docs/source/practices/trick.rst @@ -0,0 +1,199 @@ +开发技巧 +=================== + +开发 Web 过程中需要用到许多技巧来加速开发,本章节将介绍开发的小技巧与一些需要注意的细节。 + +配置数据库 +----------- + +项目采用 flask-sqlalchemy,支持多数据库连接,默认是使用 sqlite 的,可以在 `applications/config.py` 中配置,如果需要连接其他数据库需要可以参考: + +* HOSTNAME: 指数据库的IP地址 +* USERNAME:指数据库登录的用户名 +* PASSWORD:指数据库登录密码 +* PORT:指数据库开放的端口 +* DATABASE:指需要连接的数据库名称 + +.. code-block:: python + + # MSSQL + SQLALCHEMY_DATABASE_URI = f"mssql+pymssql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=cp936" + + # mysql + SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=utf8mb4" + + # Oracle + SQLALCHEMY_DATABASE_URI = f"oracle+cx_oracle://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}" + + # SQLite + SQLALCHEMY_DATABASE_URI = "sqlite://../database.db" + + # Postgres + SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}" + +.. important:: + + 使用不同的数据库需要安装另外的库,比如 mysql 要安装 pymysql 库(不同平台可能名称不一样),请自行查询资料而后进行配置。 + +权限效验 +------------ + +在开发后台管理模板的过程中会涉及到权限效验,即访问控制。Pear Admin Flask 中提供了方便的函数用于进行权限效验。详情请查看 :ref:`权限验证模块` 章节。 + +Schema 序列化 +--------------- + +项目中时常会涉及到数据库的读写,在读入数据时采用 SQLAlchemy,将模型查询的数据对象转化为字典,以此方便与前端页面进行数据交换。 + +.. important:: + + Schema 模型放在了 `applications/schemas` 文件夹中,与 `applications/models` 中的数据库模型对应(准确来说是序列化为字典的配置)。 + +进行序列化时,常常会用到 `applications/common/curd.py` 中的 `model_to_dicts` 函数,下面是一个常见的用法。 + +.. code-block:: python + + from applications.models import Dept + from applications.common import curd + from applications.schemas import DeptSchema + + dept = Dept.query.order_by(Dept.sort).all() + power_data = curd.model_to_dicts(schema=DeptSchema, data=dept) # 此处 power_data 将会是一个列表,存储了部门的数据字典 + +在自己撰写 Schema 模型时,推荐使用自动化类型转化: + +.. code-block:: python + + from flask_marshmallow.sqla import SQLAlchemyAutoSchema + from applications.models import 你的模型类 + class RoleOutSchema(SQLAlchemyAutoSchema): + class Meta: + model = 你的模型类 # table = models.Album.__table__ + # include_relationships = True # 输出模型对象时同时对外键,是否也一并进行处理 + include_fk = True # 序列化阶段是否也一并返回主键 + # fields= ["id","name"] # 启动的字段列表 + # exclude = ["id","name"] # 排除字段列表 + + +与 layui 的数据格式同步 +------------------------------ + +项目的前端页面基于 layui 框架,在一些数据展示页面(如:layui 动态表格)需要与 layui 框架进行快速的数据交换。比如前端会传入 limit 和 page 参数 +用于限定数据展示的范围。故项目中在 SQLAlchemy 中添加了专有的查询函数。详情可以查看文件 `applications/extensions/init_sqlalchemy.py` 。 +下面是对 `Query` 类的解释。 + +.. class:: Query(BaseQuery) + + 自定义查询类,扩展了 BaseQuery 的功能,支持软删除、逻辑查询、分页和序列化。 + + **示例:** + + .. code-block:: python + + # 软删除 + User.query.filter_by(id=1).soft_delete() + + # 查询所有未删除的记录 + users = User.query.logic_all() + + # 分页查询并返回 JSON 数据 + data, total, page, per_page = User.query.layui_paginate_json(UserSchema) + + + .. method:: soft_delete() + + 软删除当前查询结果集中的记录。 + + :return: 返回更新操作影响的行数。 + + + .. method:: logic_all() + + 查询所有未删除的记录。 + + :return: 返回未删除的记录列表。 + + + .. method:: all_json(schema: Schema) + + 将查询结果序列化为 JSON 格式。 + + :param schema: Marshmallow Schema 类。 + :return: 返回序列化后的 JSON 数据。 + + + .. method:: layui_paginate(page=None, limit=None) + + 分页查询,适用于 Layui 表格。 + + **需要注意的是,如果不提供 page 和 limit 则该函数必须在视图函数中使用,该函数会自动获取 GET 请求中的 limit 和 page 参数构成查询。** + + :return: 返回分页对象。 + + **示例:** + + .. code-block:: python + + # 查询邮件数据并分页 + mail = Mail.query.filter(mf.get_filter(Mail)).layui_paginate() + return model_to_dicts(schema=MailOutSchema, data=mail.items) + + + .. method:: layui_paginate_json(schema: Schema) + + 分页查询并返回 JSON 格式数据,适用于 Layui 表格。 + + :param schema: Marshmallow Schema 类。 + :return: 返回包含序列化数据、总数、当前页码和每页条数的元组。 + + + .. method:: layui_paginate_db_json() + + 分页查询并返回数据库原始数据的 JSON 格式,适用于 Layui 表格。 + + :return: 返回包含序列化数据和总数的元组。 + + **示例:** + + .. code-block:: python + + db.query(User.name).layui_paginate_db_json() + + +进行字段构造 +----------------------- + +提炼数据时常常会用到准确匹配或者模糊匹配,又或者是进行多条件大小比较的匹配,此时可以通过字段构造来解决。项目中提供了字段构造的类位于 +`applications/common/helper.py` 。详情查看 :ref:`字段构造模块` 章节。 + + +响应合适的响应数据 +----------------------- + +在进行 JSON 数据响应时,应该注重响应的 JSON 格式类型,一般情况下,项目的 JSON 响应会形如: + +.. code-block:: json + + { + "code": 0, + "msg": "请求成功", + "data": [], + "count": 0, + "limit": 0 + } + +其中,`data` 、 `total` 和 `limit` 字段是可选的,仅在传输数据的时候存在。项目提供了生成统一响应格式的函数,位于 `applications/common/utils/http.py` 。 +详情查看 :ref:`JSON 响应正文生成模块` 章节。 + + +发送硬件 +----------------------- + +程序提供了发送邮件的模块,前提是需要正确在 `applications/config.py` 中配置 SMTP 服务器。详情查看 ref:`邮件模块` 章节。 + +.. code-block:: python + + from flask_login import current_user + from applications.common.utils import mail + + mail.add("test@test.com", "subject", "

Hello

", current_user.id) \ No newline at end of file diff --git a/plugins/helloworld/__init__.py b/plugins/helloworld/__init__.py index d6b218e..a875cce 100644 --- a/plugins/helloworld/__init__.py +++ b/plugins/helloworld/__init__.py @@ -10,6 +10,7 @@ from .main import helloworld_blueprint dir_path = os.path.dirname(__file__).replace("\\", "/") folder_name = dir_path[dir_path.rfind("/") + 1:] # 插件文件夹名称 + def event_init(app: Flask): """初始化完成时会调用这里""" - app.register_blueprint(helloworld_blueprint) \ No newline at end of file + app.register_blueprint(helloworld_blueprint) diff --git a/plugins/helloworld/main.py b/plugins/helloworld/main.py index f559210..2622ab5 100644 --- a/plugins/helloworld/main.py +++ b/plugins/helloworld/main.py @@ -1,10 +1,12 @@ from flask import render_template, Blueprint # 创建蓝图 -helloworld_blueprint = Blueprint('hello_world', __name__, template_folder='templates', static_folder="static", - url_prefix="/hello_world") +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") - diff --git a/plugins/realip/__init__.py b/plugins/realip/__init__.py index 50e18b4..7d36e01 100644 --- a/plugins/realip/__init__.py +++ b/plugins/realip/__init__.py @@ -4,38 +4,21 @@ import os import logging from flask import Flask, request -from . import console # 获取插件所在的目录(结尾没有分割符号) dir_path = os.path.dirname(__file__).replace("\\", "/") folder_name = dir_path[dir_path.rfind("/") + 1:] # 插件文件夹名称 + def event_init(app: Flask): """初始化完成时会调用这里""" - # 移除原有的输出日志 - app.logger = None - log = logging.getLogger('werkzeug') - log.setLevel(logging.ERROR) - + # 更改IP地址,只有在最新版的flask中才能生效 @app.before_request def before_request(): request.remote_addr = get_user_ip(request) - - - # 使用自定义的日志输出 - @app.after_request - def after_request(rep): - if rep.status_code == 200: - console.success(f"{request.remote_addr} -- {request.full_path} 200") - elif rep.status_code == 404: - console.error(f"{request.remote_addr} -- {request.full_path} 404") - elif rep.status_code == 500: - console.warning(f"{request.remote_addr} -- {request.full_path} 500") - else: - console.info(f"{request.remote_addr} -- {request.full_path} {rep.status_code}") - return rep - + + def get_user_ip(request): """获取用户真实IP""" if 'HTTP_X_FORWARDED_FOR' in request.headers: @@ -54,4 +37,4 @@ def get_user_ip(request): return request.headers['REMOTE_ADDR'] elif 'X-Forwarded-For' in request.headers: return request.headers['X-Forwarded-For'] - return request.remote_addr \ No newline at end of file + return request.remote_addr diff --git a/plugins/realip/console.py b/plugins/realip/console.py deleted file mode 100644 index a6fe37b..0000000 --- a/plugins/realip/console.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -输出控制台日志 -""" -import sys -import time -import ctypes - -NONE = "\033[m" -RED = "\033[0;32;31m" -LIGHT_RED = "\033[1;31m" -GREEN = "\033[0;32;32m" -LIGHT_GREEN = "\033[1;32m" -BLUE = "\033[0;32;34m" -LIGHT_BLUE = "\033[1;34m" -DARY_GRAY = "\033[1;30m" -CYAN = "\033[0;36m" -LIGHT_CYAN = "\033[1;36m" -PURPLE = "\033[0;35m" -LIGHT_PURPLE = "\033[1;35m" -BROWN = "\033[0;33m" -YELLOW = "\033[1;33m" -LIGHT_GRAY = "\033[0;37m" -WHITE = "\033[1;37m" - -# 开启 Windows 下对于 ESC控制符 的支持 -if sys.platform == "win32": - kernel32 = ctypes.windll.kernel32 - kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) - - -def _print(level, msg): - time_ = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) - - level_name = {10: "Plain", - 11: "Log", - 12: "Info", - 13: "Debug", - 14: "Success", - 15: "Warning", - 16: "Error"} - - color = {10: NONE, - 11: LIGHT_CYAN, - 12: LIGHT_BLUE, - 13: PURPLE, - 14: GREEN, - 15: YELLOW, - 16: RED} - - print(f'{color.get(level, NONE)}[{time_}]({level_name.get(level, "Plain")}):', msg, f"{NONE}") - - -def plain(*args, sep=' '): - msg = sep.join(str(_) for _ in args) - _print(10, msg) - - -def log(*args, sep=' '): - msg = sep.join(str(_) for _ in args) - _print(11, msg) - - -def info(*args, sep=' '): - msg = sep.join(str(_) for _ in args) - _print(12, msg) - - -def debug(*args, sep=' '): - msg = sep.join(str(_) for _ in args) - _print(13, msg) - - -def success(*args, sep=' '): - msg = sep.join(str(_) for _ in args) - _print(14, msg) - - -def warn(*args): - warning(*args) - - -def warning(*args, sep=' '): - msg = sep.join(str(_) for _ in args) - _print(15, msg) - - -def error(*args, sep=' '): - msg = sep.join(str(_) for _ in args) - _print(16, msg)