From c53e752f5186594f4fd5b964c7e827531163a499 Mon Sep 17 00:00:00 2001 From: wojiaoyishang <422880152@qq.com> Date: Thu, 19 Jan 2023 11:17:27 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=8F=92=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E4=BF=AE=E5=A4=8D=E8=8B=A5=E5=B9=B2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .flaskenv | 11 +- README.md | 43 ++- app.py | 8 +- applications/__init__.py | 65 +++- applications/configs/config.py | 27 +- applications/dev/__init__.py | 9 + applications/dev/console.py | 89 ++++++ applications/dev/department.py | 108 +++++++ applications/dev/mail.py | 83 +++++ applications/dev/power.py | 138 +++++++++ applications/dev/role.py | 157 ++++++++++ applications/dev/user.py | 160 ++++++++++ applications/plugins/helloworld/__init__.json | 9 + applications/plugins/helloworld/__init__.py | 13 + applications/plugins/helloworld/main.py | 22 ++ .../templates/helloworld_index.html | 1 + applications/view/__init__.py | 3 +- applications/view/admin/mail.py | 4 +- applications/view/admin/monitor.py | 42 ++- applications/view/plugin/__init__.py | 165 ++++++++++ applications/view/plugin/init_plugins.py | 0 pear.sql | 12 +- plugins/helloworld/__init__.json | 9 + plugins/helloworld/__init__.py | 13 + plugins/helloworld/main.py | 22 ++ .../templates/helloworld_index.html | 1 + templates/admin/index.html | 240 +++++++------- templates/admin/login.html | 2 +- templates/admin/mail/main.html | 2 +- templates/admin/monitor.html | 76 ++++- templates/admin/plugin/main.html | 292 ++++++++++++++++++ 31 files changed, 1682 insertions(+), 144 deletions(-) create mode 100644 applications/dev/__init__.py create mode 100644 applications/dev/console.py create mode 100644 applications/dev/department.py create mode 100644 applications/dev/mail.py create mode 100644 applications/dev/power.py create mode 100644 applications/dev/role.py create mode 100644 applications/dev/user.py create mode 100644 applications/plugins/helloworld/__init__.json create mode 100644 applications/plugins/helloworld/__init__.py create mode 100644 applications/plugins/helloworld/main.py create mode 100644 applications/plugins/helloworld/templates/helloworld_index.html create mode 100644 applications/view/plugin/__init__.py create mode 100644 applications/view/plugin/init_plugins.py create mode 100644 plugins/helloworld/__init__.json create mode 100644 plugins/helloworld/__init__.py create mode 100644 plugins/helloworld/main.py create mode 100644 plugins/helloworld/templates/helloworld_index.html create mode 100644 templates/admin/plugin/main.html diff --git a/.flaskenv b/.flaskenv index 66006b5..f9fd78d 100644 --- a/.flaskenv +++ b/.flaskenv @@ -1,7 +1,7 @@ # flask配置 -FLASK_APP=app.py -FLASK_ENV=development -FLASK_DEBUG=1 +FLASK_APP = app.py +FLASK_ENV = development +FLASK_DEBUG = 1 FLASK_RUN_HOST = 127.0.0.1 FLASK_RUN_PORT = 5000 @@ -26,4 +26,7 @@ SECRET_KEY='pear-admin-flask' # 邮箱配置 MAIL_SERVER='smtp.qq.com' MAIL_USERNAME='123@qq.com' -MAIL_PASSWORD='XXXXX' # 生成的授权码 \ No newline at end of file +MAIL_PASSWORD='XXXXX' # 生成的授权码 + +# 插件配置 json 格式,填入插件的文件夹名 +PLUGIN_ENABLE_FOLDERS = ["helloworld"] \ No newline at end of file diff --git a/README.md b/README.md index 4582677..8f0e5c7 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ Pear Admin Flask │ ├─configs # 配置文件 │ │ ├─ common.py # 普通配置 │ │ └─ config.py # 配置文件对象 +│ ├─ dev # 插件开发模块 │ ├─extensions # 注册插件 │ ├─models # 数据模型 │ ├─static # 静态资源文件 @@ -83,6 +84,7 @@ Pear Admin Flask │ └─index # 前台视图模块 ├─docs # 文档说明(占坑) ├─migrations # 迁移文件记录 +├─plugins # 自定义插件文件夹 ├─requirement # 依赖文件 ├─test # 测试文件夹(占坑) └─.env # 项目的配置文件 @@ -160,4 +162,43 @@ flask new --type view --name test/a |---------------------|---------------------| | ![](docs/assets/1.jpg) | ![](docs/assets/2.jpg) | | ![](docs/assets/3.jpg)| ![](docs/assets/4.jpg) | -| ![](docs/assets/5.jpg) | ![](docs/assets/6.jpg) | \ No newline at end of file +| ![](docs/assets/5.jpg) | ![](docs/assets/6.jpg) | + + +#### 此 PR 修改内容 + +- [*] 关闭了 Flask 原有的日志输出,并采用自定义日志输出。```(applications/\_\_init\_\_.py)``` + +- [*] 在程序 上游(app.before_request) 修改了请求的 来源地址(request.remote_addr) 以便获取远程地址的真实IP。```(applications/\_\_init\_\_.py)``` + +- [*] 强制性将 .flaskenv 中的内容设置为程序运行时的环境变量,解决使用 ```python app.py``` 运行程序时不能正常读取配置的问题。```(applications/configs/config.py)``` + +- [*] 不过滤提交邮件时的内容,并更改邮件以 HTML 格式发送。```(applications/view/admin/main.py)``` + +- [*] 修改邮件管理网页模板中的一个字符串错误。“暂无人员信息” -> “暂无邮件信息” 。```(templates/admin/mail/main.html)``` + +- [*] 修改登录页面中为引入全局网页头文件(admin/common/header.html)中导致的问题(手机页面过小)。```(templates/admin/login.html)``` + +- [*] 修改系统监控为全核CPU使用率。```(applications/view/admin/monitor.py)``` + +- [*] 修正系统监控中的CPU拼写错误。“cup” -> “cpu”。```(applications/view/admin/monitor.py)``````(templates/admin/monitor.html)``` + +- [+] 直接将 pywsgi 集成,使用 ```python app.py``` 运行程序默认使用 pywsgi 运行,调试可以采用 ```python -m flask run``` 便于部署。```(app.py)``` + +- [+] 增加插件功能,并把部分数据库操作封装成模块,以便于插件调用。```(applications/dev)``````(applications/plugin)``` + +- [+] 增加关闭与重启程序功能。```(applications/view/admin/monitor.py)``````(templates/admin/monitor.html)``` 注意:写了一个 start.py 实现进程守护的功能,需要使用 ```python start.py (命令行,如 -m flask run)``` 才能使用重启功能。具体看下面的解释。 + +###### 重启程序功能的解释 + +伪守护进程,```start.py```的作用是打开一个新的进程并等待进程运行完毕,进程运行完毕时再打开一个进程(无论子进程是否正常退出,主进程始终会在子进程退出后再创建一个子进程)。```start.py```运行时会给子进程传递一个叫```pearppid```的环境变量,子进程通过判断这个变量是否存在来知道是否属于守护进程内。 + +结束程序时,若没有守护进程,直接退出;若有,先结束守护进程,再结束自身。 + +###### 插件功能的使用 + +插件功能便于开发者二次开发,且最大限度地不更改原有框架内容。 + +插件启用与禁用:在 .flaskenv 中设置。直接配置环境变量。将插件放在根目录的```plugins```文件夹下,再配置环境变量 ```PLUGIN_ENABLE_FOLDERS``` 实现插件禁用与启用。**注意:```PLUGIN_ENABLE_FOLDERS```为 json 数据格式的列表,请在列表中填入插件的文件夹名。**(当然你也可以在后台管理页面快速启用与禁用插件。) + +插件的编写:我们提供了一个实例插件 Helloworld。一个符合规格插件应该至少包含实例插件中的所有内容。 diff --git a/app.py b/app.py index a8a7d48..1c7159e 100644 --- a/app.py +++ b/app.py @@ -2,9 +2,15 @@ from applications import create_app from flask_migrate import Migrate from applications.extensions import db +from gevent import pywsgi + +import os + app = create_app() migrate = Migrate(app, db) if __name__ == '__main__': - app.run() + # app.run() + server = pywsgi.WSGIServer((os.getenv('FLASK_RUN_HOST'), int(os.getenv('FLASK_RUN_PORT'))), app, log=None) + server.serve_forever() diff --git a/applications/__init__.py b/applications/__init__.py index e24950e..ec1b789 100644 --- a/applications/__init__.py +++ b/applications/__init__.py @@ -1,15 +1,78 @@ import os -from flask import Flask +from applications.dev import * + +from flask import Flask, request from applications.common.script import init_script from applications.extensions import init_plugs from applications.view import init_view from applications.configs import config +from logging.config import dictConfig + +def get_user_ip(request): + """获取用户真实IP 来自 @FacebookLibra""" + if 'HTTP_X_FORWARDED_FOR' in request.headers: + arr = request.headers['HTTP_X_FORWARDED_FOR'].strip().split(",") + i = 0 + while i < len(arr): + if arr[i].find("unknown") != -1: + del arr[i] + else: + i += 1 + if len(arr) != 0: + return arr[0].strip() + elif 'HTTP_CLIENT_IP' in request.headers: + return request.headers['HTTP_CLIENT_IP'] + elif 'REMOTE_ADDR' in request.headers: + return request.headers['REMOTE_ADDR'] + elif 'X-Forwarded-For' in request.headers: + return request.headers['X-Forwarded-For'] + return request.remote_addr + +dictConfig({ + 'version': 1, + 'formatters': {'default': { + 'format': '', + }}, + 'handlers': {'wsgi': { + 'class': 'logging.StreamHandler', + 'stream': 'ext://flask.logging.wsgi_errors_stream', + 'formatter': 'default' + }}, + 'root': { + 'level': 'INFO', + 'handlers': ['wsgi'] + } +}) + def create_app(config_name=None): + app = Flask(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + # 移除原有的输出日志 + app.logger = None + + # 更改IP地址,只有在最新版的flask中才能生效 + @app.before_request + def before_request(): + request.remote_addr = get_user_ip(request) + + + # 使用自定义的日志输出 + @app.after_request + def after_request(rep): + if rep.status_code == 200: + console.success(f"{request.remote_addr} -- {request.full_path} 200") + elif rep.status_code == 404: + console.error(f"{request.remote_addr} -- {request.full_path} 404") + elif rep.status_code == 500: + console.warning(f"{request.remote_addr} -- {request.full_path} 500") + else: + console.info(f"{request.remote_addr} -- {request.full_path} {rep.status_code}") + return rep + if not config_name: # 尝试从本地环境中读取 config_name = os.getenv('FLASK_CONFIG', 'development') diff --git a/applications/configs/config.py b/applications/configs/config.py index cafec6b..cb5f721 100644 --- a/applications/configs/config.py +++ b/applications/configs/config.py @@ -5,9 +5,20 @@ from urllib.parse import quote_plus as urlquote from apscheduler.executors.pool import ThreadPoolExecutor from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore +# 读入 flaskenv 中的环境变量 +with open(".flaskenv", "r", encoding='utf-8') as f: + for line in f.read().split("\n"): + pos = line.find("#") + if pos != -1: + line = line[:pos] + line = line.strip() + if line == "": + continue + _ = line.split("=") + key, value = _[0], '='.join(_[1:]) + os.environ[key.strip()] = value.strip() class BaseConfig: - SYSTEM_NAME = os.getenv('SYSTEM_NAME', 'Pear Admin') # 主题面板的链接列表配置 SYSTEM_PANEL_LINKS = [ @@ -51,8 +62,9 @@ class BaseConfig: SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{MYSQL_USERNAME}:{urlquote(MYSQL_PASSWORD)}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}?charset=utf8mb4" # 默认日志等级 - LOG_LEVEL = logging.WARN - # + LOG_LEVEL = logging.WARN\ + + # 邮件配置 MAIL_SERVER = os.getenv('MAIL_SERVER') or 'smtp.qq.com' MAIL_USE_TLS = False MAIL_USE_SSL = True @@ -62,11 +74,13 @@ class BaseConfig: # 默认发件人的邮箱,这里填写和MAIL_USERNAME一致即可 MAIL_DEFAULT_SENDER = ('pear admin', os.getenv('MAIL_USERNAME') or '123@qq.com') - # 設置 APSCHEDULER 參數 + # 設置 APSCHEDULER 參數 @CHunYenc SCHEDULER_API_ENABLED = os.getenv('SCHEDULER_API_ENABLED') or False SCHEDULER_JOBSTORES: dict = { - 'default': SQLAlchemyJobStore(url=f'mysql+pymysql://{MYSQL_USERNAME}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}') + 'default': SQLAlchemyJobStore( + url=f'mysql+pymysql://{MYSQL_USERNAME}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}') } + SCHEDULER_EXECUTORS: dict = { 'default': ThreadPoolExecutor(20) } @@ -75,6 +89,9 @@ class BaseConfig: 'max_instances': 3 } + # 插件配置 + PLUGIN_ENABLE_FOLDERS = os.getenv('PLUGIN_ENABLE_FOLDERS') + class TestingConfig(BaseConfig): """ 测试配置 """ diff --git a/applications/dev/__init__.py b/applications/dev/__init__.py new file mode 100644 index 0000000..4819129 --- /dev/null +++ b/applications/dev/__init__.py @@ -0,0 +1,9 @@ +from applications.dev import user +from applications.dev import role +from applications.dev import power +from applications.dev import department +from applications.dev import console +from flask import Flask + +# 获取app应用实例,会被初始化插件时重新赋值 +app = None # type: Flask diff --git a/applications/dev/console.py b/applications/dev/console.py new file mode 100644 index 0000000..b658135 --- /dev/null +++ b/applications/dev/console.py @@ -0,0 +1,89 @@ +""" +输出控制台日志 +""" +import sys +import time +import ctypes + +NONE = "\033[m" +RED = "\033[0;32;31m" +LIGHT_RED = "\033[1;31m" +GREEN = "\033[0;32;32m" +LIGHT_GREEN = "\033[1;32m" +BLUE = "\033[0;32;34m" +LIGHT_BLUE = "\033[1;34m" +DARY_GRAY = "\033[1;30m" +CYAN = "\033[0;36m" +LIGHT_CYAN = "\033[1;36m" +PURPLE = "\033[0;35m" +LIGHT_PURPLE = "\033[1;35m" +BROWN = "\033[0;33m" +YELLOW = "\033[1;33m" +LIGHT_GRAY = "\033[0;37m" +WHITE = "\033[1;37m" + +# 开启 Windows 下对于 ESC控制符 的支持 +if sys.platform == "win32": + kernel32 = ctypes.windll.kernel32 + kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) + + +def _print(level, msg): + time_ = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + + level_name = {10: "Plain", + 11: "Log", + 12: "Info", + 13: "Debug", + 14: "Success", + 15: "Warning", + 16: "Error"} + + color = {10: NONE, + 11: LIGHT_CYAN, + 12: LIGHT_BLUE, + 13: PURPLE, + 14: GREEN, + 15: YELLOW, + 16: RED} + + print(f'{color.get(level, NONE)}[{time_}]({level_name.get(level, "Plain")}):', msg, f"{NONE}") + + +def plain(*args, sep=' '): + msg = sep.join(str(_) for _ in args) + _print(10, msg) + + +def log(*args, sep=' '): + msg = sep.join(str(_) for _ in args) + _print(11, msg) + + +def info(*args, sep=' '): + msg = sep.join(str(_) for _ in args) + _print(12, msg) + + +def debug(*args, sep=' '): + msg = sep.join(str(_) for _ in args) + _print(13, msg) + + +def success(*args, sep=' '): + msg = sep.join(str(_) for _ in args) + _print(14, msg) + + +def warn(*args): + warning(*args) + + +def warning(*args, sep=' '): + msg = sep.join(str(_) for _ in args) + _print(15, msg) + + +def error(*args, sep=' '): + msg = sep.join(str(_) for _ in args) + _print(16, msg) diff --git a/applications/dev/department.py b/applications/dev/department.py new file mode 100644 index 0000000..388e0f8 --- /dev/null +++ b/applications/dev/department.py @@ -0,0 +1,108 @@ +""" +集成了对 Pear Admin Flask 的部门的操作,并给了相对应的示例。 +""" + +from applications.common import curd +from applications.extensions import db +from applications.models import Dept, User +from applications.schemas import DeptOutSchema + + +def get_all(): + """ + 获取全部权限,会返回一个列表,每个列表是一个字典。 + + 字典构成如下:: + + { + "address":"这是总公司", # 地址 + "deptId":1, # 公司ID + "deptName":"总公司", # 公司名 + "email":"1", # 公司 email + "leader":"", # 公司领导人 + "parentId":0, # 父ID + "phone":"", # 联系电话 + "sort":1, # 排序 + "status":"1" # 状态 1-开启 0-关闭 + } + + :return: 列表 + """ + dept = Dept.query.order_by(Dept.sort).all() + return curd.model_to_dicts(schema=DeptOutSchema, data=dept) + + +def add(parentId, deptName, sort, leader, phone, email, status, address): + """ + 添加一个公司 + + :param parentId: 父公司ID,0未总公司 + :param deptName: 公司名称 + :param sort: 排序 + :param leader: 负责人 + :param phone: 手机 + :param email: 邮箱 + :param status: 状态 1-打开 0-关闭 + :param address: 地址 + :return: 是否成功 + """ + dept = Dept( + parent_id=parentId, + dept_name=deptName, + sort=sort, + leader=leader, + phone=phone, + email=email, + status=status, + address=address + ) + r = db.session.add(dept) + db.session.commit() + if r: + return True + else: + return False + + +def update(deptId, data): + """ + 更新公司信息 + + 可更新内容如下:: + + "dept_name" + "sort" + "leader" + "phone" + "email" + "status" + "address" + + :param deptId: 公司ID + :param data: 要更新公司字典 + :return: 是否成功 + """ + d = Dept.query.filter_by(id=deptId).update(data) + if not d: + return False + db.session.commit() + return True + + +def delete(deptId): + """ + 删除公司 + + :param deptId: 公司ID + :return: 是否成功 + """ + d = Dept.query.filter_by(id=deptId).delete() + if not d: + return False + res = User.query.filter_by(dept_id=deptId).update({"dept_id": None}) + db.session.commit() + if res: + return True + else: + return False + diff --git a/applications/dev/mail.py b/applications/dev/mail.py new file mode 100644 index 0000000..8c73474 --- /dev/null +++ b/applications/dev/mail.py @@ -0,0 +1,83 @@ +""" +集成了对 Pear Admin Flask 二次开发的的邮件操作,并给了相对应的示例。 +""" +from flask import current_app +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.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", # 时间 + "id": 17, # 邮件ID + "realname": "超级管理", # 创建者 + "receiver": "", # 接收者 + "subject": "" # 主题 + } + + :param receiver: 发送者 + :param subject: 邮件标题 + :param content: 邮件内容 + :return: 列表 + """ + # 查询参数构造 + mf = ModelFilter() + if receiver: + mf.contains(field_name="receiver", value=receiver) + if subject: + 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: 成功与否 + """ + 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 + + mail = Mail(receiver=receiver, subject=subject, content=content, user_id=user_id) + + db.session.add(mail) + db.session.commit() + return True + + +def delete(id): + """ + 删除邮件记录,立刻写入数据库。 + + :param id: 邮件ID + :return: 成功与否 + """ + res = Mail.query.filter_by(id=id).delete() + if not res: + return False + db.session.commit() + return True diff --git a/applications/dev/power.py b/applications/dev/power.py new file mode 100644 index 0000000..99d2757 --- /dev/null +++ b/applications/dev/power.py @@ -0,0 +1,138 @@ +""" +集成了对 Pear Admin Flask 的权限操作,并给了相对应的示例。 +注意:此权限相当于添加后台菜单。 +""" + +from applications.common import curd +from applications.extensions import db +from applications.models import Power + +from applications.extensions import ma +from marshmallow import fields + + +class PowerOutSchema2(ma.Schema): # 序列化类 + powerId = fields.Str(attribute="id") + powerName = fields.Str(attribute="name") + powerType = fields.Str(attribute="type") + powerUrl = fields.Str(attribute="url") + powerCode = fields.Str(attribute="code") + openType = fields.Str(attribute="open_type") + parentId = fields.Str(attribute="parent_id") + icon = fields.Str() + sort = fields.Integer() + create_time = fields.DateTime() + update_time = fields.DateTime() + enable = fields.Integer() + + +def get_all(): + """ + 获取所有权限,会返回一个含有菜单的列表。每个列表都是一个字典。 + + 字典构成如下:: + { + "powerType": "0", # 权限类型 0目录 1菜单 2按钮 + "powerUrl": None, # 路径 + "powerCode": "", # 权限标识 + "update_time": None, # 更新时间 + "sort": 1, # 排序 + "openType": None, # 打开方式 _iframe框架 _blank新页面 + "icon": "layui-icon layui-icon-set-fill", # 图标 + "powerName": "系统管理", # 名称 + "create_time": None, # 创建时间 + "parentId": "0", # 父id + "powerId": "1", # 自己的id + "enable": 1 # 是否启用 + } + + :return: 菜单列表。 + """ + power = Power.query.all() + res = curd.model_to_dicts(schema=PowerOutSchema2, data=power) + res.append({"powerId": 0, "powerName": "顶级权限", "parentId": -1}) + return res + + +def add(parentId, powerName, powerType, icon, sort: int, enable: bool, powerCode="", powerUrl="", openType=""): + """ + 新建一个菜单权限。 + + 参考代码:: + + dev.power.add("0", "测试", "1", "layui-icon-time", 0, True, "testfor", "https://baidu.com", "_iframe") + + :param parentId: 父ID,0为顶级菜单ID + :param powerName: 菜单名称 + :param powerType: 权限类型(状态) 0目录 1菜单 2按钮 + :param icon: 图标,详细查看layui的图标 + :param sort: 排序 + :param enable: 是否启用 + + :param powerCode: 权限标识 + :param powerUrl: 权限URL,菜单打开的网址,或者是路径。可选,菜单和按钮类型必填。 + :param openType: 打开方式,_iframe框架 _blank新页面。可选,菜单和按钮类型必填。 + + + + :return: 返回新权限ID + """ + 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) + db.session.commit() + return power.id + + +def update(powerId, data): + """ + 更新权限。 + + data可选:: + + "icon" + "open_type" + "parent_id" + "code" + "name" + "type" + "url" + "sort" + + :param powerId: 要更新的权限ID + + :return: 是否成功 + """ + res = Power.query.filter_by(id=powerId).update(data) + db.session.commit() + if res: + return True + else: + return False + + +def delete(powerId): + """ + 删除权限。 + + :param powerId: 要更新的权限ID + :return: 是否成功 + """ + power = Power.query.filter_by(id=powerId).first() + power.role = [] + + r = Power.query.filter_by(id=powerId).delete() + db.session.commit() + if r: + return True + else: + return False diff --git a/applications/dev/role.py b/applications/dev/role.py new file mode 100644 index 0000000..a0cc286 --- /dev/null +++ b/applications/dev/role.py @@ -0,0 +1,157 @@ +""" +集成了对 Pear Admin Flask 的角色操作,并给了相对应的示例。 +""" +from applications.extensions import db +from applications.models import Role, Power +from applications.schemas import PowerOutSchema2 + + +def filter_by(**kwargs): + """ + 检索角色字段信息,可用于获取角色ID等。系统中默认管理员角色id为1,普通用户为2。 + 内部采用的是使用 Role.query.filter_by(**kwargs) 进行数据库查询。 + + 注意:此函数返回的结果为构造的 SQL的查询字符串 ,以 role_filter 命名,但是并不是用户数据。 + + 返回的字段如下:: + + id name code enable remark details sort create_time update_time power + 具体参考 applications/models/admin_role.py 中的模型定义。 + + 参考调用如下:: + + roleinfo = dev.role.filter_by(code='admin').first() # 第一个符合要求的角色信息 + print(role.id, role.name) # 输出角色名称与角色标识 + + # 找出所有角色id + for role in dev.role.filter_by().all(): + print(role.id, role.name) + + :param kwargs: 查询参数 + :return: 角色SQL的查询字符串 + """ + return Role.query.filter_by(**kwargs) + + +def add(roleName, roleCode, enable, sort, details): + """ + 添加一个角色。此函数直接写入数据库。此函数不会检测角色是否已经存在(官方在API接口中也没有检测)。 + + :param roleName: 角色名称 (如管理员) + :param roleCode: 角色标识 (如admin) + :param enable: 是否启用 True or False + :param sort: 排序 + :param details: 描述 + :return: None + """ + role = Role( + details=details, + enable=enable, + code=roleCode, + name=roleName, + sort=sort + ) + db.session.add(role) + db.session.commit() + + +def get_power(role_filter, detail=False, p=0): + """ + 获取角色的权限。 + + 如果是非详细数据,会返回一个含有权限id的列表。 + 如果是详细数据返回此函数,将会返回一个列表,列表中会包含字典,字典的键如下:: + + { + "checkArr": "1", # 是否有权限 1 为有 0为无 + "create_time": null, # 权限创建 + "enable": 1, # 权限是否启用 + "icon": "layui-icon layui-icon-set-fill", # 权限图标 + "openType": null, # 开启状态 + "parentId": "0", # 父权限ID + "powerId": "1", # 权限ID + "powerName": "系统管理", # 权限名称 + "powerType": "0", # 权限类型 + "powerUrl": null, # 权限URL + "sort": 1, # 权限排序 + "update_time": null # 权限更新时间 + } + + + :param role_filter: dev.role.filter_by() 返回结果。 + :param detail: 返回详细数据 + :param p: 如果有多个结果被找到,p可以确定使用第几个结果。内部使用 role_filter.all()[p] + :return: 用户拥有的权限列表。 + """ + role = role_filter.all()[p] + check_powers = role.power + check_powers_list = [] + for cp in check_powers: + check_powers_list.append(cp.id) + if not detail: + return check_powers_list + powers = Power.query.all() + power_schema = PowerOutSchema2(many=True) # 用已继承ma.ModelSchema类的自定制类生成序列化类 + output = power_schema.dump(powers) # 生成可序列化对象 + for i in output: + if int(i.get("powerId")) in check_powers_list: + i["checkArr"] = "1" + else: + i["checkArr"] = "0" + return output + + +def set_power(role_filter, powerIds, p=0): + """ + 保存角色权限。此函数会直接写入数据库。 + + :param role_filter: dev.role.filter_by() 返回结果。 + :param powerIds: 必须是一个包含权限ID的列表。如 [1, 2, 3] + :param p: 如果有多个结果被找到,p可以确定使用第几个结果。内部使用 role_filter.all()[p] + :return: None + """ + role = role_filter.all()[p] + powers = Power.query.filter(Power.id.in_(powerIds)).all() + role.power = powers + + db.session.commit() + + +def update(role_filter, data): + """ + 更新角色数据。此功能将直接写入数据库。 + + 可更新的字段如下:: + + id name code enable remark details sort create_time update_time power + 具体参考 applications/models/admin_role.py 中的模型定义。 + + 参考调用如下:: + + role_filter = dev.role.filter_by(id=0) # 获取指定角色ID的角色,注意不要使用会引起歧义的查询条件,否则会匹配到多个角色。 + dev.role.update(role_filter, {enable: 0}) # 禁用 + + :param role_filter: dev.role.filter_by() 返回结果。 + :param data: 要更新的数据,必须是字典。 + :return: None + """ + role_filter.update(data) + db.session.commit() + + +def delete(role_filter): + """ + 删除角色。此功能将直接写入数据库。 + + :param role_filter: dev.role.filter_by() 返回结果。 + :return: 是否成功。 + """ + role = role_filter.first() + # 删除该角色的权限和用户 + role.power = [] + role.user = [] + + r = role_filter.delete() + db.session.commit() + return r + diff --git a/applications/dev/user.py b/applications/dev/user.py new file mode 100644 index 0000000..00688a3 --- /dev/null +++ b/applications/dev/user.py @@ -0,0 +1,160 @@ +""" +集成了对 Pear Admin Flask 的用户操作,并给了相对应的示例。 + +调用示例:: + + from applications import dev + dev.user.login_required # 用户是否登录 + dev.user.current_user # 当前登录用户 + dev.user.authorize("XXX", log=True) # 用户是否有此权限 + +""" + +from applications.extensions import db +from applications.models import Role +from applications.models import User + + +def filter_by(**kwargs): + """ + 用于在用户数据中查询用户信息,可以通过用户名、用户id等进行检索。(建议使用id检索) + 内部采用的是使用 User.query.filter_by(**kwargs) 进行数据库查询 + + 注意:此函数返回的结果为构造的 SQL的查询字符串 ,以 user_filter 命名,但是并不是用户数据。 + + 返回的字段如下:: + + id username password_hash create_at update_at + enable realname remark avatar dept_id + 具体参考 applications/models/admin_user.py 中的模型定义。 + + + 参考调用如下:: + + userinfo = dev.user.filter_by(username='zsq').first() # 查询符合要求的第一个用户 + print(userinfo.realname) # 获取用户真实名字 + + + :param kwargs: 查询参数 + :return: 用户数据SQL的查询字符串 + """ + return User.query.filter_by(**kwargs) + + +def update(user_filter, data): + """ + 更新用户数据,修改将直接保存到数据库中。 + 注意:更新用户角色(role)请使用 dev.user.update_role() 函数。 + + 可更新的字段如下:: + + id username password_hash create_at update_at + enable realname remark avatar dept_id + 具体参考 applications/models/admin_user.py 中的模型定义。 + + 参考调用如下:: + + user_filter = dev.user.filter_by(id=0) # 获取指定用户ID的用户,注意不要使用会引起歧义的查询条件,否则会匹配到多个用户。 + dev.user.update(user_filter, {username: 'zsq1314'}) # 更新其用户名 + + + + :param user_filter: dev.user.filter_by() 的结果。 + :param data: 要更新的数据,必须是字典。 + :return: None + """ + user_filter.update(data) + db.session.commit() + + +def update_role(user_filter, roleIds): + """ + 更新用户角色,修改将直接保存到数据库中。 + + 参考调用如下:: + + user_filter = dev.user.filter_by(username='zsq') # 获取符合要求的第一个用户 + roleIds = [] + roleIds.append(dev.role.filter_by(code='admin').first().id) # 管理员角色ID + roleIds.append(dev.role.filter_by(code='common').first().id) # 普通用户角色ID + dev.user.update_role(user_filter, roleIds) + + + :param user_filter: dev.user.filter_by() 的结果。 + :param roleIds: 要更新的角色ID,作为列表传入。 + :return: None + """ + user_filter.first().role = Role.query.filter(Role.id.in_(roleIds)).all() + db.session.commit() + + +def get_role(user_filter): + """ + 获取用户的所有角色ID,将会返回一个整数列表。 + + :param user_filter: dev.user.filter_by() 的结果。 + :return: 列表 (roleIds) + """ + checked_roles = [] + for r in user_filter.first().role: + checked_roles.append(r.id) + return checked_roles + + +def set_password(user_filter, password): + """ + 设置用户密码,此函数不会验证用户原始密码哈希值,直接写入新密码哈希值。 + + 参考调用如下:: + + user_filter = dev.user.filter_by(username='zsq') # 获取符合要求的用户 + dev.user.set_password(user_filter, 'zsq1314') # 设置密码 + + :param user_filter: dev.user.filter_by() 的结果。 + :param password: 新密码。 + :return: None + """ + user = user_filter.first() + user.set_password(password) + db.session.add(user) + db.session.commit() + + +def add(username, realname, password, roleIds): + """ + 添加一个新用户。函数会判断用户名是否已存在,存在返回 False ,成功返回用户数据(userinfo)。此函数会直接写入数据库。 + 注意:此函数创建出来的用户默认是禁用的,可以使用 enable 启用。 + + :param username: 新用户名 + :param realname: 真实名字 + :param password: 密码字符串 + :param roleIds: 角色id列表,如 [1, 2],角色id具体查看 dev.role.get_all() 函数。 + :return: 是否成功 + """ + if bool(User.query.filter_by(username=username).count()): + return False + user = User(username=username, realname=realname) + user.set_password(password) + db.session.add(user) + roles = Role.query.filter(Role.id.in_(roleIds)).all() + for r in roles: + user.role.append(r) + db.session.commit() + return user + + +def delete(user_filter): + """ + 删除一个用户。此函数立刻写入数据库。 + + :param user_filter: dev.user.filter_by() 的结果。 + :return: 是否成功 + """ + user = user_filter.first() + user.role = [] + res = user_filter.delete() + db.session.commit() + return res + + + diff --git a/applications/plugins/helloworld/__init__.json b/applications/plugins/helloworld/__init__.json new file mode 100644 index 0000000..110f9f7 --- /dev/null +++ b/applications/plugins/helloworld/__init__.json @@ -0,0 +1,9 @@ +{ + "plugin_name": "Hello World", + "plugin_version": "1.0.0.1", + "plugin_description": "一个测试的插件。", + "developer_name": "Yishang", + "developer_website": "https://lovepikachu.top", + "developer_email": "422880152@qq.com", + "developer_phone": "-" +} \ No newline at end of file diff --git a/applications/plugins/helloworld/__init__.py b/applications/plugins/helloworld/__init__.py new file mode 100644 index 0000000..d81f7a6 --- /dev/null +++ b/applications/plugins/helloworld/__init__.py @@ -0,0 +1,13 @@ +""" +初始化插件 +""" + + +def event_enable(): + """当插件被启用时会调用此处""" + print("启用插件") + + +def event_disable(): + """当插件被禁用时会调用此处""" + print("禁用插件") diff --git a/applications/plugins/helloworld/main.py b/applications/plugins/helloworld/main.py new file mode 100644 index 0000000..e51d993 --- /dev/null +++ b/applications/plugins/helloworld/main.py @@ -0,0 +1,22 @@ +from applications.dev import * + +from flask import render_template, Blueprint + +import os +import psutil + +# 获取插件所在的目录(结尾没有分割符号) +dir_path = os.path.dirname(__file__).replace("\\", "/") +folder_name = dir_path[dir_path.rfind("/") + 1:] # 插件文件夹名称 + +# 创建蓝图 +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") + + +# 绑定蓝图 +app.register_blueprint(helloworld_blueprint) diff --git a/applications/plugins/helloworld/templates/helloworld_index.html b/applications/plugins/helloworld/templates/helloworld_index.html new file mode 100644 index 0000000..c062ec2 --- /dev/null +++ b/applications/plugins/helloworld/templates/helloworld_index.html @@ -0,0 +1 @@ +

Test!

\ No newline at end of file diff --git a/applications/view/__init__.py b/applications/view/__init__.py index bec7989..6b5cf78 100644 --- a/applications/view/__init__.py +++ b/applications/view/__init__.py @@ -3,7 +3,7 @@ from applications.view.index import register_index_views from applications.view.passport import register_passport_views from applications.view.rights import register_rights_view from applications.view.department import register_dept_views - +from applications.view.plugin import register_plugin_views def init_view(app): register_admin_views(app) @@ -11,3 +11,4 @@ def init_view(app): register_rights_view(app) register_passport_views(app) register_dept_views(app) + register_plugin_views(app) diff --git a/applications/view/admin/mail.py b/applications/view/admin/mail.py index e0e2668..8908134 100644 --- a/applications/view/admin/mail.py +++ b/applications/view/admin/mail.py @@ -57,11 +57,11 @@ def save(): req_json = request.json receiver = str_escape(req_json.get("receiver")) subject = str_escape(req_json.get('subject')) - content = str_escape(req_json.get('content')) + content = req_json.get('content') user_id = current_user.id try: - msg = Message(subject=subject, recipients=receiver.split(";"), body=content) + msg = Message(subject=subject, recipients=receiver.split(";"), html=content) flask_mail.send(msg) except Exception as e: current_app.log_exception(e) diff --git a/applications/view/admin/monitor.py b/applications/view/admin/monitor.py index 40f1e9e..1fdd9e9 100644 --- a/applications/view/admin/monitor.py +++ b/applications/view/admin/monitor.py @@ -1,4 +1,5 @@ import os +import sys import platform import re from datetime import datetime @@ -22,8 +23,8 @@ def main(): python_version = platform.python_version() # 逻辑cpu数量 cpu_count = psutil.cpu_count() - # cup使用率 - cpus_percent = psutil.cpu_percent(interval=0.1) + # cpu使用率 + cpus_percent = psutil.cpu_percent(interval=0.1, percpu=False) # percpu 获取主使用率 # 内存 memory_information = psutil.virtual_memory() # 内存使用率 @@ -70,8 +71,8 @@ def main(): boot_time=boot_time, up_time_format=up_time_format, disk_partitions_list=disk_partitions_list, - time_now=time_now - + time_now=time_now, + pearppid=os.environ.get('pearppid') ) @@ -79,10 +80,37 @@ def main(): @admin_monitor_bp.get('/polling') @authorize("admin:monitor:main") def ajax_polling(): - # 获取cup使用率 - cpus_percent = psutil.cpu_percent(interval=0.1) + # 获取cpu使用率 + cpus_percent = psutil.cpu_percent(interval=0.1, percpu=False) # percpu 获取主使用率 # 获取内存使用率 memory_information = psutil.virtual_memory() 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) + return jsonify(cpus_percent=cpus_percent, memory_used=memory_usage, time_now=time_now) + + +# 重启程序 +@admin_monitor_bp.get('/restart') +@authorize("admin:monitor:main") +def restart(): + for proc in psutil.process_iter(): + if proc.pid == os.getpid(): + proc.kill() + ppid = os.getenv('pearppid', None) + if not ppid: + sys.exit(1) + +# 关闭程序 +@admin_monitor_bp.get('/kill') +@authorize("admin:monitor:main") +def kill(): + ppid = os.getenv('pearppid', None) + if ppid: + for proc in psutil.process_iter(): + if proc.pid == ppid: + proc.kill() + else: + for proc in psutil.process_iter(): + if proc.pid == os.getpid(): + proc.kill() + sys.exit(1) \ No newline at end of file diff --git a/applications/view/plugin/__init__.py b/applications/view/plugin/__init__.py new file mode 100644 index 0000000..c9db878 --- /dev/null +++ b/applications/view/plugin/__init__.py @@ -0,0 +1,165 @@ +import shutil + +from flask import Flask +from flask import Blueprint, render_template, request, jsonify, escape +from applications.common.utils.http import table_api, fail_api, success_api + +import os +import json +import traceback +import importlib + +import applications.dev +from applications.common.utils.rights import authorize + +plugin_bp = Blueprint('plugin', __name__, url_prefix='/plugin') +PLUGIN_ENABLE_FOLDERS = [] + + +def register_plugin_views(app: Flask): + global PLUGIN_ENABLE_FOLDERS + applications.dev.app = app # 对app重新赋值 便于插件简单调用 + app.register_blueprint(plugin_bp) + # 载入插件过程 + # plugin_folder 配置的是插件的文件夹名 + PLUGIN_ENABLE_FOLDERS = json.loads(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()) + print(f"-->载入插件 {plugin_info['plugin_name']} 中......") + __import__('plugins.' + plugin_folder + '.main') + print(f"-->载入插件 {plugin_info['plugin_name']} 成功......") + except BaseException as e: + info = f"-->载入插件时{plugin_info['plugin_name'] if len(plugin_info) != 0 else ''}出现下列错误:" + "\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) + + +@plugin_bp.get('/') +@authorize("admin:plugin:main", log=True) +def main(): + """此处渲染管理模板""" + return render_template('admin/plugin/main.html') + + +@plugin_bp.get('/data') +@authorize("admin:plugin:main", log=True) +def data(): + """请求插件数据""" + plugin_name = escape(request.args.get("plugin_name")) + all_plugins = [] + count = 0 + for filename in os.listdir("plugins"): + try: + with open("plugins/" + filename + "/__init__.json", "r", encoding='utf-8') as f: + info = json.loads(f.read()) + + if plugin_name is None: + if info['plugin_name'].find(plugin_name) == -1: + continue + + all_plugins.append( + { + "plugin_name": info["plugin_name"], + "plugin_version": info["plugin_version"], + "plugin_description": info["plugin_description"], + "developer_name": info["developer_name"], + "developer_website": info["developer_website"], + "developer_email": info["developer_email"], + "developer_phone": info["developer_phone"], + "plugin_folder_name": filename, + "enable": "1" if filename in PLUGIN_ENABLE_FOLDERS else "0" + } + ) + count += 1 + except BaseException as error: + print(filename, error) + continue + return table_api(data=all_plugins, count=count) + + +@plugin_bp.put('/enable') +@authorize("admin:plugin:enable", log=True) +def enable(): + """启用插件""" + plugin_folder_name = request.json.get('plugin_folder_name') + if plugin_folder_name: + try: + if plugin_folder_name not in PLUGIN_ENABLE_FOLDERS: + PLUGIN_ENABLE_FOLDERS.append(plugin_folder_name) + with open(".flaskenv", "r", encoding='utf-8') as f: + flaskenv = f.read() # type: str + pos1 = flaskenv.find("PLUGIN_ENABLE_FOLDERS") + pos2 = flaskenv.find("\n", pos1) + with open(".flaskenv", "w", encoding='utf-8') as f: + if pos2 == -1: + f.write(flaskenv[:pos1] + "PLUGIN_ENABLE_FOLDERS = " + json.dumps(PLUGIN_ENABLE_FOLDERS)) + else: + f.write( + flaskenv[:pos1] + "PLUGIN_ENABLE_FOLDERS = " + json.dumps(PLUGIN_ENABLE_FOLDERS) + flaskenv[ + pos2:]) + # 启用插件事件 + try: + importlib.import_module('plugins.' + plugin_folder_name).event_enable() + except ModuleNotFoundError: + pass + except BaseException as error: + return fail_api(msg="出错啦!错误信息:" + str(error)) + + except BaseException as error: + return fail_api(msg="出错啦!错误信息:" + str(error)) + return success_api(msg="启用成功,要使修改生效需要重启程序。") + return fail_api(msg="数据错误") + + +@plugin_bp.put('/disable') +@authorize("admin:plugin:enable", log=True) +def disable(): + """禁用插件""" + plugin_folder_name = request.json.get('plugin_folder_name') + if plugin_folder_name: + try: + if plugin_folder_name in PLUGIN_ENABLE_FOLDERS: + PLUGIN_ENABLE_FOLDERS.remove(plugin_folder_name) + with open(".flaskenv", "r", encoding='utf-8') as f: + flaskenv = f.read() # type: str + pos1 = flaskenv.find("PLUGIN_ENABLE_FOLDERS") + pos2 = flaskenv.find("\n", pos1) + with open(".flaskenv", "w", encoding='utf-8') as f: + if pos2 == -1: + f.write(flaskenv[:pos1] + "PLUGIN_ENABLE_FOLDERS = " + json.dumps(PLUGIN_ENABLE_FOLDERS)) + else: + f.write( + flaskenv[:pos1] + "PLUGIN_ENABLE_FOLDERS = " + json.dumps(PLUGIN_ENABLE_FOLDERS) + flaskenv[ + pos2:]) + + # 禁用插件事件 + try: + importlib.import_module('plugins.' + plugin_folder_name).event_disable() + except ModuleNotFoundError: + pass + except BaseException as error: + return fail_api(msg="出错啦!错误信息:" + str(error)) + + except BaseException as error: + return fail_api(msg="出错啦!错误信息:" + str(error)) + return success_api(msg="禁用成功,要使修改生效需要重启程序。") + return fail_api(msg="数据错误") + + +# 删除 +@plugin_bp.delete('/remove/') +@authorize("admin:mail:remove", log=True) +def delete(plugin_folder_name): + if plugin_folder_name in PLUGIN_ENABLE_FOLDERS: + return fail_api(msg="您必须先禁用插件!") + try: + shutil.rmtree(os.path.abspath("plugins/" + plugin_folder_name)) + return success_api(msg="删除成功") + except BaseException as error: + return fail_api(msg="删除失败!原因:" + str(error)) diff --git a/applications/view/plugin/init_plugins.py b/applications/view/plugin/init_plugins.py new file mode 100644 index 0000000..e69de29 diff --git a/pear.sql b/pear.sql index 6c29bc4..55ac25c 100644 --- a/pear.sql +++ b/pear.sql @@ -207,11 +207,13 @@ INSERT INTO `admin_power` VALUES (52, '定时任务', '0', '', '', '', '0', 'lay INSERT INTO `admin_power` VALUES (53, '任务管理', '1', 'admin:task:main', '/admin/task', '_iframe', '52', 'layui-icon ', 1, '2021-06-22 21:15:00', '2021-06-22 21:15:00', 1); INSERT INTO `admin_power` VALUES (54, '任务增加', '2', 'admin:task:add', '', '', '53', 'layui-icon ', 1, '2021-06-22 22:20:54', '2021-06-22 22:20:54', 1); INSERT INTO `admin_power` VALUES (55, '任务修改', '2', 'admin:task:edit', '', '', '53', 'layui-icon ', 2, '2021-06-22 22:21:34', '2021-06-22 22:21:34', 1); -INSERT INTO `admin_power` VALUES (56, '任务删除', '2', 'admin:task:remove', '', '', '53', 'layui-icon ', 3, '2021-06-22 22:22:18', '2021-06-22 22:22:18', 1); -INSERT INTO `admin_power` VALUES (57, '邮件管理', '1', 'admin:mail:main', '/admin/mail', '_iframe', '1', 'layui-icon layui-icon layui-icon-release', 7, '2022-10-11 11:21:05', '2022-10-11 11:21:22', 1); -INSERT INTO `admin_power` VALUES (58, '邮件发送', '2', 'admin:mail:add', '', '', '57', 'layui-icon layui-icon-ok-circle', 1, '2022-10-11 11:22:26', '2022-10-11 11:22:26', 1); -INSERT INTO `admin_power` VALUES (59, '邮件删除', '2', 'admin:mail:remove', '', '', '57', 'layui-icon layui-icon layui-icon-close', 2, '2022-10-11 11:23:06', '2022-10-11 11:23:18', 1); - +INSERT INTO `admin_power` VALUES (57, '拓展插件', '0', '', '', '', '0', 'layui-icon layui-icon layui-icon-senior', 2, '2022-12-18 12:28:19', '2022-12-18 12:30:25', 1); +INSERT INTO `admin_power` VALUES (58, '插件管理', '1', 'admin:plugin:main', '/plugin', '_iframe', '57', 'layui-icon layui-icon layui-icon layui-icon ', 1, '2022-12-18 12:30:13', '2022-12-18 13:57:20', 1); +INSERT INTO `admin_power` VALUES (59, '启禁插件', '2', 'admin:plugin:enable', '', '', '58', 'layui-icon ', 1, '2022-12-18 13:25:37', '2022-12-18 13:25:37', 1); +INSERT INTO `admin_power` VALUES (60, '删除插件', '2', 'admin:plugin:remove', '', '', '58', 'layui-icon layui-icon ', 2, '2022-12-18 13:26:30', '2022-12-18 13:27:17', 1); +INSERT INTO `admin_power` VALUES (61, '邮件管理', '1', 'admin:mail:main', '/admin/mail', '_iframe', '1', 'layui-icon layui-icon layui-icon-release', 7, '2022-10-11 11:21:05', '2022-10-11 11:21:22', 1); +INSERT INTO `admin_power` VALUES (62, '邮件发送', '2', 'admin:mail:add', '', '', '61', 'layui-icon layui-icon layui-icon-ok-circle', 1, '2022-10-11 11:22:26', '2022-12-25 10:48:11', 1); +INSERT INTO `admin_power` VALUES (63, '邮件删除', '2', 'admin:mail:remove', '', '', '61', 'layui-icon layui-icon layui-icon layui-icon-close', 2, '2022-10-11 11:23:06', '2022-12-25 10:48:22', 1); -- ---------------------------- -- Table structure for admin_role -- ---------------------------- diff --git a/plugins/helloworld/__init__.json b/plugins/helloworld/__init__.json new file mode 100644 index 0000000..110f9f7 --- /dev/null +++ b/plugins/helloworld/__init__.json @@ -0,0 +1,9 @@ +{ + "plugin_name": "Hello World", + "plugin_version": "1.0.0.1", + "plugin_description": "一个测试的插件。", + "developer_name": "Yishang", + "developer_website": "https://lovepikachu.top", + "developer_email": "422880152@qq.com", + "developer_phone": "-" +} \ No newline at end of file diff --git a/plugins/helloworld/__init__.py b/plugins/helloworld/__init__.py new file mode 100644 index 0000000..d81f7a6 --- /dev/null +++ b/plugins/helloworld/__init__.py @@ -0,0 +1,13 @@ +""" +初始化插件 +""" + + +def event_enable(): + """当插件被启用时会调用此处""" + print("启用插件") + + +def event_disable(): + """当插件被禁用时会调用此处""" + print("禁用插件") diff --git a/plugins/helloworld/main.py b/plugins/helloworld/main.py new file mode 100644 index 0000000..e51d993 --- /dev/null +++ b/plugins/helloworld/main.py @@ -0,0 +1,22 @@ +from applications.dev import * + +from flask import render_template, Blueprint + +import os +import psutil + +# 获取插件所在的目录(结尾没有分割符号) +dir_path = os.path.dirname(__file__).replace("\\", "/") +folder_name = dir_path[dir_path.rfind("/") + 1:] # 插件文件夹名称 + +# 创建蓝图 +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") + + +# 绑定蓝图 +app.register_blueprint(helloworld_blueprint) diff --git a/plugins/helloworld/templates/helloworld_index.html b/plugins/helloworld/templates/helloworld_index.html new file mode 100644 index 0000000..c062ec2 --- /dev/null +++ b/plugins/helloworld/templates/helloworld_index.html @@ -0,0 +1 @@ +

Test!

\ No newline at end of file diff --git a/templates/admin/index.html b/templates/admin/index.html index 330044c..028f79d 100644 --- a/templates/admin/index.html +++ b/templates/admin/index.html @@ -1,114 +1,130 @@ - - - - Pear Admin Flask - - - - - - - - - -
-
- -
    -
  • -
  • -
- -
- -
- -
- - - -
-
-
-
-
-
-
-
- -
- -
- -
-
-
- -{% include 'admin/common/footer.html' %} - - - + + {% include 'admin/common/header.html' %} + Pear Admin Layui + + + + + + + +
+ +
+ + + +
    +
  • +
  • +
+ +
+ + +
+ +
+ + + +
+
+
+
+ +
+ +
+
+ + + +
+ +
+ +
+
+
+ +
+ +
+ + {% include 'admin/common/footer.html' %} + + + + \ No newline at end of file diff --git a/templates/admin/login.html b/templates/admin/login.html index 3cdaf9b..86269b4 100644 --- a/templates/admin/login.html +++ b/templates/admin/login.html @@ -1,7 +1,7 @@ - + {% include 'admin/common/header.html' %} 登录 diff --git a/templates/admin/mail/main.html b/templates/admin/mail/main.html index 19dfaf9..14f2354 100644 --- a/templates/admin/mail/main.html +++ b/templates/admin/mail/main.html @@ -118,7 +118,7 @@ skin: 'line', height: 'full-148', toolbar: '#mail-toolbar', /*工具栏*/ - text: { none: '暂无人员信息' }, + text: { none: '暂无邮件信息' }, defaultToolbar: [{ layEvent: 'refresh', icon: 'layui-icon-refresh' }, 'filter', 'print', 'exports'] /*默认工具栏*/ }) diff --git a/templates/admin/monitor.html b/templates/admin/monitor.html index 29dd4dc..71187f6 100644 --- a/templates/admin/monitor.html +++ b/templates/admin/monitor.html @@ -145,6 +145,17 @@ python版本 {{ python_version }} + + 程序操作 + + {% if pearppid %} + 重启程序 + {% endif %} + 关闭程序 + + @@ -153,6 +164,65 @@ {% include 'admin/common/footer.html' %} + +{# 用户修改操作 #} + + +{# 启动与禁用 #} + + +{# 用户注册时间 #} + + +{% include 'admin/common/footer.html' %} + + + \ No newline at end of file -- Gitee From 5dbe736975ba61ac4d4cb2f7b073166d36722764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=91=E5=8F=AB=E4=BB=A5=E8=B5=8F?= <422880152@qq.com> Date: Thu, 19 Jan 2023 03:21:07 +0000 Subject: [PATCH 2/6] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 我叫以赏 <422880152@qq.com> --- README.md | 4 ++++ docs/assets/image.png | Bin 0 -> 47749 bytes 2 files changed, 4 insertions(+) create mode 100644 docs/assets/image.png diff --git a/README.md b/README.md index 8f0e5c7..34d4686 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,8 @@ flask new --type view --name test/a - [*] 修正系统监控中的CPU拼写错误。“cup” -> “cpu”。```(applications/view/admin/monitor.py)``````(templates/admin/monitor.html)``` +- [*] 更新后台框架与 pear admin layui 同步。 + - [+] 直接将 pywsgi 集成,使用 ```python app.py``` 运行程序默认使用 pywsgi 运行,调试可以采用 ```python -m flask run``` 便于部署。```(app.py)``` - [+] 增加插件功能,并把部分数据库操作封装成模块,以便于插件调用。```(applications/dev)``````(applications/plugin)``` @@ -201,4 +203,6 @@ flask new --type view --name test/a 插件启用与禁用:在 .flaskenv 中设置。直接配置环境变量。将插件放在根目录的```plugins```文件夹下,再配置环境变量 ```PLUGIN_ENABLE_FOLDERS``` 实现插件禁用与启用。**注意:```PLUGIN_ENABLE_FOLDERS```为 json 数据格式的列表,请在列表中填入插件的文件夹名。**(当然你也可以在后台管理页面快速启用与禁用插件。) +![输入图片说明](docs/assets/image.png) + 插件的编写:我们提供了一个实例插件 Helloworld。一个符合规格插件应该至少包含实例插件中的所有内容。 diff --git a/docs/assets/image.png b/docs/assets/image.png new file mode 100644 index 0000000000000000000000000000000000000000..965b87a326f799e6ed488a87e340981c49ccba2c GIT binary patch literal 47749 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 B;8y?u literal 0 HcmV?d00001 -- Gitee From eec6cf306dda9aa288cb67f19437882f306c8b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=91=E5=8F=AB=E4=BB=A5=E8=B5=8F?= <422880152@qq.com> Date: Thu, 19 Jan 2023 03:22:54 +0000 Subject: [PATCH 3/6] update requirement/dev.txt. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 我叫以赏 <422880152@qq.com> --- requirement/dev.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirement/dev.txt b/requirement/dev.txt index 901ad35..de71abf 100644 --- a/requirement/dev.txt +++ b/requirement/dev.txt @@ -12,4 +12,5 @@ Flask-Mail sqlparse Pillow python-dotenv -webargs \ No newline at end of file +webargs +gevent -- Gitee From ac817b6505fb6add001ff87ca1681ad66364f053 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=91=E5=8F=AB=E4=BB=A5=E8=B5=8F?= <422880152@qq.com> Date: Thu, 19 Jan 2023 03:33:55 +0000 Subject: [PATCH 4/6] update applications/configs/config.py. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 我叫以赏 <422880152@qq.com> --- applications/configs/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/configs/config.py b/applications/configs/config.py index cb5f721..ec1d048 100644 --- a/applications/configs/config.py +++ b/applications/configs/config.py @@ -62,7 +62,7 @@ class BaseConfig: SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{MYSQL_USERNAME}:{urlquote(MYSQL_PASSWORD)}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}?charset=utf8mb4" # 默认日志等级 - LOG_LEVEL = logging.WARN\ + LOG_LEVEL = logging.WARN # 邮件配置 MAIL_SERVER = os.getenv('MAIL_SERVER') or 'smtp.qq.com' -- Gitee From 41aec471da7da76f220bb7d86781b0ce64ec36f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=91=E5=8F=AB=E4=BB=A5=E8=B5=8F?= <422880152@qq.com> Date: Thu, 26 Jan 2023 09:54:43 +0000 Subject: [PATCH 5/6] update templates/admin/index.html. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 我叫以赏 <422880152@qq.com> --- templates/admin/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/admin/index.html b/templates/admin/index.html index 028f79d..606d7b6 100644 --- a/templates/admin/index.html +++ b/templates/admin/index.html @@ -38,7 +38,7 @@
-
基本资料
+
基本资料
注销登录
-- Gitee From 39251390026ec9dd890d139e0c8c8421c87f5080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=91=E5=8F=AB=E4=BB=A5=E8=B5=8F?= <422880152@qq.com> Date: Thu, 26 Jan 2023 09:57:20 +0000 Subject: [PATCH 6/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=97=A0=E6=B3=95=E6=98=BE=E7=A4=BA=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98=EF=BC=88=E9=83=A8=E9=97=A8=E6=9C=AA=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E7=9A=84BUG=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 我叫以赏 <422880152@qq.com> --- applications/view/admin/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/view/admin/user.py b/applications/view/admin/user.py index b5af8fe..d67e0ed 100644 --- a/applications/view/admin/user.py +++ b/applications/view/admin/user.py @@ -75,7 +75,7 @@ def save(): if bool(User.query.filter_by(username=username).count()): return fail_api(msg="用户已经存在") - user = User(username=username, realname=real_name) + user = User(username=username, realname=real_name, dept_id=1) user.set_password(password) db.session.add(user) roles = Role.query.filter(Role.id.in_(role_ids)).all() -- Gitee