Flask-爱家租房项目ihome-04-个人中心

七牛云图片存储

20200825更新

在deepin操作系统(Ubuntu亦可)下安装Typora+PicGo+gitee的教程已更新, 请查看: https://www.cnblogs.com/gcxblogs/p/13556626.html

题外话: Typora+PicGo+gitee打造Typora图床

说起图片存储这里插入一个插曲, 本人的博客都是先在本地用Typora软件编写Markdown格式的笔记, 然后复制到博客网站上的, 比直接在博客网上自带的编辑器上编写博客要方便的多, 很多平台都支持Markdown格式, 可以做到一次编写, 多平台使用. 但是使用Typora的一个缺点就在于图片的保存, 它默认是存储在本地的, 这样迁移起来就比较费劲, 还要把图片再迁移一遍, 最近发现有一款软件叫PicGo, 其本身就是用来处理图片的云端存储的, 可以选择不同的图床来存储图片, 其实就是可以配置多种云服务器来存储你上传的图片, 如:

image-20200819180205344

你可以选择保存你上传图片的服务器, 如腾讯云/阿里云/七牛/GitHub等, 这里我一开始选择的是七牛, 但是后面发现七牛需要自己的域名才能长期使用, 然后选择GitHub, 但是速度确实比较慢, 于是就选择了国内的码云gitee, 与GitHub类似是一个代码托管平台, 当然也可以存放图片等, 不仅速度快而且免费. 完成了gitee的图床配置后, 就可以选择图片进行上传了. 再搭配上Typora的偏好设置, 就可以在Typora编写的时候直接截图然后复制到Markdown文本中, 它就能自动将该图片上传到图床服务器上, 这样迁移的时候在其他平台也可以直接读取到图片了

image-20200819180923824

即使用Typora+PicGo+gitee就能解决markdown编辑图片的问题了. 相关安装配置可以参考: https://zhuanlan.zhihu.com/p/102594554 , PS:在我的测试过程中, 发现在linux如Ubuntu中, Typora对于PicGo的支持不太好, 因为下载的PicGo是Appimage可执行文件, Typora选择PicGo的路径选不到该文件, 硬填Appimage文件的路径上传也会报错, 所以后面还是改成了在Windows中使用PicGo+Typora

使用七牛图片存储

个人中心页面的头像图片选择存储在第三方服务器上, 七牛云在存储图片方面比较专业, 当然也可以选择存储在其他服务上, 这里介绍七牛云的使用方法.

创建空间

首先需要注册七牛账号并实名认证, 然后选择对象存储产品, 在空间管理页签下新建空间, 创建时设置为共开, 不然外部不好访问

image-20200821115908965

空间创建完成后, 进入空间查看空间概览, 找到CND测试域名(现在七牛的测试域名只能保持30天, 所以最好是使用自己的域名)

image-20200821120617615

最终我们上传了图片之后, 将该域名拼上上传后的文件名就可以访问到上传的图片了.

开发文档

在右上角的文档中进入开发者中心, 点击对象存储来到开发者中心, 点击上面的SDK&工具栏, 找到官方的python SDK (https://developer.qiniu.com/sdk#official-sdk)

image-20200821120947544

点击文档可以看到python的SDK文档: https://developer.qiniu.com/kodo/sdk/1242/python

image-20200821121117508

安装

pip install qiniu

上传流程

# -*- coding: utf-8 -*-
# flake8: noqa

from qiniu import Auth, put_file, etag
import qiniu.config

#需要填写你的 Access Key 和 Secret Key
access_key = 'Access_Key'
secret_key = 'Secret_Key'

#构建鉴权对象
q = Auth(access_key, secret_key)

#要上传的空间
bucket_name = 'Bucket_Name'

#上传后保存的文件名
key = 'my-python-logo.png'

#生成上传 Token,可以指定过期时间等
token = q.upload_token(bucket_name, key, 3600)

#要上传文件的本地路径
localfile = './sync/bbb.jpg'

ret, info = put_file(token, key, localfile)
print(info)
assert ret['key'] == key
assert ret['hash'] == etag(localfile)

编辑七牛上传脚本

在常亮文件ihome/utils/constants.py中添加七牛相关的常亮设置

# 七牛云图片存储设置
QN_ACCESS_KEY = 'kF97A.....'
QN_SECRET_KEY = 'sSSxlD.....'
QN_BUCKET_NAME = 'alex-ihome'
QN_HOST = 'http://qfan653gi.hd-bkt.clouddn.com/'

ihome/utils下创建image_storage.py文件

from qiniu import Auth, put_data, etag
import qiniu.config
from .constants import QN_ACCESS_KEY, QN_SECRET_KEY, QN_BUCKET_NAME, QN_HOST

def storage(data):
    """七牛上传图片接口"""
    # 需要填写你的 Access Key 和 Secret Key
    access_key = QN_ACCESS_KEY
    secret_key = QN_SECRET_KEY

    # 构建鉴权对象
    q = Auth(access_key, secret_key)

    # 要上传的空间
    bucket_name = QN_BUCKET_NAME

    # 生成上传 Token,可以指定过期时间等
    token = q.upload_token(bucket_name, None, 3600)

    # 上传二进制文件
    ret, info = put_data(token, None, data)
    print('*'*10, 'ret', '*'*10)
    print(ret)
    print('*'*10, 'info', '*'*10)
    print(info)
    url = QN_HOST + ret.get("key")
    return {'status': info.status_code, 'errmsg': info.exception, 'url': url}

if __name__ == '__main__':
    with open('ironman.jpg', 'rb') as f:
        data = f.read()
        storage(data)

注:

  1. 因为我们接口会接收前端传过来的二进制图片数据, 所以这里把官方案例中的put_file改成了put_data

  2. 我们这里不自定义上传后的重命名, 使用七牛默认的名字, 所以案例中的key都为None

  3. 七牛接口返回两个变量:

    ret: 字典dict类型变量,如 {'hash': 'FitEXQdmqE8hm1BZxd-xlELSuJtX', 'key': 'FitEXQdmqE8hm1BZxd-xlELSuJtX'}
    info: ResponseInfo对象, 如: _ResponseInfo__response:<Response [200]>, exception:None, status_code:200, text_body:{"hash":"FitEXQdmqE8hm1BZxd-xlELSuJtX","key":"FitEXQdmqE8hm1BZxd-xlELSuJtX"}, req_id:qfsAAACBLJz9LS0W, x_log:X-Log

  4. 最终访问服务器上传的图片url地址就是http://CND测试域名/文件名key

个人信息修改页

在修改页中有头像提交和用户名提交两个表单

image-20200821123242401

头像接口

头像上传接口的url是: /user/images

后端逻辑

在用户模块视图文件ihome/api_1_0/users.py中添加视图函数

# ihome/api_1_0/users.py
from flask import request, jsonify, current_app, session, g
from . import api
from ihome import models, csrf, redis_connect, db
from ihome.utils.response_codes import RET
from ihome.utils.commons import login_required, parameter_error
from ihome.utils.image_storage import storage

@api.route('/user/images', methods=['PATCH'])
@login_required
def set_user_image():
    """设置用户头像"""
    # 获取传入的图片
    image_data = request.files.get('avatar')
    if not image_data:
        return parameter_error()
    # 调用七牛上传接口
    resp = storage(image_data)
    if resp['status'] != 200:
        # 上传失败
        return jsonify(errno=RET.THIRDERR, errmsg=resp['errmsg'])
    # 上传成功, 更新数据库的url
    g.user.image_url = resp['url']
    try:
        db.session.add(g.user)
        db.session.commit()
    except Exception as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg='保存用户头像异常')

    return jsonify(errno=RET.OK, data={'url': resp['url']})

注:

1. 由于该接口结果是修改了User模型的image_url字段, 所以请求方式设置为了`PATCH`(局部更新)
2. 该接口和其他需要登录才能访问的接口需要添加之前定义好的登录装饰器`@login_required`, 且装饰器需要放在`@api.route`下面, 在装饰器中将当前登录的user对象存入了g对象中
3. 该接口因为需要传输图片二进制文件, 因此不是通过json来传了, 而是使用默认的浏览器传送文件的方式, 所以使用`request.files.get()`接收文件数据
4. 调用七牛上传接口, 上传成功后, 需要保存返回的url在数据库中

前端逻辑

由于该接口向后端传送的应该是文件形式的数据, 所以不能简单用之前的$.ajax方式直接发送请求, 但是也不能简单的使用浏览器默认的表单发送请求, 因为后端回传的还是json格式的数据, 前端需要根据回传的数据自定义相应的处理行为, 因此引入jquery的一个插件ajaxSubmit, 该插件在jquery.form.min.js中, 该插件的作用就是能够使用浏览器默认发送form表单的行为, 但是也能自定义回调函数. 即发送任务交给浏览器去做, 回传后的逻辑可以自己定义

编辑profile.html文件, 设置form为文件传输且添加jquery.form.min.js插件

<div class="menu-content">
    <img id="user-avatar" src="">
    <div class="menu-text">
        <form id="form-avatar" enctype="multipart/form-data">
            选择头像:<input type="file" accept="image/*" name="avatar">
            <input type="submit" class="btn btn-success" value="上传">
        </form>
    </div> 
</div>
....
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/jquery.form.min.js"></script>
<script src="/static/js/ihome/profile.js"></script>

编辑对应的profile.js文件

$(document).ready(function () {
    //页面一加载,阻止form表单默认的提交行为
    $('#form-avatar').submit(function (e) {
        e.preventDefault();
        //执行自定义的行为
        //利用jquery.form.min.js提供的ajaxSubmit插件对表单进行异步提交
        $(this).ajaxSubmit({
            url: '/api/v1.0/user/images',
            type: 'patch',
            dataType: 'json',
            headers: {'X-CSRFToken': getCookie('csrf_token')},
            success: function (resp) {
                if (resp.errno == '0'){
                    //上传成功, 设置当前页面的iamge的src属性
                    $('#user-avatar').attr('src', resp.data.url)
                }else {
                    //上传失败
                    alert('上传失败:'+resp.errmsg)
                }
            }
        })
    });
})

注:

  1. 阻止默认的浏览器发送表单的完整操作
  2. 使用ajaxSubmit插件发送请求, 发送的数据data和数据类型content-Type不用管, 交给浏览器去操作, 其他参数还是和普通的$.ajax请求一样的定义
  3. 上传成功后, 拿到返回的url, 设置页面image标签的src属性, 浏览器会自动请求该image的url

用户名接口

和头像接口类似, 只是把前后端传输格式又换成json, url是: /user/names

后端逻辑

在用户模块视图文件ihome/api_1_0/users.py中添加视图函数

@api.route('/user/names', methods=['PATCH'])
@login_required
def set_user_name():
    """设置用户名称接口"""
    # 接受数据
    dict_data = request.get_json()
    if not dict_data:
        return parameter_error()
    # 提取数据
    name = dict_data.get('name')
    if not name:
        return parameter_error()
    # 设置修改用户名
    g.user.name = name
    try:
        db.session.add(g.user)
        db.session.commit()
    except IntegrityError as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.PARAMERR, errmsg='该用户名已存在')
    except Exception as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg='保存用户名异常')
    # 修改session的值
    session['name'] = name

    return jsonify(errno=RET.OK)

前端逻辑

//页面一加载
$(document).ready(function () {
    ......
	//页面一加载,阻止表单默认行为
    $('#form-name').submit(function (e) {
        //阻止默认提交
        e.preventDefault()
        //设置表单提交
        //获取表单数据
        var name = $('#user-name').val()
        if (!name){
            alert('用户名必填')
        }
        data = JSON.stringify({name: name})
        $.ajax({
            url: 'api/v1.0/user/names',
            type: 'patch',
            contentType: 'application/json',
            data: data,
            dataType: 'json',
            headers: {'X-CSRFToken': getCookie('csrf_token')},
            success: function (resp) {
                if (resp.errno == '0') {
                    //设置成功
                    location.href='my.html'
                }else {
                    //设置失败
                    alert(resp.errmsg)
                }
            }
        })
    })
})

个人信息展示页

在主页点击右上角的用户名, 来到个人信息展示页, 展示用户设置的头像和用户名和手机号, 点击右上角的修改能进入上面介绍的个人信息修改页

image-20200821133633168

后端逻辑

查询用户表, 获取头像url/用户名name/手机号phone并返回给前端, url是: /user/info, 在用户模块视图文件ihome/api_1_0/users.py中添加视图函数

@api.route('/user/info')
@login_required
def get_user_info():
    """获取用户信息接口"""
    user = g.user
    return jsonify(errno=RET.OK, data={'url': user.image_url, 'name': user.name, 'mobile': user.phone})

前端逻辑

页面一加载就发送ajax请求获取用户的数据, 修改前端js文件my.js

$(document).ready(function(){
    //调用获取头像接口
    $.get('/api/v1.0/user/info', function (resp) {
        if (resp.errno == '0'){
            //获取成功,设置image的url
            $('#user-avatar').attr('src', resp.data.url);
            $('#user-name').html(resp.data.name);
            $('#user-mobile').html(resp.data.mobile);
        }else if (resp.errno == '4101'){
            //未登录
            location.href = 'login.html'
        }
    })
})

实名认证页

模拟实名认证功能, 填入真实姓名和身份证号进行保存, 保存成功后页面变成只读

image-20200821134214645

后端逻辑

分为认证信息保存和查询两个逻辑, url是: user/auth, 在用户模块视图文件ihome/api_1_0/users.py中添加视图函数

@api.route('user/auth', methods=['GET', 'PATCH'])
@login_required
def handle_auth():
    """实名认证接口"""
    if request.method == 'GET':
        return get_user_auth()
    else:
        return authenticate()

def get_user_auth():
    return jsonify(errno=RET.OK, data={'real_name': g.user.real_name, 'real_id_card': g.user.real_id_card})

def authenticate():
    # 接收数据
    dict_data = request.get_json()
    if not dict_data:
        return parameter_error()
    # 提取数据
    real_name = dict_data.get('real_name')
    real_id_card = dict_data.get('real_id_card')
    # 校验数据
    if not all([real_name, real_id_card]):
        return parameter_error()
    # 获取已认证的用户
    try:
        users = Users.query.filter(or_(Users.real_name == real_name, Users.real_id_card == real_id_card)).all()
    except Exception as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg='获取用户异常')
    if users:
        return jsonify(errno=RET.DATAEXIST, errmsg='该实名用户已存在')
    # 进行实名认证, 保存实名信息
    g.user.real_name = real_name
    g.user.real_id_card = real_id_card
    try:
        db.session.add(g.user)
        db.session.commit()
    except Exception as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg='保存用户实名信息异常')

    return jsonify(errno=RET.OK)

前端逻辑

页面一加载先调用认证信息的查询接口, 如果存在认证信息, 则设置该页面只可读, 不能再次认证, 如果不存在认证信息, 则可填入认证信息并保存, 保存后刷新该页面重新获取认证信息, 编辑对应的auth.js文件

//页面一加载
$(document).ready(function (){
    //发送请求获取已有的认证信息
    $.get('/api/v1.0/user/auth', function (resp) {
        if(resp.errno == '0'){
            //获取成功
            var realName = resp.data.real_name;
            var idCard = resp.data.real_id_card;
            if (realName && idCard){
                //设置展示
                $('#real-name').attr({'value': realName, 'readonly': 'readonly'});
                $('#id-card').attr({'value': idCard, 'readonly': 'readonly'});
                $('.btn-success').hide();
            }
        }
    }, 'json')
    
    //设置表单提交事件
    $('#form-auth').submit(function (e) {
        //禁止默认表单提交事件
        e.preventDefault()
        //设置表单提交事件
        //获取表单数据
        var realName = $('#real-name').val()
        if (!realName){
            alert('真实姓名必填')
            return;
        }
        var idCard = $('#id-card').val()
        if (!idCard){
            alert('身份证号码必填')
            return;
        }
        data = JSON.stringify({real_name: realName, real_id_card: idCard})
        //发送请求
        $.ajax({
            url: 'api/v1.0/user/auth',
            type: 'patch',
            data: data,
            contentType: 'application/json',
            dataType: 'json',
            headers: {'X-CSRFToken': getCookie('csrf_token')},
            success: function (resp) {
                if(resp.errno == '0'){
                    //认证成功
                    showSuccessMsg();
                }else{
                    //认证失败
                    alert(resp.errmsg);
                }
            }
        })
    })
})

注:

  1. jquery中.attr({'value': realName, 'readonly': 'readonly'})传入js对象的方式能够设置该标签的多个属性值, .attr('value', realName)设置一个属性值
  2. js中一个&表示位运算符, 两个&&表示逻辑运算符
原文地址:https://www.cnblogs.com/gcxblogs/p/13540761.html