From 820ab9f5c638ab9c020cccafab7e2df63d7a5b4f Mon Sep 17 00:00:00 2001 From: zhengxinonly Date: Tue, 7 Dec 2021 20:24:07 +0800 Subject: [PATCH] =?UTF-8?q?refactor(api):=E7=A7=BB=E9=99=A4marshal?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 120 ++++++++++++++++++- applications/__init__.py | 8 +- applications/api/rights/department.py | 21 +++- applications/api/rights/rights.py | 158 ++++++++++++++++---------- applications/api/system/file.py | 15 ++- applications/api/system/passport.py | 1 + applications/models/file/photo.py | 14 --- applications/models/log.py | 14 --- applications/models/rights/power.py | 36 ------ applications/models/rights/role.py | 14 --- applications/models/users/dept.py | 4 + applications/models/users/users.py | 20 +--- applications/view/logs_view.py | 29 ++++- 13 files changed, 282 insertions(+), 172 deletions(-) diff --git a/README.md b/README.md index 5c73746..d643681 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Pear Admin Flask 基于 Flask 生态的后台管理系统,该项目旨在为 p + [flask](https://dormousehole.readthedocs.io/en/latest/) + [flask-login](https://flask-login.readthedocs.io/en/latest/) + [flask-sqlalchemy](http://www.pythondoc.com/flask-sqlalchemy/quickstart.html) -+ [flask-restful](https://flask-restful.readthedocs.io/en/latest/) ++ [flask-restful](https://flask-restful.readthedocs.io/en/latest/) ## 预览 @@ -50,9 +50,9 @@ Pear Admin Flask 有以下几个版本: >flask 2.x + flask-sqlalchemy + 权限验证 + Flask-APScheduler 定时任务 + marshmallow 序列化与数据验证 -[Mini 分支版本 ](https://gitee.com/pear-admin/pear-admin-flask/tree/mini/) +[Mini 分支版本 ](https://gitee.com/pear-admin/pear-admin-flask/tree/mini/) ->flask 2.x + flask-sqlalchemy + 权限验证 + Flask-restful 序列化与数据验证 +>flask 2.x + flask-sqlalchemy + Flask-restful + 基于角色的权限管理 | | | |---------------------|---------------------| @@ -89,8 +89,9 @@ git checkout mini # 创建虚拟环境 python -m venv venv -# 然后使虚拟环境生效(windows,Linux自行解决) +# 然后使虚拟环境生效(windows) venv\Scripts\activate +# source venv/bin/activate # (Linux激活虚拟环境) # 安装依赖 pip install -r requirement\requirement-dev.txt @@ -115,16 +116,60 @@ flask init-db ## 服务器部署 ### wsgi -采用 gunicorn ,配置文件请查看 `gunicorn.conf.py` 。暂时未使用 nginx 作为反向代理,需要的可以自行配置。 +默认的 flask 程序启动一个应用,一个应用只能用于测试环境下。生成环境采用 gunicorn 开启多进行+多线程启动程序,可以提升程序的并发量。详细配置可以查看 gunicorn 的官方文档,本项目的配置文件请查看 `gunicorn.conf.py` 。 + +```python +# filename: gunicorn.conf.py +import os +import multiprocessing + +bind = '0.0.0.0:8000' # 默认部署地址,如果用了nginx的反向代理,建议改成 127.0.0.1:8000 +backlog = 512 +chdir = os.path.dirname(os.path.abspath(__file__)) +timeout = 30 +worker_class = 'sync' + +workers = multiprocessing.cpu_count() * 2 + 1 # 开启进程数为 CPU核心数 * 2 + 1 +threads = 2 # 每个进程开启两个线程 +loglevel = 'info' # 日志的等级 +access_log_format = '%(t)s %(p)s %(h)s "%(r)s" %(s)s %(L)s %(b)s %(f)s" "%(a)s"' +# 日志存放位置 +if not os.path.exists('logs'): + os.mkdir('logs') + +accesslog = os.path.join(chdir, "logs/gunicorn_access.log") +errorlog = os.path.join(chdir, "logs/gunicorn_error.log") + +``` + +因为是在服务器部署,而且一般做服务器部署的时候都会采用虚拟环境 + 命令行启动。为了能在命令行模式下从虚拟环境启动程序,所以需要专门编写一个 shell 文件进行启动。详细看看 `start.sh` + +```shell +cd /home/ubuntu/pear-admin-flask # 进入到项目的根目录 +source venv/bin/activate # 激活虚拟环境 +exec gunicorn -c gunicorn.conf.py "applications:create_app('development')" # 运行 gunicorn 指令启动程序 +``` + +注意:如果使用的是全局环境,就可以不用激活虚拟环境。 + + ### 守护进程 + +程序在服务器环境下,一般是 7*24 小时不间断运行。有时候服务器会因为一些特殊原因宕机自动重启,或者是晚上定时重启电脑让内存处于最佳状态下。在这种情况下如果想要只要是电脑在正常运行,程序就提供服务,就需要开启守护进程。 + + + 使用下面的命令安装Supervisor: ```shell script $ sudo apt install supervisor ``` 编辑配置文件 + +```shell $ sudo vim /etc/supervisor/conf.d/pear.conf +``` 写入项目配置 ```shell script @@ -141,3 +186,68 @@ killasgroup=true ```shell script $ sudo supervisorctl ``` + + + +注意:关于 supervisor 的具体功法参考 [官方文档](http://supervisord.org/index.html) + + + +### Nginx + +gunicorn 虽然提供了 Web服务器的功能,但是功能比较局限,拓展性也不强。而 Nginx 就能很好的弥补这部分的不足。所以在服务器一般采用 Nginx + gunicorn 的模式做项目部署。 + +Nginx 是一个高性能的 HTTP 和 反向代理 web服务器。在后期的项目性能优化中,可以提供非常多的帮助。例如可以对返回的网页、静态文件进行压缩提升页面的加载速度,当性能达到瓶颈之后可以通过负载均衡让后端直接可以通过加机器解决问题。 + + + +使用下面的命令安装Nginx + +```shell +$ sudo apt install nginx +``` + +可以直接在 Nginx 的默认配置文件(/etc/nginx/nginx.conf)中写入程序配置,但通常情况下,为了便于组织,我们可以在/etc/nginx/sites-enabled/或是/etc/nginx/conf.d/目录下为我们的Flask程序创建单独的Nginx配置文件。 + +```shell +$ sudo rm /etc/nginx/sites-enabled/default # 删除默认的示例 +$ sudo vi /etc/nginx/sites-enabled/pear # 添加自己的示例 +``` + + + +> /etc/nginx/sites-enabled/pear + +```shell +server { + listen 80 default_server; # 监听 80 端口 + # server_name example.com; # 域名解析 + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + location / { + proxy_pass http://127.0.0.1:8000; # 转发的地址,即Gunicorn运行的地址 + proxy_redirect off; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +更新配置文件后,我们可以通过下面的命令来测试语法正确性: + +```shell +$ sudo nginx -t +``` + +如果一切正常,那么现在可以重启Nginx让配置生效: + +```shell +$ sudo service nginx restart +``` + +当使用反向代理服务器后,Gunicorn不需要再监听外部请求,而是直接监听本地机的某个端口。我们可以使用默认值,即本地机的8000端口,默认绑定 `127.0.0.1:8000` 。 + diff --git a/applications/__init__.py b/applications/__init__.py index 3d4918c..0a8c586 100644 --- a/applications/__init__.py +++ b/applications/__init__.py @@ -21,16 +21,22 @@ def create_app(config_name=None): # 引入数据库配置 app.config.from_object(common) app.config.from_object(config[config_name]) + # 注册各种插件 init_plugs(app) # 注册路由 init_view(app) + + # 注册接口(restful api) init_api(app) + # 文件上传 configure_uploads(app, photos) - logo() + if os.environ.get('WERKZEUG_RUN_MAIN') == 'true': + logo() + return app diff --git a/applications/api/rights/department.py b/applications/api/rights/department.py index 5db8901..f2d63f6 100644 --- a/applications/api/rights/department.py +++ b/applications/api/rights/department.py @@ -1,6 +1,5 @@ from flask import jsonify -from flask_restful import Resource -from flask_restful import marshal, reqparse +from flask_restful import Resource, reqparse from applications.common.utils.http import success_api, fail_api from applications.extensions import db @@ -14,9 +13,23 @@ class DepartmentsResource(Resource): # TODO dtree 需要返回状态信息 res = { "status": {"code": 200, "message": "默认"}, - "data": marshal(dept_data, CompanyDepartment.fields()) + "data": [ + + { + 'deptId': item.id, + 'parentId': item.parent_id, + 'deptName': item.dept_name, + 'sort': item.sort, + 'leader': item.leader, + 'phone': item.phone, + 'email': item.email, + 'status': item.status, + 'comment': item.comment, + 'address': item.address, + 'create_at': item.create_at.strftime('%Y-%m-%d %H:%M:%S') + } for item in dept_data + ] } - print(dept_data) return jsonify(res) def post(self): diff --git a/applications/api/rights/rights.py b/applications/api/rights/rights.py index d0237b5..c2f0c82 100644 --- a/applications/api/rights/rights.py +++ b/applications/api/rights/rights.py @@ -3,7 +3,7 @@ from collections import OrderedDict from flask import request, jsonify, current_app from flask_login import current_user -from flask_restful import Resource, reqparse, marshal +from flask_restful import Resource, reqparse from applications.common.utils.http import success_api, fail_api from applications.extensions import db @@ -12,58 +12,61 @@ from applications.models import RightsPower, RightsRole def get_render_config(): # 网站配置 - config = dict(logo={ - # 网站名称 - "title": current_app.config.get("SYSTEM_NAME"), - # 网站图标 - "image": "/static/admin/admin/images/logo.png" - # 菜单配置 - }, menu={ - # 菜单数据来源 - "data": "/api/v1/rights/menu", - "collaspe": True, - # 是否同时只打开一个菜单目录 - "accordion": True, - "method": "GET", - # 是否开启多系统菜单模式 - "control": False, - # 默认选中的菜单项 - "select": "0", - # 是否开启异步菜单,false 时 data 属性设置为菜单数据,false 时为 json 文件或后端接口 - "async": True - }, tab={ - # 是否开启多选项卡 - "muiltTab": True, - # 切换选项卡时,是否刷新页面状态 - "keepState": True, - # 是否开启 Tab 记忆 - "session": True, - # 最大可打开的选项卡数量 - "tabMax": 30, - "index": { - # 标识 ID , 建议与菜单项中的 ID 一致 - "id": "10", - # 页面地址 - "href": "/admin/welcome", - # 标题 - "title": "首页" - } - }, theme={ - # 默认主题色,对应 colors 配置中的 ID 标识 - "defaultColor": "2", - # 默认的菜单主题 dark-theme 黑 / light-theme 白 - "defaultMenu": "dark-theme", - # 是否允许用户切换主题,false 时关闭自定义主题面板 - "allowCustom": True - }, colors=[{ - "id": "1", - "color": "#2d8cf0" - }, - { + config = { + 'logo': { + # 网站名称 + "title": current_app.config.get("SYSTEM_NAME"), + # 网站图标 + "image": "/static/admin/admin/images/logo.png" + # 菜单配置 + }, + 'menu': { + # 菜单数据来源 + "data": "/api/v1/rights/menu", + "collaspe": True, + # 是否同时只打开一个菜单目录 + "accordion": True, + "method": "GET", + # 是否开启多系统菜单模式 + "control": False, + # 默认选中的菜单项 + "select": "0", + # 是否开启异步菜单,false 时 data 属性设置为菜单数据,false 时为 json 文件或后端接口 + "async": True + }, + 'tab': { + # 是否开启多选项卡 + "muiltTab": True, + # 切换选项卡时,是否刷新页面状态 + "keepState": True, + # 是否开启 Tab 记忆 + "session": True, + # 最大可打开的选项卡数量 + "tabMax": 30, + "index": { + # 标识 ID , 建议与菜单项中的 ID 一致 + "id": "10", + # 页面地址 + "href": "/admin/welcome", + # 标题 + "title": "首页" + } + }, + 'theme': { + # 默认主题色,对应 colors 配置中的 ID 标识 + "defaultColor": "2", + # 默认的菜单主题 dark-theme 黑 / light-theme 白 + "defaultMenu": "dark-theme", + # 是否允许用户切换主题,false 时关闭自定义主题面板 + "allowCustom": True + }, + 'colors': [{ + "id": "1", + "color": "#2d8cf0" + }, { "id": "2", "color": "#5FB878" - }, - { + }, { "id": "3", "color": "#1E9FFF" }, { @@ -72,13 +75,16 @@ def get_render_config(): }, { "id": "5", "color": "darkgray" - } - ], links=current_app.config.get("SYSTEM_PANEL_LINKS"), other={ - # 主页动画时长 - "keepLoad": 1200, - # 布局顶部主题 - "autoHead": False - }, header=False) + }], + 'links': current_app.config.get("SYSTEM_PANEL_LINKS"), + 'other': { + # 主页动画时长 + "keepLoad": 1200, + # 布局顶部主题 + "autoHead": False + }, + 'header': False + } return config @@ -99,7 +105,23 @@ def make_menu_tree(): if p.type == 0 or p.type == 1: powers.append(p) - power_dict = marshal(powers, RightsPower.fields2()) # 生成可序列化对象 + # power_dict = marshal(powers, RightsPower.fields2()) # 生成可序列化对象 + power_dict = [ + { + 'id': item.id, + 'title': item.name, + 'type': item.type, + 'code': item.code, + 'href': item.url, + 'openType': item.open_type, + 'parent_id': item.parent_id, + 'icon': item.icon, + 'sort': item.sort, + 'enable': item.enable, + 'update_at': item.update_at.strftime('%Y-%m-%d %H:%M:%S'), + 'create_at': item.create_at.strftime('%Y-%m-%d %H:%M:%S'), + } for item in powers + ] power_dict.sort(key=lambda x: x['id'], reverse=True) menu_dict = OrderedDict() @@ -156,12 +178,26 @@ class RightRightsResource(Resource): """获取选择父节点""" power = RightsPower.query.all() - power_data = marshal(power, RightsPower.fields()) + # power_data = marshal(power, RightsPower.fields()) + power_data = [ + { + 'powerId': item.id, + 'powerName': item.name, + 'powerType': item.type, + 'powerUrl': item.url, + 'openType': item.open_type, + 'parentId': item.parent_id, + 'icon': item.icon, + 'sort': item.sort, + 'create_at': item.create_at.strftime('%Y-%m-%d %H:%M:%S'), + 'update_at': item.update_at.strftime('%Y-%m-%d %H:%M:%S'), + 'enable': item.enable, + } for item in power + ] power_data.append({"powerId": 0, "powerName": "顶级权限", "parentId": -1}) res = { "status": {"code": 200, "message": "默认"}, "data": power_data - } return res diff --git a/applications/api/system/file.py b/applications/api/system/file.py index 5ec5495..d23aaba 100644 --- a/applications/api/system/file.py +++ b/applications/api/system/file.py @@ -1,7 +1,7 @@ import os from flask import request, jsonify, current_app -from flask_restful import Resource, marshal +from flask_restful import Resource from sqlalchemy import desc from applications.common.utils.http import fail_api, success_api, table_api @@ -19,7 +19,18 @@ class FilePhotosResource(Resource): ).paginate(page=page, per_page=limit, error_out=False) - data = marshal(photo_paginate.items, FilePhoto.fields()) + # data = marshal(photo_paginate.items, FilePhoto.fields()) + data = [ + { + 'id': item.id, + 'name': item.name, + 'href': item.href, + 'mime': item.mime, + 'size': item.size, + 'ext': item.ext, + 'create_at': item.create_at, + } for item in photo_paginate.items + ] return table_api(result={'items': data, 'total': photo_paginate.total, }, code=0) diff --git a/applications/api/system/passport.py b/applications/api/system/passport.py index 0781b1f..b2cf63b 100644 --- a/applications/api/system/passport.py +++ b/applications/api/system/passport.py @@ -24,6 +24,7 @@ class LoginResource(Resource): req = login_req.parse_args() s_code = session.get("code", None) + session["code"] = None if req.captcha != s_code: return fail_api(message="验证码错误") diff --git a/applications/models/file/photo.py b/applications/models/file/photo.py index 814165d..cb685fb 100644 --- a/applications/models/file/photo.py +++ b/applications/models/file/photo.py @@ -1,5 +1,3 @@ -from flask_restful import fields - from applications.extensions import db from ..base import BaseModel @@ -12,15 +10,3 @@ class FilePhoto(db.Model, BaseModel): href = db.Column(db.String(255)) mime = db.Column(db.CHAR(50), nullable=False) size = db.Column(db.CHAR(30), nullable=False) - - @staticmethod - def fields(): - return { - 'id': fields.Integer, - 'name': fields.String, - 'href': fields.String, - 'mime': fields.String, - 'size': fields.String, - 'ext': fields.String, - 'create_at': fields.DateTime, - } diff --git a/applications/models/log.py b/applications/models/log.py index 52d6eaa..fab1952 100644 --- a/applications/models/log.py +++ b/applications/models/log.py @@ -14,17 +14,3 @@ class LoggingModel(db.Model, BaseModel): ip = db.Column(db.String(255)) success = db.Column(db.Boolean, default=True) user_agent = db.Column(db.Text) - - @staticmethod - def fields(): - return { - 'id': fields.Integer, - 'method': fields.String, - 'uid': fields.String, - 'url': fields.Url, - 'desc': fields.String, - 'ip': fields.String, - 'success': fields.Boolean, - 'user_agent': fields.String, - 'create_at': fields.DateTime, - } diff --git a/applications/models/rights/power.py b/applications/models/rights/power.py index 1dc71fe..7b8f015 100644 --- a/applications/models/rights/power.py +++ b/applications/models/rights/power.py @@ -1,5 +1,3 @@ -from flask_restful import fields - from applications.extensions import db from ..base import BaseModel @@ -18,37 +16,3 @@ class RightsPower(db.Model, BaseModel): enable = db.Column(db.Boolean, comment='是否开启') parent = db.relationship("RightsPower", remote_side=[id]) # 自关联 - - @staticmethod - def fields(): - return { - 'powerId': fields.String(attribute="id"), - 'powerName': fields.String(attribute="name"), - 'powerType': fields.String(attribute="type"), - 'powerUrl': fields.String(attribute="url"), - 'openType': fields.String(attribute="open_type"), - 'parentId': fields.String(attribute="parent_id"), - 'icon': fields.String, - 'sort': fields.Integer, - 'create_at': fields.DateTime, - 'update_at': fields.DateTime, - 'enable': fields.Integer, - } - - @staticmethod - def fields2(): - return { - 'id': fields.Integer, - 'title': fields.String(attribute="name"), - 'type': fields.String, - 'code': fields.String, - 'href': fields.String(attribute="url"), - 'openType': fields.String(attribute="open_type"), - 'parent_id': fields.Integer, - 'icon': fields.String, - 'sort': fields.Integer, - 'enable': fields.Boolean, - 'update_at': fields.DateTime, - 'create_at': fields.DateTime, - - } diff --git a/applications/models/rights/role.py b/applications/models/rights/role.py index 8d9dff5..0e2224f 100644 --- a/applications/models/rights/role.py +++ b/applications/models/rights/role.py @@ -15,17 +15,3 @@ class RightsRole(db.Model, BaseModel): sort = db.Column(db.Integer, comment='排序') power = db.relationship('RightsPower', secondary="rt_role_power", backref=db.backref('role')) - - @staticmethod - def fields(): - return { - 'id': fields.Integer, - 'roleName': fields.String(attribute="name"), - 'roleCode': fields.String(attribute="code"), - 'enable': fields.Boolean, - 'comment': fields.String, - 'details': fields.String, - 'sort': fields.Integer, - 'create_at': fields.DateTime, - 'update_at': fields.DateTime, - } diff --git a/applications/models/users/dept.py b/applications/models/users/dept.py index c64f4cc..c7b9409 100644 --- a/applications/models/users/dept.py +++ b/applications/models/users/dept.py @@ -18,6 +18,10 @@ class CompanyDepartment(db.Model, BaseModel): @staticmethod def fields(): + """ + 定义模型的常用输出字段,新手请忽略。可以简化字段序列化操作, + 详细操作请查看 flask-restful marshal 的用法 + """ return { 'deptId': fields.Integer(attribute="id"), 'parentId': fields.Integer(attribute="parent_id"), diff --git a/applications/models/users/users.py b/applications/models/users/users.py index a362f86..74711ab 100644 --- a/applications/models/users/users.py +++ b/applications/models/users/users.py @@ -21,25 +21,9 @@ class CompanyUser(db.Model, UserMixin, BaseModel): role = db.relationship('RightsRole', secondary="rt_user_role", backref=db.backref('user'), lazy='dynamic') def set_password(self, password): + """设置密码,对密码进行加密存储""" self.password_hash = generate_password_hash(password) def validate_password(self, password): + """校验密码方法""" return check_password_hash(self.password_hash, password) - - @staticmethod - def fields(): - return { - 'id': fields.Integer, - 'username': fields.String, - 'realname': fields.String, - 'mobile': fields.String, - 'avatar': fields.Url, - 'comment': fields.String, - # 'password_hash': fields.String, - 'enable': fields.Boolean, - # 'dept_id': fields.Integer, - 'dept_id': fields.Integer, - 'create_at': fields.DateTime, - 'update_at': fields.DateTime, - } - diff --git a/applications/view/logs_view.py b/applications/view/logs_view.py index 5b26311..7e544b4 100644 --- a/applications/view/logs_view.py +++ b/applications/view/logs_view.py @@ -1,6 +1,5 @@ from flask import Blueprint, request, render_template from sqlalchemy import desc -from flask_restful import marshal from applications.common.utils.http import table_api from applications.common.utils.rights import permission_required @@ -24,7 +23,19 @@ def login_log(): url='/api/v1/passport/login').order_by( desc(LoggingModel.create_at)).paginate( page=page, per_page=limit, error_out=False) - data = marshal(log_paginate.items, LoggingModel.fields()) + data = [ + { + 'id': item.id, + 'method': item.method, + 'uid': item.uid, + 'url': item.url, + 'desc': item.desc, + 'ip': item.ip, + 'success': item.success, + 'user_agent': item.user_agent, + 'create_at': item.create_at.strftime('%Y-%m-%d %H:%M:%S'), + } for item in log_paginate.items + ] return table_api(result={'items': data, 'total': log_paginate.total, }, @@ -40,7 +51,19 @@ def operate_log(): LoggingModel.url != '/api/v1/passport/login').order_by( desc(LoggingModel.create_at)).paginate( page=page, per_page=limit, error_out=False) - data = marshal(log_paginate.items, LoggingModel.fields()) + data = [ + { + 'id': item.id, + 'method': item.method, + 'uid': item.uid, + 'url': item.url, + 'desc': item.desc, + 'ip': item.ip, + 'success': item.success, + 'user_agent': item.user_agent, + 'create_at': item.create_at.strftime('%Y-%m-%d %H:%M:%S'), + } for item in log_paginate.items + ] return table_api(result={'items': data, 'total': log_paginate.total, }, code=0)