Flask-基础框架搭建
说明文档
flask相关
基于restful风格的开发基础环境包
基础依赖包
- Flask # 主框架
- PyMySQL # SQLAlchemy数据库驱动 5.7k星星(MySQLdb已不支持python3)
- Flask-SQLAlchemy # 数据库ORM
- Flask-Migrate # 数据库迁移软件
- Flask-Cors # 跨域处理
- Flask-RESTful # RESTful风格,不要使用flask-restplus(已停止维护)
- flask-swagger # api接口调试以及文档生成
- flask-authz # 基于PyCasbin的中间件(一个轻量级的访问控制框架),授权控制:authorization
- casbin-sqlalchemy-adapter # PyCasbin的数据库适配器
flask-security # This project is non maintained anymore. Consider the Flask-Security-Too project as an alternative.- Flask-Security-Too # 身份认证:authentication,需要单独安装依赖包:pip install bcrypt
结构规划
创建app目录
# 主目录
mkdir app
# 移动框架自动生成的目录(static和templates),没有的话则创建
mv static templates app/
# 缓存文件
mkdir tmp
初始化脚本
# vim app/__init__.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# 加载主框架
from flask import Flask
# 加载扩展库
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_cors import CORS
from flask_restful import Api
# 实例化
db = SQLAlchemy()
cors = CORS()
migrate = Migrate()
# 声明app方法
def create_app(config_name):
.....
改名app.py文件run.py
因为上面的目录也为app,会导致冲突
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import os
from app import create_app
# 初始化app
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
运行
声明环境方式
# 先声明环境
# linux或者MINGW64:
export FLASK_APP=app.py
export FLASK_ENV=development
# winwows:
set FLASK_APP=app.py
set FLASK_ENV=development
# 运行
python.exe -m flask run
环境变量设置方式
# Flask的自动发现程序实例机制还有第三条规则:如果安装了python-dotenv,那么在使用flask run或其它命令时
# 会使用它自动从.flaskenv文件和.env文件中加载环境变量
# 当安装了python-dotenv时,Flask在加载环境变量的优先级是:手动设置的环境变量>.env中设置的环境变>.flaskenv设置的环境变量
# 安装模块
pip install python-dotenv
# 编辑配置文件
vim .flaskenv
FLASK_APP=run.py
# 运行
flask run
flask-security token获取
# 配置文件关闭
WTF_CSRF_ENABLED = False
https://www.jianshu.com/p/882070fad1fb?_ad0.08689877468409224
核心使用auth_token的话,需要设置:Headers: Content-Type application/json
postman使用
- get请求http://localhost:5000/login,获取csrf_token的值
- posthttp://localhost:5000/login,添加header->Content-Type:application/json,添加Body->row->{“email”:“admin@qq.com”, “password”:“admin”, “csrf_token”: “值”}
curl使用(无法使用csrf_token)
# 1、获取csrf_token值
curl http://localhost:5000/login | grep csrf_token
# 2、获取数据
curl -X POST -H "Content-Type: application/json" -d '{"email":"admin@qq.com", "password":"admin", "csrf_token": "csrf_token值"}' http://localhost:5000/login
token测试
# 没有权限访问
curl http://localhost:5000/api/v1/todos/todo3
# 正常访问
curl -H "Authentication-Token: token值" http://localhost:5000/api/v1/todos/todo3
swagger
https://www.cnblogs.com/tianshifu/p/7156563.html
https://segmentfault.com/a/1190000010144742
token生成原理
https://blog.csdn.net/chenchong_88/article/details/78013332
添加账号
# shell操作
flask shell
# 创建管理员
admin = User(username='zaza', nick_name='张小强', sex='男', phone=18712345678, email='admin@qq.com', password='admin', active=1)
# db.session.add(admin)
# db.session.commit()
# 创建普通用户角色和Admin角色
user_role = ud.create_role(name='User', remark='Generic user role')
admin_role = ud.create_role(name='Admin', remark='Admin user role')
# 为admin添加Admin角色(admin_role)
ud.add_role_to_user(admin, admin_role)
db.create_all()
db.session.commit()
# 单独添加
user = ud.find_user(email="admin@qq.com")
role = ud.find_role("User")
ud.add_role_to_user(user, role)
db.create_all()
db.session.commit()
sqlalchemy
数据库命令转换:http://www.leeladharan.com/sqlalchemy-query-with-or-and-like-common-filters
数据库
参考文档:
外键
# 外键定义:在字段后面加上Foreignkey(主表.主键)
# 定义主键时往往还会定义一个relationship
company_name = Column(String(32),ForeignKey("company.name"))
# relationship主要用于查询外键关联的具体数据
company = relationship("Company",backref="phone_of_company")
# ForeignKey:指示应将此列中的值限制为指定的远程列中存在的值
user_id = Column(Integer, ForeignKey('users.id'))
# relationship定义的不是字段,数据库没有相关的字段,实际上是
# company = relationship("Company",backref="phone_of_company")
# 第一个参数:代表关联的类名称,正向查询:phoen_obj.Company查询到company中外键关联的数据
back_populates:反向关联的名称,反向查询:company_obj.phone_of_company查询到phone表的外键关联数据
# backref:老版本参数,新版本用back_populates替代
secondary:指定了外键的的中间表,主要用于多对多
外键示例
# 多对多关联表的数据
roles_users = db.Table(
'sys_role_user',
db.Column('sys_user_id', db.Integer(), db.ForeignKey('sys_user.id')),
db.Column('sys_role_id', db.Integer(), db.ForeignKey('sys_role.id'))
)
# use的外键多对多声明
class UserModel(db.Model, UserMixin, TimestampMixin):
# roles属性不是sql的字段,只是一个关联属性
# secondary:多对多的中间表
# backref:反向查询关联属性名称
# lazy:dynamic,生成的是查询对象
# backref中声明意味着:在方向查询中使用dynamic
roles = db.relationship('RoleModel', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
# 正向查询
user_obj = db.session.query(UserModel).filter_by(id = 1).first()
user_obj.roles
user_obj.roles[0].id
user_obj.roles[0].name
# 反向查询,管理字段是users
role_obj = db.session.query(RoleModel).filter_by(id = 2).first()
# 一对多查询
# lazy='dynamic'代表生成的是查询对象
# role_obj.users
# 查看执行的sql:print(role_obj.users)
# 查看数据:
role_obj.users.all()
role_obj.users.all()[0].nickname
# 用户增加角色
# 通过提交的角色id查出角色对象列表
role_obj = db.session.query(RoleModel).filter(RoleModel.id.in_([1,2,3])).all()
# 查出用户
user_obj = db.session.query(UserModel).filter_by(id = 1).first()
# all生成的是对象“列表”,first是列表的第一个“对象”
# 相关用户增加单角色对象则使用append
# user_obj.roles.append(role_obj)
# 用户添加多角色对象列表则使用extend,相关id存在的话也会插入数据
user_obj.roles.extend(role_obj)
# 提交数据
db.session.commit()
# 查看帮助文档,主要查看每个方法的使用说明
help(user_obj.roles)
# 删除单个对象
user_obj.roles.remove(role_obj)
# 删除多个对象
# 没有相关方法
# 删除所有关联的外键数据
user_obj.roles.clear()
# 更新数据,通过web提交的数据,数据库不存在的则增加,多余的则删除
# 实现方案:删除所有数据并新增添加的数据
user_obj.roles.clear()
user_obj.roles.extend(role_obj)
# 通过外键查询数据(role是relationship)
# db.session.query(MenuModel).filter(MenuModel.role.any(id=_id)).filter(MenuModel.type.in_(('M', 'C'))).all()
db.session.query(MenuModel).filter(MenuModel.role.any(id=_id)).all()
# 查询独立字段
db.session.query(MenuModel.id).filter(MenuModel.role.any(id=_id)).all()
# 删除权限字段
del_rule = db.session.query(CasbinRule).filter(CasbinRule.v0 == 'zaza')
del_rule.delete()
结构同步
- 第一次初始化目录:flask db init
- 生成变更代码:flask db migrate -m “初始化”
- 同步变更到数据库:flask db upgrade
- 初始化基础数据:python init_data.py
注意事项:https://stackoverflow.com/questions/48762191/why-do-i-keep-getting-importerror-cannot-import-name-db/48762342
flask db init;flask db migrate -m "初始化";flask db upgrade
SET foreign_key_checks = 0;
truncate table sys_menu;
SET foreign_key_checks = 1;
常见字段
status的值含义
# 用bool值来理解就行了,所有通常0代表禁用,1代表激活
<select name="status">
<option value="1">Active</option>
<option value="0">Inactive</option>
</select>
>>> bool(0)
False
>>> bool(1)
True
python上下文
- https://www.cnblogs.com/wangyanyan/p/11231599.html
- https://www.liaoxuefeng.com/wiki/1016959663602400/1115615597164000
数据库结构说明
casbin_rule
CREATE TABLE `casbin_rule` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ptype` varchar(255) DEFAULT NULL,
`v0` varchar(255) DEFAULT NULL,
`v1` varchar(255) DEFAULT NULL,
`v2` varchar(255) DEFAULT NULL,
`v3` varchar(255) DEFAULT NULL,
`v4` varchar(255) DEFAULT NULL,
`v5` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
# 字段说明:目前只使用到ptype,v0,v1,v2
ptype v0 v1 v2 v3 v4 v5
p/g
p, yaya, /dataset2/resource2, GET
p, superAdmin, project, read
g, zaza, superAdmin
无极菜单
后端实现原理
# https://stackoverflow.com/questions/8191683/building-a-menu-tree-from-an-adjacency-list-in-python
data = [
{ 'id': 1, 'parent_id': 2, 'name': "Node1" },
{ 'id': 2, 'parent_id': 5, 'name': "Node2" },
{ 'id': 3, 'parent_id': 0, 'name': "Node3" },
{ 'id': 4, 'parent_id': 5, 'name': "Node4" },
{ 'id': 5, 'parent_id': 0, 'name': "Node5" },
{ 'id': 6, 'parent_id': 3, 'name': "Node6" },
{ 'id': 7, 'parent_id': 3, 'name': "Node7" },
{ 'id': 8, 'parent_id': 0, 'name': "Node8" },
{ 'id': 9, 'parent_id': 1, 'name': "Node9" }
]
def list_to_tree(data):
out = {
'root': { 'id': 0, 'parent_id': 0, 'name': "Root node", 'sub': [] }
}
for p in data:
pid = p['parent_id'] or 'root'
out.setdefault(pid, { 'sub': [] })
out.setdefault(p['id'], { 'sub': [] })
out[p['id']].update(p)
out[pid]['sub'].append(out[p['id']])
return out['root']
# 查看详细生成流程
def list_to_tree(data):
out = {
'root': {'id': 0, 'parent_id': 0, 'name': "Root node", 'sub': []}
}
for p in data:
print('---{0}---'.format(p))
# 获取父id值
pid = p['parent_id'] or 'root'
# 设置父id对象
print('1、设置父id:{0}对象'.format(pid))
out.setdefault(pid, {'sub': []})
pprint.pprint(out)
# 设置当前id对象
print('2、设置当前id:{0}对象'.format(p['id']))
out.setdefault(p['id'], {'sub': []})
pprint.pprint(out)
# 将p对象更新到当前id对象上
print('3、更新当前id:{0}对象'.format(p['id']))
out[p['id']].update(p)
pprint.pprint(out)
# 将当前id对象的追加到父对象的sub下面
print('4、当前对象追加到父对象的sub下面')
out[pid]['sub'].append(out[p['id']])
pprint.pprint(out)
print('最后的结果')
return out['root']
# vue-element-admin 后台动态加载菜单
https://studygolang.com/articles/26932?fr=sidebar
https://stackoverflow.com/questions/24779093/query-self-referential-list-relationship-to-retrieve-several-level-child
后端实现
# model
def serialize(self):
"""
实现children嵌套即可,后端会解析数据的
"""
return {
'id': self.id,
'parent_id': self.parent_id,
'title': self.title,
'menu_name': self.menu_name,
'icon': self.icon,
'path': self.path,
'paths': self.paths,
'component': self.component,
'redirect': ''
}
# view
def get(self):
# todo:基于角色返回数据
menu = MenuModel.query.all()
out = {
'root': {
'id': 0,
'parent_id': 0,
'children': []}
}
for m in menu:
p = m.serialize()
pid = p['parent_id'] or 'root'
out.setdefault(pid, {'children': []})
out.setdefault(p['id'], {'children': []})
out[p['id']].update(p)
out[pid]['children'].append(out[p['id']])
return jsonify(response=out['root']['children'])
前端实现
export function generaMenu(routes, data) {
data.forEach(item => {
const menu = {
path: item.path,
component: item.component === 'Layout' ? Layout : loadView(item.component),
hidden: item.visible !== '0',
children: [],
name: item.menuName,
meta: {
title: item.title,
icon: item.icon,
noCache: true
}
}
if (item.children) {
generaMenu(menu.children, item.children)
}
routes.push(menu)
})
}
分页处理
# id连续分页
SELECT * FROM table WHERE id BETWEEN 1000000 AND 1000010;
# id不连续分页
SELECT * FROM table WHERE id IN(10000, 100000, 1000000...);
# mysql id不是连续数据排序分页优化方法
select ... from
tab a
inner join
(select lastid from tab where ... order by date limit 10000,10) b
on a.lastid=b.lastid;
派生表b建个覆盖索引,试试效率;
lastid如果不是主键,也要建个索引。
api设计规范
入门:
https://zhuanlan.zhihu.com/p/34289466
https://www.ruanyifeng.com/blog/2018/10/restful-api-best-practices.html
此规范同时适用: RPC API 和 HTTP REST API
https://cloud.google.com/apis/design/resources
微软规范:
https://github.com/onlyfor2/Microsoft-REST-API-Guidelines-Chinese
设计流程
设计指南建议在设计面向资源的 API 时采取以下步骤(更多细节将在下面的特定部分中介绍):
- 确定 API 提供的资源类型。
- 确定资源之间的关系。
- 根据类型和关系确定资源名称方案。
- 确定资源架构。
- 将最小的方法集附加到资源。
gmail为示例
用户集合:users/*。每个用户都拥有以下资源。
消息集合:users/*/messages/*。
线程集合:users/*/threads/*。
标签集合:users/*/labels/*。
变更历史记录集合:users/*/history/*。
表示用户个人资料的资源:users/*/profile。
表示用户设置的资源:users/*/settings。
资源操作
# 获取ticket列表
GET /tickets
# 查看某个具体的ticket
GET /tickets/12
# 新建一个ticket
POST /tickets
# 更新ticket 12
PUT /tickets/12
# 删除ticekt 12
DELETE /tickets/12
url命名规则
# 使用spinal-case规则
# 关键字:
# 驼峰命名法:CamelCase
# 蛇形命名法:snake_case,也称为下划线命名法:(UnderScoreCase),
# 脊柱命名法:spinal-case
# So go with the option two or three. Most of the companies like google, PayPal & other big companies are using spinal-case for the url.
# It is recommended to use the spinal-case (which is highlighted by RFC3986), this case is used by Google, PayPal, and other big companies.
# questions/41595/what-is-the-casing-convention-for-url-routes
Double-click this in Chrome: camelCase
Double-click this in Chrome: under_score
Double-click this in Chrome: hyphen-ated
post的json数据命名规范
# 这个目前没有明确的规范,看个人喜好吧,主要是前后端都保持统一就行了...
建议下划线,mysql字段名大小写不敏感的,字段名一般是下划线分隔的,如果json的key对应数据库的字段,那么就比较好对应了。当然其实无所谓了,就像变量命名,有用驼峰的,也有下划线的。
不过下划线用的比较多,题主也可以关注一下google/facebook/github...这些公司开放的 API 全部都是使用下划线分隔单词的。
# 本人建议:
# Flask-RESTful关联的是mysql的话,建议使用下划线分隔,和后端/数据库匹配即可
# java(bean)开发的话,应该用驼峰,方便和对象对应
请求参数格式
# 同json数据key格式
http://api.twitter.com/1/statuses/home_timeline.json?since_id=12345&max_id=5432
响应数据格式
# 成功
{
"code": 200,
"message": "success",
"data": {
"name": "小明",
"age": 16,
"sex": 0
}
}
# 失败
{
"code": 401,
"message": "error message",
"data": null
}
标准参数
# https://cloud.google.com/apis/design/standard_fields
name string name 字段应包含相对资源名称。
parent string 对于资源定义和 List/Create 请求,parent 字段应包含父级相对资源名称。
create_time Timestamp 创建实体的时间戳。
update_time Timestamp 最后更新实体的时间戳。注意:执行 create/patch/delete 操作时会更新 update_time。
delete_time Timestamp 删除实体的时间戳,仅当它支持保留时才适用。
expire_time Timestamp 实体到期时的到期时间戳。
start_time Timestamp 标记某个时间段开始的时间戳。
end_time Timestamp 标记某个时间段或操作结束的时间戳(无论其成功与否)。
read_time Timestamp 应读取(如果在请求中使用)或已读取(如果在响应中使用)特定实体的时间戳。
time_zone string 时区名称。它应该是 IANA TZ 名称,例如“America/Los_Angeles”。如需了解详情,请参阅 https://en.wikipedia.org/wiki/List_of_tz_database_time_zones。
region_code string 位置的 Unicode 国家/地区代码 (CLDR),例如“US”和“419”。如需了解详情,请访问 http://www.unicode.org/reports/tr35/#unicode_region_subtag。
language_code string BCP-47 语言代码,例如“en-US”或“sr-Latn”。如需了解详情,请参阅 http://www.unicode.org/reports/tr35/#Unicode_locale_identifier。
mime_type string IANA 发布的 MIME 类型(也称为媒体类型)。如需了解详情,请参阅 https://www.iana.org/assignments/media-types/media-types.xhtml。
display_name string 实体的显示名称。
title string 实体的官方名称,例如公司名称。它应被视为 display_name 的正式版本。
description string 实体的一个或多个文本描述段落。
filter string List 方法的标准过滤器参数。
query string 如果应用于搜索方法(即 :search),则与 filter 相同。
page_token string List 请求中的分页令牌。
page_size int32 List 请求中的分页大小。
total_size int32 列表中与分页无关的项目总数。
next_page_token string List 响应中的下一个分页令牌。它应该用作后续请求的 page_token。空值表示不再有结果。
order_by string 指定 List 请求的结果排序。
request_id string 用于检测重复请求的唯一字符串 ID。
resume_token string 用于恢复流式传输请求的不透明令牌。
labels map<string, string> 表示 Cloud 资源标签。
show_deleted bool 如果资源允许恢复删除行为,相应的 List 方法必须具有 show_deleted 字段,以便客户端可以发现已删除的资源。
update_mask FieldMask 它用于 Update 请求消息,该消息用于对资源执行部分更新。此掩码与资源相关,而不是与请求消息相关。
validate_only bool 如果为 true,则表示仅应验证给定请求,而不执行该请求。
常见校验数据
https://stackoverflow.com/questions/46155/how-to-validate-an-email-address-in-javascript
# 谷歌邮箱校验
function validateEmail(email) {
const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
}
- 原文作者:zaza
- 原文链接:https://zazayaya.github.io/2020/12/25/flask-project-struct.html
- 说明:转载本站文章请标明出处,部分资源来源于网络,如有侵权请及时与我联系!