!133 Update: 同步项目框架与 Pear Admin 4.0 同步,完善部分功能
Merge pull request !133 from 我叫以赏/master
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
| | |
|
||||
| ---------------------- | ---------------------- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
|
||||
# 其他说明
|
||||
|
||||
@@ -2,6 +2,5 @@ from applications import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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):
|
||||
逻辑删除混入类,为模型提供软删除功能。
|
||||
|
||||
示例:
|
||||
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的定义模型进行序列化
|
||||
基本功能,待完善
|
||||
示例
|
||||
自动序列化模型数据为 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:
|
||||
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
|
||||
@@ -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脚本运行")
|
||||
|
||||
@@ -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)
|
||||
# 打印字符
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,75 +35,105 @@ 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 ,验证失败时返回
|
||||
|
||||
def email(*args, **kwargs):
|
||||
"""
|
||||
验证给定值是否为有效的电子邮件地址。
|
||||
|
||||
:param value: 需要验证的电子邮件地址。
|
||||
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
|
||||
|
||||
示例:
|
||||
>>> email('someone@example.com')
|
||||
True
|
||||
|
||||
>>> email('bogus@@')
|
||||
ValidationFailure(func=email, ...)
|
||||
'''
|
||||
"""
|
||||
return validators.email(*args, **kwargs)
|
||||
|
||||
iban = validators.iban
|
||||
'''
|
||||
返回给定值是否为有效的IBAN代码。
|
||||
如果值是有效的IBAN,则此函数返回 True ,否则返回 ValidationFailure 。
|
||||
|
||||
def iban(*args, **kwargs):
|
||||
"""
|
||||
验证给定值是否为有效的 IBAN 代码。
|
||||
|
||||
:param value: 需要验证的 IBAN 代码。
|
||||
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
|
||||
|
||||
示例:
|
||||
>>> iban('DE29100500001061045672')
|
||||
True
|
||||
|
||||
>>> iban('123456')
|
||||
ValidationFailure(func=iban, ...)
|
||||
'''
|
||||
"""
|
||||
return validators.iban(*args, **kwargs)
|
||||
|
||||
ipv4 = validators.ipv4
|
||||
'''
|
||||
返回给定值是否为有效的IPv4地址。
|
||||
|
||||
def ipv4(*args, **kwargs):
|
||||
"""
|
||||
验证给定值是否为有效的 IPv4 地址。
|
||||
|
||||
:param value: 需要验证的 IPv4 地址。
|
||||
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
|
||||
|
||||
示例:
|
||||
>>> ipv4('123.0.0.7')
|
||||
True
|
||||
|
||||
>>> ipv4('900.80.70.11')
|
||||
ValidationFailure(func=ipv4, args={'value': '900.80.70.11'})
|
||||
'''
|
||||
"""
|
||||
return validators.ipv4(*args, **kwargs)
|
||||
|
||||
ipv6 = validators.ipv6
|
||||
'''
|
||||
返回给定值是否为有效的IP版本6地址。
|
||||
|
||||
def ipv6(*args, **kwargs):
|
||||
"""
|
||||
验证给定值是否为有效的 IPv6 地址。
|
||||
|
||||
:param value: 需要验证的 IPv6 地址。
|
||||
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
|
||||
|
||||
示例:
|
||||
>>> ipv6('abcd:ef::42:1')
|
||||
True
|
||||
|
||||
>>> ipv6('abc.0.0.1')
|
||||
ValidationFailure(func=ipv6, args={'value': 'abc.0.0.1'})
|
||||
'''
|
||||
"""
|
||||
return validators.ipv6(*args, **kwargs)
|
||||
|
||||
length = validators.length
|
||||
'''
|
||||
返回给定字符串的长度是否在指定范围内。
|
||||
|
||||
def length(*args, **kwargs):
|
||||
"""
|
||||
验证给定字符串的长度是否在指定范围内。
|
||||
|
||||
:param value: 需要验证的字符串。
|
||||
:param min: 字符串的最小长度(可选)。
|
||||
:param max: 字符串的最大长度(可选)。
|
||||
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
|
||||
|
||||
示例:
|
||||
>>> length('something', min=2)
|
||||
True
|
||||
|
||||
@@ -106,41 +142,54 @@ length = validators.length
|
||||
|
||||
>>> length('something', max=5)
|
||||
ValidationFailure(func=length, ...)
|
||||
'''
|
||||
"""
|
||||
return validators.length(*args, **kwargs)
|
||||
|
||||
mac_address = validators.mac_address
|
||||
'''
|
||||
返回给定值是否为有效MAC地址。
|
||||
如果该值是有效的MAC地址,则此函数返回 True ,否则返回 ValidationFailure 。
|
||||
|
||||
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)
|
||||
|
||||
slug = validators.slug
|
||||
'''
|
||||
验证给定值是否为有效的块。
|
||||
有效的短信息只能包含字母数字字符、连字符和下划线。
|
||||
|
||||
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)
|
||||
|
||||
#truthy = validators.truthy
|
||||
'''
|
||||
验证给定值不是错误值。
|
||||
'''
|
||||
|
||||
url = validators.url
|
||||
'''
|
||||
返回给定值是否为有效URL。
|
||||
如果值是有效URL,则此函数返回 True ,否则返回 ValidationFailure 。
|
||||
def url(*args, **kwargs):
|
||||
"""
|
||||
验证给定值是否为有效的 URL。
|
||||
|
||||
:param value: 需要验证的 URL。
|
||||
:param public: 是否仅允许公共 URL(可选)。
|
||||
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
|
||||
|
||||
示例:
|
||||
>>> url('http://foobar.dk')
|
||||
True
|
||||
|
||||
@@ -152,36 +201,40 @@ url = validators.url
|
||||
|
||||
>>> url('http://10.0.0.1', public=True)
|
||||
ValidationFailure(func=url, ...)
|
||||
'''
|
||||
"""
|
||||
return validators.url(*args, **kwargs)
|
||||
|
||||
uuid = validators.uuid
|
||||
'''
|
||||
返回给定值是否为有效UUID。
|
||||
如果值是有效的UUID,则此函数返回 True ,否则返回 ValidationFailure 。
|
||||
|
||||
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})
|
||||
'''
|
||||
|
||||
@@ -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_PERMANENT = False # 会话是否持久化
|
||||
SESSION_USE_SIGNER = True # 是否对发送到浏览器上 session 的 cookie 值进行加密
|
||||
|
||||
SECRET_KEY = "pear-system-flask"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
|
||||
@@ -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='更新时间')
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
@@ -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="删除部门成功")
|
||||
@@ -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="删除成功")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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="发送失败,请检查日志。")
|
||||
|
||||
|
||||
# 删除用户
|
||||
|
||||
@@ -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:
|
||||
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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
if 'permissions' in session:
|
||||
session.pop('permissions')
|
||||
|
||||
return success_api(msg="注销成功")
|
||||
|
||||
@@ -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()
|
||||
|
||||
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="批量删除成功")
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
# }
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 +0,0 @@
|
||||
CREATE DATABASE PearAdminFlask DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;;
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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群更新二维码。
|
||||
|
||||

|
||||
|
||||
**[master分支版本](https://gitee.com/pear-admin/pear-admin-flask/tree/master/)**
|
||||
|
||||
flask 2.0.1 + flask-sqlalchemy + 权限验证 + Flask-APScheduler 定时任务 + marshmallow 序列化与数据验证
|
||||
|
||||
master 分支为主分支,是功能最全、页面最多的分支。
|
||||
|
||||

|
||||
|
||||
## 下载使用 :id=download
|
||||
|
||||
|
||||
#### 1. 官网地址
|
||||
|
||||
官网提供稳定版本的 Release 发行版本 [前往](http://www.pearadmin.com)
|
||||
|
||||

|
||||
|
||||
#### 2. 源码仓库
|
||||
|
||||
如果你需要最新代码,请前往 Gitee 仓库 [前往](https://gitee.com/pear-admin/pear-admin-flask)
|
||||
|
||||

|
||||
|
||||
|
||||
如果您完成了这一步,请参阅[下载安装](install.md)章节。
|
||||
|
||||
@@ -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="成功")
|
||||
```
|
||||
@@ -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) 章节
|
||||
|
||||
其它章节等待更新。
|
||||
@@ -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 # 主页模板
|
||||
```
|
||||
@@ -0,0 +1,2 @@
|
||||
del _build /s /q
|
||||
sphinx-build .\source .\_build
|
||||
@@ -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"] # 排除字段列表
|
||||
```
|
||||
@@ -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("加载完毕后,我会输出一句话。")
|
||||
```
|
||||
@@ -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="<div><span style="background-color: transparent; color: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));">app = create_app()</span></div><div><font style="font-size: 9px; color: rgb(77, 77, 77);">app.py</font></div>" 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="<div>初始化 Flask()</div><div><font style="font-size: 9px; color: rgb(77, 77, 77);">applications/__init__.py</font></div>" 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="<div>载入配置</div><div><font style="font-size: 9px; color: rgb(77, 77, 77);">applications/__init__.py</font></div>" 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="<div>注册组件</div><div><font style="font-size: 9px; color: rgb(77, 77, 77);">applications/__init__.py</font></div>" 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="<div>注册应用蓝图</div><div><font style="font-size: 9px; color: rgb(77, 77, 77);">applications/__init__.py</font></div>" 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="<div>注册命令</div><div><font style="font-size: 9px; color: rgb(77, 77, 77);">applications/__init__.py</font></div>" 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="<div>读取 BaseConfig</div><div><font style="font-size: 9px; color: rgb(77, 77, 77);">配置来自&nbsp;applications/config.py</font></div>" 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="<div>初始化插件</div><div><font style="font-size: 9px; color: rgb(77, 77, 77);">applications/extensions/init_plugins.py</font></div>" 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="<div>广播插件&nbsp;event_begin 事件</div><div><font style="font-size: 9px; color: rgb(77, 77, 77);">applications/extensions/init_plugins.py</font></div>" 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="<div>初始化 Flask LoginManager</div><div><font style="font-size: 9px; color: rgb(77, 77, 77);">applications/extensions/init_login.py</font></div>" 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="<div>初始化 SQLAlchemy 数据库</div><div><font style="color: rgb(77, 77, 77); font-size: 8px;">applications/extensions/init_sqlalchemy.py</font></div>" 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="<div>初始化 Mail 邮件组件</div><div><font style="color: rgb(77, 77, 77); font-size: 8px;">applications/extensions/init_error_views.py</font></div>" 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="<div>初始化 服务器错误蓝图</div><div><font style="color: rgb(77, 77, 77); font-size: 8px;">applications/extensions/init_error_views.py</font></div>" 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="<div>初始化 网页模板公用函数</div><div><font style="color: rgb(77, 77, 77); font-size: 7px;">applications/extensions/init_template_directives.py</font></div>" 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="<div>初始化<font style="font-size: 9px;"> Migrate </font>数据库迁移组件</div><div><font style="color: rgb(77, 77, 77); font-size: 9px;">applications/extensions/init_migrate.py</font></div>" 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="<div>初始化<font size="1">&nbsp;Session 会话</font>组件</div><div><font style="color: rgb(77, 77, 77); font-size: 9px;">applications/extensions/init_session.py</font></div>" 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="<div>注册应用子页面路由</div><div><font style="font-size: 9px; color: rgb(77, 77, 77);">applications/view/__init__.py</font></div>" 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="<div>广播插件&nbsp;event_init 事件</div><div><span style="color: rgb(77, 77, 77); font-size: 9px;">applications/view/__init__.py</span></div>" 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="<div>注册 用户管理 蓝图</div><div><font style="font-size: 9px; color: rgb(77, 77, 77);">applications/view/system/__init__.py</font></div>" 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="<div>注册 文件上传 蓝图</div><div><font style="font-size: 9px; color: rgb(77, 77, 77);">applications/view/system/__init__.py</font></div>" 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="<div>注册 系统监控 蓝图</div><div><font style="font-size: 9px; color: rgb(77, 77, 77);">applications/view/system/__init__.py</font></div>" 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="<div>注册 后台首页 蓝图</div><div><font style="font-size: 9px; color: rgb(77, 77, 77);">applications/view/system/__init__.py</font></div>" 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="<div>注册 启用插件所提供的 蓝图</div><div><font style="font-size: 9px; color: rgb(77, 77, 77);">plugins/*/__init__.py</font></div>" 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="<div>注册 部门管理 蓝图</div><div><font style="font-size: 9px; color: rgb(77, 77, 77);">applications/view/system/__init__.py</font></div>" 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="<div>注册 应用 cli 命令</div><div><font style="font-size: 9px; color: rgb(77, 77, 77);">applications/view/__init__.py</font></div>" 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="<div>广播插件&nbsp;event_finish 事件</div><div><span style="color: rgb(77, 77, 77); font-size: 9px;">applications/view/__init__.py</span></div>" 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="<div>初始化完成</div>" 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="<div>初始化开始</div>" 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="<font style="color: rgb(0, 127, 255);">注册项目的视图函数,</font><div><font style="color: rgb(0, 127, 255);">比如用户管理、文件上传等</font></div><div><font style="color: rgb(0, 127, 255);">功能都是在这里初始化的</font></div>" 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="<div>广播插件&nbsp;event_context 事件</div>" 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="<font style="color: rgb(153, 153, 153);">等同于 with app.app_context()</font>" 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="<font style="color: rgb(255, 128, 0);">flask admin init 数据库</font><div><font style="color: rgb(255, 128, 0);">初始化的<span style="background-color: transparent;">命令是在这里注册的</span></font></div>" 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="<font style="color: rgb(0, 153, 0);">开发插件的蓝图在这里注册</font>" 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="<font style="color: rgb(150, 150, 150);">默认读取的是</font><div><font style="color: rgb(150, 150, 150);">&nbsp;BaseConfig 类中的配置。</font><div><span style="background-color: transparent; color: light-dark(rgb(150, 150, 150), rgb(108, 108, 108));">修改其他类请先在文件</span></div><div><div><font style="color: rgb(150, 150, 150);"><span style="background-color: transparent;">applications/__init__.py&nbsp;</span></font></div><div><font style="color: rgb(150, 150, 150);"><span style="background-color: transparent;">中进行修改</span></font></div></div></div>" 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="<font style="color: rgb(204, 0, 0);">404 500 等请求、服务器错误的视图会在这里注册</font>" 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="导入所有数据库模型<div><span style="color: rgb(77, 77, 77); font-size: 9px;">applications/models/__init__.py</span></div>" 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>
|
||||
|
After Width: | Height: | Size: 835 KiB |
|
After Width: | Height: | Size: 328 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 258 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
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 |
|
After Width: | Height: | Size: 14 KiB |
@@ -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' # 忽略代码块高亮失败警告
|
||||
]
|
||||
@@ -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。
|
||||
@@ -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。
|
||||
@@ -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: 返回组合后的过滤条件。
|
||||
@@ -0,0 +1,22 @@
|
||||
.. title:: 公有函数
|
||||
|
||||
目录索引
|
||||
|
||||
公共模块
|
||||
------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
admin
|
||||
curd
|
||||
helper
|
||||
|
||||
辅助模块
|
||||
------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
utils/index.rst
|
||||
|
||||
@@ -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"(如果缓存不存在或已过期)
|
||||
@@ -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()
|
||||
|
||||
在画布上打印验证码字符。
|
||||
@@ -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)
|
||||
@@ -0,0 +1,14 @@
|
||||
.. title:: 辅助模块
|
||||
|
||||
目录索引
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
cache
|
||||
captcha
|
||||
http
|
||||
mail
|
||||
rights
|
||||
upload
|
||||
validate
|
||||
@@ -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 格式)。
|
||||
@@ -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'):
|
||||
...
|
||||
|
||||
...
|
||||
|
||||
|
||||
@@ -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)。
|
||||
@@ -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})
|
||||
@@ -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
|
||||
|
||||
@@ -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:`以插件的方式接入项目` 章节。
|
||||
|
||||
@@ -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:`以插件的方式接入项目` 章节。
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
.. title:: 最佳实践
|
||||
|
||||
目录索引
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
plugin
|
||||
trick
|
||||
frontend
|
||||
backend
|
||||
@@ -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") # 要这样写
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,24 @@
|
||||
构建文档
|
||||
==================
|
||||
|
||||
本章节讲述如何构建文档,构建文档使用到 Python 中的 sphinx 工具,需要使用 Python 进行安装。
|
||||
|
||||
安装必要依赖
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
文档位置位于 `docs` 文件夹,所以先切换到 `docs` 文件。
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
cd docs
|
||||
pip install -r requirements.txt
|
||||
|
||||
构建文档
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
使用 `sphinx-build .\source .\_build` 进行构建文档。
|
||||
|
||||
编辑文档
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
如果修改文档,请在 `source` 中修改并重新构建。
|
||||