08-Flask之淘票票(前后端分离)

一、区域选择模块

  • 数据库建模
from App.ext import db

# 字母模型类
class Letter(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.String(2))
    citys = db.relationship('City', backref='letter', lazy=True)

# 城市模型类
class City(db.Model):
    # 注意,不是自增长
    id = db.Column(db.Integer, primary_key=True)
    regionName = db.Column(db.String(100))
    cityCode = db.Column(db.String(10))
    pinYin = db.Column(db.String(10))
    c_letter = db.Column(db.Integer, db.ForeignKey(Letter.id))

备注: https://dianying.taobao.com/cityAction.json?activityId&_ksTS=1531740557472_417&jsoncallback=jsonp418&action=cityAction&n_s=new&event_submit_doGetAllRegion=true

  • 数据导入(数据库操作)
# 从JSON到到数据库脚本 city-mysql.py
import json
import pymysql

# 链接数据库
db = pymysql.Connect(host="localhost", port=3306, user="root", password="123456", database="Tpp", charset="utf8")
# 数据库游标
cursor = db.cursor()

# 打开文件
with open('city.json', 'r') as f:
    # json形式加载
    city_collection = json.load(f)

    # 获取所有的键
    returnValue = city_collection.get('returnValue')
    letters = returnValue.keys()

    # 遍历插入到数据库中
    for letter in letters:
        # 游标,执行SQL语句 (注意values中的值是字符串)
        db.begin()
        cursor.execute("insert into letter(name) values('{}')".format(letter))
        db.commit()

        # 获取字母对应的主键
        db.begin()
        cursor.execute("select id from letter where name='{}';".format(letter))
        db.commit()
        result = cursor.fetchone()
        letter_id = result[0]

        # 获取key对应的value
        citys = returnValue.get(letter)
        for c_obj in citys:
            # insert into city(regionName,cityCode,pinYin,c_letter)
            regionName = c_obj.get('regionName')
            cityCode = c_obj.get('cityCode')
            pinYin = c_obj.get('pinYin')

            db.begin()
            cursor.execute("insert into city(regionName,cityCode,pinYin,c_letter) values('{}','{}','{}',{});".format(regionName,cityCode,pinYin,letter_id))
            db.commit()

开始使用数据库时,驱动程序会发出一个BEGIN之后COMMIT,符合规范的pythonDBAPI始终以这种方式工作.

  • 返回JSON数据
{
    'status':200,
    'msg': '获取城市列表数据成功',
    "data":{
        "A":[
            {
                "id":3643,
                "parentId":0,
                "regionName":"阿坝",
                "cityCode":513200,
                "pinYin":"ABA"
            },
            {
                "id":3090,
                "parentId":0,
                "regionName":"阿克苏",
                "cityCode":652901,
                "pinYin":"AKESU"
            }],
        "B":[
            {
                "id":3643,
                "parentId":0,
                "regionName":"阿坝",
                "cityCode":513200,
                "pinYin":"ABA"
            },
            {
                "id":3090,
                "parentId":0,
                "regionName":"阿克苏",
                "cityCode":652901,
                "pinYin":"AKESU"
            }]
        ...
}

@marshal_with()装饰器,而在flask-RESTful文档中高级:嵌套字段并没有使用装饰器,而是通过函数调用的方式实现格式化输出的

二、用户系统分析

  • 字段
    用户名
    密码
    邮箱
    手机号
    用户状态(是否激活)
    用户权限
    用户token
    头像
    逻辑删除
  • 业务流程
    用户名
    密码
    邮箱
        发一个邮件,点击激活
        不激活权限会被限制
用户注册业务

三、用户注册

  • 数据模型(建模)
class User(db.Model):
    # 主键
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    # 用户名
    name = db.Column(db.String(30), unique=True)
    # 密码
    password = db.Column(db.Integer(255))
    # 邮箱
    email = db.Column(db.String(30), unique=True)
    # 手机
    iphone = db.Column(db.String(20))
    # 头像
    icon = db.Colum(db.String(100), default='head.png')
    # 是否激活
    is_active = db.Column(db.Boolean, default=False)
    # 用户令牌
    token = db.Column(db.String(255))
    # 权限 
    permissions = db.Column(db.Integer, default=1)
    # 是否被删除
    is_delete = db.Column(db.Boolean, default=False)

RESTful前后端分离,而要给移动端写接口,移动端是没有cookie的,就是用token来作代替方案。

  • 注册接口
""" 注册接口数据
{
    "returnCode": "0",
    "returnValue": {
        "token": "8f715ea6-62c5-45a1-9dab-4367f1bf24a5",
        "username": "MM",
        "permissions": "1"
    },
    "status": "200",
    "err": "None"
}
"""

# 请求参数格式
parser = reqparse.RequestParser()
parser.add_argument('username', type=str, required=True, help='请提供用户名')
parser.add_argument('password', type=str, required=True, help='请提供密码')
parser.add_argument('email', type=str, required=True, help='请提供邮箱')
parser.add_argument('iphone', type=str, required=True, help='请提供手机号码')



# 定义格式的需求,可以继承 fields.Raw 类并且实现格式化函数
class IconForm(fields.Raw): 
    def format(self, value):
        return '/static/img/' + value

# 输出格式
user_fields = {
    'username': fields.String,
    'token': fields.String,
    'permissions': fields.String,
    'icon': IconForm(attribute='icon')  # attribute='对应key'
}

result_fields = {
    'returnCode': fields.String,
    'returnValue': fields.Nested(user_fields),
    'status': fields.String,
    'err': fields.String(default='None')
}

# 用户注册接口
class RegisterUser(Resource):
    @marshal_with(result_fields)
    def post(self):
        parse = parser.parse_args()

        user = User()
        user.username = parse.get('username')
        user.password = parse.get('password')
        user.email = parse.get('email')
        user.iphone = parse.get('iphone')
        user.token = str(uuid.uuid4())
        print(user.username)

        data = {
            'returnCode': '0',
            'returnValue': user,
            'status': '200'
        }

        try:
            print(user.token)
            db.session.add(user)
            db.session.commit()
            print('hello')
            return data
        except Exception as e:
            data['err'] = '用户已经存在!'
            data['returnValue'] = None
            data['status'] = '406'
            return data

四、flask-mail插件

- 安装
    pip install flask-mail
    
- 配置(app.config配置)
    MAIL_SERVER = "smtp.163.com"
    MAIL_USERNAME = "[email protected]"
    MAIL_PASSWORD = "xxxxxx"
    
- 初始化
    from flask_mail import Mail
    mail = Mail()
    mail.init_app(app)

- 使用
    # 邮件信息
    msg = Message(subject="Tpp激活邮件",        # 主题
                  recipients=[user.email],      # 收件人
                  sender="[email protected]") # 发件人
    # 传入网页(即主体内容,可以为空)
    body_html = render_template('active.html', username=user.username,active_url='http://localhost:5000/api/v1/useractive?token='+user.token)
    msg.html = body_html
    # 发送邮件
    mail.send(msg)

MAIL_PASSWORD密码设置,可以在官网中设置客户端授权密码开启,即可以不使用登录密码!

五、用户激活

用户激活,其实也就是一个接口。
这个接口可以根据链接找到对应用户,并修改用户的状态。
可以该{token:userId}存储信息。


(注册接口)
# 注册请求
# 获取用户信息
# 存储数据库
# token:userid 存储cache[超时设置]
    cache.set(user.token, user.id, timeout=60)
# 发送邮件


(激活接口)
# 激活请求
# 获取用户token
# 根据token在cache中获取对应的userid
    userid = cache.get(token)
# 删除token
    cache.delete(token)
# 根据userid找到对应用户对象
# 修改用户状态
# 保存到数据库

redis缓存{token:userId}就可以使用flask-cache,它可以缓存视图,也可以直接使用原生操作用于存取数据。


用户激活

六、用户登录

# 登录请求
# 获取用户名、密码
# 根据用户名和密码验证
     users = User.query.filter(User.username==username).filter(User.password==password)
     if users.count()>0:    # 账号密码正确
# 再验证是否激活
# 返回数据
    成功,返回用户信息(用户名、token...)
    失败,返回用户名或密码错误提示

七、密码安全模块

generate_password_hash(password): 输入相同,但每次输出结果都是不一样的
check_password_hash(hash,password): 出入hash与输入的值比较是否相等

八、用户修改密码

# 修改密码请求
# 获取token、旧密码、新密码
# 根据token获取用户信息
# 验证操作
    旧密码一致,修改
    旧密码不一致,不修改
# 返回数据

九、用户权限

# 权限设计与限制
    0 未登陆
        列表A(预览权限)
    1 普通用户
        列表A + 列表B(预览权限)
    2 会员
        列表A + 列表B
    4 超级会员  
        列表A + 列表B + 下载权限
        
# 资源限制
    if user.permissions == 1:
        return {'msg': '麻麻地啦', 'data': ' 列表A + 列表B(预览权限)'}
    elif user.permissions == 2:
        return {'msg': '会员,奔小康水平', 'data': ' 列表A + 列表B'}
    elif user.permissions == 4:
        return {'msg': '超级会员,请叫我土豪', 'data': ' 列表A + 列表B + 下载'}

十、自定义权限(装饰器)

# 很多资源都有权限问题,那都会需要上述判断处理
# 类似接口Blueprint定义接口时,通过装饰器实现统一
# 添加一个权限装饰器,给需要权限限制的加上装饰器即可


- Linux文件读写权限 
    r 》 4 》 100
    w 》 2 》 010
    x 》 1 》 001
    
    6表示有读写权限 》 110 》 
        判断是否有读权限?      位运算: 110 & 100  》 100  》 r
        判断是否有可执行权限?   位运算: 110 & 001  》 000  》 无 
        
- 装饰器
# 权限管理装饰器  [只管有无权限,什么数据不管]
def check_permissions_control(permissions):
    def check_permissions(func):
        def check(*args, **kwargs):
            parse = parser.parse_args()
            token = parse.get('token')
            if token:  # 验证token
                users = User.query.filter(User.token == token)
                if users.count() > 0:  # 有用户
                    user = users.first()
                    if user.permissions & permissions == permissions:   # 有权限
                        # 权限,即执行装饰的函数,否则报错跳出
                        return func(*args, **kwargs)
                    else:
                        abort(403,message='你没有操作权限,请联系管理员')
                else:  # 未登录
                    abort(401, message='你还没登录,请登录后操作')
            else:  # 未登录
                abort(401, message='你还没登录,请登录后操作')
        return check
    return check_permissions

按位与: &
按位或: |

if user.permissions & permissions == permissions: 权限判断

十一、电影信息接口+权限管理

  • 电影信息接口
- 数据库结构
- 模型结构
- 插入数据(数据库)
- 定义接口
- 参数设置
    flag: 0 全部
    flag: 1 热映
    flag: 2 即将上映
    flag = parse.get('flag') or 0 
- 返回数据
  • 添加电影接口(权限管理)
- 权限判断(添加装饰器即可)
    通过上述装饰器方式,处理权限
    有权限,才会调用post接口的函数处理
- post接口(只管数据)
    获取数据
    存入数据库
    返回数据

十二、电影院信息接口

- 数据库结构
- 模型结构
- 插入数据
- 定义接口
- 参数设置
    city: 城市
    district: 地区
    sort: 排序
    limit: 显示条数 
- 返回数据

十三、图片上传

# settings.py文件中
    BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    UPLOAD_FOLDER = os.path.join(BASE_DIR, 'App/static/img/')

# UploadFile.py文件上传api
parser = reqparse.RequestParser()
parser.add_argument('token', type=str, required=True, help='缺少token')
parser.add_argument('headimg', type=werkzeug.datastructures.FileStorage, location='files',required=True, help='请选择图片')

class UserHeadResource(Resource):
    @marshal_with(result_fields)
    def post(self):
        parse = parser.parse_args()
        token = parse.get('token')

        returndata = {}

        users = User.query.filter(User.token == token)
        if users.count()>0:

            user = users.first()

            # 图片数据
            imgfile = parse.get('headimg')
            # 图片名称 secure_filename(imgFile.filename)
            filename = '%d-%s' % (user.id,secure_filename(imgfile.filename))
            # 图片路径
            filepath = os.path.join(UPLOAD_FOLDER, filename)
            # 保存文件
            imgfile.save(filepath)

            # 保存到数据库
            user.icon = filename
            db.session.add(user)
            db.session.commit()

            # 返回数据
            returndata['status'] = 200
            returndata['msg'] = '文件上传成功'
            returndata['data'] = user

            return returndata

        else:
            returndata['status'] = 401
            returndata['msg'] = '上传文件失败'
            returndata['err'] = 'token错误'

            return returndata

备注: img目录需要有!

十四、项目依赖问题

requirements.txt 文件 里面记录了当前程序的所有依赖包及其精确版本号。
其作用是用来在另一台PC上重新构建项目所需要的运行环境依赖。

- 生成requirements.txt
    pip freeze > requirements.txt
- 安装requirements.txt依赖
    pip install -r requirements.txt

你可能感兴趣的:(08-Flask之淘票票(前后端分离))