爱家租房项目①

文章目录

  • 01-项目说明
    • 01_项目资料说明
    • 02_前后端分离的认识
    • 03_前端后分离对于搜索引擎的缺点
      • 介绍
      • 优势:
    • 04_租房需求功能
  • 02-项目文件目录结构
    • session存储
    • Flask-Migrate
    • 05_日志功能
  • 03-数据库设计
    • 01_项目数据库设计
    • 03_日志级别bug处理
    • 04_数据库迁移处理
  • 04-静态文件接口
    • 路由转换
    • 02_csrf防护机制
    • 03_在静态路由中添加生成csrf_token的cookie值
  • 05-图片验证码
    • 01_图片验证码的使用流程
    • 02_restful风格介绍
    • 03_图片验证码后端接口编写
    • 04_开发流程与接口文档编写
    • 05_图片验证码的前端编写
    • 05_图片验证码的前端编写
  • 06-短信验证
    • 02_云通讯发送短信工具封装
    • 短信验证后端逻辑
    • 07发送短信验证码前端编写
  • 07-注册
    • 01注册的后端编写
    • 04_注册后端测试
    • 05_注册前端编写
    • 06_补充csrf的逻辑

01-项目说明

01_项目资料说明

爱家租房项目①_第1张图片

02_前后端分离的认识

爱家租房项目①_第2张图片

03_前端后分离对于搜索引擎的缺点

介绍

前后端分离已成为互联网项目开发的业界标准使用方式,通过nginx+tomcat的方式(也可以中间加一个nodejs)有效的进行解耦,并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务(多种客户端,例如:浏览器,车载终端,安卓,IOS等等)打下坚实的基础。这个步骤是系统架构从猿进化成人的必经之路。

核心思想是前端html页面通过ajax调用后端的restuful api接口并使用json数据进行交互

优势:

1、可以实现真正的前后端解耦,前端服务器使用nginx。前端/WEB服务器放的是css,js,图片等等一系列静态资源(甚至你还可以css,js,图片等资源放到特定的文件服务器,例如阿里云的oss,并使用cdn加速),前端服务器负责控制页面引用&跳转&路由,前端页面异步调用后端的接口,后端/应用服务器使用tomcat(把tomcat想象成一个数据提供者),加快整体响应速度。(这里需要使用一些前端工程化的框架比如nodejs,react,router,react,redux,webpack)

2、发现bug,可以快速定位是谁的问题,不会出现互相踢皮球的现象。页面逻辑,跳转错误,浏览器兼容性问题,脚本错误,页面样式等问题,全部由前端工程师来负责。接口数据出错,数据没有提交成功,应答超时等问题,全部由后端工程师来解决。双方互不干扰,前端与后端是相亲相爱的一家人。

3、在大并发情况下,我可以同时水平扩展前后端服务器,比如淘宝的一个首页就需要2000+台前端服务器做集群来抗住日均多少亿+的日均pv。(去参加阿里的技术峰会,听他们说他们的web容器都是自己写的,就算他单实例抗10万http并发,2000台是2亿http并发,并且他们还可以根据预知洪峰来无限拓展,很恐怖,就一个首页。。。)

4、减少后端服务器的并发/负载压力。除了接口以外的其他所有http请求全部转移到前端nginx上,接口的请求调用tomcat,参考nginx反向代理tomcat。且除了第一次页面请求外,浏览器会大量调用本地缓存。

5、即使后端服务暂时超时或者宕机了,前端页面也会正常访问,只不过数据刷不出来而已。

6、也许你也需要有微信相关的轻应用,那样你的接口完全可以共用,如果也有app相关的服务,那么只要通过一些代码重构,也可以大量复用接口,提升效率。(多端应用)

7、页面显示的东西再多也不怕,因为是异步加载。

8、nginx支持页面热部署,不用重启服务器,前端升级更无缝。

9、增加代码的维护性&易读性(前后端耦在一起的代码读起来相当费劲)。

10、提升开发效率,因为可以前后端并行开发,而不是像以前的强依赖。

11、在nginx中部署证书,外网使用https访问,并且只开放443和80端口,其他端口一律关闭(防止黑客端口扫描),内网使用http,性能和安全都有保障。

12、前端大量的组件代码得以复用,组件化,提升开发效率,抽出来!

04_租房需求功能

爱家租房项目①_第3张图片
爱家租房项目①_第4张图片

02-项目文件目录结构

session存储

爱家租房项目①_第5张图片

Flask-Migrate

  • 需求: 随着开发进度不断向前,你会发现你的数据库模型需要更改,而当这种情况发生时需要更新数据库。

  • 解决: Flask-SQLAlchemy只有当数据库表不存在了才从模型创建它们,所以更新表的唯一途径就是销毁旧的表,当然这将导致所有数据库中的数据丢失。

    有个更好的解决方案就是使用数据库迁移框架。和源码版本控制工具跟踪更改源码文件一样,数据库迁移框架跟踪更改数据库模型,然后将增量变化应用到数据库中。

    SQLAlchemy的主要开发人员写了一个Alembic迁移框架,但我们不直接使用Alembic,Flask应用可以使用Flask-Migrate扩展,一个集成了Flask-Script来提供所有操作命令的轻量级Alembic包。

  • 功能: 管理升级迁移数据库

    Flask-Migrate 插件提供了和 Django 自带的 migrate 类似的功能。
    多数情况下 Flask-Migrate 是会和命令行工具插件 Flask-Script 和数据库插件 flask_sqlalchemy 一起使用的

    即 Alembic(Database migration 数据迁移跟踪记录)提供的数据库升级和降级的功能。它所能实现的效果有如 Git 管理项目代码一般。

  • 安装:

    pip install flask-migrate
    
  • 配置flask- script的命令

    manager.add_command('db',MigrateCommand)
    
  • 指令使用

    python manage.py db init
    python manage.py db migrate
    python manage.py db upgrade
    python manage.py db --help
    
    manage.py db migrate --message '更新了XX'
    

05_日志功能

init.py

import logging
from logging.handlers import RotatingFileHandler


# 设置日志的记录等级
logging.basicConfig(level=logging.DEBUG)  # 调试debug级
# 创建日志记录器,指明日志保存的路径、每个日志文件的最大大小、保存的日志文件个数上限
file_log_handler = RotatingFileHandler("logs/log", maxBytes=1024*1024*100, backupCount=10)
# 创建日志记录的格式                 日志等级    输入日志信息的文件名 行数    日志信息
formatter = logging.Formatter('%(levelname)s %(filename)s:%(lineno)d %(message)s')
# 为刚创建的日志记录器设置日志记录格式
file_log_handler.setFormatter(formatter)
# 为全局的日志工具对象(flask app使用的)添加日志记录器
logging.getLogger().addHandler(file_log_handler)

03-数据库设计

01_项目数据库设计

爱家租房项目①_第6张图片
爱家租房项目①_第7张图片

03_日志级别bug处理

开发模式默DBUG=TRUE ,会将
logging.basicConfig(level=logging.DEBUG) 忽略

04_数据库迁移处理

爱家租房项目①_第8张图片
爱家租房项目①_第9张图片
在这里插入图片描述
创建成功
爱家租房项目①_第10张图片

04-静态文件接口

路由转换

common.py

from werkzeug.routing import BaseConverter

class ReConverter(BaseConverter):
    """ 自定义re转换器 """
	# url 传递的正则表达式
    def __init__(self, url_map, regex):
        super().__init__(url_map)
        # 保存正则表达式
        self.regex = regex
from flask import Blueprint, current_app, make_response
from ihome.utils.common import csrf_wrap

html = Blueprint('web_html', __name__)

@html.route("/")
@csrf_wrap
def get_html(html_file_name):
    """ 提供静态文件访问 """
    if not html_file_name:
        html_file_name = 'index.html'

    if html_file_name != 'favicon.ico':
        html_file_name = 'html/' + html_file_name

    # 前后端分离后,给rsp添加csrf的cookie
    # rsp = make_response(current_app.send_static_file(html_file_name))
    # rsp.set_cookie('csrf_token', csrf.generate_csrf())
    # return rsp

    return current_app.send_static_file(html_file_name)

flask的应用对象中添加

将自定义转换器类,添加到默认的转换列表中,注册蓝图

app.url_map.converters['re'] = ReConverter

from ihome.web_html import html
app.register_blueprint(html)

02_csrf防护机制

爱家租房项目①_第11张图片

03_在静态路由中添加生成csrf_token的cookie值

common.py

# 前后端分离后,给rsp添加csrf的cookie
    rsp = make_response(current_app.send_static_file(html_file_name))
    rsp.set_cookie('csrf_token', csrf.generate_csrf())
    return rsp

    # return current_app.send_static_file(html_file_name)

05-图片验证码

01_图片验证码的使用流程

爱家租房项目①_第12张图片

02_restful风格介绍

爱家租房项目①_第13张图片
爱家租房项目①_第14张图片
爱家租房项目①_第15张图片
爱家租房项目①_第16张图片
爱家租房项目①_第17张图片
爱家租房项目①_第18张图片
爱家租房项目①_第19张图片
爱家租房项目①_第20张图片

03_图片验证码后端接口编写

@api.route('/image_code/')
def get_image_code(image_code_id):
    """ 
    获取图片验证码, 并将验证值保存到redis中
    :param image_code_id: 浏览器端带入的图片验证码编号
    :return 正常: 返回图片验证码, 异常:返回json
    """
    # 1. 业务处理逻辑

    # 2. 生成图片验证码
    # 名字, 真是文本, 图片二进制数据
    name, text, image_data = captcha.generate_captcha()

    # 3. 如果使用哈希表,只能同一设置过期时间,不合适
    # 将图片验证吗保存到redis中, 字符串
    try:
        redis_store.setex(
            f'image_code_{image_code_id}',
            constants.IMAGE_CODE_REDIS_EXPIRES,
            text
        )
    except Exception as e:
        current_app.logger.error(e)
        # 图片保存失败, 返回错误json
        return jsonify(errno=RET.DBERR, errmsg='保存图片验证码失败!')

    # 4. 返回
    rsp = make_response(image_data)
    rsp.headers['Content-Type'] = 'image/jpg'
    return rsp

04_开发流程与接口文档编写

爱家租房项目①_第21张图片
爱家租房项目①_第22张图片

05_图片验证码的前端编写

http://127.0.0.1:5000/api/v1.0/image_code/123

爱家租房项目①_第23张图片

05_图片验证码的前端编写

register.html

<div class="form-group form-group-lg">
	<div class="input-group">
     	<div class="input-group-addon"><i class="fa fa-image fa-lg fa-fw">i>div>
     	<input type="text" class="form-control" name="imagecode" id="imagecode" placeholder="图片验证码" required>
     	<div class="input-group-addon image-code" onclick="generateImageCode();"><img src="">div>
	div>
div>

register.js

var imageCodeId = "";

// 生成uuid
function generateUUID() {
    var d = new Date().getTime();
    if (window.performance && typeof window.performance.now === "function") {
        d += performance.now(); //use high-precision timer if available
    }
    var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        var r = (d + Math.random() * 16) % 16 | 0;
        d = Math.floor(d / 16);
        return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
    });
    return uuid;
}


/**
 * 生成图片验证码
 */
function generateImageCode() {
    imageCodeId = generateUUID()
    $('.image-code img').attr('src', '/api/v1.0/image_code/' + imageCodeId)
}


$(document).ready(function () {
    generateImageCode();

06-短信验证

02_云通讯发送短信工具封装

爱家租房项目①_第24张图片
爱家租房项目①_第25张图片

短信验证后端逻辑

@api.route('/sms_code/')
def get_sms_code(mobile_num):
    """
    验证图片验证码, 再发送手机短信验证码
    :param mobile_num 手机号码
    :return sms_code 手机验证码
    """
    # 1. 验证图片验证码
    image_code = request.args.get('image_code')
    image_code_id = request.args.get('image_code_id')

    # 检验参数完整性
    if not all([image_code_id, image_code]):
        return jsonify(errno=RET.PARAMERR, errmsg='参数不完整')

    # 检验参数正确性
    try:
        real_image_code = redis_store.get(f'image_code_{image_code_id}')
    except Exception as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg='redis数据库异常')

    if not real_image_code:
        return jsonify(errno=RET.NODATA, errmsg='图片验证码失效')

    # 删除redis中图片验证码,防止用户多次尝试, 生产环境开启
    try:
        redis_store.delete(f'image_code_{image_code_id}')
    except Exception as e:
        current_app.logger.error(e)

    if real_image_code.lower() != image_code.lower():
        return jsonify(errno=RET.DATAERR, errmsg='图片验证失败')

    # 验证手机是否在一分钟内已发送验证码,限制一分钟内只能发送一次
    try:
        send_flag = redis_store.get(f'send_sms_code_{mobile_num}')
    except Exception as e:
        current_app.logger.error(e)
    else:
        if send_flag:
            return jsonify(errno=RET.REQERR, errmsg='请求过于频繁,请60秒后重试')

    # 2. 验证手机是否以注册
    try:
        user = User.query.filter_by(mobile=mobile_num).first()
    except Exception as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg='数据库异常')
    else:
        if user is not None:
            return jsonify(errno=RET.DATAEXIST, errmsg='手机已注册,请直接登录')

    # 3. 验证通过,保存到redis中
    sms_code = '%06d' % randint(0, 999999)

    try:
        redis_store.setex(
            f'sms_code_{mobile_num}',
            constants.SMS_CODE_REDIS_EXPIRES,
            sms_code
        )
        # 保存手机发送短信验证码的记录,限制60秒内重复发送
        redis_store.setex(
            f'send_sms_code_{mobile_num}',
            constants.SEND_SMS_CODE_INTERVAL,
            1
        )
    except Exception as e:
        return jsonify(errno=RET.DBERR, errmsg='redis数据库异常')

    # 4. 发送手机验证码
    # try:
    #     status = ccp.send_template_sms(
    #         mobile_num,
    #         [sms_code, str(constants.SMS_CODE_REDIS_EXPIRES//60)],
    #         1
    #     )
    # except Exception as e:
    #     current_app.logger.error(e)
    #     return jsonify(errno=RET.THIRDERR, errmsg='短信发送异常')

    # celery 发布异步任务
    send_sms.delay(mobile_num, sms_code, str(
        constants.SMS_CODE_REDIS_EXPIRES//60), 1)

    return jsonify(errno=RET.OK, errmsg='短信发送成功')

07发送短信验证码前端编写

/**
 * 获取短信验证码方法
 */
function sendSMSCode() {
    $(".phonecode-a").removeAttr("onclick");
    var mobile = $("#mobile").val();
    if (!mobile) {
        $("#mobile-err span").html("请填写正确的手机号!");
        $("#mobile-err").show();
        $(".phonecode-a").attr("onclick", "sendSMSCode();");
        return;
    }
    var imageCode = $("#imagecode").val();
    if (!imageCode) {
        $("#image-code-err span").html("请填写验证码!");
        $("#image-code-err").show();
        $(".phonecode-a").attr("onclick", "sendSMSCode();");
        return;
    }
    $.get("/api/v1.0/sms_code/" + mobile, {
            image_code: imageCode,
            image_code_id: imageCodeId
        },
        function (data) {
            if (0 != data.errno) {
                $("#image-code-err span").html(data.errmsg);
                $("#image-code-err").show();
                if (2 == data.errno || 3 == data.errno) {
                    generateImageCode();
                }
                $(".phonecode-a").attr("onclick", "sendSMSCode();");
            } else {
                var $time = $(".phonecode-a");
                var duration = 60;
                var intervalid = setInterval(function () {
                    $time.html(duration + "秒");
                    if (duration === 1) {
                        clearInterval(intervalid);
                        $time.html('获取验证码');
                        $(".phonecode-a").attr("onclick", "sendSMSCode();");
                    }
                    duration = duration - 1;
                }, 1000, 60);
            }
        }, 'json');
}

07-注册

01注册的后端编写

@api.route('/users', methods=['POST'])
def register():
    """
    用户注册
    :return:
    """
    # 1. 检查参数缺失
    req_dict = request.get_json()
    print(req_dict)
    mobile = req_dict.get('mobile')
    password = req_dict.get('password')
    password2 = req_dict.get('password2')
    sms_code = req_dict.get('sms_code')

    if not all([mobile, password, password2, sms_code]):
        return jsonify(errno=RET.PARAMERR, errmsg='参数不完整')

    # 2. 业务处理
    # 检查手机号
    if not re.match(r'1[345789]\d{9}', mobile):
        return jsonify(errno=RET.PARAMERR, errmsg='手机号码格式错误')

    # 检查密码是否一致
    if password != password2:
        return jsonify(errno=RET.PWDERR, errmsg='两次密码不一致')

    # 检查短信验证码是否正确
    try:
        real_sms_code = int(redis_store.get(f'sms_code_{mobile}'))
    except Exception as e:
        return jsonify(errno=RET.DBERR, errmsg='读取真实手机验证码错误')

    if real_sms_code is None:
        return jsonify(errno=RET.NODATA, errmsg='短信验证码失效')

    # 删除短信验证码,防止重复利用
    try:
        redis_store.delete(f'sms_code_{mobile}')
    except Exception as e:
        current_app.logger.error(e)

    # print(real_sms_code, sms_code)
    if real_sms_code == sms_code:
        return jsonify(errno=RET.DATAERR, errmsg='短信验证码错误')

    # 3. 检查手机唯一性
    # try:
    #     user = User.query.filter_by(mobile=mobile).first()
    # except Exception as e:
    #     current_app.logger.error(e)
    #     return jsonify(errno=RET.DATAEXIST, errmsg='手机已注册')

    # 4. 保存用户信息
    user = User(name=mobile, mobile=mobile)
    user.password = password

    try:
        db.session.add(user)
        db.session.commit()
    # 由于手机号码具有唯一性, 一旦重复插入就会报错, 利用此性质, 不需要进行查询
    # 会抛出 IntegrityError 异常,减少数据库操作
    except IntegrityError as e:
        db.session.rollback()
        # 手机号重复
        current_app.logger.error(e)
        return jsonify(errno=RET.DATAEXIST, errmsg='手机已注册')
    except Exception as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg='数据库异常')

    # 保存登录状态到session中
    session['name'] = mobile
    session['mobile'] = mobile
    session['user_id'] = user.id

    return jsonify(errno=RET.OK, errmsg='注册成功')

04_注册后端测试

@api.route('/users', methods=['POST'])
def register():
    """
    用户注册
    :return:
    """
    # 1. 检查参数缺失
    req_dict = request.get_json()
    print(req_dict)
    mobile = req_dict.get('mobile')
    password = req_dict.get('password')
    password2 = req_dict.get('password2')
    sms_code = req_dict.get('sms_code')

    if not all([mobile, password, password2, sms_code]):
        return jsonify(errno=RET.PARAMERR, errmsg='参数不完整')

    # 2. 业务处理
    # 检查手机号
    if not re.match(r'1[345789]\d{9}', mobile):
        return jsonify(errno=RET.PARAMERR, errmsg='手机号码格式错误')

    # 检查密码是否一致
    if password != password2:
        return jsonify(errno=RET.PWDERR, errmsg='两次密码不一致')

    # 检查短信验证码是否正确
    try:
        real_sms_code = int(redis_store.get(f'sms_code_{mobile}'))
    except Exception as e:
        return jsonify(errno=RET.DBERR, errmsg='读取真实手机验证码错误')

    if real_sms_code is None:
        return jsonify(errno=RET.NODATA, errmsg='短信验证码失效')

    # 删除短信验证码,防止重复利用
    try:
        redis_store.delete(f'sms_code_{mobile}')
    except Exception as e:
        current_app.logger.error(e)

    # print(real_sms_code, sms_code)
    if real_sms_code == sms_code:
        return jsonify(errno=RET.DATAERR, errmsg='短信验证码错误')

    # 3. 检查手机唯一性
    # try:
    #     user = User.query.filter_by(mobile=mobile).first()
    # except Exception as e:
    #     current_app.logger.error(e)
    #     return jsonify(errno=RET.DATAEXIST, errmsg='手机已注册')

    # 4. 保存用户信息
    user = User(name=mobile, mobile=mobile)
    user.password = password

    try:
        db.session.add(user)
        db.session.commit()
    # 由于手机号码具有唯一性, 一旦重复插入就会报错, 利用此性质, 不需要进行查询
    # 会抛出 IntegrityError 异常,减少数据库操作
    except IntegrityError as e:
        db.session.rollback()
        # 手机号重复
        current_app.logger.error(e)
        return jsonify(errno=RET.DATAEXIST, errmsg='手机已注册')
    except Exception as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg='数据库异常')

    # 保存登录状态到session中
    session['name'] = mobile
    session['mobile'] = mobile
    session['user_id'] = user.id

    return jsonify(errno=RET.OK, errmsg='注册成功')

05_注册前端编写

$(document).ready(function () {
    generateImageCode();
    $("#mobile").focus(function () {
        $("#mobile-err").hide();
    });
    $("#imagecode").focus(function () {
        $("#image-code-err").hide();
    });
    $("#phonecode").focus(function () {
        $("#phone-code-err").hide();
    });
    $("#password").focus(function () {
        $("#password-err").hide();
        $("#password2-err").hide();
    });
    $("#password2").focus(function () {
        $("#password2-err").hide();
    });
    // 为表单的提交添加自定义函数 (提交事件)
    $(".form-register").submit(function (e) {
        // 阻止浏览器默认的表单提交事件
        e.preventDefault();
        mobile = $("#mobile").val();
        phoneCode = $("#phonecode").val();
        passwd = $("#password").val();
        passwd2 = $("#password2").val();
        if (!mobile) {
            $("#mobile-err span").html("请填写正确的手机号!");
            $("#mobile-err").show();
            return;
        }
        if (!phoneCode) {
            $("#phone-code-err span").html("请填写短信验证码!");
            $("#phone-code-err").show();
            return;
        }
        if (!passwd) {
            $("#password-err span").html("请填写密码!");
            $("#password-err").show();
            return;
        }
        if (passwd != passwd2) {
            $("#password2-err span").html("两次密码不一致!");
            $("#password2-err").show();
            return;
        }

        var req_data = {
            "mobile": mobile,
            "password": passwd,
            "password2": passwd2,
            "sms_code": phoneCode
        };
        var req_json = JSON.stringify(req_data);
        $.ajax({
            type: "post",
            url: "/api/v1.0/users",
            data: req_json,
            contentType: "application/json",
            dataType: "json",
            headers: {
                "X-CSRFToken": getCookie("csrf_token")
            },
            success: function (rsp) {
                if (rsp.errno == 0) {
                    location.href = "/index.html"
                } else {
                    alert(rsp.errmsg)
                }
            }
        });
    });
})

爱家租房项目①_第26张图片

06_补充csrf的逻辑

// js读取cookie的方法
function getCookie(name) {
    var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
    return r ? r[1] : undefined;
}

var req_json = JSON.stringify(req_data);
       $.ajax({
           type: "post",
           url: "/api/v1.0/users",
           data: req_json,
           contentType: "application/json",
           dataType: "json",
           headers: {
               "X-CSRFToken": getCookie("csrf_token")
           }, // 请求头
           success: function (rsp) {
               if (rsp.errno == 0) {
                   location.href = "/index.html"
               } else {
                   alert(rsp.errmsg)
               }
           }
       });

你可能感兴趣的:(#,Flask)