diff --git a/.flaskenv b/.flaskenv index 66006b5cdbb308a9fc4a89a4898d61ee8f5c06a3..f9fd78d337000e5d38a47bd3181f7108e3115122 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 458267784ee33f430d1a0dd553dbd981220d0e0d..34d4686e8aa7f3428d1439710af093a34ad1c57e 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,47 @@ 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)``` + +- [*] 更新后台框架与 pear admin layui 同步。 + +- [+] 直接将 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 数据格式的列表,请在列表中填入插件的文件夹名。**(当然你也可以在后台管理页面快速启用与禁用插件。) + +![输入图片说明](docs/assets/image.png) + +插件的编写:我们提供了一个实例插件 Helloworld。一个符合规格插件应该至少包含实例插件中的所有内容。 diff --git a/app.py b/app.py index a8a7d487eb3a3102705b0ee1649da0d910b69065..1c7159e1c0483aa406b273f01452416acdf93312 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 e24950e234f681780b3ae3f3516f3c6fccc0ced1..ec1b789494018a99fff2103ddabc8d5b2cf83eb7 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 cafec6b08990440b609232b01d9fd084f22f957d..ec1d048161f06b5777261b26065d01a534483941 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 = [ @@ -52,7 +63,8 @@ class BaseConfig: # 默认日志等级 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 0000000000000000000000000000000000000000..48191296e75d3ad80d8356a22a9a6b89495b8ed3 --- /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 0000000000000000000000000000000000000000..b658135ee9d894424dd0a51421f248690ac1d3c2 --- /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 0000000000000000000000000000000000000000..388e0f8e99450ad96e0991be1c287afd77714f6e --- /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 0000000000000000000000000000000000000000..8c734746021ca9d65d6dd03d2a21ebc77952dd05 --- /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 0000000000000000000000000000000000000000..99d275723067a25f3da080a39a43877f36bde507 --- /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 0000000000000000000000000000000000000000..a0cc28682c9eb818d05f7c1297b99e00b4d656be --- /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 0000000000000000000000000000000000000000..00688a3ccee296138f247cbc705798914f211dad --- /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 0000000000000000000000000000000000000000..110f9f70e2583f1faaf8e32a8e2c0ba8bd4631d7 --- /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 0000000000000000000000000000000000000000..d81f7a6c81103bf385cd07f1927d4f20905c9a38 --- /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 0000000000000000000000000000000000000000..e51d99342b409d4da4e5255b3653dad30a08cdb4 --- /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 0000000000000000000000000000000000000000..c062ec24acd834eb02ced5e0eca02f7dcdbdc0eb --- /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 bec798960b046575ba34748b589a3e50027eb1b0..6b5cf78b6fe07f81df34aeba4829e5a1639a64d5 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 e0e2668be2854c98ae78dad311a8fcef0054d09c..89081349743fa6ef325b081dc6f05958667501fb 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 40f1e9e54615547c4a921a2d8c8d25f0730d893f..1fdd9e9f029d71d5cf03cbe4f11db6e38b343dcf 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/admin/user.py b/applications/view/admin/user.py index b5af8febef21b9473fa5e98c56bc636677f15e46..d67e0ed6a4ce25baa8906285bba9667290c67b62 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() diff --git a/applications/view/plugin/__init__.py b/applications/view/plugin/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c9db878edc815a6614f121a851e4922433e7fb0d --- /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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/docs/assets/image.png b/docs/assets/image.png new file mode 100644 index 0000000000000000000000000000000000000000..965b87a326f799e6ed488a87e340981c49ccba2c Binary files /dev/null and b/docs/assets/image.png differ diff --git a/pear.sql b/pear.sql index 6c29bc4093c40c7d2df1ea1e2ff83d153086afd3..55ac25c536dfdb8a43b6f2c572c6325d72e51047 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 0000000000000000000000000000000000000000..110f9f70e2583f1faaf8e32a8e2c0ba8bd4631d7 --- /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 0000000000000000000000000000000000000000..d81f7a6c81103bf385cd07f1927d4f20905c9a38 --- /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 0000000000000000000000000000000000000000..e51d99342b409d4da4e5255b3653dad30a08cdb4 --- /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 0000000000000000000000000000000000000000..c062ec24acd834eb02ced5e0eca02f7dcdbdc0eb --- /dev/null +++ b/plugins/helloworld/templates/helloworld_index.html @@ -0,0 +1 @@ +

Test!

\ No newline at end of file diff --git a/requirement/dev.txt b/requirement/dev.txt index 901ad35cf5bee3bf823c585574559d7c77633fe5..de71abf120acc1c5c962b69b4e0fdd81c593ba6c 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 diff --git a/templates/admin/index.html b/templates/admin/index.html index 330044c26df046534969d014ed2b74d3e7a86951..606d7b6e73209a38ebe502beb50eee34bbbf6e09 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 3cdaf9b147a0d429522e1a3f8687a78536f52b69..86269b4616b3b1fe53218db796c10af7c4fb9016 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 19dfaf90d78c8b10b8e55613d2860ec194c2f518..14f23545ff882d2045dc292b5effc05e4d5e99a2 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 29dd4dcd12c27bdb0150dacdbe46bb2fd4f98326..71187f6da4ad4ef834057b4f94a40b1cdb121316 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