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 |---------------------|---------------------| |  |  | | |  | -|  |  | \ No newline at end of file +|  |  | + + +#### 此 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 @@ +