说明文档

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()

结构同步

  1. 第一次初始化目录:flask db init
  2. 生成变更代码:flask db migrate -m “初始化”
  3. 同步变更到数据库:flask db upgrade
  4. 初始化基础数据: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上下文

数据库结构说明

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());
}