!133 Update: 同步项目框架与 Pear Admin 4.0 同步,完善部分功能

Merge pull request !133 from 我叫以赏/master
This commit is contained in:
我叫以赏
2025-02-13 02:14:07 +00:00
committed by Gitee
211 changed files with 112892 additions and 5410 deletions
+12 -5
View File
@@ -1,8 +1,15 @@
#build时候不会copy以下文件
# 构建的相关文件
dockerdata
docker-compose.yml
Dockerfile.server
README.md
!dockerdata/config.py
# Python 相关文件
migrations/
instance/
flask_session/
venv/
.idea/
*.log
*.db
# 允许拷贝的文件
!dockerdata/start.sh
!dockerdata/gunicorn.conf.py
-18
View File
@@ -1,18 +0,0 @@
FROM pearadminflask/python3.7-flask:pillow
COPY . /app/
COPY dockerdata/start.sh /app/
COPY dockerdata/gunicorn.conf.py /app/
WORKDIR /app/
ENV TIME_ZONE Asia/Shanghai
ENV PIPURL "https://pypi.tuna.tsinghua.edu.cn/simple/ --trusted-host pypi.douban.com"
RUN echo "${TIME_ZONE}" > /etc/timezone \
&& ln -sf /usr/share/zoneinfo/${TIME_ZONE} /etc/localtime
RUN apk update && apk add mysql-client
RUN chmod +x start.sh
RUN sed -i 's/MYSQL_HOST = "127.0.0.1"/MYSQL_HOST = "mysql"/' applications/config.py
RUN sed -i 's/REDIS_HOST = "127.0.0.1"/REDIS_HOST = "redis"/' applications/config.py
CMD /bin/sh
+21 -21
View File
@@ -8,14 +8,14 @@
开 箱 即 用 的 Flask 快 速 开 发 平 台
</h4>
[预览](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) | [文档](https://lab.lovepikachu.top/document/pear-admin-flask)
<p align="center">
<a href="#">
<img src="https://img.shields.io/badge/pear%20admin%20flask-1.0.0-green" alt="Pear Admin Layui Version">
<img src="https://img.shields.io/badge/pear%20admin%20flask-2.0.0-green" alt="Pear Admin Layui Version">
</a>
<a href="#">
<img src="https://img.shields.io/badge/Python-3.6+-green.svg" alt="Python Version">
<img src="https://img.shields.io/badge/Python-3.8+-green.svg" alt="Python Version">
</a>
<a href="#">
<img src="https://img.shields.io/badge/Mysql-5.3.2+-green.svg" alt="Mysql Version">
@@ -24,7 +24,7 @@
</div>
<div align="center">
<img width="92%" style="border-radius:10px;margin-top:20px;margin-bottom:20px;box-shadow: 2px 0 6px gray;" src="https://images.gitee.com/uploads/images/2020/1019/104805_042b888c_4835367.png" />
<img width="92%" style="border-radius:10px;margin-top:20px;margin-bottom:20px;box-shadow: 2px 0 6px gray;" src="docs/source/_static/feature.png" />
</div>
# 项目简介
@@ -59,7 +59,7 @@ Pear Admin Flask 基于 Flask 的后台管理系统,拥抱应用广泛的pytho
# 版本支持情况
经过测试,此项目的(master分支)运行要求是 `>= Python 3.8` ,推荐使用 `Python 3.9`
经过测试,此项目的(master分支)运行要求是 `>= Python 3.8` ,推荐使用 `Python 3.11`
> **💡提示** 由于 Flask 中使用的 Werkzeug 模块更新,Flask 官方并未进行更新,所以可能会出现 ImportError 。
> 此类情况的出现可以通过正确安装 `requirements.txt` 中的模块(以及其对应版本)解决。
@@ -69,19 +69,19 @@ Pear Admin Flask 基于 Flask 的后台管理系统,拥抱应用广泛的pytho
## 应用结构
```应用结构
Pear Admin Flask
├─applications # 应用
│ ├─extensions # 注册插件
│ ├─models # 数据模型
│ ├─static # 静态资源文件
│ ├─templates # 静态模板文件
─views # 视图部分
├─admin # 后台管理视图模块
└─index # 前台视图模块
Pear Admin Flask (master)
├─applications # 项目核心模块
│ ├─common # 公共模块(初始化数据库、公用函数)
│ ├─extensions # 注册项目插件
│ ├─schemas # 序列化模型
│ ├─models # 数据库模型
─views # 视图部分
├─config.py # 项目配置
└─__init__.py # 项目初始化入口
├─docs # 文档说明
├─migrations # 迁移文件记录
├─requirement # 依赖文件
└─.env # 项目的配置文件
├─static # 静态资源文件
├─templates # 静态模板文件
└─app.py # 程序入口
```
## 资源结构
@@ -159,7 +159,7 @@ python -m venv venv
# 进入虚拟环境下
venv\Scripts\activate.bat # Windows 提示命令符
venv\Scripts\Activate.ps1 # Windows Powershell
. venv/bin/activate # Linux
source venv/bin/activate # Linux
# 使用 pip 安装
pip install -r requirements.txt
@@ -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) |
# 其他说明
-1
View File
@@ -2,6 +2,5 @@ from applications import create_app
app = create_app()
if __name__ == '__main__':
app.run()
+1
View File
@@ -8,6 +8,7 @@ from applications.view import init_bps
def create_app():
app = Flask(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
# 引入配置
app.config.from_object(BaseConfig)
+83 -5
View File
@@ -1,18 +1,96 @@
from io import BytesIO
from flask import make_response
from flask_login import current_user
from flask import session, make_response
from applications.common.utils.gen_captcha import vieCode
from applications.common.utils.validate import str_escape
from applications.common.utils.captcha import vieCode
from applications.extensions import db
from applications.models import AdminLog
# 生成验证码
def get_captcha():
"""
生成验证码图片及其对应的验证码字符串。
:return: 返回验证码图片的响应对象和验证码字符串。
"""
image, code = vieCode().GetCodeImage()
code = ''.join(code).lower()
out = BytesIO()
# session["code"] = code
image.save(out, 'png')
out.seek(0)
resp = make_response(out.read())
resp.content_type = 'image/png'
return resp, code
def normal_log(method, url, ip, user_agent, desc, uid, is_access):
"""
记录通用日志信息到数据库。
:param method: 请求方法(如 GET、POST)。
:param url: 请求的 URL。
:param ip: 客户端的 IP 地址。
:param user_agent: 客户端的 User-Agent 信息。
:param desc: 日志描述信息。
:param uid: 用户 ID。
:param is_access: 是否成功访问(True 或 False)。
:return: 返回日志记录的 ID。
"""
info = {
'method': method,
'url': url,
'ip': ip,
'user_agent': user_agent,
'desc': desc,
'uid': uid,
'success': int(is_access)
}
log = AdminLog(
url=info.get('url'),
ip=info.get('ip'),
user_agent=info.get('user_agent'),
desc=info.get('desc'),
uid=info.get('uid'),
method=info.get('method'),
success=info.get('success')
)
db.session.add(log)
db.session.commit()
return log.id
def login_log(request, uid, is_access):
"""
记录用户登录日志。
:param request: Flask 请求对象。
:param uid: 用户 ID。
:param is_access: 是否成功登录(True 或 False)。
:return: 返回日志记录的 ID。
"""
method = request.method
url = request.path
ip = request.remote_addr
user_agent = str_escape(request.headers.get('User-Agent'))
desc = str_escape(request.form.get('username'))
return normal_log(method, url, ip, user_agent, desc, uid, is_access)
def admin_log(request, is_access, desc=None):
"""
记录管理员操作日志。
:param request: Flask 请求对象。
:param is_access: 是否成功操作(True 或 False)。
:param desc: 日志描述信息(可选)。如果未提供,则从请求数据中提取。
:return: 返回日志记录的 ID。
"""
method = request.method
url = request.path
ip = request.remote_addr
user_agent = str_escape(request.headers.get('User-Agent'))
request_data = request.json if request.headers.get('Content-Type') == 'application/json' else request.values
if desc is None:
desc = str_escape(str(dict(request_data)))
return normal_log(method, url, ip, user_agent, desc, current_user.id, is_access)
-58
View File
@@ -1,58 +0,0 @@
from flask_login import current_user
from applications.common.utils.validate import str_escape
from applications.extensions import db
from applications.models import AdminLog
def login_log(request, uid, is_access):
info = {
'method': request.method,
'url': request.path,
'ip': request.remote_addr,
'user_agent': str_escape(request.headers.get('User-Agent')),
'desc': str_escape(request.form.get('username')),
'uid': uid,
'success': int(is_access)
}
log = AdminLog(
url=info.get('url'),
ip=info.get('ip'),
user_agent=info.get('user_agent'),
desc=info.get('desc'),
uid=info.get('uid'),
method=info.get('method'),
success=info.get('success')
)
db.session.add(log)
db.session.flush()
db.session.commit()
return log.id
def admin_log(request, is_access):
request_data = request.json if request.headers.get('Content-Type') == 'application/json' else request.values
info = {
'method': request.method,
'url': request.path,
'ip': request.remote_addr,
'user_agent': str_escape(request.headers.get('User-Agent')),
'desc': str_escape(str(dict(request_data))),
'uid': current_user.id,
'success': int(is_access)
}
log = AdminLog(
url=info.get('url'),
ip=info.get('ip'),
user_agent=info.get('user_agent'),
desc=info.get('desc'),
uid=info.get('uid'),
method=info.get('method'),
success=info.get('success')
)
db.session.add(log)
db.session.commit()
return log.id
+55 -30
View File
@@ -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,8 +103,14 @@ 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:
+105 -58
View File
@@ -1,126 +1,173 @@
from sqlalchemy import and_
from sqlalchemy import and_, func
from applications.extensions import db
class ModelFilter:
"""
orm多参数构造器
"""
filter_field = {}
filter_list = []
ORM 多条件查询构造器,支持多种查询条件组合,自动转义特殊字符防止SQL注入。
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 = []
@staticmethod
def escape_like(value: str, escape_char: str = '\\') -> str:
"""
转义LIKE查询中的特殊字符(%, _ 和转义字符本身)
:param value: 需要转义的原始字符串
:param escape_char: 转义字符(默认反斜杠)
:return: 转义后的安全字符串
"""
return (
value.replace(escape_char, escape_char * 2)
.replace('%', escape_char + '%')
.replace('_', escape_char + '_')
)
def exact(self, field_name, value):
"""
准确查询字段
添加精确匹配条件(自动处理字符串类型参数)
:param field_name: 模型字段名称
:param value:
:param value: 匹配的值(自动过滤空字符串)
"""
if value and value != '':
self.filter_field[field_name] = {"data": value, "type": self.type_exact}
if value is not None and value != '':
# 字符串类型自动调用escape_like(防止特殊字符影响精确匹配)
processed_value = self.escape_like(str(value)) if isinstance(value, str) else value
self.filter_field[field_name] = {"data": processed_value, "type": self.type_exact}
def neq(self, field_name, value):
"""
不等于查询字段
添加不等于条件(自动处理字符串类型参数)
:param field_name: 模型字段名称
:param value:
:param value: 不匹配的值(自动过滤空字符串)
"""
if value and value != '':
self.filter_field[field_name] = {"data": value, "type": self.type_neq}
if value is not None and value != '':
# 字符串类型自动调用escape_like
processed_value = self.escape_like(str(value)) if isinstance(value, str) else value
self.filter_field[field_name] = {"data": processed_value, "type": self.type_neq}
def greater(self, field_name, value):
"""
大于查询字段
添加大于条件(数值/日期比较)
:param field_name: 模型字段名称
:param value:
:param value: 比较的数值/日期
"""
if value and value != '':
if value is not None 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 value: 比较的数值/日期
"""
if value and value != '':
if value is not None 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 value: 需要模糊匹配的字符串(自动过滤空值)
"""
if value and value != '':
self.filter_field[field_name] = {"data": ('%' + value + '%'), "type": self.type_vague}
escaped_value = self.escape_like(value)
self.filter_field[field_name] = {"data": f'%{escaped_value}%', "type": self.type_vague}
def left_vague(self, field_name, value: str):
"""
左模糊查询字段
添加安全左模糊匹配(自动转义特殊字符,左侧加%
:param field_name: 模型字段名称
:param value:
:param value: 需要左模糊匹配的字符串
"""
if value and value != '':
self.filter_field[field_name] = {"data": ('%' + value), "type": self.type_vague}
escaped_value = self.escape_like(value)
self.filter_field[field_name] = {"data": f'%{escaped_value}', "type": self.type_vague}
def right_vague(self, field_name, value: str):
"""
左模糊查询字段
添加安全右模糊匹配(自动转义特殊字符,右侧加%
:param field_name: 模型字段名称
:param value:
:param value: 需要右模糊匹配的字符串
"""
if value and value != '':
self.filter_field[field_name] = {"data": (value + '%'), "type": self.type_vague}
escaped_value = self.escape_like(value)
self.filter_field[field_name] = {"data": f'{escaped_value}%', "type": self.type_vague}
def contains(self, field_name, value: str):
"""
包含查询字段
添加安全包含条件(自动转义特殊字符,等效于vague)
:param field_name: 模型字段名称
:param value:
:param value: 需要包含的字符串
"""
if value and value != '':
self.filter_field[field_name] = {"data": value, "type": self.type_contains}
escaped_value = self.escape_like(value)
self.filter_field[field_name] = {"data": f'%{escaped_value}%', "type": self.type_contains}
def between(self, field_name, value1, value2):
"""
范围查询字段
添加范围查询条件(自动过滤无效值)
:param field_name: 模型字段名称
:param value: 值
:param value1: 范围起始
:param value2: 范围结束值
"""
if value1 and value2 and value1 != '' and value2 != '':
if all([v is not None and v != '' for v in [value1, 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: 组合后的过滤条件(使用and_连接)
"""
for k, v in self.filter_field.items():
if v.get("type") == self.type_vague:
self.filter_list.append(getattr(model, k).like(v.get("data")))
if v.get("type") == self.type_contains:
self.filter_list.append(getattr(model, k).contains(v.get("data")))
if v.get("type") == self.type_exact:
self.filter_list.append(getattr(model, k) == v.get("data"))
if v.get("type") == self.type_neq:
self.filter_list.append(getattr(model, k) != v.get("data"))
if v.get("type") == self.type_greater:
self.filter_list.append(getattr(model, k) > v.get("data"))
if v.get("type") == self.type_less:
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]))
field = getattr(model, k)
data = v.get("data")
query_type = v.get("type")
if query_type == self.type_vague:
self.filter_list.append(field.like(data, escape='\\'))
elif query_type == self.type_contains:
self.filter_list.append(field.like(data, escape='\\'))
elif query_type == self.type_exact:
self.filter_list.append(field == data)
elif query_type == self.type_neq:
self.filter_list.append(field != data)
elif query_type == self.type_greater:
self.filter_list.append(field > data)
elif query_type == self.type_less:
self.filter_list.append(field < data)
elif query_type == self.type_between:
self.filter_list.append(field.between(data[0], data[1]))
return and_(*self.filter_list)
+2
View File
@@ -1,7 +1,9 @@
from flask import Flask
from .admin import admin_cli
from applications.extensions.init_plugins import broadcast_execute
def init_script(app: Flask):
app.cli.add_command(admin_cli)
broadcast_execute(app, 'event_finish')
+9 -9
View File
@@ -174,7 +174,7 @@ powerdata = [
name='角色管理',
type='1',
code='system:role:main',
url='/system/role',
url='/system/role/',
open_type='_iframe',
parent_id='1',
icon='layui-icon layui-icon-username',
@@ -187,7 +187,7 @@ powerdata = [
name='系统监控',
type='1',
code='system:monitor:main',
url='/system/monitor',
url='/system/monitor/',
open_type='_iframe',
parent_id='1',
icon='layui-icon layui-icon-vercode',
@@ -200,7 +200,7 @@ powerdata = [
name='日志管理',
type='1',
code='system:log:main',
url='/system/log',
url='/system/log/',
open_type='_iframe',
parent_id='1',
icon='layui-icon layui-icon-read',
@@ -226,7 +226,7 @@ powerdata = [
name='图片上传',
type='1',
code='system:file:main',
url='/system/file',
url='/system/file/',
open_type='_iframe',
parent_id='17',
icon='layui-icon layui-icon-camera',
@@ -301,7 +301,7 @@ powerdata = [
), Power(
id=26,
name='用户删除',
name='权限删除',
type='2',
code='system:power:remove',
url='',
@@ -395,7 +395,7 @@ powerdata = [
name='数据字典',
type='1',
code='system:dict:main',
url='/system/dict',
url='/system/dict/',
open_type='_iframe',
parent_id='1',
icon='layui-icon layui-icon-console',
@@ -447,7 +447,7 @@ powerdata = [
name='部门管理',
type='1',
code='system:dept:main',
url='/system/dept',
url='/system/dept/',
open_type='_iframe',
parent_id='1',
icon='layui-icon layui-icon-group',
@@ -499,7 +499,7 @@ powerdata = [
name='邮件管理',
type='1',
code='system:mail:main',
url='/system/mail',
url='/system/mail/',
open_type='_iframe',
parent_id='1',
icon='layui-icon ',
@@ -572,4 +572,4 @@ def init_db():
print("用户角色数据存入")
add_role_power()
print("角色权限数据存入")
print("数据初始化完成,请使用run脚本运行")
print("数据初始化完成请使用run脚本运行")
+55
View File
@@ -0,0 +1,55 @@
import time
cache_dict = {}
def cache_set_internal(key, value, expired=5):
"""
程序内部实现的记录缓存,用于简单、体量不大的缓存记录,在程序结束后销毁。对于高速、体量大的环境请配置 Redis 等服务自行记录。
记录缓存,存储键值对,并记录当前时间作为缓存的时间戳。
:param key: 键
:param value: 值
:param expired: 过期时间(秒),默认5秒
"""
cache_dict[key] = {
'value': value,
'expired_time': time.time() + expired
}
def cache_get_internal(key):
"""
获取缓存,根据键从缓存中获取值,并检查是否过期。
:param key: 键
:return: 如果缓存存在且未过期,返回缓存的值;否则返回 None
"""
if key in cache_dict:
cache_item = cache_dict[key]
if time.time() < cache_item['expired_time']:
return cache_item['value']
else:
# 如果缓存已过期,删除该缓存
del cache_dict[key]
return None
def cache_auto_internal(key, call, expired=5):
"""
如果缓存存在直接返回缓存内容,缓存不存在或者过期执行 call 函数,并取得返回值记录并返回。
:param key: 键
:param call: 获取新值的地方
:param expired: 过期时间(秒),默认5秒
"""
data = cache_get_internal(key)
if data is not None:
return data
data = call()
cache_set_internal(key, data, expired)
return data
@@ -7,20 +7,23 @@ class vieCode:
__width = 120 # 画布宽度
__heigth = 45 # 画布高度
__length = 4 # 验证码长度
__draw = None # 画布
__img = None # 图片资源
__draw = None # 画布对象
__img = None # 图片对象
__code = None # 验证码字符
__str = None # 自定义验证码字符集
__inCurve = True # 是否干扰线
__inNoise = True # 是否干扰点
__type = 2 # 验证码类型 1、纯字母 2、数字字母混合
__fontPatn = 'applications/common/utils/fonts/1.ttf' # 字体
__inCurve = True # 是否绘制干扰线
__inNoise = True # 是否绘制干扰点
__type = 2 # 验证码类型1-纯字母,2-数字字母混合
__fontPatn = 'applications/common/utils/fonts/captcha.ttf' # 字体路径
def GetCodeImage(self, size=80, length=4):
'''获取验证码图片
@param int size 验证码大小
@param int length 验证码长度
'''
"""
生成验证码图片及其对应的验证码字符
:param size: 验证码字体大小默认为 80
:param length: 验证码字符长度默认为 4
:return: 返回验证码图片对象和验证码字符
"""
# 准备基础数据
self.__length = length
self.__fontSize = size
@@ -37,14 +40,18 @@ class vieCode:
return self.__img, self.__code
def __cerateFilter(self):
'''模糊处理'''
"""
对验证码图片进行模糊处理增加识别难度
"""
self.__img = self.__img.filter(ImageFilter.BLUR)
filter = ImageFilter.ModeFilter(8)
self.__img = self.__img.filter(filter)
def __createCode(self):
'''创建验证码字符'''
# 是否自定义字符集合
"""
生成验证码字符
"""
# 是否使用自定义字符集
if not self.__str:
# 源文本
number = "3456789"
@@ -55,32 +62,37 @@ class vieCode:
else:
self.__str = srcLetter + srcUpper + number
# 构造验证码
# 随机生成验证码字符
self.__code = random.sample(self.__str, self.__length)
def __createImage(self):
'''创建画布'''
"""
创建画布并设置背景颜色
"""
bgColor = (random.randint(200, 255), random.randint(200, 255), random.randint(200, 255))
self.__img = Image.new('RGB', (self.__width, self.__heigth), bgColor)
self.__draw = ImageDraw.Draw(self.__img)
def __createNoise(self):
'''画干扰点'''
"""
在验证码图片上绘制干扰点
"""
if not self.__inNoise:
return
font = ImageFont.truetype(self.__fontPatn, int(self.__fontSize / 1.5))
for i in range(5):
# 点颜色
# 干扰点颜色
noiseColor = (random.randint(150, 200), random.randint(150, 200), random.randint(150, 200))
putStr = random.sample(self.__str, 2)
for j in range(2):
# 绘
# 绘制干扰
size = (random.randint(-10, self.__width), random.randint(-10, self.__heigth))
self.__draw.text(size, putStr[j], font=font, fill=noiseColor)
pass
def __createCurve(self):
'''画干扰线'''
"""
在验证码图片上绘制干扰曲线
"""
if not self.__inCurve:
return
x = y = 0
@@ -93,7 +105,7 @@ class vieCode:
xend = random.randint(self.__width / 2, self.__width * 2)
w = (2 * math.pi) / t
# 曲线
# 绘制曲线
color = (random.randint(30, 150), random.randint(30, 150), random.randint(30, 150))
for x in range(xend):
if w != 0:
@@ -107,14 +119,16 @@ class vieCode:
i -= i
def __printString(self):
'''打印验证码字符串'''
"""
在画布上打印验证码字符
"""
font = ImageFont.truetype(self.__fontPatn, self.__fontSize)
x = 0
# 打印字符到画
# 打印字符到画
for i in range(self.__length):
# 设置字体随机颜色
color = (random.randint(30, 150), random.randint(30, 150), random.randint(30, 150))
# 计算
# 计算
x = random.uniform(self.__fontSize * i * 0.95, self.__fontSize * i * 1.1)
y = self.__fontSize * random.uniform(0.3, 0.5)
# 打印字符
+21 -4
View File
@@ -2,23 +2,40 @@ from flask import jsonify
def success_api(msg: str = "成功"):
""" 成功响应 默认值“成功” """
"""
返回成功的 API 响应。
:param msg: 成功消息内容,默认为 "成功"
:return: 返回 JSON 格式的响应,包含 `success` 和 `msg` 字段。
"""
return jsonify(success=True, msg=msg)
def fail_api(msg: str = "失败"):
""" 失败响应 默认值“失败” """
"""
返回失败的 API 响应。
:param msg: 失败消息内容,默认为 "失败"
:return: 返回 JSON 格式的响应,包含 `success` 和 `msg` 字段。
"""
return jsonify(success=False, msg=msg)
def table_api(msg: str = "", count=0, data=None, limit=10):
""" 动态表格渲染响应 """
"""
返回动态表格渲染所需的 API 响应。
:param msg: 响应消息内容,默认为空字符串。
:param count: 数据总数,默认为 0。
:param data: 表格数据,默认为 None。
:param limit: 每页数据条数,默认为 10。
:return: 返回 JSON 格式的响应,包含 `msg`、`code`、`data`、`count` 和 `limit` 字段。
"""
res = {
'msg': msg,
'code': 0,
'data': data,
'count': count,
'limit': limit
}
return jsonify(res)
+34 -37
View File
@@ -1,5 +1,5 @@
"""
集成了对 Pear Admin Flask 二次开发的邮件操作,并给了相应的示例。
集成了对 Pear Admin Flask 二次开发的邮件操作模块,并提供了相应的示例。
"""
from flask import current_app
from flask_mail import Message
@@ -7,32 +7,31 @@ from flask_mail import Message
from applications.common.curd import model_to_dicts
from applications.common.helper import ModelFilter
from applications.extensions import db, flask_mail
from applications.extensions.init_mail import mail
from applications.models import Mail
from applications.schemas import MailOutSchema
def get_all(receiver=None, subject=None, content=None):
"""
获取邮件
获取邮件列表,支持根据接收者、主题和内容进行筛选。
返回的列表中的字典构如下::
返回的列表中的字典构如下::
{
"content": "", # html内容
"create_at": "2022-12-25T10:51:17", # 时间
"content": "", # HTML 内容
"create_at": "2022-12-25T10:51:17", # 创建时间
"id": 17, # 邮件ID
"realname": "超级管理", # 创建者
"realname": "超级管理", # 创建者姓名
"receiver": "", # 接收者
"subject": "" # 主题
"subject": "" # 邮件主题
}
:param receiver: 发送者
:param subject: 邮件标题
:param content: 邮件内容
:return: 列表
:param receiver: 接收者邮箱地址,支持模糊查询。
:param subject: 邮件主题,支持模糊查询。
:param content: 邮件内容,支持模糊查询。
:return: 返回符合条件的邮件列表
"""
# 查询参数构造
# 构造查询条件
mf = ModelFilter()
if receiver:
mf.contains(field_name="receiver", value=receiver)
@@ -40,31 +39,27 @@ def get_all(receiver=None, subject=None, content=None):
mf.contains(field_name="subject", value=subject)
if content:
mf.exact(field_name="content", value=content)
# orm查询
# 使用分页获取data需要.items
# 查询邮件数据并分页
mail = Mail.query.filter(mf.get_filter(Mail)).layui_paginate()
return model_to_dicts(schema=MailOutSchema, data=mail.items)
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: 成功与否
:param receiver: 接收者邮箱地址,多个邮箱用英文分号隔开
:param subject: 邮件主题
:param content: 邮件内容(HTML 格式)。
:param user_id: 发送用户ID,表示谁发送了这封邮件。
可以使用 `from flask_login import current_user; current_user.id` 获取当前登录用户的ID。
:return: 发送成功返回 True,失败报错。
"""
try:
msg = Message(subject=subject, recipients=receiver.split(";"), html=content)
flask_mail.send(msg)
except BaseException as e:
current_app.log_exception(e)
return False
send_mail(subject=subject, recipients=receiver.split(";"), content=content)
# 保存邮件记录到数据库
mail = Mail(receiver=receiver, subject=subject, content=content, user_id=user_id)
db.session.add(mail)
db.session.commit()
return True
@@ -72,10 +67,10 @@ def add(receiver, subject, content, user_id):
def delete(id):
"""
删除邮件记录,立刻写入数据库
删除指定的邮件记录。
:param id: 邮件ID
:return: 成功与否
:param id: 邮件ID
:return: 删除成功返回 True,失败返回 False。
"""
res = Mail.query.filter_by(id=id).delete()
if not res:
@@ -83,14 +78,16 @@ def delete(id):
db.session.commit()
return True
def send_mail(subject, recipients, content):
"""原发送邮件函数,不会记录邮件发送记录
"""
发送邮件(不记录发送日志)。
失败报错,请注意使用 try 拦截
注意:如果发送失败会抛出异常,请使用 try-except 进行捕获
:param subject: 主题
:param recipients: 接收者 多个用英文分号隔开
:param content: 邮件 html
:param subject: 邮件主题
:param recipients: 接收者邮箱地址,多个邮箱用英文分号隔开
:param content: 邮件内容(HTML 格式)。
"""
message = Message(subject=subject, recipients=recipients, html=content)
mail.send(message)
flask_mail.send(message)
+11 -2
View File
@@ -1,11 +1,13 @@
from functools import wraps
from flask import abort, request, jsonify, session, current_app
from flask_login import login_required, current_user
from applications.common.admin_log import admin_log
from applications.common.admin import admin_log
def authorize(power: str, log: bool = False):
"""用户权限判断,用于判断目前会话用户是否拥有访问权限
"""
用户权限判断,用于判断目前会话用户是否拥有访问权限。
在模板中有与之对应的全局非修饰函数 authorize ,此函数定义位于 `applications/extensions/init_template_directives.py` 。
:param power: 权限标识
:type power: str
@@ -18,7 +20,12 @@ def authorize(power: str, log: bool = False):
def wrapper(*args, **kwargs):
# 定义管理员的id为1
if current_user.username == current_app.config.get("SUPERADMIN"):
if log:
admin_log(request=request, is_access=True)
return func(*args, **kwargs)
if not power in session.get('permissions'):
if log:
admin_log(request=request, is_access=False)
@@ -26,8 +33,10 @@ def authorize(power: str, log: bool = False):
abort(403)
else:
return jsonify(success=False, msg="权限不足!")
if log:
admin_log(request=request, is_access=True)
return func(*args, **kwargs)
return wrapper
+1 -1
View File
@@ -17,7 +17,7 @@ def get_photo(page, limit):
def upload_one(photo, mime):
filename = photos.save(photo)
file_url = '/_uploads/photos/'+filename
file_url = '/_uploads/photos/' + filename
# file_url = photos.url(filename)
upload_url = current_app.config.get("UPLOADED_PHOTOS_DEST")
size = os.path.getsize(upload_url + '/' + filename)
+178 -125
View File
@@ -1,26 +1,32 @@
# xss过滤
# XSS 过滤
import validators
from markupsafe import escape
from validators import validator
def str_escape(s):
"""
对字符串进行 XSS 过滤,返回转义后的安全字符串。
:param s: 需要转义的字符串。
:return: 返回转义后的字符串,如果输入为空则返回 None。
"""
if not s:
return None
return str(escape(s))
between = validators.between
'''
验证数字是否介于最小值和/或最大值之间。
这将适用于任何类似的类型,如浮点数、小数和日期,而不仅仅是整数。
between(value, min=None, max=None)
def between(*args, **kwargs):
"""
验证数字是否介于最小值和最大值之间。
适用于整数、浮点数、小数和日期等类型。
min-数字的最小必需值。如果未提供,则不会检查最小值。
max-数字的最大值。如果未提供,将不检查最大值。
>>> from datetime import datetime
:param value: 需要验证的数字。
:param min: 数字的最小值(可选)。
:param max: 数字的最大值(可选)。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
示例:
>>> between(5, min=2)
True
@@ -29,159 +35,206 @@ between(value, min=None, max=None)
>>> between(500, max=400)
ValidationFailure(func=between, args=...)
"""
return validators.between(*args, **kwargs)
>>> between(
... datetime(2000, 11, 11),
... min=datetime(1999, 11, 11)
... )
True
'''
domain = validators.domain
'''
返回给定值是否为有效
如果值是有效域名,则此函数返回 True ,否则返回 ValidationFailure
domain(value)
value-要验证的属性域字符串
def domain(*args, **kwargs):
"""
验证给定值是否为有效的域名。
:param value: 需要验证的域名字符串。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
示例:
>>> domain('example.com')
True
>>> domain('example.com/')
ValidationFailure(func=domain, ...)
'''
"""
return validators.domain(*args, **kwargs)
email = validators.email
'''
验证电子邮件地址。验证成功时返回 True ,验证失败时返回
>>> email('someone@example.com')
True
def email(*args, **kwargs):
"""
验证给定值是否为有效的电子邮件地址。
>>> email('bogus@@')
ValidationFailure(func=email, ...)
'''
:param value: 需要验证的电子邮件地址。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
iban = validators.iban
'''
返回给定值是否为有效的IBAN代码。
如果值是有效的IBAN,则此函数返回 True ,否则返回 ValidationFailure 。
示例:
>>> email('someone@example.com')
True
>>> iban('DE29100500001061045672')
True
>>> email('bogus@@')
ValidationFailure(func=email, ...)
"""
return validators.email(*args, **kwargs)
>>> iban('123456')
ValidationFailure(func=iban, ...)
'''
ipv4 = validators.ipv4
'''
返回给定值是否为有效的IPv4地址。
def iban(*args, **kwargs):
"""
验证给定值是否为有效的 IBAN 代码。
>>> ipv4('123.0.0.7')
True
:param value: 需要验证的 IBAN 代码。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
>>> ipv4('900.80.70.11')
ValidationFailure(func=ipv4, args={'value': '900.80.70.11'})
'''
示例:
>>> iban('DE29100500001061045672')
True
ipv6 = validators.ipv6
'''
返回给定值是否为有效的IP版本6地址。
>>> ipv6('abcd:ef::42:1')
True
>>> iban('123456')
ValidationFailure(func=iban, ...)
"""
return validators.iban(*args, **kwargs)
>>> ipv6('abc.0.0.1')
ValidationFailure(func=ipv6, args={'value': 'abc.0.0.1'})
'''
length = validators.length
'''
返回给定字符串的长度是否在指定范围内。
>>> length('something', min=2)
True
def ipv4(*args, **kwargs):
"""
验证给定值是否为有效的 IPv4 地址。
>>> length('something', min=9, max=9)
True
:param value: 需要验证的 IPv4 地址。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
>>> length('something', max=5)
ValidationFailure(func=length, ...)
'''
示例:
>>> ipv4('123.0.0.7')
True
mac_address = validators.mac_address
'''
返回给定值是否为有效MAC地址。
如果该值是有效的MAC地址,则此函数返回 True ,否则返回 ValidationFailure 。
>>> ipv4('900.80.70.11')
ValidationFailure(func=ipv4, args={'value': '900.80.70.11'})
"""
return validators.ipv4(*args, **kwargs)
>>> mac_address('01:23:45:67:ab:CD')
True
>>> mac_address('00:00:00:00:00')
ValidationFailure(func=mac_address, args={'value': '00:00:00:00:00'})
'''
def ipv6(*args, **kwargs):
"""
验证给定值是否为有效的 IPv6 地址。
slug = validators.slug
'''
验证给定值是否为有效的块。
有效的短信息只能包含字母数字字符、连字符和下划线。
>>> slug('my.slug')
ValidationFailure(func=slug, args={'value': 'my.slug'})
:param value: 需要验证的 IPv6 地址。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
>>> slug('my-slug-2134')
True
'''
示例:
>>> ipv6('abcd:ef::42:1')
True
#truthy = validators.truthy
'''
验证给定值不是错误值。
'''
>>> ipv6('abc.0.0.1')
ValidationFailure(func=ipv6, args={'value': 'abc.0.0.1'})
"""
return validators.ipv6(*args, **kwargs)
url = validators.url
'''
返回给定值是否为有效URL。
如果值是有效URL,则此函数返回 True ,否则返回 ValidationFailure 。
>>> url('http://foobar.dk')
True
def length(*args, **kwargs):
"""
验证给定字符串的长度是否在指定范围内。
>>> url('http://10.0.0.1')
True
:param value: 需要验证的字符串。
:param min: 字符串的最小长度(可选)。
:param max: 字符串的最大长度(可选)。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
>>> url('http://foobar.d')
ValidationFailure(func=url, ...)
示例:
>>> length('something', min=2)
True
>>> url('http://10.0.0.1', public=True)
ValidationFailure(func=url, ...)
'''
>>> length('something', min=9, max=9)
True
uuid = validators.uuid
'''
返回给定值是否为有效UUID。
如果值是有效的UUID,则此函数返回 True ,否则返回 ValidationFailure 。
>>> length('something', max=5)
ValidationFailure(func=length, ...)
"""
return validators.length(*args, **kwargs)
>>> uuid('2bc1c94f-0deb-43e9-92a1-4775189ec9f8')
True
>>> uuid('2bc1c94f 0deb-43e9-92a1-4775189ec9f8')
ValidationFailure(func=uuid, ...)
'''
def mac_address(*args, **kwargs):
"""
验证给定值是否为有效的 MAC 地址。
:param value: 需要验证的 MAC 地址。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
示例:
>>> mac_address('01:23:45:67:ab:CD')
True
>>> mac_address('00:00:00:00:00')
ValidationFailure(func=mac_address, args={'value': '00:00:00:00:00'})
"""
return validators.mac_address(*args, **kwargs)
def slug(*args, **kwargs):
"""
验证给定值是否为有效的 Slug 格式。
有效的 Slug 只能包含字母数字字符、连字符和下划线。
:param value: 需要验证的字符串。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
示例:
>>> slug('my.slug')
ValidationFailure(func=slug, args={'value': 'my.slug'})
>>> slug('my-slug-2134')
True
"""
return validators.slug(*args, **kwargs)
def url(*args, **kwargs):
"""
验证给定值是否为有效的 URL。
:param value: 需要验证的 URL。
:param public: 是否仅允许公共 URL(可选)。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
示例:
>>> url('http://foobar.dk')
True
>>> url('http://10.0.0.1')
True
>>> url('http://foobar.d')
ValidationFailure(func=url, ...)
>>> url('http://10.0.0.1', public=True)
ValidationFailure(func=url, ...)
"""
return validators.url(*args, **kwargs)
def uuid(*args, **kwargs):
"""
验证给定值是否为有效的 UUID。
:param value: 需要验证的 UUID。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
示例:
>>> uuid('2bc1c94f-0deb-43e9-92a1-4775189ec9f8')
True
>>> uuid('2bc1c94f 0deb-43e9-92a1-4775189ec9f8')
ValidationFailure(func=uuid, ...)
"""
return validators.uuid(*args, **kwargs)
@validator
def even(value):
"""
验证给定值是否为偶数。
:param value: 需要验证的数字。
:return: 如果是偶数返回 True,否则返回 ValidationFailure。
示例:
>>> even(4)
True
>>> even(5)
ValidationFailure(func=even, args={'value': 5})
"""
return not (value % 2)
'''
一个装饰器,它使给定的函数验证器
每当给定函数被调用并返回 False 值时,这个装饰器返回 ValidationFailure 对象。
>>> @validator
... def even(value):
... return not (value % 2)
>>> even(4)
True
>>> even(5)
ValidationFailure(func=even, args={'value': 5})
'''
+27 -38
View File
@@ -2,13 +2,13 @@ import logging
from datetime import timedelta
# from urllib.parse import quote_plus as urlquote
class BaseConfig:
# 超级管理员账号
SUPERADMIN = 'admin'
# 系统名称
SYSTEM_NAME = 'Pear Admin'
# 主题面板的链接列表配置
SYSTEM_PANEL_LINKS = [
{
@@ -28,42 +28,14 @@ class BaseConfig:
}
]
# 上传图片目标文件夹
UPLOADED_PHOTOS_DEST = 'static/upload'
UPLOADED_FILES_ALLOW = ['gif', 'jpg']
UPLOADED_FILES_ALLOW = ['gif', 'jpg', 'jpeg', 'png', 'webp']
UPLOADS_AUTOSERVE = True
# JSON配置
# JSON 配置
JSON_AS_ASCII = False
SECRET_KEY = "pear-system-flask"
# mysql 配置
# MYSQL_USERNAME = "root"
# MYSQL_PASSWORD = "123456"
# MYSQL_HOST = "127.0.0.1"
# MYSQL_PORT = 3306
# MYSQL_DATABASE = "PearAdminFlask"
# 数据库的配置信息
SQLALCHEMY_DATABASE_URI = 'sqlite:///../pear.db'
# SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{MYSQL_USERNAME}:{urlquote(MYSQL_PASSWORD)}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}?charset=utf8mb4"
# 默认日志等级
LOG_LEVEL = logging.WARN
"""
flask-mail配置
"""
MAIL_SERVER = 'smtp.qq.com'
MAIL_USE_TLS = False
MAIL_USE_SSL = True
MAIL_PORT = 465
MAIL_USERNAME = '123@qq.com'
MAIL_PASSWORD = 'XXXXX' # 生成的授权码
MAIL_DEFAULT_SENDER = MAIL_USERNAME
# 插件配置,填写插件的文件名名称,默认不启用插件。
PLUGIN_ENABLE_FOLDERS = []
# 配置多个数据库连接的连接串写法示例
# HOSTNAME: 指数据库的IP地址、USERNAME:指数据库登录的用户名、PASSWORD:指数据库登录密码、PORT:指数据库开放的端口、DATABASE:指需要连接的数据库名称
# MSSQL: f"mssql+pymssql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=cp936"
@@ -83,13 +55,30 @@ class BaseConfig:
# 'testOracle': 'oracle+cx_oracle://test:123456@192.168.1.1:1521/test',
# 'testSQLite': 'sqlite:///database.db
# }
"""
session
"""
# 数据库的配置信息
SQLALCHEMY_DATABASE_URI = 'sqlite:///../pear.db'
# 默认日志等级
LOG_LEVEL = logging.WARN
# 发信设置
MAIL_SERVER = 'smtp.qq.com'
MAIL_USE_TLS = False
MAIL_USE_SSL = True
MAIL_PORT = 465
MAIL_USERNAME = '123@qq.com'
MAIL_PASSWORD = 'XXXXX' # 生成的授权码
MAIL_DEFAULT_SENDER = MAIL_USERNAME
# 插件配置,填写插件的文件名名称,默认不启用插件。
PLUGIN_ENABLE_FOLDERS = []
# Session 设置
PERMANENT_SESSION_LIFETIME = timedelta(days=7)
SESSION_TYPE = "filesystem" # 默认使用文件系统来保存会话
SESSION_TYPE = "filesystem" # 默认使用文件系统来保存会话
SESSION_PERMANENT = False # 会话是否持久化
SESSION_USE_SIGNER = True # 是否对发送到浏览器上 session 的 cookie 值进行加密
SECRET_KEY = "pear-system-flask"
+10 -2
View File
@@ -7,14 +7,22 @@ from .init_mail import init_mail, mail as flask_mail
from .init_upload import init_upload
from .init_migrate import init_migrate
from .init_session import init_session
from .init_plugins import register_plugin, broadcast_execute
def init_plugs(app: Flask) -> None:
# 注册插件
register_plugin(app)
broadcast_execute(app, 'event_begin')
# 注册 Flask 功能
init_login_manager(app)
init_databases(app)
init_template_directives(app)
init_error_views(app)
init_mail(app)
init_upload(app)
init_migrate(app)
init_session(app)
# 系统蓝图相关
init_template_directives(app)
init_error_views(app)
+61
View File
@@ -0,0 +1,61 @@
import os
from flask import Flask
from flask import Blueprint
import json
import importlib
plugin_bp = Blueprint('plugin', __name__, url_prefix='/plugin')
PLUGIN_ENABLE_FOLDERS = []
PLUGIN_IMPORTLIB = []
def register_plugin(app: Flask):
"""
获取所有插件并加载
"""
global PLUGIN_ENABLE_FOLDERS
app.register_blueprint(plugin_bp)
# 载入插件过程
# plugin_folder 配置的是插件的文件夹名
PLUGIN_ENABLE_FOLDERS = app.config['PLUGIN_ENABLE_FOLDERS']
for plugin_folder in PLUGIN_ENABLE_FOLDERS:
plugin_info = {
'plugin_name': plugin_folder
}
try:
if os.path.exists("plugins/" + plugin_folder + "/__init__.json"):
with open("plugins/" + plugin_folder + "/__init__.json", "r", encoding='utf-8') as f:
plugin_info = json.loads(f.read())
# 将插件全部载入
PLUGIN_IMPORTLIB.append(importlib.import_module('plugins.' + plugin_folder))
print(f" * Plugin: Loaded plugin: {plugin_info['plugin_name']} .")
except BaseException as e:
info = f" * Plugin: Crash a error when loading {plugin_info['plugin_name'] if len(plugin_info) != 0 else 'plugin'} :" + "\n"
app.logger.error(info)
app.logger.exception(e)
def broadcast_execute(app: Flask, function_name):
for plugin in PLUGIN_IMPORTLIB:
try:
# 初始化完成事件
try:
getattr(plugin, function_name)(app)
except AttributeError: # 没有插件启用事件就不调用
pass
except BaseException as e:
app.logger.exception(e)
if function_name == 'event_finish':
with app.app_context():
broadcast_execute(app, 'event_context')
+40 -18
View File
@@ -60,30 +60,52 @@ 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),
error_out=False)
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, page=None, limit=None):
if page is None:
page = request.args.get('page', 1, type=int) # 添加默认值
if limit is None:
limit = request.args.get('limit', 10, type=int) # 添加默认值
def layui_paginate_json(self, schema: Marshmallow().Schema):
"""
返回dict
"""
_res = self.paginate(
page=request.args.get('page', type=int),
per_page=request.args.get('limit', type=int),
page=page,
per_page=limit,
error_out=False
)
return schema(many=True).dump(_res.items), _res.total, _res.page, _res.per_page
def layui_paginate_db_json(self):
"""
db.query(A.name).layui_paginate_db_json()
"""
_res = self.paginate(page=request.args.get('page', type=int),
per_page=request.args.get('limit', type=int),
error_out=False)
return [dict(i) for i in _res.items], _res.total
def layui_paginate_db_json(self, page=None, limit=None):
if page is None:
page = request.args.get('page', 1, type=int) # 添加默认值
if limit is None:
limit = request.args.get('limit', 10, type=int) # 添加默认值
_res = self.paginate(
page=page,
per_page=limit,
error_out=False
)
# 获取查询的列名列表
column_names = [col["name"] for col in self.column_descriptions]
# 将元组转换为字典(支持单列或多列)
data = [
dict(zip(column_names, row))
for row in _res.items
]
return data, _res.total, _res.page, _res.per_page
db = SQLAlchemy(query_class=Query)
+1 -1
View File
@@ -15,4 +15,4 @@ class Dept(db.Model):
remark = db.Column(db.Text, comment="备注")
address = db.Column(db.String(255), comment="详细地址")
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='更新时间')
+4 -2
View File
@@ -1,7 +1,9 @@
from applications.view.system import register_system_bps
from applications.view.plugin import register_plugin_views
from applications.extensions.init_plugins import broadcast_execute
def init_bps(app):
register_system_bps(app)
register_plugin_views(app)
# 插件初始化函数
broadcast_execute(app, 'event_init')
-34
View File
@@ -1,34 +0,0 @@
from flask import Flask
from flask import Blueprint
import json
import traceback
import importlib
plugin_bp = Blueprint('plugin', __name__, url_prefix='/plugin')
PLUGIN_ENABLE_FOLDERS = []
def register_plugin_views(app: Flask):
global PLUGIN_ENABLE_FOLDERS
app.register_blueprint(plugin_bp)
# 载入插件过程
# plugin_folder 配置的是插件的文件夹名
PLUGIN_ENABLE_FOLDERS = app.config['PLUGIN_ENABLE_FOLDERS']
for plugin_folder in PLUGIN_ENABLE_FOLDERS:
plugin_info = {}
try:
with open("plugins/" + plugin_folder + "/__init__.json", "r", encoding='utf-8') as f:
plugin_info = json.loads(f.read())
# 初始化完成事件
try:
getattr(importlib.import_module('plugins.' + plugin_folder), "event_init")(app)
except AttributeError: # 没有插件启用事件就不调用
pass
print(f" * Plugin: Loaded plugin: {plugin_info['plugin_name']} .")
except BaseException as e:
info = f" * Plugin: Crash a error when loading {plugin_info['plugin_name'] if len(plugin_info) != 0 else 'plugin'} :" + "\n"
info += 'str(Exception):\t' + str(Exception) + "\n"
info += 'str(e):\t\t' + str(e) + "\n"
info += 'repr(e):\t' + repr(e) + "\n"
info += 'traceback.format_exc():\n%s' + traceback.format_exc()
print(info)
+58 -19
View File
@@ -2,7 +2,7 @@ from flask import Blueprint, render_template, request, jsonify
from applications.common import curd
from applications.common.utils import validate
from applications.common.utils.http import success_api, fail_api
from applications.common.utils.http import success_api, fail_api, table_api
from applications.common.utils.rights import authorize
from applications.common.utils.validate import str_escape
from applications.extensions import db
@@ -11,7 +11,6 @@ from applications.schemas import DeptSchema
bp = Blueprint('dept', __name__, url_prefix='/dept')
@bp.get('/')
@authorize("system:dept:main", log=True)
def main():
@@ -21,11 +20,26 @@ def main():
@bp.post('/data')
@authorize("system:dept:main", log=True)
def data():
data = Dept.query.order_by(Dept.sort).all()
res = {
"data": DeptSchema(many=True).dump(data)
}
return jsonify(res)
dept = Dept.query.order_by(Dept.sort).all()
data = DeptSchema(many=True).dump(dept)
# 创建一个字典,用于存储每个节点的子节点
tree = {}
for item in data:
item["children"] = []
tree[item["id"]] = item
# 构建树形结构
root_nodes = []
for item in data:
parent_id = item["parent_id"] if item["parent_id"] != 0 else None
if parent_id is None:
root_nodes.append(item)
else:
if parent_id in tree:
tree[parent_id]["children"].append(item)
return table_api(msg="请求成功", data=root_nodes)
@bp.get('/add')
@@ -63,7 +77,7 @@ def save():
)
r = db.session.add(dept)
db.session.commit()
return success_api(msg="成功")
return success_api(msg="添加部门成功")
@bp.get('/edit')
@@ -84,7 +98,7 @@ def enable():
d = Dept.query.filter_by(id=id).update({"status": enable})
if d:
db.session.commit()
return success_api(msg="启用成功")
return success_api(msg="启用部门成功")
return fail_api(msg="出错啦")
return fail_api(msg="数据错误")
@@ -99,7 +113,7 @@ def dis_enable():
d = Dept.query.filter_by(id=id).update({"status": enable})
if d:
db.session.commit()
return success_api(msg="禁用成功")
return success_api(msg="禁用部门成功")
return fail_api(msg="出错啦")
return fail_api(msg="数据错误")
@@ -108,7 +122,7 @@ def dis_enable():
@authorize("system:dept:edit", log=True)
def update():
json = request.get_json(force=True)
#id = json.get("deptId"),
# id = json.get("deptId"),
id = str_escape(json.get("deptId"))
data = {
"dept_name": validate.str_escape(json.get("deptName")),
@@ -121,20 +135,45 @@ def update():
}
d = Dept.query.filter_by(id=id).update(data)
if not d:
return fail_api(msg="更新失败")
return fail_api(msg="更新部门失败")
db.session.commit()
return success_api(msg="更新成功")
return success_api(msg="更新部门成功")
@bp.delete('/remove/<int:_id>')
@authorize("system:dept:remove", log=True)
def remove(_id):
d = Dept.query.filter_by(id=_id).delete()
if not d:
return fail_api(msg="删除失败")
res = User.query.filter_by(dept_id=_id).update({"dept_id": None})
return fail_api(msg="删除部门失败")
User.query.filter_by(dept_id=_id).update({"dept_id": None})
db.session.commit()
if res:
return success_api(msg="删除成功")
else:
return fail_api(msg="删除失败")
return success_api(msg="删除部门成功")
# 批量删除
@bp.delete('/batchRemove')
@authorize("system:dept:remove", log=True)
def batch_remove():
ids = request.form.getlist('ids[]')
if not ids:
return fail_api(msg="未提供删除 ID")
for id in ids:
if not id.isdigit():
db.session.rollback()
return fail_api(msg="参数提供错误")
d = Dept.query.filter_by(id=id).delete()
if not d:
return fail_api(msg="删除部门失败")
User.query.filter_by(dept_id=id).update({"dept_id": None})
db.session.commit()
return success_api(msg="删除部门成功")
+91 -24
View File
@@ -33,6 +33,7 @@ def dict_type_data():
dict_all = DictType.query.filter(mf.get_filter(DictType)).layui_paginate()
count = dict_all.total
data = curd.model_to_dicts(schema=DictTypeOutSchema, data=dict_all.items)
return table_api(data=data, count=count)
@@ -46,13 +47,24 @@ def dict_type_add():
@authorize("system:dict:add", log=True)
def dict_type_save():
req_json = request.get_json(force=True)
description = str_escape(req_json.get("description"))
enable = str_escape(req_json.get("enable"))
type_code = str_escape(req_json.get("typeCode"))
if type_code is None:
return fail_api(msg="标识必须填写")
type_name = str_escape(req_json.get("typeName"))
d = DictType(type_name=type_name, type_code=type_code, enable=enable, description=description)
data = {
'type_name': str_escape(req_json.get("typeName")),
'type_code': str_escape(req_json.get("typeCode")),
'enable': str_escape(req_json.get("enable")),
'description': str_escape(req_json.get("description"))
}
description = data['description']
del data['description']
if not all(data.values()):
return fail_api(msg="参数提供不足")
if description is not None:
data['description'] = description
d = DictType(**data)
db.session.add(d)
db.session.commit()
if d.id is None:
@@ -75,16 +87,26 @@ def dict_type_edit():
def dict_type_update():
req_json = request.get_json(force=True)
id = str_escape(req_json.get("id"))
description = str_escape(req_json.get("description"))
enable = str_escape(req_json.get("enable"))
type_code = str_escape(req_json.get("typeCode"))
type_name = str_escape(req_json.get("typeName"))
DictType.query.filter_by(id=id).update({
"description": description,
"enable": enable,
"type_code": type_code,
"type_name": type_name
})
data = {
"description": str_escape(req_json.get("description")),
"enable": str_escape(req_json.get("enable")),
"type_code": str_escape(req_json.get("typeCode")),
"type_name": str_escape(req_json.get("typeName"))
}
if id is None:
return fail_api(msg="字典类型不存在")
description = data['description']
del data['description']
if not all(data.values()):
return fail_api(msg="参数提供不足")
data['description'] = description
DictType.query.filter_by(id=id).update(data)
db.session.commit()
return success_api(msg="更新成功")
@@ -94,11 +116,11 @@ def dict_type_update():
@authorize("system:dict:edit", log=True)
def dict_type_enable():
_id = request.get_json(force=True).get('id')
if id:
res = curd.enable_status(DictType,_id)
if _id:
res = curd.enable_status(DictType, _id)
if not res:
return fail_api(msg="出错啦")
return success_api("成功")
return success_api("成功")
return fail_api(msg="数据错误")
@@ -107,8 +129,8 @@ def dict_type_enable():
@authorize("system:dict:edit", log=True)
def dict_type_dis_enable():
_id = request.get_json(force=True).get('id')
if id:
res = curd.disable_status(DictType,_id)
if _id:
res = curd.disable_status(DictType, _id)
if not res:
return fail_api(msg="出错啦")
return success_api("禁用成功")
@@ -119,9 +141,15 @@ def dict_type_dis_enable():
@bp.delete('/dictType/remove/<int:_id>')
@authorize("system:dict:remove", log=True)
def dict_type_delete(_id):
res = curd.delete_one_by_id(DictType,_id)
DictData.query.filter_by(
type_code=DictType.query.filter_by(id=_id).first().type_code
).all()
res = DictType.query.filter_by(id=_id).delete()
if not res:
return fail_api(msg="删除失败")
db.session.commit()
return success_api(msg="删除成功")
@@ -148,11 +176,13 @@ def dict_data_add():
@authorize("system:dict:add", log=True)
def dict_data_save():
req_json = request.get_json(force=True)
data_label = str_escape(req_json.get("dataLabel"))
data_value = str_escape(req_json.get("dataValue"))
enable = str_escape(req_json.get("enable"))
remark = str_escape(req_json.get("remark"))
type_code = str_escape(req_json.get("typeCode"))
d = DictData(data_label=data_label, data_value=data_value, enable=enable, remark=remark, type_code=type_code)
db.session.add(d)
db.session.commit()
@@ -213,7 +243,7 @@ def dict_data_disenable():
return fail_api(msg="数据错误")
# 删除字典类型
# 删除字典数据
@bp.delete('dictData/remove/<int:id>')
@authorize("system:dict:remove", log=True)
def dict_data_delete(id):
@@ -221,3 +251,40 @@ def dict_data_delete(id):
if not res:
return fail_api(msg="删除失败")
return success_api(msg="删除成功")
# 批量删除字典
@bp.delete('dictData/batchRemoveDictType')
@authorize("system:dict:remove", log=True)
def dict_type_batch_remove():
ids = request.form.getlist('ids[]')
for _id in ids:
DictData.query.filter_by(
type_code=DictType.query.filter_by(id=_id).first().type_code
).all()
res = DictType.query.filter_by(id=_id).delete()
if res == 0:
db.session.rollback()
return fail_api(msg="删除失败,请重试")
db.session.commit()
return success_api(msg="删除成功")
# 批量删除字典数据
@bp.delete('dictData/batchRemoveDictData')
@authorize("system:dict:remove", log=True)
def dict_data_batch_remove():
ids = request.form.getlist('ids[]')
for _id in ids:
res = curd.delete_one_by_id(model=DictData, id=_id)
if not res:
db.session.rollback()
return fail_api(msg="删除失败,请重试")
db.session.commit()
return success_api(msg="删除成功")
+6 -3
View File
@@ -22,9 +22,11 @@ def index():
def login_log():
# orm查询
# 使用分页获取data需要.items
log = AdminLog.query.filter_by(url='/passport/login').order_by(desc(AdminLog.create_time)).layui_paginate()
log = AdminLog.query.filter_by(
url='/system/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)
return table_api(data=model_to_dicts(schema=LogOutSchema, data=log.items), count=count)
# 操作日志
@@ -34,7 +36,8 @@ def operate_log():
# orm查询
# 使用分页获取data需要.items
log = AdminLog.query.filter(
AdminLog.url != '/passport/login').order_by(
AdminLog.url != '/system/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)
+9 -11
View File
@@ -1,14 +1,16 @@
from flask import Blueprint, render_template, request, current_app
from flask_login import current_user
from flask_mail import Message
from applications.common.curd import model_to_dicts
from applications.common.helper import ModelFilter
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, flask_mail
from applications.extensions import db
from applications.models import Mail
from applications.schemas import MailOutSchema
from applications.common.utils import mail
from applications.common.admin import admin_log
bp = Blueprint('adminMail', __name__, url_prefix='/mail')
@@ -20,7 +22,7 @@ def main():
return render_template('system/mail/main.html')
# 用户分页查询
# 用户分页查询
@bp.get('/data')
@authorize("system:mail:main")
def data():
@@ -61,17 +63,13 @@ def save():
user_id = current_user.id
try:
msg = Message(subject=subject, recipients=receiver.split(";"), body=content)
flask_mail.send(msg)
if mail.add(receiver=receiver, subject=subject, content=content, user_id=user_id):
return success_api(msg="增加成功")
except Exception as e:
current_app.log_exception(e)
return fail_api(msg="发送失败,请检查邮件配置或发送人邮箱是否写错")
admin_log(request, False, desc="发送日志失败:" + str(e))
mail = Mail(receiver=receiver, subject=subject, content=content, user_id=user_id)
db.session.add(mail)
db.session.commit()
return success_api(msg="增加成功")
return success_api(msg="发送失败,请检查日志。")
# 删除用户
+116 -47
View File
@@ -1,47 +1,33 @@
import os
import platform
import re
import sys
import time
import psutil
import platform
from datetime import datetime
import psutil
from flask import Blueprint, render_template, jsonify
from flask import Blueprint, render_template
from applications.common.utils.http import table_api, success_api
from applications.common.utils.rights import authorize
from applications.common.utils.cache import cache_auto_internal
bp = Blueprint('adminMonitor', __name__, url_prefix='/monitor')
# 系统监控
@bp.get('/')
@authorize("system:monitor:main")
def main():
# 主机名称
hostname = platform.node()
# 系统版本
system_version = platform.platform()
# python版本
python_version = platform.python_version()
# 逻辑cpu数量
cpu_count = psutil.cpu_count()
# cpu使用率
cpus_percent = psutil.cpu_percent(interval=0.1, percpu=False) # percpu 获取主使用率
# 内存
memory_information = psutil.virtual_memory()
# 内存使用率
memory_usage = memory_information.percent
memory_used: int = memory_information.used
memory_total: int = memory_information.total
memory_free: int = memory_information.free
# 磁盘信息
def get_disk_partitions_list():
disk_partitions_list = []
# 判断是否在容器中
if not os.path.exists('/.dockerenv'):
disk_partitions = psutil.disk_partitions()
for i in disk_partitions:
a = psutil.disk_usage(i.device)
try:
a = psutil.disk_usage(i.device)
except PermissionError:
continue
disk_partitions_dict = {
'device': i.device,
'fstype': i.fstype,
@@ -51,30 +37,58 @@ def main():
'percent': a.percent
}
disk_partitions_list.append(disk_partitions_dict)
else:
try:
usage = psutil.disk_usage('/')
except PermissionError:
pass
disk_partitions_list.append({
'device': '/', # 设备名称(根文件系统)
'fstype': psutil.disk_partitions()[0].fstype, # 文件系统类型
'total': usage.total, # 总容量(字节)
'used': usage.used, # 已用空间(字节)
'free': usage.free, # 可用空间(字节)
'percent': usage.percent # 使用百分比
})
return disk_partitions_list
def get_basic_info():
# 主机名称
hostname = platform.node()
# 系统版本
system_version = platform.platform()
# Python 版本
python_version = platform.python_version()
# 开机时间
boot_time = datetime.fromtimestamp(psutil.boot_time()).replace(microsecond=0)
up_time = datetime.now().replace(microsecond=0) - boot_time
up_time_list = re.split(r':', str(up_time))
up_time_format = " {} 小时{} 分钟{}".format(up_time_list[0], up_time_list[1], up_time_list[2])
up_time_format = "{} 小时 {} 分钟 {}".format(up_time_list[0], up_time_list[1], up_time_list[2])
up_time_format = up_time_format.replace("days,", "")
return {
'hostname': hostname,
'system_version': system_version,
'python_version': python_version,
'boot_time': boot_time,
'up_time_format': up_time_format
}
# 系统监控
@bp.get('/')
@authorize("system:monitor:main")
def main():
# 当前时间
time_now = time.strftime('%H:%M:%S ', time.localtime(time.time()))
return render_template(
'system/monitor.html',
hostname=hostname,
system_version=system_version,
python_version=python_version,
cpus_percent=cpus_percent,
memory_usage=memory_usage,
cpu_count=cpu_count,
memory_used=memory_used,
memory_total=memory_total,
memory_free=memory_free,
boot_time=boot_time,
up_time_format=up_time_format,
disk_partitions_list=disk_partitions_list,
time_now=time_now
time_now=time_now,
**get_basic_info()
)
@@ -82,19 +96,74 @@ def main():
@bp.get('/polling')
@authorize("system:monitor:main")
def ajax_polling():
# 获取cpu使用率
cpus_percent = psutil.cpu_percent(interval=0.1, percpu=False) # percpu 获取主使用率
# 获取内存使用率
memory_information = psutil.virtual_memory()
# 获取 CPU 核心数
cpu_count = cache_auto_internal('cpu_count', lambda: psutil.cpu_count(), expired=999999999)
# 获取 CPU 使用率
cpus_percent = cache_auto_internal('cpus_percent',
lambda: psutil.cpu_percent(interval=1, percpu=False),
expired=5)
# 每个 CPU 的使用率
cpu_percent_per_core = cache_auto_internal('cpu_percent_per_core',
lambda: list(enumerate(psutil.cpu_percent(interval=1, percpu=True))),
expired=5)
# 获取空闲率、等待率
cpu_times_percent = cache_auto_internal('cpu_times_percent',
lambda: psutil.cpu_times_percent(interval=1, percpu=False),
expired=5)
# 内存信息
memory_information = cache_auto_internal('memory_information',
psutil.virtual_memory,
expired=5)
# 硬盘信息
disk_partitions_list = cache_auto_internal('disk_partitions_list',
get_disk_partitions_list,
expired=5)
# 系统信息
basic_info = cache_auto_internal('basic_info',
get_basic_info,
expired=5)
memory_usage = memory_information.percent
time_now = time.strftime('%H:%M:%S ', time.localtime(time.time()))
return jsonify(cups_percent=cpus_percent, memory_used=memory_usage, time_now=time_now)
memory_used = memory_information.used
memory_total = memory_information.total
memory_free = memory_information.free
cpu_idle_percent = cpu_times_percent.idle
if hasattr(cpu_times_percent, 'iowait'):
cpu_wait_percent = cpu_times_percent.iowait
else:
cpu_wait_percent = "-"
return table_api(msg="请求成功",
count=0,
data={
'cpu_count': cpu_count,
'cpus_percent': cpus_percent,
'cpu_idle_percent': cpu_idle_percent,
'cpu_wait_percent': cpu_wait_percent,
'cpu_percent_per_core': cpu_percent_per_core,
'memory_used': memory_used,
'memory_total': memory_total,
'memory_free': memory_free,
'memory_usage': memory_usage,
'disk_partitions_list': disk_partitions_list,
'time_now': time.strftime('%H:%M:%S', time.localtime(time.time())),
'basic_info': basic_info
})
# 关闭程序
@bp.get('/kill')
@authorize("system:monitor:main")
def kill():
# 注:若是多 worker 则不生效
return success_api(msg="关闭命令已发送,请修改代码以生效。")
for proc in psutil.process_iter():
if proc.pid == os.getpid():
proc.kill()
+9 -6
View File
@@ -1,8 +1,7 @@
from flask import Blueprint, session, redirect, url_for, render_template, request
from flask_login import current_user, login_user, login_required, logout_user
from applications.common import admin as index_curd
from applications.common.admin_log import login_log
from applications.common.admin import get_captcha, login_log
from applications.common.utils.http import fail_api, success_api
from applications.models import User
@@ -11,8 +10,8 @@ bp = Blueprint('passport', __name__, url_prefix='/passport')
# 获取验证码
@bp.get('/getCaptcha')
def get_captcha():
resp, code = index_curd.get_captcha()
def captcha():
resp, code = get_captcha()
session["code"] = code
return resp
@@ -21,7 +20,7 @@ def get_captcha():
@bp.get('/login')
def login():
if current_user.is_authenticated:
return redirect(url_for('system.index'))
return redirect(url_for('index.index'))
return render_template('system/login.html')
@@ -75,6 +74,7 @@ def login_post():
# session['role'] = [roles]
return success_api(msg="登录成功")
login_log(request, uid=user.id, is_access=False)
return fail_api(msg="用户名或密码错误")
@@ -84,5 +84,8 @@ def login_post():
@login_required
def logout():
logout_user()
session.pop('permissions')
if 'permissions' in session:
session.pop('permissions')
return success_api(msg="注销成功")
+75 -28
View File
@@ -1,7 +1,7 @@
from flask import Blueprint, render_template, request, jsonify
from applications.common import curd
from applications.common.utils.http import success_api, fail_api
from applications.common.utils.http import success_api, fail_api, table_api
from applications.common.utils.rights import authorize
from applications.common.utils.validate import str_escape
from applications.extensions import db
@@ -22,10 +22,25 @@ def index():
@authorize("system:power:main")
def data():
power = Power.query.all()
res = {
"data": PowerSchema(many=True).dump(power)
}
return jsonify(res)
data = PowerSchema(many=True).dump(power)
# 创建一个字典,用于存储每个节点的子节点
tree = {}
for item in data:
item["children"] = []
tree[item["id"]] = item
# 构建树形结构
root_nodes = []
for item in data:
parent_id = item["parent_id"] if item["parent_id"] != 0 else None
if parent_id is None:
root_nodes.append(item)
else:
if parent_id in tree:
tree[parent_id]["children"].append(item)
return table_api(msg="请求成功", data=root_nodes)
@bp.get('/add')
@@ -53,28 +68,25 @@ def select_parent():
@authorize("system:power:add", log=True)
def save():
req = request.get_json(force=True)
icon = str_escape(req.get("icon"))
openType = str_escape(req.get("openType"))
parentId = str_escape(req.get("parentId"))
powerCode = str_escape(req.get("powerCode"))
powerName = str_escape(req.get("powerName"))
powerType = str_escape(req.get("powerType"))
powerUrl = str_escape(req.get("powerUrl"))
sort = str_escape(req.get("sort"))
power = Power(
icon=icon,
open_type=openType,
parent_id=parentId,
code=powerCode,
name=powerName,
type=powerType,
url=powerUrl,
sort=sort,
enable=1
)
r = db.session.add(power)
data = {
"icon": str_escape(req.get("icon")),
"open_type": str_escape(req.get("openType")),
"parent_id": str_escape(req.get("parentId")),
"code": str_escape(req.get("powerCode")),
"name": str_escape(req.get("powerName")),
"type": str_escape(req.get("powerType")),
"url": str_escape(req.get("powerUrl")),
"sort": str_escape(req.get("sort"))
}
if not data['sort'].isdigit():
return fail_api(msg="参数 sort 需为整数")
power = Power(**data)
db.session.add(power)
db.session.commit()
return success_api(msg="成功")
return success_api(msg="权限添加成功")
# 权限编辑
@@ -96,6 +108,7 @@ def edit(_id):
def update():
req_json = request.get_json(force=True)
id = request.get_json(force=True).get("powerId")
data = {
"icon": str_escape(req_json.get("icon")),
"open_type": str_escape(req_json.get("openType")),
@@ -106,6 +119,10 @@ def update():
"url": str_escape(req_json.get("powerUrl")),
"sort": str_escape(req_json.get("sort"))
}
if not data['sort'].isdigit():
return fail_api(msg="参数 sort 需为整数")
res = Power.query.filter_by(id=id).update(data)
db.session.commit()
if not res:
@@ -118,7 +135,7 @@ def update():
@authorize("system:power:edit", log=True)
def enable():
_id = request.get_json(force=True).get('powerId')
if id:
if _id:
res = curd.enable_status(Power, _id)
if not res:
return fail_api(msg="出错啦")
@@ -144,11 +161,41 @@ def dis_enable():
@authorize("system:power:remove", log=True)
def remove(id):
power = Power.query.filter_by(id=id).first()
power.role = []
if power:
power.role = []
r = Power.query.filter_by(id=id).delete()
db.session.commit()
if r:
return success_api(msg="删除成功")
else:
return fail_api(msg="删除失败")
# 批量删除
@bp.delete('/batchRemove')
@authorize("system:power:remove", log=True)
def batch_remove():
ids = request.form.getlist('ids[]')
if not ids:
return fail_api(msg="未提供删除 ID")
for id in ids:
if not id.isdigit():
db.session.rollback()
return fail_api(msg="参数提供错误")
id = int(id)
power = Power.query.filter_by(id=id).first()
if power:
# 清空关联的角色(如果需要)
power.role = []
db.session.delete(power)
db.session.commit()
return success_api(msg="批量删除成功")
+114 -3
View File
@@ -4,6 +4,7 @@ from collections import OrderedDict
from flask import jsonify, current_app, Blueprint, render_template
from flask_login import login_required, current_user
from ...common.utils.http import table_api
from ...models import Power
from ...schemas import PowerOutSchema
@@ -43,6 +44,8 @@ def configs():
"keepState": True,
# 是否开启 Tab 记忆
"session": True,
# 预加载
"preload": False,
# 最大可打开的选项卡数量
"max": 30,
"index": {
@@ -83,10 +86,118 @@ def configs():
"keepLoad": 0,
# 布局顶部主题
"autoHead": False
}, header=False)
}, header={
'message': '/system/rights/message'
})
return jsonify(config)
# 消息
@bp.get('/message')
@login_required
def message():
return dict(code=200,
data=[
{
"id": 1,
"title": "通知",
"children": [
{
"id": 11,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
"title": "你收到了 14 份新周报",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
},
{
"id": 12,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
"title": "曲妮妮 已通过第三轮面试",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
},
{
"id": 11,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png",
"title": "可以区分多种通知类型",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
},
{
"id": 12,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
"title": "左侧图标用于区分不同的类型",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
},
{
"id": 11,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
"title": "内容不要超过两行字",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
}
]
},
{
"id": 2,
"title": "消息",
"children": [
{
"id": 11,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
"title": "你收到了 14 份新周报",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
},
{
"id": 12,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
"title": "曲妮妮 已通过第三轮面试",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
},
{
"id": 11,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png",
"title": "可以区分多种通知类型",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
},
{
"id": 12,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
"title": "左侧图标用于区分不同的类型",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
},
{
"id": 11,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
"title": "内容不要超过两行字",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
}
]
},
{
"id": 3,
"title": "代办",
"children": []
}
])
# 菜单
@bp.get('/menu')
@login_required
@@ -121,7 +232,6 @@ def menu():
del menu_dict[_dict['id']]
if _dict['parent_id'] not in menu_dict:
menu_dict[_dict['parent_id']] = [_dict]
else:
menu_dict[_dict['parent_id']].append(_dict)
@@ -147,8 +257,9 @@ def menu():
menu_dict[_dict['parent_id']].append(_dict)
return jsonify(sorted(menu_dict.get(0), key=lambda item: item['sort']))
# 控制台页面
@bp.get('/welcome')
@login_required
def welcome():
return render_template('system/console/console.html')
return render_template('system/analysis/main.html')
+42 -16
View File
@@ -45,21 +45,31 @@ def add():
@authorize("system:role:add", log=True)
def save():
req = request.get_json(force=True)
details = str_escape(req.get("details"))
enable = str_escape(req.get("enable"))
roleCode = str_escape(req.get("roleCode"))
roleName = str_escape(req.get("roleName"))
sort = str_escape(req.get("sort"))
role = Role(
details=details,
enable=enable,
code=roleCode,
name=roleName,
sort=sort
)
data = {
"details": str_escape(req.get("details")),
"enable": str_escape(req.get("enable")),
"code": str_escape(req.get("roleCode")), # 角色标识
"name": str_escape(req.get("roleName")), # 角色名称
"sort": str_escape(req.get("sort"))
}
details = data.get('details')
del data['details']
if not all(data.values()):
return fail_api(msg="参数不足")
data['details'] = details
# 参数效验
if not data['sort'].isdigit():
return fail_api(msg="参数 sort 需为整数")
role = Role(**data)
db.session.add(role)
db.session.commit()
return success_api(msg="成功")
return success_api(msg="添加角色成功")
# 角色授权
@@ -103,6 +113,9 @@ def save_role_power():
role_id = req_form.get("roleId")
role = Role.query.filter_by(id=role_id).first()
if not role:
return fail_api(msg="角色不存在")
powers = Power.query.filter(Power.id.in_(power_list)).all()
role.power = powers
@@ -124,6 +137,7 @@ def edit(id):
def update():
req_json = request.get_json(force=True)
id = req_json.get("roleId")
data = {
"code": str_escape(req_json.get("roleCode")),
"name": str_escape(req_json.get("roleName")),
@@ -131,10 +145,18 @@ def update():
"enable": str_escape(req_json.get("enable")),
"details": str_escape(req_json.get("details"))
}
if not data['enable'].isdigit():
return fail_api(msg="参数 enable 需为整数")
if not data['sort'].isdigit():
return fail_api(msg="参数 sort 需为整数")
role = Role.query.filter_by(id=id).update(data)
db.session.commit()
if not role:
return fail_api(msg="更新角色失败")
return success_api(msg="更新角色成功")
@@ -142,9 +164,9 @@ def update():
@bp.put('/enable')
@authorize("system:role:edit", log=True)
def enable():
id = request.get_json(force=True).get('roleId')
if id:
res = enable_status(Role, id)
_id = request.get_json(force=True).get('roleId')
if _id:
res = enable_status(Role, _id)
if not res:
return fail_api(msg="出错啦")
return success_api(msg="启动成功")
@@ -169,6 +191,10 @@ def dis_enable():
@authorize("system:role:remove", log=True)
def remove(id):
role = Role.query.filter_by(id=id).first()
if not role:
return fail_api(msg="角色不存在")
# 删除该角色的权限和用户
role.power = []
role.user = []
+6 -5
View File
@@ -57,9 +57,7 @@ def data():
} for user, dept in query.items],
count=query.total)
# 用户增加
# 用户增加
@bp.get('/add')
@authorize("system:user:add", log=True)
def add():
@@ -75,6 +73,7 @@ def save():
username = str_escape(req_json.get('username'))
real_name = str_escape(req_json.get('realName'))
password = str_escape(req_json.get('password'))
dept_id = str_escape(req_json.get('deptId'))
role_ids = a.split(',')
if not username or not real_name or not password:
@@ -82,12 +81,14 @@ def save():
if bool(User.query.filter_by(username=username).count()):
return fail_api(msg="用户已经存在")
user = User(username=username, realname=real_name,enable=1)
user = User(username=username, realname=real_name, enable=1, dept_id=dept_id)
user.set_password(password)
db.session.add(user)
roles = Role.query.filter(Role.id.in_(role_ids)).all()
for r in roles:
user.role.append(r)
db.session.commit()
return success_api(msg="增加成功")
@@ -196,7 +197,7 @@ def edit_password_put():
if res_json.get("newPassword") == '':
return fail_api("新密码不得为空")
if res_json.get("newPassword") != res_json.get("confirmPassword"):
return fail_api("次密码不一样")
return fail_api("次密码不一样")
user = current_user
is_right = user.validate_password(res_json.get("oldPassword"))
if not is_right:
-30
View File
@@ -1,30 +0,0 @@
version: '3'
services:
redis:
image: redis
container_name: redis
ports:
- "63799:6379"
mysql:
image: mysql
container_name: mysql
environment:
MYSQL_ROOT_PASSWORD: 123456
LANG: C.UTF-8
TZ: Asia/Shanghai
MYSQL_CHARSET: utf8mb4
MYSQL_COLLATION: utf8mb4_unicode_ci
ports:
- "3306:3306"
flask:
build:
context: .
dockerfile: Dockerfile
container_name: flask
ports:
- "8000:8000"
depends_on:
- redis
- mysql
restart: always
command: sh -c "./start.sh"
+31
View File
@@ -0,0 +1,31 @@
FROM python:3.11-bookworm
# 复制项目文件
RUN mkdir -p /app
COPY . /app/
COPY ./dockerdata/start.sh /app/start.sh
WORKDIR /app/
# 设置必备项
ENV TIME_ZONE Asia/Shanghai
RUN echo "${TIME_ZONE}" > /etc/timezone \
&& ln -sf /usr/share/zoneinfo/${TIME_ZONE} /etc/localtime
# 安装必备库
RUN python3 -m pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/
# 安装
RUN python3 -m pip install gunicorn -i https://pypi.tuna.tsinghua.edu.cn/simple/
# 初始化数据库
RUN flask db init
RUN flask db migrate
RUN flask db upgrade
RUN flask admin init
# 运行命令
RUN chmod +x start.sh
# 保持执行
CMD /bin/sh
-26
View File
@@ -1,26 +0,0 @@
FROM python:3.7-alpine
COPY . /app/
COPY dockerdata/config.py /app/applications/
COPY dockerdata/start.sh /app/
WORKDIR /app/
ENV TIME_ZONE Asia/Shanghai
ENV PIPURL "https://pypi.tuna.tsinghua.edu.cn/simple/ --trusted-host pypi.douban.com"
RUN apk update \
&& apk add --virtual mysqlclient-build gcc python3-dev musl-dev \
&& apk add --no-cache mariadb-dev \
&& apk add --virtual system-build linux-headers libffi-dev \
&& apk add --no-cache jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev \
&& apk add --no-cache bash bash-doc bash-completion \
&& apk add --no-cache libxslt-dev tzdata g++
RUN echo "${TIME_ZONE}" > /etc/timezone \
&& ln -sf /usr/share/zoneinfo/${TIME_ZONE} /etc/localtime
RUN pip --no-cache-dir install -i ${PIPURL} --upgrade pip \
&& pip --no-cache-dir install -i ${PIPURL} -r requirements.txt \
&& pip --no-cache-dir install -i ${PIPURL} gunicorn \
&& chmod +x /app/start.sh
CMD ./start.sh
-14
View File
@@ -1,14 +0,0 @@
FROM python:3.7-alpine
ENV PIPURL "https://pypi.tuna.tsinghua.edu.cn/simple/ --trusted-host pypi.douban.com"
RUN apk update \
&& apk add --virtual mysqlclient-build gcc python3-dev musl-dev \
&& apk add --no-cache mariadb-dev \
&& apk add --virtual system-build linux-headers libffi-dev \
&& apk add --no-cache jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev \
&& apk add --no-cache bash bash-doc bash-completion \
&& apk add --no-cache libxslt-dev tzdata g++
COPY requirements.txt /requirements.txt
RUN pip --no-cache-dir install -i ${PIPURL} --upgrade pip \
&& pip --no-cache-dir install -i ${PIPURL} -r /requirements.txt \
&& pip --no-cache-dir install -i ${PIPURL} gunicorn
-15
View File
@@ -1,15 +0,0 @@
FROM pearadminflask/python3.7-flask:pillow
COPY . /app/
COPY dockerdata/config.py /app/applications/
COPY dockerdata/start.sh /app
WORKDIR /app/
ENV TIME_ZONE Asia/Shanghai
RUN echo "${TIME_ZONE}" > /etc/timezone \
&& ln -sf /usr/share/zoneinfo/${TIME_ZONE} /etc/localtime \
&& chmod +x /app/start.sh
CMD ./start.sh
-105
View File
@@ -1,105 +0,0 @@
import logging
from urllib.parse import quote_plus as urlquote
from apscheduler.executors.pool import ThreadPoolExecutor
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
class BaseConfig:
DEBUG = True
HOST = '127.0.0.1'
PORT = 5000
SUPERADMIN = 'system'
SYSTEM_NAME = 'Pear Admin'
# 主题面板的链接列表配置
SYSTEM_PANEL_LINKS = [
{
"icon": "layui-icon layui-icon-auz",
"title": "官方网站",
"href": "http://www.pearadmin.com"
},
{
"icon": "layui-icon layui-icon-auz",
"title": "开发文档",
"href": "http://www.pearadmin.com"
},
{
"icon": "layui-icon layui-icon-auz",
"title": "开源地址",
"href": "https://gitee.com/Jmysy/Pear-Admin-Layui"
}
]
UPLOADED_PHOTOS_DEST = 'static/upload'
UPLOADED_FILES_ALLOW = ['gif', 'jpg']
UPLOADS_AUTOSERVE = True
# JSON配置
JSON_AS_ASCII = False
SECRET_KEY = "pear-system-flask"
# redis配置
REDIS_HOST = "127.0.0.1"
REDIS_PORT = 6379
# mysql 配置
MYSQL_USERNAME = "root"
MYSQL_PASSWORD = "123456"
MYSQL_HOST = "172.10.1.31"
MYSQL_PORT = 3306
MYSQL_DATABASE = "PearAdminFlask"
# mysql 数据库的配置信息
SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{MYSQL_USERNAME}:{urlquote(MYSQL_PASSWORD)}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}?charset=utf8mb4"
# 默认日志等级
LOG_LEVEL = logging.WARN
#
MAIL_SERVER = 'smtp.qq.com'
MAIL_USE_TLS = False
MAIL_USE_SSL = True
MAIL_PORT = 465
MAIL_USERNAME = '123@qq.com'
MAIL_PASSWORD = 'XXXXX' # 生成的授权码
MAIL_DEFAULT_SENDER = MAIL_USERNAME
# 設置 APSCHEDULER 參數
SCHEDULER_API_ENABLED = False
SCHEDULER_JOBSTORES: dict = {
'default': SQLAlchemyJobStore(
url=f'mysql+pymysql://{MYSQL_USERNAME}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}')
}
SCHEDULER_EXECUTORS: dict = {
'default': ThreadPoolExecutor(20)
}
SCHEDULER_JOB_DEFAULTS: dict = {
'coalesce': False,
'max_instances': 3
}
# 插件配置
PLUGIN_ENABLE_FOLDERS = ["helloworld"]
# 配置多个数据库连接的连接串写法示例
# HOSTNAME: 指数据库的IP地址、USERNAME:指数据库登录的用户名、PASSWORD:指数据库登录密码、PORT:指数据库开放的端口、DATABASE:指需要连接的数据库名称
# MSSQL: f"mssql+pymssql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=cp936"
# MySQL: f"mysql+pymysql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=utf8"
# Oracle: f"oracle+cx_oracle://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}"
# SQLite "sqlite:/// database.db"
# Postgres f"postgresql+psycopg2://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}"
# Oracle的第二种连接方式
# dsnStr = cx_Oracle.makedsn({HOSTNAME}, 1521, service_name='orcl')
# connect_str = "oracle://%s:%s@%s" % ('{USERNAME}', ' {PASSWORD}', dsnStr)
# 在SQLALCHEMY_BINDS 中设置:'{数据库连接别名}': '{连接串}'
# 最后在models的数据模型class中,在__tablename__前设置 __bind_key__ = '{数据库连接别名}' 即可,表示该数据模型不使用默认的数据库连接,改用“SQLALCHEMY_BINDS”中设置的其他数据库连接
# SQLALCHEMY_BINDS = {
# 'testMySQL': 'mysql+pymysql://test:123456@192.168.1.1:3306/test?charset=utf8',
# 'testMsSQL': 'mssql+pymssql://test:123456@192.168.1.1:1433/test?charset=cp936',
# 'testOracle': 'oracle+cx_oracle://test:123456@192.168.1.1:1521/test',
# 'testSQLite': 'sqlite:///database.db
# }
+17
View File
@@ -0,0 +1,17 @@
version: '3'
services:
flask:
build:
context: ..
dockerfile: dockerdata/Dockerfile
container_name: flask
ports:
- "5000:5000"
restart: always
command: sh -c "/app/start.sh"
networks:
- bridge_network
networks:
bridge_network:
driver: bridge
-40
View File
@@ -1,40 +0,0 @@
version: '3'
services:
mysql:
image: "mysql:5.7"
container_name: mysql
restart: always
ports:
- "3306:3306"
volumes:
- "./dockerdata/mysql/data:/var/lib/mysql"
- "./dockerdata/mysql/initdb:/docker-entrypoint-initdb.d"
environment:
LANG: C.UTF-8
TZ: Asia/Shanghai
MYSQL_ROOT_PASSWORD: 123456
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
networks:
flask-network:
ipv4_address: 172.10.1.31
flask:
build:
context: .
dockerfile: Dockerfile.server
container_name: flask
restart: always
ports:
- "8000:8000"
depends_on:
- mysql
networks:
flask-network:
ipv4_address: 172.10.1.11
networks:
flask-network:
driver: bridge
ipam:
config:
- subnet: 172.10.0.0/16
-19
View File
@@ -1,19 +0,0 @@
import os
import multiprocessing
bind = '0.0.0.0:8000'
backlog = 512
chdir = os.path.dirname(os.path.abspath(__file__))
timeout = 30
worker_class = 'sync'
workers = multiprocessing.cpu_count() * 2 + 1
threads = 2
loglevel = 'info'
access_log_format = '%(t)s %(p)s %(h)s "%(r)s" %(s)s %(L)s %(b)s %(f)s" "%(a)s"'
if not os.path.exists('logs'):
os.mkdir('logs')
accesslog = os.path.join(chdir, "logs/gunicorn_access.log")
errorlog = os.path.join(chdir, "logs/gunicorn_error.log")
-1
View File
@@ -1 +0,0 @@
CREATE DATABASE PearAdminFlask DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;;
-31
View File
@@ -1,31 +0,0 @@
[mysql]
default-character-set=utf8
[client]
default-character-set = utf8
[mysqld]
server_id = 1
# 开启二进制日志
log-bin = mysql-bin
# 只保留7天的二进制日志,以防磁盘被日志占满
expire-logs-days = 7
# 二进制日志自动删除的天数,默认值为0,表示“没有自动删除”,启动时和二进制日志循环时可能删除
expire_logs_days = 7
# 设置编码与字符集
init_connect='SET collation_connection = utf8_unicode_ci'
init_connect='SET NAMES utf8'
character-set-server=utf8
collation-server=utf8_unicode_ci
# 设置错误日志路径
log-error = /usr/local/mysql/log/error.log
# 设置查询日志路径
general_log = on
general_log_file = /usr/local/mysql/log/mysql.log
#开启慢查询日志路径
slow_query_log = on
long_query_time = 3
log-queries-not-using-indexes = on
slow-query-log-file = /usr/local/mysql/log/slowquery.log
-14
View File
@@ -1,14 +0,0 @@
Flask
Flask-APScheduler
Flask-Migrate
Flask-Login
Flask_Reuploaded
Flask-SQLAlchemy
flask-marshmallow
marshmallow-sqlalchemy
PyMySQL
psutil
Flask-Mail
Pillow
validators
cryptography
-4
View File
@@ -1,4 +0,0 @@
#!/bin/bash
rm -rf /root/pear-admin-flask/dockerdata/mysql/data/
docker exec mysql /bin/bash -c "mysql -uroot -p123456 -e 'drop database IF EXISTS PearAdminFlask;CREATE DATABASE PearAdminFlask DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;'"
docker exec flask /bin/sh -c "rm -rf migrations ; sh /app/start.sh"
+5 -21
View File
@@ -1,25 +1,9 @@
#!/bin/bash
# Wait for MySQL container to be ready
echo "Waiting for MySQL container to start..."
until mysql -h mysql -uroot -p123456 -e ";" 2>/dev/null; do
echo "MySQL container not ready, sleeping for 5 seconds..."
sleep 5
done
echo "MySQL container started successfully!"
# 这里可以设置同步时间等操作
# to start create the dababase
echo " start to create the databse... "
mysql -uroot -p123456 -hmysql -e 'CREATE DATABASE PearAdminFlask DEFAULT CHARSET UTF8;'
# 运行程序
echo "Starting application..."
# Initialize Flask database
echo "Initializing Flask database..."
flask db init
flask db migrate
flask db upgrade
flask admin init
# Start gunicorn application
echo "Starting gunicorn application..."
exec gunicorn -c gunicorn.conf.py "applications:create_app()"
# 这里可以修改运行命令
gunicorn -b 0.0.0.0:5000 app:app
+20
View File
@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
-44
View File
@@ -1,44 +0,0 @@
## 项目介绍 :id=start
欢迎阅读 Pear Admin Flask 的开发文档!Pear Admin Flask 是一个基于 Flask 的后台管理系统,拥抱应用广泛的 Python 语言,通过使用本系统,即可快速构建你的功能业务。
项目旨在为 Python 开发者提供一个后台管理系统的模板,成为您构建信息管理系统、物联网后台等应用时灵活、简单的工具。
同时,Pear Admin Flask 项目也是一个对于 Python 初学者友好的项目。此项目处于开发初期,欢迎各位 Python 爱好者加入到 Pear Admin Flask 项目建设中来。如果您在使用此项目的过程中,发现项目代码存在问题,请在 Gitee 上提交 PR ,一起开源共建!
接下来,我们将会为您详细介绍该项目搭建方法与开发架构。
> 如果对项目有不理解的地方欢迎加入我们的讨论群。
![QQ群](assets/qqgroup.jpg)
> 微信群不定期在qq群更新二维码。
![开始使用](assets/界面演示.jpeg)
**[master分支版本](https://gitee.com/pear-admin/pear-admin-flask/tree/master/)**
flask 2.0.1 + flask-sqlalchemy + 权限验证 + Flask-APScheduler 定时任务 + marshmallow 序列化与数据验证
master 分支为主分支,是功能最全、页面最多的分支。
![开始使用](assets/界面演示.jpeg)
## 下载使用 :id=download
#### 1. 官网地址
官网提供稳定版本的 Release 发行版本 [前往](http://www.pearadmin.com)
![官方网址](assets/官网地址.jpg)
#### 2. 源码仓库
如果你需要最新代码,请前往 Gitee 仓库 [前往](https://gitee.com/pear-admin/pear-admin-flask)
![源码仓库](assets/源码仓库.jpg)
如果您完成了这一步,请参阅[下载安装](install.md)章节。
-279
View File
@@ -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") %}
<button class="pear-btn pear-btn-primary pear-btn-sm" lay-event="edit">
<i class="pear-icon pear-icon-edit"></i>
</button>
{% endif %}
{% if authorize("system:user:remove") %}
<button class="pear-btn pear-btn-danger pear-btn-sm" lay-event="remove">
<i class="pear-icon pear-icon-ashbin"></i>
</button>
{% 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", "<h1>Hello</h1>")
```
+ 基于二次开发的邮件发送函数
### 函数原型
函数调用位于项目代码 ```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", "<h1>Hello</h1>", 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="成功")
```
-114
View File
@@ -1,114 +0,0 @@
### 环境要求 :id=install
- Python >= 3.6
- Mysql >= 5.7.0
### 安装配置
#### 克隆远程仓库
您可以使用 git 来克隆远程仓库:
```shell
# 进入项目主目录
cd Pear Admin Flask
# 使用 git 克隆远程仓库
git clone https://gitee.com/pear-admin/pear-admin-flask.git
# 切换分支
git checkout master # master, main or mini
```
或者直接前往 Pear Admin Flask 项目的[Gitee 主页](https://gitee.com/pear-admin/pear-admin-flask)下载项目仓库。
#### 搭建开发环境
我们推荐使用 Python 的虚拟环境来开发该项目,这样便于项目的迁移与二次开发。当然,您也可以选择使用原 Python 环境。
如果你想创建 Python 虚拟环境,你可以使用下面的命令行:
```shell
# 在当前目录的venv文件夹创建虚拟环境
python -m venv venv
# Windows 激活虚拟环境
.\test_env\Scripts\Activate.ps1
# Linux 和 Mac 激活虚拟环境
source ./test_env/bin/activate
```
**如果在创建虚拟环境时报错 “ModuleNotFoundError” ,这说明您的 Python 版本小于 3.3 。**
#### 安装项目依赖
```shell
# 使用 pip 安装必要模块(对于 master 分支)
pip install -r requirements.txt
# 使用 pip 安装必要模块(对于 mini 分支)
pip install -r requirement.txt
```
或者您可以尝试:
```shell
# 使用 pip 安装必要模块
python -m pip install -r requirement.txt
```
#### 配置数据库
`applications/config.py` 中配置好数据库。
> 由于结构与目录调整,我们简化了项目结构,废弃了文件 ```.flaskenv``` ,请使用 config.py 配置程序。
并使用以下命令生成数据库文件:
```shell
# 初始化数据库
flask db init
flask db migrate
flask db upgrade
flask admin init
```
#### 运行项目
```shell
# windows
run.bat
# linux
./run.sh
```
#### 使用docker-compose运行项目
```shell
# 安装docker-compose
curl -L https://github.com/docker/compose/releases/download/1.26.2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
docker-compose --version
docker-compose up -d # -d后台运行
docker-compose stop # 停止启动
docker-compose down # 清除容器
dockerdata/config.py # 配置文件
dockerdata/mysql/initdb/ # MySQL初始化数据在
rm -rf dockerdata/mysql/{log,data}/* # down掉容器后启动需要清除删除log,dat
```
### 二次开发
恭喜!现在您已经成功搭建并运行了 Pear Admin Flask ,是时候参与开发了:
请阅读:
+ Pear Admin Flask [目录结构](list.md) 章节
+ Pear Admin Flask [开发函数](function.md) 章节
+ Pear Admin Flask [插件开发](plugin.md) 章节
其它章节等待更新。
-43
View File
@@ -1,43 +0,0 @@
## 应用结构 :id=config
```应用结构
Pear Admin Flask
├─applications # 应用
│ ├─configs # 配置文件
│ │ ├─ common.py # 普通配置
│ │ └─ config.py # 配置文件对象
│ ├─extensions # 注册插件
│ ├─models # 数据模型
│ ├─static # 静态资源文件
│ ├─templates # 静态模板文件
│ └─views # 视图部分
│ ├─admin # 后台管理视图模块
│ └─index # 前台视图模块
├─docs # 文档说明
├─migrations # 迁移文件记录
├─requirement # 依赖文件
└─.env # 项目的配置文件
```
## 资源结构 :id=static
```资源结构
Pear Admin Flask
├─static # 项目设定的 Flask 资源文件夹
│ ├─admin # pear admin flask 的后端资源文件(与 pear admin layui 同步)
│ ├─index # pear admin flask 的前端资源文件
│ └─upload # 用户上传保存目录
└─templates # 项目设定的 Flask 模板文件夹
├─admin # pear admin flask 的后端管理页面模板
│ ├─admin_log # 日志页面
│ ├─common # 基本模板页面(头部模板与页脚模板)
│ ├─console # 系统监控页面模板
│ ├─dept # 部门管理页面模板
│ ├─dict # 数据自动页面模板
│ ├─mail # 邮件管理页面模板
│ ├─photo # 图片上传页面模板
│ ├─power # 权限(菜单)管理页面模板
│ ├─role # 角色管理页面模板
│ ├─task # 任务设置页面模板
│ └─user # 用户管理页面模板
├─errors # 错误页面模板
└─index # 主页模板
```
+2
View File
@@ -0,0 +1,2 @@
del _build /s /q
sphinx-build .\source .\_build
-50
View File
@@ -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"] # 排除字段列表
```
-47
View File
@@ -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("加载完毕后,我会输出一句话。")
```
+3
View File
@@ -0,0 +1,3 @@
sphinx>=5
docutils<=0.21
sphinx-rtd-theme

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

@@ -0,0 +1,253 @@
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0" version="26.0.6">
<diagram name="第 1 页" id="MiUs9oOc82b6Rctnqw0G">
<mxGraphModel dx="1838" dy="612" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="oXrC6FassujB1rqgt1jG-76" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="oXrC6FassujB1rqgt1jG-1" target="oXrC6FassujB1rqgt1jG-2" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-1" value="&lt;div&gt;&lt;span style=&quot;background-color: transparent; color: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));&quot;&gt;app = create_app()&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 9px; color: rgb(77, 77, 77);&quot;&gt;app.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="-20" y="220" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-8" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="oXrC6FassujB1rqgt1jG-2" target="oXrC6FassujB1rqgt1jG-3" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-2" value="&lt;div&gt;初始化 Flask()&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 9px; color: rgb(77, 77, 77);&quot;&gt;applications/__init__.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="-20" y="130" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-9" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="oXrC6FassujB1rqgt1jG-3" target="oXrC6FassujB1rqgt1jG-4" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-23" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;" parent="1" source="oXrC6FassujB1rqgt1jG-3" target="oXrC6FassujB1rqgt1jG-22" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-3" value="&lt;div&gt;载入配置&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 9px; color: rgb(77, 77, 77);&quot;&gt;applications/__init__.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="150" y="130" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-10" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="oXrC6FassujB1rqgt1jG-4" target="oXrC6FassujB1rqgt1jG-5" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-26" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;" parent="1" source="oXrC6FassujB1rqgt1jG-4" target="oXrC6FassujB1rqgt1jG-25" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-4" value="&lt;div&gt;注册组件&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 9px; color: rgb(77, 77, 77);&quot;&gt;applications/__init__.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="330" y="130" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-11" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="oXrC6FassujB1rqgt1jG-5" target="oXrC6FassujB1rqgt1jG-6" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-50" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;" parent="1" source="oXrC6FassujB1rqgt1jG-5" target="oXrC6FassujB1rqgt1jG-49" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-5" value="&lt;div&gt;注册应用蓝图&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 9px; color: rgb(77, 77, 77);&quot;&gt;applications/__init__.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="516" y="130" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-67" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;" parent="1" source="oXrC6FassujB1rqgt1jG-6" target="oXrC6FassujB1rqgt1jG-66" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-70" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="oXrC6FassujB1rqgt1jG-6" target="oXrC6FassujB1rqgt1jG-71" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="910" y="351.33331298828125" as="targetPoint" />
<Array as="points">
<mxPoint x="860" y="150" />
<mxPoint x="860" y="330" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-6" value="&lt;div&gt;注册命令&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 9px; color: rgb(77, 77, 77);&quot;&gt;applications/__init__.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="700" y="130" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-22" value="&lt;div&gt;读取 BaseConfig&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 9px; color: rgb(77, 77, 77);&quot;&gt;配置来自&amp;nbsp;applications/config.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="140" y="190" width="140" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-28" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="oXrC6FassujB1rqgt1jG-25" target="oXrC6FassujB1rqgt1jG-27" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-25" value="&lt;div&gt;初始化插件&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 9px; color: rgb(77, 77, 77);&quot;&gt;applications/extensions/init_plugins.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="310" y="190" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-30" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="oXrC6FassujB1rqgt1jG-27" target="oXrC6FassujB1rqgt1jG-29" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-27" value="&lt;div&gt;广播插件&amp;nbsp;event_begin 事件&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 9px; color: rgb(77, 77, 77);&quot;&gt;applications/extensions/init_plugins.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;dashed=1;" parent="1" vertex="1">
<mxGeometry x="310" y="250" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-32" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="oXrC6FassujB1rqgt1jG-29" target="oXrC6FassujB1rqgt1jG-31" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-29" value="&lt;div&gt;初始化 Flask LoginManager&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 9px; color: rgb(77, 77, 77);&quot;&gt;applications/extensions/init_login.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="310" y="310" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-42" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="oXrC6FassujB1rqgt1jG-31" target="oXrC6FassujB1rqgt1jG-37" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-31" value="&lt;div&gt;初始化 SQLAlchemy 数据库&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;color: rgb(77, 77, 77); font-size: 8px;&quot;&gt;applications/extensions/init_sqlalchemy.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="310" y="370" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-44" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="oXrC6FassujB1rqgt1jG-37" target="oXrC6FassujB1rqgt1jG-43" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-37" value="&lt;div&gt;初始化 Mail 邮件组件&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;color: rgb(77, 77, 77); font-size: 8px;&quot;&gt;applications/extensions/init_error_views.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="310" y="430" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-39" value="&lt;div&gt;初始化 服务器错误蓝图&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;color: rgb(77, 77, 77); font-size: 8px;&quot;&gt;applications/extensions/init_error_views.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="310" y="670" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-48" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="oXrC6FassujB1rqgt1jG-41" target="oXrC6FassujB1rqgt1jG-39" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-41" value="&lt;div&gt;初始化 网页模板公用函数&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;color: rgb(77, 77, 77); font-size: 7px;&quot;&gt;applications/extensions/init_template_directives.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="310" y="610" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-46" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="oXrC6FassujB1rqgt1jG-43" target="oXrC6FassujB1rqgt1jG-45" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="1tN3H2LKsTGCSpzGhpWD-4" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;" edge="1" parent="1" source="oXrC6FassujB1rqgt1jG-43" target="1tN3H2LKsTGCSpzGhpWD-3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-43" value="&lt;div&gt;初始化&lt;font style=&quot;font-size: 9px;&quot;&gt; Migrate &lt;/font&gt;数据库迁移组件&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;color: rgb(77, 77, 77); font-size: 9px;&quot;&gt;applications/extensions/init_migrate.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="310" y="490" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-47" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="oXrC6FassujB1rqgt1jG-45" target="oXrC6FassujB1rqgt1jG-41" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-45" value="&lt;div&gt;初始化&lt;font size=&quot;1&quot;&gt;&amp;nbsp;Session 会话&lt;/font&gt;组件&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;color: rgb(77, 77, 77); font-size: 9px;&quot;&gt;applications/extensions/init_session.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="310" y="550" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-54" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;" parent="1" source="oXrC6FassujB1rqgt1jG-49" target="oXrC6FassujB1rqgt1jG-52" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-49" value="&lt;div&gt;注册应用子页面路由&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 9px; color: rgb(77, 77, 77);&quot;&gt;applications/view/__init__.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="496" y="190" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-63" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;" parent="1" source="oXrC6FassujB1rqgt1jG-51" target="oXrC6FassujB1rqgt1jG-62" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-51" value="&lt;div&gt;广播插件&amp;nbsp;event_init 事件&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: rgb(77, 77, 77); font-size: 9px;&quot;&gt;applications/view/__init__.py&lt;/span&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;dashed=1;" parent="1" vertex="1">
<mxGeometry x="496" y="610" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-56" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="oXrC6FassujB1rqgt1jG-52" target="oXrC6FassujB1rqgt1jG-55" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-52" value="&lt;div&gt;注册 用户管理 蓝图&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 9px; color: rgb(77, 77, 77);&quot;&gt;applications/view/system/__init__.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="496" y="250" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-58" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="oXrC6FassujB1rqgt1jG-55" target="oXrC6FassujB1rqgt1jG-57" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-55" value="&lt;div&gt;注册 文件上传 蓝图&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 9px; color: rgb(77, 77, 77);&quot;&gt;applications/view/system/__init__.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="496" y="310" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-60" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=none;endFill=0;dashPattern=1 4;strokeWidth=4;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="575.66" y="428" as="sourcePoint" />
<mxPoint x="575.66" y="478" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-57" value="&lt;div&gt;注册 系统监控 蓝图&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 9px; color: rgb(77, 77, 77);&quot;&gt;applications/view/system/__init__.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="496" y="370" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-61" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="oXrC6FassujB1rqgt1jG-59" target="oXrC6FassujB1rqgt1jG-51" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-59" value="&lt;div&gt;注册 后台首页 蓝图&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 9px; color: rgb(77, 77, 77);&quot;&gt;applications/view/system/__init__.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="496" y="550" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-62" value="&lt;div&gt;注册 启用插件所提供的 蓝图&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 9px; color: rgb(77, 77, 77);&quot;&gt;plugins/*/__init__.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="496" y="670" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-65" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="oXrC6FassujB1rqgt1jG-64" target="oXrC6FassujB1rqgt1jG-59" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-64" value="&lt;div&gt;注册 部门管理 蓝图&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 9px; color: rgb(77, 77, 77);&quot;&gt;applications/view/system/__init__.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="496" y="490" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-69" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;" parent="1" source="oXrC6FassujB1rqgt1jG-66" target="oXrC6FassujB1rqgt1jG-68" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-66" value="&lt;div&gt;注册 应用 cli 命令&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;font-size: 9px; color: rgb(77, 77, 77);&quot;&gt;applications/view/__init__.py&lt;/font&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="680" y="190" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-68" value="&lt;div&gt;广播插件&amp;nbsp;event_finish 事件&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: rgb(77, 77, 77); font-size: 9px;&quot;&gt;applications/view/__init__.py&lt;/span&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;dashed=1;" parent="1" vertex="1">
<mxGeometry x="680" y="250" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-81" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;" parent="1" source="oXrC6FassujB1rqgt1jG-71" target="oXrC6FassujB1rqgt1jG-80" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-71" value="&lt;div&gt;初始化完成&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="700" y="310" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-75" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="oXrC6FassujB1rqgt1jG-72" target="oXrC6FassujB1rqgt1jG-1" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-72" value="&lt;div&gt;初始化开始&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="-20" y="310" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-78" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=none;dashed=1;strokeColor=#007FFF;strokeWidth=2;" parent="1" vertex="1">
<mxGeometry x="490" y="245" width="172" height="355" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-79" value="&lt;font style=&quot;color: rgb(0, 127, 255);&quot;&gt;注册项目的视图函数,&lt;/font&gt;&lt;div&gt;&lt;font style=&quot;color: rgb(0, 127, 255);&quot;&gt;比如用户管理、文件上传等&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;color: rgb(0, 127, 255);&quot;&gt;功能都是在这里初始化的&lt;/font&gt;&lt;/div&gt;" style="text;html=1;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="720" y="550" width="160" height="60" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-80" value="&lt;div&gt;广播插件&amp;nbsp;event_context 事件&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;dashed=1;" parent="1" vertex="1">
<mxGeometry x="680" y="370" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-82" value="&lt;font style=&quot;color: rgb(153, 153, 153);&quot;&gt;等同于 with app.app_context()&lt;/font&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="800" y="430" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-84" value="&lt;font style=&quot;color: rgb(255, 128, 0);&quot;&gt;flask admin init 数据库&lt;/font&gt;&lt;div&gt;&lt;font style=&quot;color: rgb(255, 128, 0);&quot;&gt;初始化的&lt;span style=&quot;background-color: transparent;&quot;&gt;命令是在这里注册的&lt;/span&gt;&lt;/font&gt;&lt;/div&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;strokeColor=none;strokeWidth=1;" parent="1" vertex="1">
<mxGeometry x="880" y="230" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-86" value="" style="endArrow=none;html=1;rounded=0;dashed=1;endFill=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;strokeColor=#FF8000;" parent="1" target="oXrC6FassujB1rqgt1jG-84" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="840" y="210" as="sourcePoint" />
<mxPoint x="880" y="240" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-88" value="" style="endArrow=none;html=1;rounded=0;dashed=1;endFill=0;strokeColor=#999999;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="840" y="412.5" as="sourcePoint" />
<mxPoint x="873" y="432.5" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-89" value="" style="endArrow=none;html=1;rounded=0;dashed=1;endFill=0;strokeColor=#007FFF;exitX=1;exitY=0.75;exitDx=0;exitDy=0;" parent="1" source="oXrC6FassujB1rqgt1jG-78" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="670" y="520" as="sourcePoint" />
<mxPoint x="720" y="550" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-90" value="&lt;font style=&quot;color: rgb(0, 153, 0);&quot;&gt;开发插件的蓝图在这里注册&lt;/font&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="700" y="650" width="150" height="30" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-92" value="" style="endArrow=none;html=1;rounded=0;dashed=1;endFill=0;strokeColor=#009900;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="662" y="630" as="sourcePoint" />
<mxPoint x="720" y="649" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-93" value="&lt;font style=&quot;color: rgb(150, 150, 150);&quot;&gt;默认读取的是&lt;/font&gt;&lt;div&gt;&lt;font style=&quot;color: rgb(150, 150, 150);&quot;&gt;&amp;nbsp;BaseConfig 类中的配置。&lt;/font&gt;&lt;div&gt;&lt;span style=&quot;background-color: transparent; color: light-dark(rgb(150, 150, 150), rgb(108, 108, 108));&quot;&gt;修改其他类请先在文件&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;font style=&quot;color: rgb(150, 150, 150);&quot;&gt;&lt;span style=&quot;background-color: transparent;&quot;&gt;applications/__init__.py&amp;nbsp;&lt;/span&gt;&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font style=&quot;color: rgb(150, 150, 150);&quot;&gt;&lt;span style=&quot;background-color: transparent;&quot;&gt;中进行修改&lt;/span&gt;&lt;/font&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="125" y="260" width="145" height="90" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-95" value="" style="endArrow=none;html=1;rounded=0;dashed=1;endFill=0;strokeColor=#999999;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" parent="1" source="oXrC6FassujB1rqgt1jG-22" target="oXrC6FassujB1rqgt1jG-93" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="200" y="240" as="sourcePoint" />
<mxPoint x="233" y="260" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-96" value="&lt;font style=&quot;color: rgb(204, 0, 0);&quot;&gt;404 500 等请求、服务器错误的视图会在这里注册&lt;/font&gt;" style="text;html=1;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="130" y="620" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="oXrC6FassujB1rqgt1jG-97" value="" style="endArrow=none;html=1;rounded=0;dashed=1;endFill=0;strokeColor=#CC0000;exitX=0.709;exitY=0.962;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;exitPerimeter=0;" parent="1" source="oXrC6FassujB1rqgt1jG-96" target="oXrC6FassujB1rqgt1jG-39" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="240" y="670" as="sourcePoint" />
<mxPoint x="298" y="709" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="1tN3H2LKsTGCSpzGhpWD-3" value="导入所有数据库模型&lt;div&gt;&lt;span style=&quot;color: rgb(77, 77, 77); font-size: 9px;&quot;&gt;applications/models/__init__.py&lt;/span&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;align=center;" vertex="1" parent="1">
<mxGeometry x="130" y="490" width="160" height="40" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 835 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+32
View File
@@ -0,0 +1,32 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'Pear Admin Flask master 分支'
copyright = '2025, Yishang'
author = 'Yishang'
release = 'master'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = ["sphinx.ext.intersphinx"]
templates_path = ['_templates']
exclude_patterns = []
language = 'zh_CN'
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']
suppress_warnings = [
'misc.highlighting_failure' # 忽略代码块高亮失败警告
]
+61
View File
@@ -0,0 +1,61 @@
:mod:`admin` -- 后台函数模块
=======================================
:mod:`admin` 模块源代码在文件 `applications/common/admin.py` 下,主要集结了一些常用的后台需要频繁调用的函数。
.. module:: admin
函数
-------------
.. function:: get_captcha()
生成验证码图片及其对应的验证码字符串。
: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)
记录通用日志信息到数据库。
:param method: 请求方法(如 GET、POST)。
:param url: 请求的 URL。
:param ip: 客户端的 IP 地址。
:param user_agent: 客户端的 User-Agent 信息。
:param desc: 日志描述信息。
:param uid: 用户 ID。
:param is_access: 是否成功访问(True 或 False)。
:return: 返回日志记录的 ID。
.. function:: login_log(request, uid, is_access)
记录用户登录日志。
:param request: Flask 请求对象。
:param uid: 用户 ID。
:param is_access: 是否成功登录(True 或 False)。
:return: 返回日志记录的 ID。
.. function:: admin_log(request, is_access, desc=None)
记录管理员操作日志。
:param request: Flask 请求对象。
:param is_access: 是否成功操作(True 或 False)。
:param desc: 日志描述信息(可选)。如果未提供,则从请求数据中提取。
:return: 返回日志记录的 ID。
+92
View File
@@ -0,0 +1,92 @@
.. _简单增删改查模块:
: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。
+129
View File
@@ -0,0 +1,129 @@
.. _字段构造模块:
: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:: escape_like(value: str, escape_char: str = '\\')
转义LIKE查询中的特殊字符(%, _ 和转义字符本身)
:param value: 需要转义的原始字符串
:param escape_char: 转义字符(默认反斜杠)
:return: 转义后的安全字符串
.. 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: 返回组合后的过滤条件。
+22
View File
@@ -0,0 +1,22 @@
.. title:: 公有函数
目录索引
公共模块
------------
.. toctree::
:maxdepth: 1
admin
curd
helper
辅助模块
------------
.. toctree::
:maxdepth: 1
utils/index.rst
+61
View File
@@ -0,0 +1,61 @@
:mod:`cache` -- 应用缓存模块
================================
:mod:`cache` 模块源代码在文件 `applications/common/utils/cache.py` 下,主要用于简单的程序数据缓存。
目前此模块仅启用应用程序缓存,暂时没有联动 Redis 等数据库缓存的功能,后续有意向添加。您可以在自己的项目中添加相关的函数,当然也非常欢迎提交 PR ,一起完善项目。
.. warning::
此模块目前仅用于简单的缓存记录,不能记录大量的、持久的缓存。缓存内容存在内存中,在程序结束后清空!具体的例子是 `系统监控` 页面,用于缓存 5 秒内的 CPU 与内存
的监控数据。
.. module:: cache
变量
-----------
.. py:data:: cache_dict
一个字典,用于存放缓存数据与缓存的过期时间戳。
函数
-----------
.. function:: cache_set_internal(key, value, expired=5)
程序内部实现的记录缓存,用于简单、体量不大的缓存记录,在程序结束后销毁。对于高速、体量大的环境请配置 Redis 等服务自行记录。
记录缓存,存储键值对,并记录当前时间作为缓存的时间戳。
:param key:
:param value:
:param expired: 过期时间(秒),默认5秒
.. function:: cache_get_internal(key)
获取缓存,根据键从缓存中获取值,并检查是否过期。
:param key:
:return: 如果缓存存在且未过期,返回缓存的值;否则返回 None
.. function:: cache_auto_internal(key, call, expired=5)
如果缓存存在直接返回缓存内容,缓存不存在或者过期执行 call 函数,并取得返回值记录并返回。
:param key:
:param call: 获取新值的地方
:param expired: 过期时间(秒),默认5秒
**示例:**
.. code-block:: python
from application.common.utils.cache import cache_auto_internal
def fetch_data():
# 模拟从数据库或接口获取数据
return "new_data"
# 使用 cache_auto_internal 获取缓存或调用 fetch_data 获取新值
result = cache_auto_internal("my_key", fetch_data, expired=10)
print(result) # 输出: "new_data"(如果缓存不存在或已过期)
+114
View File
@@ -0,0 +1,114 @@
:mod:`captcha` -- 验证码生成模块
==================================
:mod:`captcha` 模块源代码在文件 `applications/common/utils/captcha.py` 下,主要用于生成验证码图片。
.. module:: captcha
------
.. class:: vieCode
生成验证码图片。
.. py:attribute:: __fontSize
:type: int
字体大小,默认为 20。
.. py:attribute:: __width
:type: int
画布宽度,默认为 120。
.. py:attribute:: __heigth
:type: int
画布高度,默认为 45。
.. py:attribute:: __length
:type: int
验证码长度,默认为 4。
.. py:attribute:: __draw
:type: ImageDraw.Draw
画布对象。
.. py:attribute:: __img
:type: Image.Image
图片对象。
.. py:attribute:: __code
:type: list
验证码字符。
.. py:attribute:: __str
:type: str
自定义验证码字符集。
.. py:attribute:: __inCurve
:type: bool
是否绘制干扰曲线,默认为 True。
.. py:attribute:: __inNoise
:type: bool
是否绘制干扰点,默认为 True。
.. py:attribute:: __type
:type: int
验证码类型:1-纯字母,2-数字字母混合,默认为 2。
.. py:attribute:: __fontPatn
:type: str
字体路径,默认为 ``applications/common/utils/fonts/captcha.ttf``
.. method:: GetCodeImage(size=80, length=4)
生成验证码图片及其对应的验证码字符。
:param size: 验证码字体大小,默认为 80。
:param length: 验证码字符长度,默认为 4。
:return: 返回验证码图片对象和验证码字符。
**示例:**
.. code-block:: python
vc = vieCode()
img, code = vc.GetCodeImage(size=60, length=6)
img.show() # 显示验证码图片
print("验证码:", code) # 输出验证码字符
.. method:: __cerateFilter()
对验证码图片进行模糊处理,增加识别难度。
.. method:: __createCode()
生成验证码字符。
.. method:: __createImage()
创建画布并设置背景颜色。
.. method:: __createNoise()
在验证码图片上绘制干扰点。
.. method:: __createCurve()
在验证码图片上绘制干扰曲线。
.. method:: __printString()
在画布上打印验证码字符。
+59
View File
@@ -0,0 +1,59 @@
.. _JSON 响应正文生成模块:
:mod:`http` -- JSON 响应正文生成模块
=======================================
:mod:`http` 模块源代码在文件 `applications/common/utils/http.py` 下,主要用于生成 JSON 格式的响应正文。
对于大部分 JSON 格式响应的数据,请尽量遵循响应格式规范。如此方便后续前后端的分离和项目的构建。
.. module:: http
函数
------------
.. function:: success_api(msg: str = "成功")
返回成功的 API 响应。
:param msg: 成功消息内容,默认为 "成功"。
:return: 返回 JSON 格式的响应,包含 `success` 和 `msg` 字段。
.. function:: fail_api(msg: str = "失败")
返回失败的 API 响应。
:param msg: 失败消息内容,默认为 "失败"。
:return: 返回 JSON 格式的响应,包含 `success` 和 `msg` 字段。
.. function:: table_api(msg: str = "", count=0, data=None, limit=10)
返回动态表格渲染所需的 API 响应。
:param msg: 响应消息内容,默认为空字符串。
:param count: 数据总数,默认为 0。
:param data: 表格数据,默认为 None。
: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)
+14
View File
@@ -0,0 +1,14 @@
.. title:: 辅助模块
目录索引
.. toctree::
:maxdepth: 2
cache
captcha
http
mail
rights
upload
validate
+73
View File
@@ -0,0 +1,73 @@
.. _邮件模块:
:mod:`mail` -- 邮件模块
==================================
:mod:`mail` 模块源代码在文件 `applications/common/utils/mail.py` 下,主要用于邮件的发送。
使用前,需要正确在 `applications/config.py` 中配置 SMTP 服务器。
.. module:: mail
函数
---------------
.. function:: get_all(receiver=None, subject=None, content=None)
获取邮件列表,支持根据接收者、主题和内容进行筛选。
返回的列表中的字典结构如下::
{
"content": "", # HTML 内容
"create_at": "2022-12-25T10:51:17", # 创建时间
"id": 17, # 邮件ID
"realname": "超级管理", # 创建者姓名
"receiver": "", # 接收者
"subject": "" # 邮件主题
}
:param receiver: 接收者邮箱地址,支持模糊查询。
:param subject: 邮件主题,支持模糊查询。
:param content: 邮件内容,支持模糊查询。
:return: 返回符合条件的邮件列表。
.. function:: 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` 获取当前登录用户的ID。
:return: 发送成功返回 True,失败报错。
**示例**
.. code-block:: python
from flask_login import current_user
from applications.common.utils import mail
mail.add("test@test.com", "subject", "<h1>Hello</h1>", current_user.id)
.. function:: delete(id)
删除指定的邮件记录。
:param id: 邮件ID。
:return: 删除成功返回 True,失败返回 False。
.. function:: send_mail(subject, recipients, content)
发送邮件(不记录发送日志)。
注意:如果发送失败会抛出异常,请使用 try-except 进行捕获。
:param subject: 邮件主题。
:param recipients: 接收者邮箱地址,多个邮箱用英文分号隔开。
:param content: 邮件内容(HTML 格式)。
+65
View File
@@ -0,0 +1,65 @@
.. _权限验证模块:
:mod:`rights` -- 权限验证模块
==================================
:mod:`rights` 模块源代码在文件 `applications/common/utils/rights.py` 下,主要用于权限验证。
.. module:: rights
函数
---------------
.. function:: authorize(power: str, log: bool = False)
用户权限判断,用于判断目前会话用户是否拥有访问权限。此函数是一个修饰器,可用于修饰视图函数。
在模板中有与之对应的全局非修饰函数 authorize ,此函数定义位于 `applications/extensions/init_template_directives.py`
示例中将会展示两种方式的用法。
:param power: 权限标识
:type power: str
:param log: 是否记录日志,默认为 False
:type log: bool, optional
**修饰函数示例:**
.. code-block:: python
from applications.common.utils.rights import authorize
@app.route("/test")
@authorize("system:power:remove", log=True)
def test_index():
return 'You are allowed.'
**在前端模板中:**
.. code-block:: html
{% if authorize("system:user:edit") %}
<button class="pear-btn pear-btn-primary pear-btn-sm" lay-event="edit">
<i class="pear-icon pear-icon-edit"></i>
</button>
{% endif %}
.. important::
`if authorize("system:user:edit")` 的方式仅适用于 **前端模板渲染** ,不得用于后端代码判断。
如果后端想要使用,请使用 **power in session.get('permissions')** 来判断, `power``权限标识`
.. code-block:: python
from flask import session
...
@bp.get('/test')
def test():
if 'system:user:edit' in session.get('permissions'):
...
...
+34
View File
@@ -0,0 +1,34 @@
:mod:`upload` -- 文件上传模块
==================================
:mod:`upload` 模块源代码在文件 `applications/common/utils/upload.py` 下,主要用于文件上传,目前主要用于图片上传。
.. module:: upload
函数
---------------
.. function:: get_photo(page, limit)
分页获取图片列表,并按创建时间降序排列。
:param page: 当前页码。
:param limit: 每页显示的图片数量。
:return: 返回图片列表和图片总数。
.. function:: upload_one(photo, mime)
上传一张图片,并保存图片信息到数据库。
:param photo: 图片文件对象。
:param mime: 图片的 MIME 类型。
:return: 返回图片的访问 URL。
.. function:: delete_photo_by_id(_id)
根据图片 ID 删除图片及其记录。
:param _id: 图片 ID。
:return: 返回删除操作的结果(数据库中删除的个数,成功非0)。
+231
View File
@@ -0,0 +1,231 @@
:mod:`validate` -- 效验模块
==================================
:mod:`validate` 模块源代码在文件 `applications/common/utils/validate.py` 下,主要用于数据效验与过滤。
.. module:: validate
函数
-------------
.. function:: str_escape(s)
对字符串进行 XSS 过滤,返回转义后的安全字符串。
:param s: 需要转义的字符串。
:return: 返回转义后的字符串,如果输入为空则返回 None。
.. function:: between(*args, **kwargs)
验证数字是否介于最小值和最大值之间。
适用于整数、浮点数、小数和日期等类型。
:param value: 需要验证的数字。
:param min: 数字的最小值(可选)。
:param max: 数字的最大值(可选)。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
**示例:**
.. code-block:: python
from applications.common.utils.validate import between
between(5, min=2) # True
between(13.2, min=13, max=14) # True
between(500, max=400) # ValidationFailure(func=between, args=...)
.. function:: domain(*args, **kwargs)
验证给定值是否为有效的域名。
:param value: 需要验证的域名字符串。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
**示例:**
.. code-block:: python
from applications.common.utils.validate import domain
domain('example.com') # True
domain('example.com/') # ValidationFailure(func=domain, ...)
.. function:: email(*args, **kwargs)
验证给定值是否为有效的电子邮件地址。
:param value: 需要验证的电子邮件地址。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
**示例:**
.. code-block:: python
from applications.common.utils.validate import email
email('someone@example.com') # True
email('bogus@@') # ValidationFailure(func=email, ...)
.. function:: iban(*args, **kwargs)
验证给定值是否为有效的 IBAN 代码。
:param value: 需要验证的 IBAN 代码。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
**示例:**
.. code-block:: python
from applications.common.utils.validate import iban
iban('DE29100500001061045672') # True
iban('123456') # ValidationFailure(func=iban, ...)
.. function:: ipv4(*args, **kwargs)
验证给定值是否为有效的 IPv4 地址。
:param value: 需要验证的 IPv4 地址。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
**示例:**
.. code-block:: python
from applications.common.utils.validate import ipv4
ipv4('123.0.0.7') # True
ipv4('900.80.70.11') # ValidationFailure(func=ipv4, args={'value': '900.80.70.11'})
.. function:: ipv6(*args, **kwargs)
验证给定值是否为有效的 IPv6 地址。
:param value: 需要验证的 IPv6 地址。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
**示例:**
.. code-block:: python
from applications.common.utils.validate import ipv6
ipv6('abcd:ef::42:1') # True
ipv6('abc.0.0.1') # ValidationFailure(func=ipv6, args={'value': 'abc.0.0.1'})
.. function:: length(*args, **kwargs)
验证给定字符串的长度是否在指定范围内。
:param value: 需要验证的字符串。
:param min: 字符串的最小长度(可选)。
:param max: 字符串的最大长度(可选)。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
**示例:**
.. code-block:: python
from applications.common.utils.validate import length
length('something', min=2) # True
length('something', min=9, max=9) # True
length('something', max=5) # ValidationFailure(func=length, ...)
.. function:: mac_address(*args, **kwargs)
验证给定值是否为有效的 MAC 地址。
:param value: 需要验证的 MAC 地址。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
**示例:**
.. code-block:: python
from applications.common.utils.validate import mac_address
mac_address('01:23:45:67:ab:CD') # True
mac_address('00:00:00:00:00') # ValidationFailure(func=mac_address, args={'value': '00:00:00:00:00'})
.. function:: slug(*args, **kwargs)
验证给定值是否为有效的 Slug 格式。
有效的 Slug 只能包含字母数字字符、连字符和下划线。
:param value: 需要验证的字符串。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
**示例:**
.. code-block:: python
from applications.common.utils.validate import slug
slug('my.slug') # ValidationFailure(func=slug, args={'value': 'my.slug'})
slug('my-slug-2134') # True
.. function:: url(*args, **kwargs)
验证给定值是否为有效的 URL。
:param value: 需要验证的 URL。
:param public: 是否仅允许公共 URL(可选)。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
**示例:**
.. code-block:: python
from applications.common.utils.validate import url
url('http://foobar.dk') # True
url('http://10.0.0.1') # True
url('http://foobar.d') # ValidationFailure(func=url, ...)
url('http://10.0.0.1', public=True) # ValidationFailure(func=url, ...)
.. function:: uuid(*args, **kwargs)
验证给定值是否为有效的 UUID。
:param value: 需要验证的 UUID。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
**示例:**
.. code-block:: python
from applications.common.utils.validate import uuid
uuid('2bc1c94f-0deb-43e9-92a1-4775189ec9f8') # True
uuid('2bc1c94f 0deb-43e9-92a1-4775189ec9f8') # ValidationFailure(func=uuid, ...)
.. function:: even(value)
验证给定值是否为偶数。
:param value: 需要验证的数字。
:return: 如果是偶数返回 True,否则返回 ValidationFailure。
**示例:**
.. code-block:: python
from applications.common.utils.validate import even
even(4) # True
even(5) # ValidationFailure(func=even, args={'value': 5})
+29
View File
@@ -0,0 +1,29 @@
目录索引
==================
.. note::
您好!欢迎查看 Pear Admin Flask **master 分支** 开发文档!你可以查看 :ref:`welcome` 章节来查看更多信息。
Pear Admin Flask 项目经过长时间的发展现已是一个比较成熟的 Python 后台框架,非常适合新手学习入门 Python Flask ~
.. warning::
项目经过几次大型修改之后,为了同步项目 Pear Admin Layui ,部分(特别是前端页面的)代码改动较大,若要从旧版本进行迁移,
请根据文档的 :ref:`migration` 章节进行迁移与改变。
.. toctree::
:maxdepth: 1
:caption: 介绍、配置与使用
welcome/index
.. toctree::
:maxdepth: 1
:caption: 公有函数
function/index
.. toctree::
:maxdepth: 1
:caption: 最佳实践
practices/index
+500
View File
@@ -0,0 +1,500 @@
.. _后端页面编写:
后端页面编写
======================
该章节将介绍如何在 Pear Admin Flask 中编写一个新后端页面,此章节将会以编写兑换码管理页面为例,编写一个兑换码管理的后台页面。
.. note::
前端页面制作,请参考 :ref:`简单前端页面示例` 章节。
.. _项目初始化逻辑:
项目初始化逻辑
-----------------------
我们知道,一个简单的 Flask 项目是从 `app = Flask(__name__)` 开始的,而 Pear Admin Flask 的初始化的逻辑同理也是在此基础上,不过稍加复杂了一点。
下面这张图片将会揭示项目初始化的逻辑。(点击可查看大图)
|
.. image:: ../_static/Pear\ Admin\ Flask\ 启动流程图.png
:target: ../_images/Pear\ Admin\ Flask\ 启动流程图.png
:align: center
|
由此可以看出,我们想要添加自己的后端页面,可以在 “注册项目的视图函数”(蓝框) 的地方添加,当然,同样页面可以作为插件的方式接入以提高项目的拓展性。
下面将介绍如何在这两种方式下添加自己的后台页面。
设计数据库
-----------------------
兑换码一定是保存在数据库中的,我们现在要求改程序至少有以下几个功能:
* 可以通过 flask admin init 或者等价的命令初始化数据库
* 数据库存在统一管理的模型
* 数据库的内容方便数据转化
数据的字段可以定为:
* id -- 唯一主键
* key -- 兑换码
* content -- 具体的内容
* enable -- 是否启用
* used -- 是否使用
* create_at -- 创建时间
根据上述需求,我们可以设计出这样一个数据库 Model :
.. code-block:: python
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='创建时间')
我们将该文件命名为 `admin_gift.py` 放置在 `applications/models/admin_gift.py` ,而后为了使程序可以调用到这个模型,
需要在 `applications/models/__init__.py` 中导入这个模型。
.. code-block:: python
from .admin_gift import Gift
.. note::
由于 Gift 模型是 db.Model 的子类,在使用 `flask db init` 等命令行初始化数据库时,自动对 Gift 表进行创建,前提是这个类已经被 Python 加载,
换而言之,Python 会自动将 db.Model 的子类作为数据库的一部分,加入到数据库初始化中。
初始化数据库
-----------------------
通过上面的操作,我们已经成功将数据库中的 admin_gift 表进行创建。现在我们希望在使用 `flask admin init` 的时候,可以将我们已经定义的数据写入到数据表中。
`applications/common/script` 目录中,撰写了默认数据的写入脚本(也就是 Flask 启动最后加载的项目),
我们要做的是在 `applications/common/script/admin.py` 中添加自己需要的数据,我们可以对其进行修改,添加如下的代码:
.. code-block:: python
...
from applications.models import Gift
...
now_time = datetime.datetime.now()
...
powerdata = [
...
Power(
id=60,
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
), Power(
id=61,
name='兑换码添加',
type='2',
code='system:gift:add',
url='',
open_type='',
parent_id='60',
icon='',
sort=0,
create_time=now_time,
enable=1
), Power(
id=62,
name='兑换码删除',
type='2',
code='system:gift:remove',
url='',
open_type='',
parent_id='60',
icon='',
sort=0,
create_time=now_time,
enable=1
), Power(
id=63,
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
)
]
...
def add_role_power():
admin_powers = Power.query.filter(Power.id.in_([1, 3, 4, 9, 12, 13, 17, 18, 44, 48, 60])).all()
...
@admin_cli.command("init")
def init_db():
...
db.session.add_all(giftdata)
...
这样就可以将数据库内容写入了。
.. note::
powerdata 中添加了对兑换码的操作权限,可以先去“权限管理”中添加,而后再从数据中抄取。
或者忽略初始化时对 powerdata 的添加,在初始化之后,手动在权限管理中添加。
使用 Schema 序列化
---------------------------
与前端交互大多用的是 JSON 格式的数据,这就涉及到将数据库查询的结果对象(Query)转化为 JSON 这一步骤。
我们可以使用 flask_marshmallow 中的 SQLAlchemyAutoSchema 将数据库查询对象转换为 JSON 格式。
创建文件 `applications/schemas/admin_gift.py` ,并继承 SQLAlchemyAutoSchema ,更改其中的目标模型为我们创建的 Gift 模型。
.. code-block:: python
from flask_marshmallow.sqla import SQLAlchemyAutoSchema
from applications.models import Gift
class GiftSchema(SQLAlchemyAutoSchema):
class Meta:
model = Gift # table = models.Album.__table__
include_fk = True # 序列化阶段是否也一并返回主键
.. note::
更多序列化参数可以参考 :ref:`Schema 序列化` 章节。
随后,在 `applications/schemas/__init__.py` 引用,
.. code-block:: python
...
from .admin_gift import GiftSchema
这一步不是必须的,但是应通过这一步的引用,可以在蓝图页面中,方便的使用 `from applications.schemas import *` 的方式导入。
编写后端视图函数
-----------------------
注册蓝图
~~~~~~~~~~~~~~
接着我们需要设计后端的数据增删改查部分的视图函数,创建文件 `applications/view/system/gift.py` ,并写上基本的蓝图初始化逻辑:
.. code-block:: python
from flask import Blueprint
bp = Blueprint('gift', __name__, url_prefix='/gift')
根据流程图,我们需要在 `applications/view/system/__init__.py` 中,注册 `gift.py` 的蓝图:
.. code-block::
...
from applications.view.system.gift import bp as gift_bp
...
def register_system_bps(app: Flask):
...
system_bp.register_blueprint(gift_bp)
...
.. _编写数据获取路由:
编写数据获取路由
~~~~~~~~~~~~~~~~~~~~
.. important::
因为目标是让前端 layui 的动态表格获取数据,而根据 layui 的文档,表格将会提供 limit 和 page 两个查询参数来进行分页查询,所以要对 limit 和 page 进行处理。
现在开始编写数据获取路由,路由是以 JSON 格式响应数据库中 `admin_gift` 的数据,下面提供一种实现方法:
.. code-block:: python
from flask import Blueprint
from applications.models import Gift
from applications.schemas import GiftSchema
from applications.extensions import db
from applications.common.utils.http import table_api
from applications.common.utils.rights import authorize
bp = Blueprint('gift', __name__, url_prefix='/gift')
@bp.get('/data')
@authorize("system:gift:main")
def data():
query = db.session.query(Gift).layui_paginate()
return table_api(
data=GiftSchema(many=True).dump(query),
count=query.total,
limit=query.per_page
)
可以发现,在没有搜索的情况下,正确处理前端的分页查询,实际上只有简单 5 行代码就可以完成(自动处理了 limit 和 page 参数),
另外,也可以采用已经封装好的 `layui_paginate_json` 方法:
.. code-block:: python
...
@bp.get('/data')
@authorize("system:gift:main")
def data():
data, total, page, limit = db.session.query(Gift).layui_paginate_json(GiftSchema)
return table_api(
data=data,
count=total,
limit=limit
)
`layui_paginate_json` 函数完成了分页、解析与转化,适用于一些比较简单数据转化场景。
.. warning::
对于任何形式的后台管理员路由,切记不要忘记添加 `authorize` 装饰函数对请求效验权限!!!!!!
.. note::
对于 layui_paginate 方法定义,可以查看 :ref:`与 layui 的数据格式同步` 章节。
访问路由 `/system/gift/data` 可以获得如下数据:
.. code-block:: json
{
"code": 0,
"count": 2,
"data": [
{
"content": "8折优惠",
"create_at": "2025-01-28T19:10:48.607165",
"enable": 1,
"id": 0,
"key": "myTestCode",
"used": 0
},
{
"content": "1折优惠",
"create_at": "2025-01-28T19:10:48.607165",
"enable": 0,
"id": 1,
"key": "DisableCode",
"used": 0
}
],
"limit": 10,
"msg": ""
}
随后,我们加入查询:
.. code-block:: python
...
@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
)
.. _编写启用与禁用视图函数:
编写启用与禁用视图函数
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
启用与禁用的基本思路是通过 ID 筛选到合适的记录行并设置其 enable 为 1 或者 0。在设计数据库时,我们有意将表示启用禁用字段设置为 `enable` 以此可以使用项目已经封装好的函数。
.. code-block:: python
from applications.common.curd import enable_status, disable_status
@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="禁用失败")
.. note::
对于上述的 `enable_status` `disable_status` 函数,可以参考文档 :ref:`简单增删改查模块` 章节。
数据的修改经历如下步骤:获取目标兑换码 ID、获取对应修改的新数据、应用修改,而数据的添加仅没有“获取目标兑换码 ID”这一步骤。
我们先来撰写添加这一部分的视图函数,
编写删除视图函数
~~~~~~~~~~~~~~~~~~~~~~~~~~
删除的视图函数,实则和启用禁用是一样的,这里直接给出代码:
.. code-block:: python
@bp.delete('/remove/<int:_id>')
@authorize("system:gift:remove")
def remove_api(_id):
if delete_one_by_id(Gift, _id):
return success_api(msg="删除成功")
return success_api(msg="删除失败")
你会注意到,由于 `curd` 模块的封装,使编写路由变的简洁。
.. _编写增加视图函数:
编写增加视图函数
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
添加视图函数经历下面几个步骤:获取参数、效验参数、写入数据库。
.. code-block:: python
@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="添加失败")
.. important::
效验参数是必不可少的,要尽可能一切不相信用户的输入。
.. _编写修改视图函数:
编写修改视图函数
~~~~~~~~~~~~~~~~~~~~~~~~
修改视图函数的编写就照葫芦画瓢即可,代码如下:
.. code-block:: python
@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="编辑失败")
.. note::
插件方式接入项目,请查看 :ref:`以插件的方式接入项目` 章节。
+559
View File
@@ -0,0 +1,559 @@
前端页面编写
========================
Pear Admin Flask 并没有实现前后端分离,因为存在部分页面的数据是直接通过模板渲染的方式直接渲染在 HTML 中的。但我们将展示给用户浏览器里的页面都称为前端。
公用模板文件
--------------------
项目使用 Flask 搭建,其所有模板文件均存放在 `templates` 文件夹中。公用模板文件保存在 `templates/system/common` ,分别是:
* header.html -- 头部包含文件,包含了后台页面通用的 css 文件
* footer.html -- 页脚包含文件,包含了后台页面通用的 js 文件
在写后台内嵌页面时,可以参考下面的模板:
.. code-block:: html
<!DOCTYPE html>
<html>
<head>
<title>这是标题</title>
{% include 'system/common/header.html' %} <!-- 组件式嵌入不需要这行 -->
<!-- 这里可以包含其他文件 -->
</head>
<body class="pear-container">
<!-- 正文内容 -->
</body>
{% include 'system/common/footer.html' %} <!-- 组件式嵌入不需要这行 -->
<script> // </script>
</html>
.. note::
需要注意的是,`header.html``footer.html` 包含了主题色和夜间模式切换的脚本,如果不想包含这两个文件的话,
还想进行主题色和夜间模式切换可能需要自行添加脚本。(可以参考这两个文件中的内容)
正确的后台页面嵌入方式
-----------------------
Pear Admin Layui 主项目更新之后,提供了 组件式嵌入(_component 和 iframe嵌入(_iframe) 两种方式。在编写前端页面时,
不同的嵌入方式有所不同,各有优劣。
对于组件式嵌入,可以提供更良好的用户体验,如果页面不存在则会弹出 404 提示信息,对于iframe嵌入,则会直接打开(即使不存在)。
.. important::
**使用组件式嵌入时,上述的参考模板不在需要包含 header.html 和 footer.html 文件,如果包含这两个文件会直接影响到后台框架页面的排版和脚本调用!**
经过测试,组件式嵌入仅适合于唯一且静态的页面,不建议在其中 **添加事件绑定的脚本** ,因为组件式嵌入会将页面内容直接嵌入 div 元素中,
并动态执行脚本,但是执行的脚本绑定的事件并不会因为页面关闭而销毁。简单说明就是,假设脚本中存在计时器,计时器不会在组件式嵌入的页面销毁之后而销毁。
所以在 Pear Admin Flask 项目中,仅首页(后台数据统计页面)和个人资料页面使用组件式嵌入,其余均使用iframe嵌入。
关于主题色和夜间模式
-----------------------
如果开发的是后台管理页面,主题色和夜间模式是必要的,这样可以增加观感。
Pear Admin Layui 的控制主题色逻辑是通过设置全局的 css 属性:`--global-primary-color`
比如对于 `.layui-btn`
.. code-block:: css
.layui-btn {
background-color: var(--global-primary-color);
}
所以,如果您添加了自定义元素,并想要其跟随主题色变化,请确保引用了 `--global-primary-color` 属性。
对于夜间模式,本质上就是修改 body 的 class ,使其添加上 `pear-admin-dark` ,比如 `.layui-btn` 的夜间模式 css 为:
.. code-block:: css
.pear-admin-dark .layui-btn {
color: #ffffff;
border-color: #4C4D4F;
}
.. _简单前端页面示例:
简单前端页面示例
-------------------------
此部分我们来以制作一个兑换码管理的前端页面为例。
.. note::
配套后端的制作可以查看 :ref:`后端页面编写` 章节。
规划模板存放位置
~~~~~~~~~~~~~~~~~~
模板一般存放在 `templates/system` 的目录下,该目录下的每一个子文件(夹)都是一个特定功能的实现的网页模板。
我们在其中创建一个 `gift` 文件夹,并放入 `main.html``add.html``edit.html`
|
.. image:: ../_static/规划模板存放位置.png
:align: center
|
加入动态表格与查询表单
~~~~~~~~~~~~~~~~~~~~~~~~
随后,我们可以制作一个写一个简单的页面,设想是页面中存在一个查询表单和一个动态表格:
.. code-block:: html
<!DOCTYPE html>
<html>
<head>
<title>兑换码管理</title>
{% include 'system/common/header.html' %}
</head>
<body class="pear-container">
{# 查询表单 #}
<div class="layui-card">
<div class="layui-card-body">
<form class="layui-form" action="" lay-filter="query-form">
<div class="layui-form-item" style="margin-bottom: unset;">
<label class="layui-form-label">激活码</label>
<div class="layui-input-inline">
<input type="text" name="key" placeholder="" class="layui-input">
</div>
<button class="layui-btn layui-btn-md" lay-submit lay-filter="gift-query">
<i class="layui-icon layui-icon-search"></i>
查询
</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-md">
<i class="layui-icon layui-icon-refresh"></i>
重置
</button>
</div>
</form>
</div>
</div>
{# 用户表格 #}
<div>
<div class="layui-card">
<div class="layui-card-body">
<table id="gift-table" lay-filter="gift-table"></table>
</div>
</div>
</div>
</body>
<!-- 这里写的都是表格一行元素组件 -->
{% raw %}
<script type="text/html" id="enable-element">
<input type="checkbox" name="enable" value="{{ d.id }}" lay-skin="switch" lay-text="启用|禁用"
lay-filter="gift-enable"
{{# if(d.enable==1){ }} checked {{# } }}/>
</script>
<script type="text/html" id="used-element">
{{# if(d.used==1){ }}
<span style="color: red">已用</span>
{{# } else { }}
<span style="color: green">未用</span>
{{# } }}
</script>
<script type="text/html" id="createTime-element">
{{layui.util.toDateString(d.create_at, "yyyy-MM-dd HH:mm:ss")}}
</script>
{% endraw %}
<script type="text/html" id="table-bar">
{% if authorize("system:gift:edit") %}
<button class="layui-btn layui-btn-xs" lay-event="edit"><i class="pear-icon pear-icon-edit"> 编辑</i>
</button>
{% endif %}
{% if authorize("system:gift:remove") %}
<button class="layui-btn layui-btn-danger layui-btn-xs" lay-event="remove"><i
class="pear-icon pear-icon-ashbin"> 删除</i>
</button>
{% endif %}
</script>
<!-- 这里是表格的工具栏 -->
<script type="text/html" id="table-toolbar">
{% if authorize("system:gift:add") %}
<button class="layui-btn layui-btn-primary layui-btn-sm" lay-event="add">
<i class="pear-icon pear-icon-add"></i>
新增
</button>
{% endif %}
</script>
{% include 'system/common/footer.html' %}
<script>
layui.use(['table'], function () {
let table = layui.table;
// 表格数据
let cols = [
[
{title: '编号', field: 'id', align: 'center'},
{title: '激活码', field: 'key', align: 'center'},
{title: '内容', field: 'content', align: 'center'},
{title: '启用', field: 'enable', align: 'center', templet: '#enable-element'},
{title: '已用', field: 'used', align: 'center', templet: '#used-element'},
{title: '创建时间', field: 'create_at', templet: '#createTime-element', align: 'center'},
{title: '操作', toolbar: '#table-bar', align: 'center', width: 180}
]
]
// 渲染表格数据
table.render({
elem: '#gift-table',
url: '/system/gift/data', // 请求链接
page: true,
cols: cols,
skin: 'line',
toolbar: '#table-toolbar',
text: {none: '暂无激活码信息'},
defaultToolbar: [{layEvent: 'refresh', icon: 'layui-icon-refresh'}, 'filter', 'print', 'exports']
})
})
</script>
</html>
注意还要在 Python 中加上渲染路由:
.. code-block:: python
@bp.get('/')
@authorize("system:gift:main")
def index():
return render_template('system/gift/main.html')
前端的效果如下:
|
.. image:: ../_static/兑换码管理页面.png
:align: center
|
完善查询功能
~~~~~~~~~~~~~~~~~~~~~~~
.. note::
查询数据库的视图函数可以参考 :ref:`编写数据获取路由` 章节。
接着,我们完善查询功能,确保获取的路由在有查询功能之后,我们在前端编写表单提交的处理。
.. code-block:: javascript
layui.use(['table', 'form'], function () {
...
let form = layui.form;
...
// 表单查询
form.on('submit(gift-query)', function (data) {
table.reload('gift-table', {where: data.field})
return false;
})
}
监听启用和禁用事件
~~~~~~~~~~~~~~~~~~~~~~~~~~
在动态表格中存在启用与禁用的切换开关,我们需要对开关进行监听,在用户切换开关状态时,自动在数据库中设置兑换码的启用与禁用状态。
.. note::
后台视图函数,参考 :ref:`编写启用与禁用视图函数` 章节。
.. code-block:: python
let $ = layui.jquery;
ley popup = layui.popup;
...
// 启用与禁用
form.on('switch(gift-enable)', function (obj) {
let operate;
if (obj.elem.checked) {
operate = 'enable'
} else {
operate = 'disable'
}
let loading = layer.load()
$.ajax({
url: '/system/gift/' + operate,
data: JSON.stringify({id: this.value}),
dataType: 'json',
contentType: 'application/json',
type: 'put',
success: function (result) {
layer.close(loading)
if (result.success) {
popup.success(result.msg)
} else {
popup.failure(result.msg)
}
}
})
})
.. important::
如果对前端编写存在问题,可以自行查阅 `layui 官方文档 <https://layui.dev/>`_ ,需要注意的是,由于页面中的组件元素增多,
最好使用准确无误的表示区分这些表单组件,以便在监听时正确绑定到事件。
监听删除数据事件
~~~~~~~~~~~~~~~~~~~~~~~~~~
删除数据事件就与监听启用禁用其实是同理的,这里直接给出代码,
.. code-block:: javascript
// 表格各行工具事件
table.on('tool(gift-table)', function (obj) {
if (obj.event === 'remove') {
layer.confirm('确定要删除该兑换码?', {icon: 3, title: '提示'}, function (index) {
layer.close(index)
let loading = layer.load()
$.ajax({
url: '/system/gift/remove/' + obj.data['id'],
dataType: 'json',
type: 'delete',
success: function (result) {
layer.close(loading)
if (result.success) {
popup.success(result.msg, function () {
obj.del()
})
} else {
popup.failure(result.msg)
}
}
})
})
} else if (obj.event === 'edit') {
// 待定
}
})
编写新建与编辑页面
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. note::
对应的视图函数,可以查看 :ref:`编写增加视图函数` 章节。
编写这一部分涉及到设计表单,编辑实际上就是已经填好值的新建页面。由于目前项目暂未进行前后端分离,所以为了方便直接使用模板渲染的方式,直接将内容渲染到编辑页面上。
这就导致需要保留这两个略微有差别的页面,后续的更新,将会尝试将渲染的方式剥离项目,直接动态请求,可以实现动态分离。
此处给出表单页面基本的写法,所有的新建页面表单可以参考这个模板:
.. code-block:: html
<!DOCTYPE html>
<html>
<head>
<title>激活码管理</title>
{% include 'system/common/header.html' %}
</head>
<body>
<form class="layui-form">
<div class="mainBox">
<div class="main-container">
<div class="main-container">
<!-- 这里填写表单元素 -->
</div>
</div>
</div>
<div class="bottom">
<div class="button-container">
<button type="submit" class="layui-btn layui-btn-sm" lay-submit="" lay-filter="save">
<i class="layui-icon layui-icon-ok"></i>
提交
</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">
<i class="layui-icon layui-icon-refresh"></i>
重置
</button>
</div>
</div>
</form>
{% include 'system/common/footer.html' %}
<script>
layui.use(['form', 'jquery'], function () {
let form = layui.form
let $ = layui.jquery
form.on('submit(save)', function (data) {
$.ajax({
url: '目标保存页面',
data: JSON.stringify(data.field),
dataType: 'json',
contentType: 'application/json',
type: 'post',
success: function (result) {
if (result.success) {
layer.msg(result.msg, {icon: 1, time: 1000}, function () {
parent.layer.close(parent.layer.getFrameIndex(window.name))//关闭当前页
parent.layui.table.reload('gift-table') // 目标表格
})
} else {
layer.msg(result.msg, {icon: 2, time: 1000})
}
}
})
return false
})
})
</script>
</body>
</html>
撰写表单的工作较为简单,代码如下:
.. code-block:: html
<div class="layui-form-item">
<label class="layui-form-label">兑换码</label>
<div class="layui-input-block">
<input type="text" name="key" lay-verify="title" autocomplete="off" placeholder="请输入兑换码"
class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">兑换内容</label>
<div class="layui-input-block">
<textarea placeholder="请输入兑换内容" name="content" class="layui-textarea"></textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<input type="radio" name="enable" value="1" title="开启" checked>
<input type="radio" name="enable" value="0" title="关闭">
</div>
</div>
注意还要在管理页面加上窗口弹出的绑定:
.. code-block:: javascript
// 顶部工具栏
table.on('toolbar(gift-table)', function (obj) {
if (obj.event === 'add') {
layer.open({
type: 2,
title: '新增',
shade: 0.1,
area: ['550px', '550px'],
content: '/system/gift/add'
})
}
})
|
.. image:: ../_static/兑换码添加页面.png
:align: center
|
现在编写编辑页面,编辑页面相较于新建页面仅有两个区别:增加了 ID 编辑框、修改了提交的地址,最重要的是将后端传入的内容渲染到页面上。
我们先编写如下的路由视图:
.. code-block:: python
@bp.get('/edit/<int:_id>')
@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)
绑定编辑事件(就是在上面 “// 待定” 的地方添加内容):
.. code-block:: javascript
} else if (obj.event === 'edit') {
layer.open({
type: 2,
title: '修改',
shade: 0.1,
area: ['550px', '500px'],
content: '/system/gift/edit/' + obj.data['id']
})
}
设计新表单:
.. code-block:: html
<div class="layui-form-item">
<label class="layui-form-label">编号</label>
<div class="layui-input-block">
<input type="text" name="id" lay-verify="title" autocomplete="off" placeholder="请输入编号"
class="layui-input" value="{{ gift.id }}" disabled>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">兑换码</label>
<div class="layui-input-block">
<input type="text" name="key" lay-verify="title" autocomplete="off" placeholder="请输入兑换码"
class="layui-input" value="{{ gift.key }}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">兑换内容</label>
<div class="layui-input-block">
<textarea placeholder="请输入兑换内容" name="content" class="layui-textarea">{{ gift.content }}</textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<input type="radio" name="enable" value="1" title="开启" {{ 'checked' if gift.enable == 1 }}>
<input type="radio" name="enable" value="0" title="关闭" {{ 'checked' if gift.enable == 0 }}>
</div>
</div>
.. important::
要注意把内容渲染到网页上哦,其中 id 字段设置为禁用。随后不要忘记,将表单的提交地址改为 `/system/gift/update`
.. note::
对应的视图函数,可以查看 :ref:`编写修改视图函数` 章节。
.. note::
插件方式接入项目,请查看 :ref:`以插件的方式接入项目` 章节。
+11
View File
@@ -0,0 +1,11 @@
.. title:: 最佳实践
目录索引
.. toctree::
:maxdepth: 1
plugin
trick
frontend
backend
+377
View File
@@ -0,0 +1,377 @@
插件开发
=================
插件功能旨在最大限度不修改原框架的前提下添加新功能,并可以像程序原有框架一样进行流程注册,而且不需要修改任何程序框架原有代码(仅在配置文件中设置即可)。
所有插件放置在 `plugins` 文件夹中,项目提供了四个示例插件,分别是 `helloworld``realip``replacePage``giftManager`
分别用于示例页面的注册、修改 Flask 上下文、页面替换和新功能接入。
像项目自带的用户管理、部门管理等基本功能属于程序自身的“功能插件”,对于大多数衍生项目来说,多的是修改字符串和删除部分不需要的功能,
而插件开发主要可以用于添加自己的视图函数和功能,可以完美于项目融合,增加可拓展性,尤其是用于既想保留原项目功能又想填写新功能的项目开发。
插件的启用
-----------------
插件需要在 `applications/config.py` 中配置,你会找到如下的内容:
.. code-block:: python
PLUGIN_ENABLE_FOLDERS = []
而在目录 `plugins` 中,你会发现存在 文件夹名称 为 `helloworld``realip``replacePage``giftManager` 四个插件。
比如我们想要启用 `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
|
.. note::
对于 `giftManager` 插件,需要在启用的情况下,先使用 `flask gift init` 来初始化其数据库。假设项目已经搭建完成(已经初始化数据库),需要进行以下步骤:
.. code-block:: bash
flask db migrate
flask db upgrade
flask gift init
该命令行用于创建新 admin_gift 表并写入数据。
插件的目录架构
-------------------
插件的目录架构如下:
.. 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)
这样可以使目录架构更加清晰。
另外,插件还有其他三个事件:
.. code-block:: python
def event_begin(app: Flask): # 在项目所有功能注册之前调用
pass
def event_finish(app: Flask): # 在项目所有功能注册之后调用(插件已经加载完毕)
pass
def event_context(app: Flask): # Flask 初始化完成,等待第一个请求之前,等同于 with app.app_context():
# 此时数据库已经初始化完成,尝试读取
pass
.. note::
事件的时机调用可以参考 :ref:`项目初始化逻辑` 章节。
.. important::
注意不要直接在 `__init__.py` 的函数外直接写存在阻塞的代码,不然项目 Flask 将不能初始化完成。
.. note::
在编写插件的前端模板(template)时,请尽量将模板文件放在项目根目录的 `templates` 文件中,这样可以保持良好的项目架构。
当然另一种做法是像 helloworld 插件那样,直接放在插件目录的 templates 中,但是一定要做好模板名称的区分,
因为 flask 默认找模板行为是从根目录开始找的,如果根目录 templates 和插件目录的 templates 中存在的模板重名,
则会优先使用根目录 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") # 要这样写
+224
View File
@@ -0,0 +1,224 @@
开发技巧
===================
开发 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 中,菜单的管理归属于 “权限管理” ,这样做的原因是为了使不同用户可以使用不同的访问控制。所以需要修改菜单,需要在 “权限管理” 页面编辑即可。
配置后台站内消息
-------------------
后台站内消息是异步获取的,其路由在 `applications/view/system/rights.py``message` 函数中,后续会考虑写入数据库,并添加管理函数进行统一的管理。
权限效验
------------
在开发后台管理模板的过程中会涉及到权限效验,即访问控制。Pear Admin Flask 中提供了方便的函数用于进行权限效验。详情请查看 :ref:`权限验证模块` 章节。
.. _Schema 序列化:
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"] # 排除字段列表
.. note::
更多参数可以参考官方文档对其的解释,链接如下:`SQLAlchemyAutoSchema <https://marshmallow-sqlalchemy.readthedocs.io/en/latest/api_reference.html>`_
.. _与 layui 的数据格式同步:
与 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 参数构成查询。**
:param page: 页码
:param limit: 页数据个数
: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, page=None, limit=None)
分页查询并通过 Marshmallow Schema 类 转化为 JSON,适用于 Layui 表格。
:param schema: Marshmallow Schema 类。
:param page: 页码
:param limit: 页数据个数
:return: 返回包含序列化数据、总数、当前页码和每页条数的元组。
.. method:: layui_paginate_db_json(page=None, limit=None)
分页查询并返回数据库原始数据的 JSON 格式,适用于 Layui 表格。
:param page: 页码
:param limit: 页数据个数
:return: 返回包含序列化数据(列表)、总数、当前页码和每页条数的元组。
**示例:**
.. code-block:: python
>> db.session.query(Gift.id, Gift.key).layui_paginate_db_json()
([{'id': 0, 'key': 'myTestCode'}, {'id': 1, 'key': 'DisableCode'}], 2, 1, 10)
进行字段构造
-----------------------
提炼数据时常常会用到准确匹配或者模糊匹配,又或者是进行多条件大小比较的匹配,此时可以通过字段构造来解决。项目中提供了字段构造的类位于
`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", "<h1>Hello</h1>", current_user.id)
+24
View File
@@ -0,0 +1,24 @@
构建文档
==================
本章节讲述如何构建文档,构建文档使用到 Python 中的 sphinx 工具,需要使用 Python 进行安装。
安装必要依赖
~~~~~~~~~~~~~~~
文档位置位于 `docs` 文件夹,所以先切换到 `docs` 文件。
.. code-block:: bash
cd docs
pip install -r requirements.txt
构建文档
~~~~~~~~~~~~~~~
使用 `sphinx-build .\source .\_build` 进行构建文档。
编辑文档
~~~~~~~~~~~~~~~
如果修改文档,请在 `source` 中修改并重新构建。

Some files were not shown because too many files have changed in this diff Show More