8Flask-----------Flask框架------------安装使用、基本介绍

Flask

  • 一、Flask 简介
    • (一) 环境搭建
    • (二)运行项目
  • 二、项⽬内容介绍
    • (一)app.route路由URL设定
        • 1)传参
        • 2)请求方式
    • (二)def视图函数
        • 1)构造URL(url_for)
        • 2)重定向(页面跳转)
        • 3)返回对象
        • 4)调用模板
    • (三)配置文件
    • (四)静态⽂件的配置
    • (五)类视图
    • (六)蓝图
    • (七)子域名
    • (八)Jinja2模版
      • 1)过滤器
      • 2)控制语句
        • `1.if`
        • `2.for…in…`
        • `3.宏macro`
        • `4.include`
        • `4.赋值(set)语句`
        • `5.模板继承block`
    • (九)数据库
      • 1)SQLAlchemy介绍
      • 2)ORM 介绍
        • 3)外键
        • 4)Flask-SQLAlchemy插件
        • 5)数据库迁移Migrate
    • (十)Flask-Script
    • (十一)cookie和session
    • (十一)Flask的上下文
    • (十二)Restful API规范
    • (十二)Flask-Restful插件
    • (十三)memcached
  • 三、项⽬结构搭建
    • (一) 项目介绍
    • (二) 项目搭建
    • (三)业务逻辑实现
      • 1.用户注册
        • `1.1用户注册表单接口`
        • `1.2图形验证码接口`
        • `1.3短信验证码接口`
      • 2.用户登录
        • `2.1用户登录接口`
        • `2.2退出登录接口`
        • `2.3检查登录状态接口`
        • `2.4 登入装饰器`
      • 3.个人信息
        • `3.1 获取个人信息`
        • `3.2 用户上传头像`
        • `3.3 修改用户名接口设计`
        • `3.4 获取用户的实名认证信息`
        • `3.5 保存实名认证信息`
      • 4. 发布房源
        • `4.1 城区信息数据`
        • `4.2 保存房屋的基本信息`
        • `4.3 保存房屋的图片`
        • `4.4 获取房东发布的房源`
        • `4.5 获取主页展示的房屋基本信息`
        • `4.6 获取房源详情接口定义`
        • `4.7 房屋的搜索页面`
      • 5.订单
        • `5.1 保存订单`
        • `5.2 查询用户的订单信息`
      • 6.支付平台--支付宝
        • `6.1 发起支付宝支付`
        • `6.2 保存订单结果`

一、Flask 简介

引入

  • Flask 是⼀款当下⾮常流⾏的 Python Web框架,出⽣于2010年,作者是Armin Ronacher,本来这个项⽬只是作者在愚⼈节的⼀个玩笑,后来由于⾮常受欢迎, 进⽽成为⼀个正式的项⽬。
  • 用来完成网页开发后端部分

Flask流⾏原因

  • 微框架、简洁、只做他需要做的,给开发者提供了很⼤的扩展性;
  • Flask和相应的插件写得很好,⽤起来很爽;
  • 开发效率⾮常⾼,⽐如使⽤ SQLAlchemy 的 ORM 操作数据库可以节省开发者⼤量书写 sql 的时间。
  • Flask的灵活度⾮常之⾼,他不会帮你做太多的决策,⼀些你都可以按照⾃⼰的意愿进⾏更改。
  • 使⽤Flask开发数据库的时候,具体是使⽤ SQLAlchemy 还是 MongoEngine,选择权完全掌握在你⾃⼰的⼿中。区别于 Django,Django 内置了⾮常完善和丰富的功能,并且如果你想替换成你⾃⼰想要的,要么 不⽀持,要么 ⾮常麻烦。
  • 把默认的 Jinija2 模板引擎替换成其他模板引擎都是⾮常容易的

在这里插入图片描述

(一) 环境搭建

1.安装pipenv

  • pip install pipenv

2.创建虚拟环境

  • 创建启动进入虚拟环境方式:
    • 新建一个文件夹,文件夹地址下运行pipenv shell
    • pipenv 有个缺点,lock 不稳定⽽且时间⾮常⻓,所以安装包的时候记得加上 – skip-lock ,最后开发完成要提价到仓库的时候在 pipenv lock8Flask-----------Flask框架------------安装使用、基本介绍_第1张图片
  • 会生成这个文件
    8Flask-----------Flask框架------------安装使用、基本介绍_第2张图片
    [[source]]
    name = "pypi"
    url = "https://pypi.org/simple"				# 指定国内 pip 源,不然下载库会很慢
    verify_ssl = true							# 安全验证信息
    
    [dev-packages]								# 开发环境
    
    [packages]									# ⽣产环境
    django = "*"								# * 表示最新版本
    
    [requires]									# Python 的版本号
    python_version = "3.6"
    

3.创建系统环境变量

  • WORKON_HOME 这个 环境变量名 是一个固定写法。
    8Flask-----------Flask框架------------安装使用、基本介绍_第3张图片

4.创建项目
8Flask-----------Flask框架------------安装使用、基本介绍_第4张图片
8Flask-----------Flask框架------------安装使用、基本介绍_第5张图片

5.进入 / 退出 / 删除 虚拟环境

pipenv shell	# 进⼊虚拟环境
exit			# 退出虚拟环境
pipenv --rm		# 删除整个环境 不会删除 pipfile
pipenv install --dev itchat #安装在开发环境下
freeze > c:/xxx/requirements.txt    # requirements.txt 多人开发使用包的整合:

6.下载 Flask 框架

  • pip install flask
  • 查看 Flask 框架是否完成下载 pip list
    8Flask-----------Flask框架------------安装使用、基本介绍_第6张图片

(二)运行项目

1.初始化Flask实例

  • app = Flask(_name_)

2.创建视图函数:给函数加上app.route装饰器,就是一个视图函数

  • 固定写法@app.route(’/’) route(self, rule, **options) rule是url地址,字符串类型 就是一个装饰器
  • @app.route(’/’) # 路由,这个例子是返回本机的

3.运行项目app.run()

  • 点击运⾏,在浏览器中输⼊ http://127.0.0.1:5000 就能看到视图函数的运行结果

  • app.run 这种⽅式只适合于开发,如果在⽣产环境中, 应该使⽤ Gunicorn 或者 uWSGI 来启动。如果是在终端运⾏的,可以按 ctrl + c 来让服务停⽌。

    • app.run(self, host=None, port=None, debug=None, **options)
      • port默认5000,可以自己定义
      • debug:开启debug=True。开发过程中开启有利于观察代码问题,生产环境就把这个删掉
  • 开启debug方法

    • 硬编码
      • app.run(debug=True) 常用
      • app.debug = True
      • app.config:app.config是字典的子类,instance(app.config,dict)
        • app.config.update(DEBUG=True)
        • app.config.update({‘DEBUG’: True})
        • app.config[‘DEBUG’]=True
    • 设置配置文件
      • app.config.from_object(config) 或 app.config.from_object(‘config’)
      • app.config.from_pyfile(‘configs.py’, silent=True) #3-1 这个方法要写全称 相对路径,silent=True如果文件错误代码行不会报错
  • 注意:

    • 在开启了 DEBUG模式 后,当程序有异常⽽进⼊错误堆栈模式,你第⼀次点击某个堆栈想查看变量值的时候,⻚⾯会弹出⼀个对话框,让你输⼊ PIN值, 这个 PIN值 在你启动的时候就会出现,比如在刚刚启动的项⽬中的 PIN值 为 294- 745-044,你输⼊这个值后,Werkzeug 会把这个 PIN值 作为 cookie 的⼀部分保存起来,并在8⼩时候过期,8⼩时以内不需要再输⼊PIN值。这样做的⽬的是为 了更加的安全,让调试模式下的攻击者更难攻击到本站
      from flask import Flask
      
      # 主程序入口__name__
      app = Flask(__name__)
      
      # 固定写法@app.route('/') route(self, rule, **options) rule是url地址,字符串类型 就是一个装饰器
      @app.route('/')  # 路由,这个例子是返回本机的
      def hello_world():
          a = 1/0
          return 'hello world!'
      
      if __name__ == '__main__':
          # app.run(debug=8000)# 端口默认是5000,修改的话添加参数port=
          # 交互模式接口console
          # debug模式,程序如果出错了,也会帮我处理,没有开debug出错不好观察,开发过程才开debug模式,要不然会暴露错误
          # app.config.update(DEBUG=True, secret_key=xxxxxxxx) #3  update字典集合也有,app.config是字典的子类,instance(app.config,dict)
          # app.config.update({'DEBUG': True}) #4
          # app.config['DEBUG']=True #5 硬编码,不好修改
          # app.debug = True  #2
      

二、项⽬内容介绍

(一)app.route路由URL设定

app.route(self, rule, **options)

  • rule:路由,网站地址,string类型
    • /:是作为参数的分隔符
      • 如果结尾没加/,访问就不能加/结尾,会抛出⼀个 404错误 页面,
      • 如果结尾加了/,访问结尾加与不加都可以,网页最终都会自动带上有斜线的URL
1)传参
  • <>传参:
    • :converter 就是类型名称,可以指定参数的类型,也可以不指定8Flask-----------Flask框架------------安装使用、基本介绍_第7张图片
  • ?=关键字传参:如果不想定制⼦路径来传递参数,也可以通过传统的 ?= 的形式来传递参数,例 如:/article?id=xxx,这种情况下,可以通过 request.args.get(‘id’) 来获取 id 的值。如果是 post ⽅法,则可以通过 request.form.get(‘id’) 来进⾏获取。
    from flask import Flask, request
    
    # app实例化
    app = Flask(__name__)
    
    # 视图函数,路由来装饰视图函数
    # URL 统一资源定位符,视图是由路由映射到函数上面
    # 让他们产生这样一个关系,然后就可以通过路由去调用函数,得到函数结果
    @app.route('/')  #视图函数
    def hello_python():
        return 'hello python'
    
    # string类型:/是作为参数的分隔符,不能再后面加/,如果URL定义加了/,网页地址栏加不加就都可以,不加会自动补齐
    # 需要以/为严格的结尾,传参,path类型(而且还可以添加参数,会把整体看成一个字符串),
    # int...,string类型也可以传数字
    # uuid类似md5、64加密的,同一个类型的md5是一直的,是会有重复的 ,uuid不会重复
    @app.route('/list/')
    def article(sid): # sid参数,url/list/sid
        return '这是第{}篇文章'.format(sid)
    
    # any()字符约束dict, tuple写了几个就能访问几个,调用同一个函数
    # url_path固定写法
    @ app.route('//')
    def item(url_path):
        # 查看是访问哪个路由
        return url_path
    
    # 用?作为分隔符
    @app.route('/ie')
    def baidu():
        return request.args.get('name')
    
    
    if __name__ == '__main__':
        app.run(debug=True)
    
    
2)请求方式
  • methods请求方法
    • 默认是get请求,如果需要post请求,则添加这个参数并设置为methods=[‘GET’,‘POST’],post请求会把信息携带在form表单上,
    • 接收数据:get请求—request.args.get(‘name’)拿到url的?后面的参数,post请求—request.form.get(‘name’)拿到表单的数据
      • 拿到的是post请求,参数放在的方式不同,postman模拟访问请求,可以模拟post请求。 print(request.form.get(‘name’))
      from flask import Flask, url_for, request
      
      app = Flask(__name__)
      
      # 默认是get请求,u请求信息体现在url上,
      # post请求会把信息携带在form表单上
      # postman模拟访问请求,可以模拟post请求
      @app.route('/', methods=['GET','POST'])
      def hello_world():
          # print(request.args.get('name'))request让url可以携带参数name,会返回参数值
          # 拿到的是post请求,参数放在的方式不同
          print(request.form.get('name'))
          return 'hello world'
      
      
      if __name__ == '__main__':
          app.run(debug=True)
      

(二)def视图函数

1)构造URL(url_for)

引入

  • ⼀般我们通过⼀个 URL 就可以执⾏到某⼀个函数。如果反过来,我们知道⼀个函数,怎么去获得这个 URL 呢?url_for 函数就可以帮我们实现这个功能。url_for() 函数接收两个及以上的参数
    • 第⼀个参数:接收函数名作为第⼀个参数
    • 命名参数:接收对应 URL 规则的命名参数,如果还出现其他的参数,则会添加到 URL 的后⾯作为查询参数。

定义

  • 通过构建 URL 的方式,直接在代码中拼接 URL

特点

  • 将来如果修改了URL,但没有修改该URL对应的函数名,就不⽤到处去替换 URL了。
  • url_for()函数会转义⼀些特殊字符和unicode字符串,这些事情url_for会⾃动的帮我们搞定
    from flask import Flask,url_for
    app = Flask(__name__)
    
    @app.route('/article//')
    def article(id):
    	return '%s article detail' % id
    
    @app.route('/')
    def index():
    	print(url_for("article",id=1))
    	return "⾸⻚"
    
2)重定向(页面跳转)

重定向定义

  • 重定向分为 永久性重定向 和 暂时性重定向,在⻚⾯上体现的操作就是浏览器会从⼀个⻚⾯⾃动跳转到另外⼀个⻚⾯。⽐如⽤户访问了⼀个需要权限的⻚⾯, 但是该⽤户当前并没有登录,因此我们应该给他重定向到登录页面。
    • 永久性重定向:http 的状态码是 301,多⽤于旧⽹址被废弃了要转到⼀个新的 ⽹址确保⽤户的访问,最经典的就是京东⽹站,你输⼊ www.jingdong.com 的时候,会被重定向到 www.jd.com,因为 jingdong.com 这个网址已经被废弃了,被改成 jd.com,所以这种情况下应该⽤永久重定向。
    • 暂时性重定向:http 的状态码是 302,表示⻚⾯的暂时性跳转。⽐如访问⼀个需要权限的⽹址,如果当前⽤户没有登录,应该重定向到登录⻚⾯,这种情况下,应该⽤暂时性重定向。

设置重定向

  • flask.redirect(location,code=302)
    • location 表示需要重定向到的 URL,应该配合之前讲的 url_for() 函数来使用
    • code 表示采⽤哪个重定向,默认是 302 也即暂时性重定向,可以修改成 301 来实现永久性重定向
    from flask import Flask,url_for,redirect
    
    app = Flask(__name__)
    app.debug = True
    
    @app.route('/login/',methods=['GET','POST'])
    def login():
    	return 'login page'
    
    @app.route('/profile/',methods=['GET','POST'])
    def profile():
    	name = request.args.get('name')
    	if not name:
    		return redirect(url_for('login'))
    	else:
    		return name
    
3)返回对象
  • return:返回页面的信息
    • 可以返回字符串、元组、Response对象、字典(只显示key)
      • 字符串:其实 Flask 是根据返回的字符串类型,重新创建⼀个 werkzeug.wrappers.Response 对象,Response 将该字符串作为主体,状态码为 200,MIME类型 为 text/html,然后返回该 Response对象。
      • 元组:元组中格式是 (response,status,headers),response 为⼀个字符串, status 值是状态码,headers 是⼀些响应头。不加括号也是元组的格式
    • 返回的对象不可以是列表,这样写的([‘python’, ‘JAVA’])也不行
  • Response响应:
    • 1.直接使⽤ Response 创建
    • 2.可以使⽤ make_response 函数来创建 Response 对象
    1.直接使⽤ Response 创建
    from werkzeug.wrappers import Response
    
    @app.route('/about/')
    def about():
    resp = Response(response='about page',status=200,content_type='te xt/html;charset=utf-8')
    return resp
    
    # 2.可以使⽤ make_response 函数来创建 Response 对象,这个⽅法可以设置额外的数据,⽐如设置 cookie,header信息
    from flask import make_response
    
    @app.route('/about/')
    def about():
    	return make_response('about page')
    
4)调用模板
  • 模板是⼀个 web开发 必备的模块。因为我们在渲染⼀个网页的时候,并不是只渲染⼀个纯⽂本字符串,⽽是需要渲染⼀个有富⽂本标签的⻚⾯。这时候我们 就需要使⽤模板了。
  • Flask 中,配套的模板是 Jinja2,Jinja2 的作者也是 Flask 的作者。这个模板⾮常的强⼤,并且执⾏效率⾼。以下对 Jinja2 做⼀个简单介绍。
  • template_folder是模板的文件夹,默认templates,一般情况不要修改templates存放文件夹的名称,所以也不需要添加这个参数

Flask 渲染 Jinja模板

  • 要渲染⼀个模板,通过 render_template ⽅法即可
    # 1.当访问 /about/ 的时候,about() 函数会在当前⽬录下的 templates 文件夹下寻找 about.html 模板⽂件。
    from flask import Flask,render_template
    app = Flask(__name__)
    
    @app.route('/about/')
    def about():
    	return render_template('about.html') 
    # 2.如果想更改模板⽂件地址,应该在创建 app 的时候,给 Flask 传递⼀个关键字参数 template_folder,指定具体的路径:
    from flask import Flask,render_template
    app = Flask(__name__,template_folder=r'C:\templates')
    
    @app.route('/about/')
    def about():
    	return render_template('about.html')
    

模板的参数传递

  • render_template 需要传递的是⼀个关键字参数,所以第⼀种⽅式是顺其⾃然的。但是当你的模板中要传递的参数过多的时候,把所有参数放在⼀个函数中显然不是⼀个好的选择,因此我们使⽤字典进⾏包装,并且加两个 * 号,来转换成关键字参数。
  • 传过去的是变量,接收显示{{变量}},可以username='ad’传人,但参数一般都多,用字典传,前端也可以context.username,参数context=context就可以,但是网页编写就必须取值,所以可以直接解包
  • **context拆包,关键字传参,这样页面编写代码就可以直接用变量了
    from flask import Flask,render_template
    app = Flask(__name__)
    # app = Flask(__name__, template_folder='templates')
    
    @app.route('/about/')
    def about():
    	# 第⼀种⽅式
    	# return render_template('about.html',user='luoji')  
    	# 第二种⽅式
    	return render_template('about.html',**{'user':'zhiliao'}) 
    

(三)配置文件

配置文件

  • Flask项⽬的配置,都是通过 app.config 对象来进⾏配置的,app.config是字典的子类,instance(app.config,dict),所以可以通过字典的的类型来设置值
  • 如果你的配置项特别多,你可以把所有的配置项都放在⼀个模块中,然后通过加载模块的⽅式进⾏配置,假设有⼀个 settings.py 模块,专门用来存储配置项 的,此时你可以通过 app.config.from_object() ⽅法进⾏加载,并且该⽅法既可 以接收模块的的字符串名称,也可以模块对象:
    # 1. 通过模块字符串
    import settings
    app.config.from_object('settings')
    
    # 2. 通过模块对象
    import settings
    app.config.from_object(settings)
    
    2. 通过文件
    app.config.from_pyfile('settings.py',silent=True)
    

(四)静态⽂件的配置

  • Web应⽤中会出现⼤量的静态⽂件来使得⽹⻚更加⽣动美观。类似于 CSS样式⽂件 、JavaScript脚本⽂件 、图⽚⽂件 、字体⽂件 等 静态资源。在 Jinja2 中加载静态⽂件⾮常简单,只需要通过 url_for 全局函数就可以实现
  • url_for 函数默认会在项⽬根⽬录下的 static⽂件夹 中寻找 about.css⽂件,如果 找到了,会⽣成⼀个相对于项⽬根⽬录下的 /static/about.css 路径。当然我们也可以把静态⽂件不放在 static⽂件夹 中,此时就需要具体指定了:
    # 默认
    
    # 指定路径
    app = Flask(__name__,static_folder='C:\static')
    

(五)类视图

  • 之前我们接触的视图都是函数,所以⼀般简称视图函数。其实视图也可以基于 类来实现,类视图的好处是⽀持继承,但是类视图不能跟函数视图⼀样,写完 类视图还需要通过 app.add_url_rule(url_rule,view_func) 来进⾏注册。

1.标准类视图

  • 标准类视图是继承⾃ flask.views.View,并且在⼦类中必须实现 dispatch_request 方法,这个方法类似于视图函数,也要返回⼀个基于 Response 或者其子类的对象。
    from flask.views import View
    class PersonalView(View):
    	def dispatch_request(self):
    		return "CSDN"
    
    # 类视图通过add_url_rule⽅法和url做映射
    app.add_url_rule('/users/',view_func=PersonalView.as_view('persona lview'))
    

2.基于调度方法的视图

  • Flask 还为我们提供了另外⼀种类视图 flask.views.MethodView,对每个 HTTP 方法执行不同的函数(映射到对应方法的小写的同名方法上)
    class LoginView(views.MethodView):
    	# 当客户端通过get⽅法进⾏访问的时候执⾏的函数
    	def get(self):
    		return render_template("login.html")
    
    	# 当客户端通过post⽅法进⾏访问的时候执⾏的函数 
    	def post(self):
    		email = request.form.get("email")
    		password = request.form.get("password")
    		if email == '[email protected]' and password == '111111':
    			return "登录成功!"
    		else:
    			return "⽤户名或密码错误!"
    
    # 通过add_url_rule添加类视图和url的映射,并且在as_view⽅法中指定该url的名 称,⽅便url_for函数调⽤
    
    app.add_url_rule('/myuser/',view_func=LoginView.as_view('loginview'))
    

3.装饰器下的类视图

  • 用类视图的⼀个缺陷就是⽐较难⽤装饰器来装饰,⽐如有时候需要做权限验证 的时候
    from flask import session 
    def login_required(func):
    	def wrapper(*args,**kwargs):
    		if not session.get("user_id"):
    			return 'auth failure'
    		return func(*args,**kwargs)
    	return wrapper 
    # 装饰器写完后,可以在类视图中定义⼀个属性叫做decorators,然后存储装饰器。以后每次调⽤这个类视图的时候,就会执⾏这个装饰器
    class UserView(views.MethodView):
    	decorators = [user_required]
    	...
    

(六)蓝图

  • 之前我们写的 url 和视图函数都是处在同⼀个⽂件,如果项⽬⽐较⼤的话,这显然不是⼀个合理的结构,⽽蓝图可以优雅的帮我们实现这种需求。
    -----------------------------urls.py----------------------------
    from flask import Flask, url_for
    from blueprints.book import book_bp
    from blueprints.news import news_bp
    from blueprints.cms import cms_bp
    
    app = Flask(__name__)
    app.register_blueprint(book_bp)
    app.register_blueprint(news_bp)
       
    if __name__ == '__main__':
       app.run(debug=True)
    
     --------------------------blueprints/book.py---------------------
    from flask import Blueprint, render_template
    
    book_bp = Blueprint('book', __name__, url_prefix='/book', template_folder='logic', static_folder='lgcode')
    
    @book_bp.route('/')
    def book():
       return render_template('book.html')
    
    @book_bp.route('/detail/')
    def book_detail(bid):
       return '这是图书第%s页' % bid
    --------------------------news.py---------------------  
    from flask import Blueprint
    
    news_bp = Blueprint('news', __name__)
    
    
    @news_bp.route('/news/')
    def news():
       return '这是新闻首页'
    

寻找静态文件

  • 默认不设置任何静态⽂件路径, Jinja2 会在项⽬的 static ⽂件夹中寻找静态⽂件。也可以设置其他的路径,在初始化蓝图的时候, Blueprint 这个构造 函数,有⼀个参数 static_folder 可以指定静态⽂件的路径:
    • bp = Blueprint(‘admin’,name,url_prefix=’/admin’,static_folder= ‘static’)
  • static_folder 可以是相对路径(相对蓝图⽂件所在的⽬录),也可以是绝对路径。在配置完蓝图后,还有⼀个需要注意的地⽅是如何在模板中引⽤静态 ⽂件。在模板中引⽤蓝图,应该要使⽤ 蓝图名+.+static 来引⽤:

寻找模板文件

  • 跟静态文件⼀样,默认不设置任何模板⽂件的路径,将会在项⽬的 templates 中寻找模板⽂件。也可以设置其他的路径,在构造函数 Blueprint 中有⼀个 template_folder 参数可以设置模板的路径:
    • bp = Blueprint(‘admin’,name,url_prefix=’/admin’,template_folde r=‘templates’)
  • 模板文件 和 静态文件 有点区别,以上代码写完以后,如果你渲染⼀个模板 return render_template(‘admin.html’) , Flask 默认会去项目根目录下的 templates 文件夹中查找 admin.html ⽂件,如果找到了就直接返回,如果没有找到,才会去蓝图⽂件所在的⽬录下的 templates 文件夹中寻找。

url_for 生成 url

  • ⽤ url_for ⽣成蓝图的 url ,使⽤的格式是: 蓝图名称+.+视图函数名称 。比如要获取 admin 这个蓝图下的 index 视图函数的 url :
    • url_for(‘admin.index’)
    • 其中这个蓝图名称是在创建蓝图的时候,传⼊的第⼀个参数。
      bp = Blueprint(‘admin’,name,url_prefix=’/admin’,template_folde r=‘templates’)

(七)子域名

  • 子域名在许多⽹站中都⽤到了,⽐如⼀个⽹站叫做 xxx.com ,那么我们可以定义⼀个⼦域名 cms.xxx.com 来作为 cms管理系统 的⽹址,⼦域名的实现⼀般也是通过蓝图来实现。
  • 在之前章节中,我们创建蓝图的时候添加了⼀个 url_prefix=/user 作为 url前缀,那样我们就可以通过 /user/ 来访问 user 下的 url,但 使⽤⼦域名则不需要。
  • 另外,还需要配置 SERVER_NAME ,比如 app.config[SERVER_NAME]=‘example.com:9000’ 。并且在注册蓝图的时候,还需要添加⼀个 subdomain 的参数,这个参数就是⼦域名的名称:
    from flask import Blueprint
    bp = Blueprint('admin',__name__,subdomain='admin')
    
    @bp.route('/')
    def admin():
    	return 'Admin Page' 
    
    # 这个没有多⼤区别,接下来看主 app 的实现:
    from flask import Flask
    import admin
    
    # 配置`SERVER_NAME`
    app.config['SERVER_NAME'] = 'example.com:8000'
    
    # 注册蓝图,指定了subdomain
    app.register_blueprint(admin.bp)
    if __name__ == '__main__':
    	app.run(host='0.0.0.0',port=8000,debug=True) 
    
    # 写完以上两个文件后,还是不能正常的访问 admin.example.com:8000 这个⼦域名
    # 因为我们没有在 host ⽂件中添加域名解析,你可以在最后添加⼀⾏ 127.0.0.1 admin.example.com ,就可以访问到了。另外,⼦域名不能 在 127.0.0.1 上出现,也不能在 localhost 上出现。
    

(八)Jinja2模版

1)过滤器

  • 过滤器是通过管道符号 | 进⾏使⽤的,例如:{{ name | length }},将返回 name 的⻓度。过滤器相当于是⼀个函数,把当前的变量传⼊到过滤器中,然后 过滤器根据⾃⼰的功能,再返回相应的值,之后再将结果渲染到⻚⾯中。Jinja2 中内置了许多过滤器,在这⾥可以看到所有的过滤器:
过滤器 作用
default(value,default_value,boolean=false) 设置默认值,如果当前变量没有值,则会使⽤参数中的值来代替。boolean=False 默认是在只有这个变量为 undefined 的时 候才会使⽤
safe(value) 如果开启了全局转义,那么 safe 过滤器会将变量关掉转义。
abs(value) 返回⼀个数值的绝对值。
escape(value) 或 e 转义字符 会将 < 、> 等符号转义成 HTML 中的符号。
int、float、string 将值转换为 int 、 float 、字符串类型
length(value) 返回⼀个 序列 或者 字典 的⻓度。
first、last 返回⼀个序列的第⼀个、最后⼀个元素。
join(value,d=u’’) 将⼀个序列⽤ d 这个参数的值拼接成字符串
format(value,*args,**kwargs) 格式化字符串。例如这段代码:{{ “%s” - “%s”|format(‘Hello?’,“Foo!”) }}将输出:Helloo? - Foo!
trim 截取字符串前⾯和后⾯的空⽩字符。
truncate(value,length=255,killwords=False) 截取 length ⻓度的字符串
striptags(value) 删除字符串中所有的 HTML标签,如果出现多个空格,将替换成⼀个空格。
replace(value,old,new) 替换将 old 替换为 new 的字符串。
wordcount(s) 计算⼀个⻓字符串中单词的个数。
lower、upper 将字符串转换为⼩、大写。

2)控制语句

定义

  • Jinja2 中所有的控制语句都是放在 {% … %} 中,并且有⼀个语句 {% endxxx %} 来进行结束。常用的控制语句if、for … in
1.if
  • Jinja2 中的 if 语句的使用 和 python中的使用方法类似
  • 可以使⽤ > ,< ,<= ,>= ,== ,!= 来进⾏判断,也可以通过 and ,or ,not ,() 来进⾏逻辑合并操作
    {% if kenny.sick %}
    	Kenny is sick.
    {% elif kenny.dead %}
    	You killed Kenny! You bastard!!!
    {% else %}
    	Kenny looks okay --- so far
    {% endif %} 
    
2.for…in…
  • or 循环可以遍历任何⼀个序列包括 列表 、字典 、元组。并且可以进行反向遍历。
  • 不可以使⽤ continue 和 break 表达式来控制循环的执⾏。
    # 1.普通的遍历:
    
      {% for user in users %}
    • {{ user }}
    • {% endfor %}
    # 2.遍历字典:
    {% for key, value in my_dict.items() %}
    {{ key }}
    {{ value }}
    {% endfor %}
    # 3.如果序列中没有值的时候,进⼊ else :
      {% for user in users %}
    • {{ user.username }}
    • {% else %}
    • no users found
    • {% endfor %}
    • Jinja2 中的 for循环 还包含以下变量,可以⽤来获取当前的遍历状态
      8Flask-----------Flask框架------------安装使用、基本介绍_第8张图片
3.宏macro

定义

  • 模板中的 宏 跟 Python 中的 函数 类似,可以传递参数,但是不能有返回值,可以将⼀些经常⽤到的代码⽚段放到宏中,然后把⼀些 不固定的值 抽取出来当成⼀个 变量。
# 抽取出了⼀个  标签,指定了⼀些 默认参数。
{% macro input(name, value='', type='text') %}
	
{% endmacro %} 
# 那么我们以后创建  标签的时候,可以通过他快速的创建:

{{ input('username') }}

{{ input('password', type='password') }}

导入宏

  • 在真实的开发中,会将⼀些 常⽤的宏 单独 放在⼀个⽂件中,在需要使⽤的时候,再从这个⽂件中进⾏导⼊。
  • import 语句的⽤法跟 Python 中的 import 类似,可以直接 import…as…,也可以 from…import… 或者 from…import…as… 。
  • 另外需要注意的是,导⼊模板并不会把当前上下⽂中的变量添加到被导⼊的模 板中,如果你想要导⼊⼀个需要访问当前上下⽂变量的宏,有两种可能的⽅法
    • 1.显式地传⼊请求或请求对象的属性作为宏的参数;
    • 2.与上下⽂⼀起(with context)导⼊宏。
      # 假设现在有⼀个⽂件,叫做 forms.html,⾥⾯有两个宏分别为 input 和 textarea
      # forms.html:
      {% macro input(name, value='', type='text') %}
      	
      {% endmacro %}
      
      {% macro textarea(name, value='', rows=10, cols=40) %}
      	
      {% endmacro %} 
      
      # 在其他文件中导入宏
      # import...as... 形式 :
      {% import 'macro.html' as macro %}
      
      	⽤户名:
      	{{ macro.input('username') }}
       
      # from...import...as... / from...import... 形式 :
      {% from "macro.html" import input %} 2  3 密码: 4 {{ input("password",type="password") }} 5 
      
      # 与上下⽂中⼀起(with context)导⼊的⽅式:
      {% import 'macro.html' as macro with context %}
      
4.include
  • include 语句可以把 ⼀个模板 引⼊到 另外⼀个模板 中,类似于 把⼀个模板的代码 copy到 另外⼀个模板的 指定位置
{% include 'header.html' %}
	主体内容
{% include 'footer.html' %}
4.赋值(set)语句
  • 有时候我们想在模板中 添加变量,这时候 赋值语句(set) 就派上⽤场了
    # 那么以后就可以使⽤ name 来代替 juran 这个值了;
    {% set name='juran' %}
    # 同时,也可以给他赋值为列表 和 元组:
    {% set navigation = [('index.html', 'Index'), ('about.html', 'Abou t')] %}
    

with 代码块

  • 赋值语句创建的变量 在其之后 都是有效的,如果不想让⼀个变量污染全局环境,可以使⽤ with 语句来创建⼀个内部的作⽤域,将 set 语句放在其中,这样创建的变量只在 with 代码块中才有效。
    # 这两种⽅式都是等价的,⼀旦超出 with 代码块,就不能再使⽤ foo 这个变量 了
    {% with %}
    	{% set foo = 42 %}
    	{{ foo }}		foo is 42 here
    {% endwith %}
    # 或
    {% with foo = 42 %}
    	{{ foo }}
    {% endwith %} 
    
5.模板继承block
  • Flask 中的模板可以继承,通过继承可以把模板中许多重复出现的元素抽取出 来,放在⽗模板中,并且⽗模板通过定义 block 给⼦模板开⼀个⼝,⼦模板根 据需要,再实现这个 block
  • 另外,模板中不能出现重名的 ,如果⼀个地⽅需要⽤到另外⼀个 中的内容,可以使⽤ self.blockname 的⽅式进⾏引⽤
  • ⾥⾯调⽤了 super() 这个函数,这个函数的⽬的是执⾏⽗模板中的代码,把⽗模板中的内容添加到⼦模板中,如果没有这⼀句,则⽗模板中代码将会被⼦模板中的代码给覆盖掉。
  • ⼦模板中,所有的⽂本标签和代码都要添加到从⽗模板中继承的 中。否则,这些⽂本和标签将不会被渲染。
    # 假设现在有⼀个 base.html 这个⽗模板:
    ---------------------------base.html--------------父模板----------
    
    
    
    	
    	{% block title %}{% endblock %}
    	{% block head %}{% endblock %}
    
    
    	
    {% block body %}{% endblock %}
    # 以上base.html⽗模板中,抽取了所有模板都需要⽤到的元素 、 等 # 并且对于⼀些所有模板都要⽤到的样式⽂件 style.css 也进⾏了抽取, # 同时对于⼀些⼦模板需要重写的地⽅,⽐如 、 <head> 、 <body> 都定义成了 <block> ,然后⼦模板可以根据⾃⼰的需要,再具体的实现 ---------------------------index.html--------------子模板---------- # ⾸先第⼀⾏就定义了 ⼦模板 继承的⽗模板,并且可以看到 ⼦模板 实现了 <title> 这个 <block> ,并填充了⾃⼰的内容. {% extends "base.html" %} {% block title %}⾸⻚{% endblock %} {% block head %} {{ super() }} <style type="text/css"> .detail{ color: red; } </style> {% endblock %} {% block content %} <h1>这⾥是⾸⻚</h1> <p class="detail"> ⾸⻚的内容 </p> {% endblock %} </code></pre> <ul> <li>另外,模板中不能出现重名的 ,如果⼀个地⽅需要⽤到另外⼀个 中的内容,可以使⽤ <code>self.blockname</code>的⽅式进⾏引⽤:<pre><code><title> {% block title %} 这是标题 {% endblock %}

    {{ self.title() }}

(九)数据库

MySQL 简介

  • MySQL是⼀个关系型数据库管理系统,由瑞典MySQL AB公司开发,后来被 Sun 公司收购,Sun 公司后来又被 Oracle公司收购,⽬前属于 Oracle旗下产品。

特点

  • 使用C和C++编写,并使⽤了多种编译器进⾏测试,保证源代码的可移植性;
    • ⽀持多种操作系统,如Linux、Windows、AIX、FreeBSD、HP-UX、 MacOS、NovellNetware、OpenBSD、OS/2 Wrap、Solaris等
  • 为多种编程语⾔提供了API,如C、C++、Python、Java、Perl、PHP、 Eiffel、Ruby等
  • ⽀持多线程,充分利⽤CPU资源
  • 优化的SQL查询算法,有效地提⾼查询速度
  • 提供多语⾔⽀持,常⻅的编码如GB2312、BIG5、UTF8
  • 提供TCP/IP、ODBC和JDBC等多种数据库连接途径
  • 提供⽤于管理、检查、优化数据库操作的管理⼯具
  • ⼤型的数据库。可以处理拥有上千万条记录的⼤型数据库
  • 支持多种存储引擎
    • MySQL 软件采⽤了双授权政策,它分为社区版和商业版,由于其体积⼩、 速度快、总体拥有成本低,尤其是开放源码这⼀特点,⼀般中⼩型⽹站的开发都选择MySQL作为⽹站数据库
    • MySQL使⽤标准的SQL数据语⾔形式
    • Mysql是可以定制的,采⽤了GPL协议,你可以修改源码来开发⾃⼰的Mysql 系统
  • 在线DDL更改功能
  • 复制全局事务标识
  • 复制⽆崩溃从机
  • 复制多线程从机

操作数据库

  • 数据库是⼀个网站的基础。Flask 可以使⽤很多种数据库。⽐如 MySQL ,MongoDB ,SQLite ,PostgreSQL 等。这⾥我们以 MySQL 为例进⾏讲解。⽽在 Flask中,如果想要操作数据库,我们可以使⽤ORM来操作数据库,使⽤ORM 操作数据库将变得⾮常简单。
  • 讲解Flask中的数据库操作之前,先要安装这些模块:
    • mysql:如果是在 Windows上,到官⽹下载。如果是 Ubuntu,通过命令 sudo apt-get install mysql-server libmysqlclient-dev -yq 进⾏下载安装 ;
    • pymysql:pymysql 是⽤ Python 来操作 mysql 的包,因此通过 pip来安装,命令如下:pip3 install pymysql ;
    • SQLAlchemy:SQLAlchemy 是⼀个数据库的 ORM框架,我们在后⾯会⽤到。安装命令为:pip3 install SQLAlchemy。学习这个

1)SQLAlchemy介绍

通过 SQLAlchemy 连接数据库 (固定格式,必须紧记!)

  • ⾸先从 sqlalchemy 中导⼊ create_engine,⽤这个函数来创建引擎,然后⽤ engine.connect() 来连接数据库。其中⼀个⽐较重要的⼀点是,通过 create_engine 函数的时候,需要传递⼀个满⾜某种格式的字符串,对这个字符串的格式来进⾏解释:
    • DB_URI = ‘dialect+driver://username:password@host:port/database?charset=utf8’
      • dialect 是数据库的实现,比如 MySQL 、PostgreSQL 、SQLite ,并且转换成小写。
      • driver 是 Python 对应的驱动,如果不指定,会选择默认的驱动,⽐如 MySQL的 默认驱动 是 MySQLdb。
      • username 是连接数据库的 ⽤户名,
      • password 是连接数据库的 密码,
      • host 是连接数据库的 域名,
      • port 是数据库监听的 端⼝号,
      • database 是连接哪个数据库的名字。
      from sqlalchemy import create_engine
      
      # 数据库的配置变量
      HOSTNAME = '127.0.0.1'
      PORT = '3306'
      DATABASE = 'demo0417'
      USERNAME = 'root'
      PASSWORD = 'root'
      DB_URI = 'mysql+pymysql://{}:{}@{}:{}/{}'.format(USERNAME,PASSWOR D,HOSTNAME,PORT,DATABASE)
      
      # 创建数据库引擎
      engine = create_engine(DB_URI)
      
      #创建连接
      with engine.connect() as con:
      	result = con.execute('select * from students')
      	print(result.fetchone()) 
      

操作数据库方法

  • 原生SQL操作数据库----了解
  • 利用ORM 把表映射成类-----掌握

用 SQLAlchemy 执行 原生SQL

from sqlalchemy import create_engine
from constants import DB_URI

#连接数据库
engine = create_engine(DB_URI,echo=True)

# 使⽤with语句连接数据库,如果发⽣异常会被捕获
with engine.connect() as con:

	# 先删除users表
	con.execute('drop table if exists authors')
	
	# 创建⼀个users表,有⾃增⻓的id和name
	con.execute('create table authors(id int primary key auto_inc rement,'name varchar(25))')
	
	# 插⼊两条数据到表中
	con.execute('insert into persons(name) values("abc")')
	con.execute('insert into persons(name) values("xiaotuo")')
	
	# 执⾏查询操作
	results = con.execute('select * from persons')
	
	# 从查找的结果中遍历
	for result in results:
		print(result)

2)ORM 介绍

引入原因

  • 随着项⽬越来越⼤,采⽤ 原⽣SQL 的⽅式在代码中会出现 ⼤量的SQL语句,对项⽬的进展⾮常不利:SQL语句重复利⽤率不⾼,越复杂的SQL语句条件越多,代码越⻓。会出现很多相近似的SQL语句,很多SQL语句是在业务逻辑中拼出来的,如果有数据库需要更改,就要去修改这 些逻辑,很容易漏掉某些SQL语句的修改
    写SQL时容易忽略web安全问题
  • 利用ORM 把表映射成类

ORM介绍:

  • Object Relationship Mapping,对象关系映射,通过ORM我们可以通过类 的⽅式去操作数据库,⽽不⽤写原⽣的SQL语句。通过把表映射成类,把⾏作为实例,把字段作为属性,ORM在执⾏对象操作时候最终还是会把对应的操作转换为 数据库原⽣语句
  • ⽤ORM的优点
    • 易⽤性:使⽤ORM做数据库的开发可以有效的减少SQL语句,写出来的模型也更加直观
      性能损耗⼩
    • 设计灵活:可以轻松写出来复杂的查询
    • 可移植性:SQLAlchemy封装了底层的数据库实现,⽀持多个关系型数据库,包括MySQL,SQLite

使⽤ORM来操作数据库

  • 1.创建表

    • 1>.先建立类
      • 现在 以 User表 来做为例⼦,它有⾃增⻓的 id 、name 、fullname 、password 这些字段,那么对应的类为:
      • SQLAlchemy 会⾃动的设置第⼀个 Integer 的 主键 并且没有被标记为外键的字段添加⾃增⻓的属性。因此以上例⼦中 id ⾃动的变成⾃增⻓的。
    • 2>.类关联到SQLAlchemy
      • from sqlalchemy import create_engine
      • from sqlalchemy.ext.declarative import declarative_base
      • engine = create_engine() 创建实例,并连接数据库
      • Base = declarative_base(engine)所有的类都要继承⾃declarative_base这个函数⽣成的基类
    • 3>.映射到数据库
      • Base.metadata.create_all() 创建完和表映射的类后,还没有真正的映射到数据库当中
        from sqlalchemy import Column,Integer,String
        from constants import DB_URI
        from sqlalchemy import create_engine
        from sqlalchemy.ext.declarative import declarative_base
        
        engine = create_engine(DB_URI,echo=True)
        
        # 所有的类都要继承⾃`declarative_base`这个函数⽣成的基类
        Base = declarative_base(engine)
        
        class User(Base):
        	# 定义表名为users
        	__tablename__ = 'users'
        
        	# 将id设置为主键,并且默认是⾃增⻓的
        	id = Column(Integer,primary_key=True)
        	# name字段,字符类型,最⼤的⻓度是50个字符
        	name = Column(String(50))
        	fullname = Column(String(50))
        	password = Column(String(100))
        
        	# 让打印出来的数据更好看,可选的
        	def __repr__(self):
        		return "" % (self.id,self.name,self.fullname,self.password)
        
        Base.metadata.create_all() 
        
    sqlalchemy常用数据类型 作用
    Integer 整形;
    Float 浮点类型;
    Boolean 传递 True / False 进去;
    DECIMAL 定点类型;
    Enum 枚举类型;
    Date 传递 datetime.date() 进去;
    DateTime 传递 datetime.datetime() 进去;
    Time 传递 datetime.time() 进去;
    String 字符类型,使⽤时需要指定⻓度,区别于 Text类型;
    Text ⽂本类型;
    LONGTEXT ⻓⽂本类型。
    Column常用参数 作用
    default 默认值;
    nullable 是否可空;
    primary_key 是否为主键;
    unique 是否唯⼀;
    autoincrement 是否⾃动增⻓;
    onupdate 更新的时候执⾏的函数;
    name 该属性在数据库中的字段映射。

.metadata.create_all()

  • 2.添加数据。在创建完数据表,并且做完和数据库的映射后,接下来让我们添加数据进去:
    • 创建数据:user = User(name=‘ed’,fullname=‘Ed Jones’,password=‘edspasswor d’)
    • 映射数据:
      • 数据添加到 session 中
        Session = sessionmaker(bind=engine) # 或 Session = sessionmaker() Session.configure(bind=engine)
        session = Session() 
        session.add(user)
        
      • 提交数据:session.commit()
        ----------创建数据-----------
        ed_user = User(name='ed',fullname='Ed Jones',password='edspasswor d')
        # 打印id
        print(ed_user.id) 
        # None
        #  id 为 None,这是因为 id 是⼀个⾃增长的 主键,还未插⼊到数据库中,id 是不存在的
        ------------映射数据插⼊到数据库---------
        # 把创建 的数据插⼊到数据库中。和数据库打交道的,是⼀个叫做 Session 的对象:
        from sqlalchemy.orm import sessionmaker
        Session = sessionmaker(bind=engine)  #  或者Session = sessionmaker()  Session.configure(bind=engine)		
        session = Session()
        ed_user = User(name='ed',fullname='Ed Jones',password='edspasswor d')
        session.add(ed_user)
        # commit 操作才是真正的添加成功
        session.commit()
        # 打印ed_user的id,这时候,ed_user 就已经有 id。 说明已经插⼊到数据库中了
        print(ed_user.id)
        
      • commit 操作的作用
        • 将事务中的操作真正的映射到数据库中
        • 为什么添加到 session 中后还要做⼀次 commit 操作呢,这是因为,在 SQLAlchemy 的 ORM 实现中,在做 commit 操作之前,所有的操作都是在事务中进⾏的,因此如果你要将事务中的操作真正的映射到数据库中,还需要做 commit 操作。
  • 4.修改数据:ed_user.name = ‘Edwardo’
  • 5.查询数据:session.query()
    • 返回 Query对象。Query对象相当于⼀个数组,装载了查找出来的数据,并且可以进⾏迭代。具体⾥⾯装的什么数据,就要看向 session.query() ⽅法传的什么参数了,如果只是传⼀个ORM的类名作为参数, 那么提取出来的数据就是都是这个类的实例:
      • query可用参数
      • 模型对象User。指定查找这个模型中所有的对象;
      • 模型中的属性User.name。可以指定只查找某个模型的其中⼏个属性;
      • 聚合函数
        • func.count:统计⾏的数量;
        • func.avg:求平均值;
        • func.max:求最⼤值;
        • func.min:求最⼩值;
        • func.sum:求和。
        for instance in session.query(User).order_by(User.id):
        	print(instance) 
        # 如果传递了两个及其两个以上的对象,或者是传递的是ORM类的属性,那么查 找出来的就是元组:
        for instance in session.query(User.name):
        	print(instance)
        # .all()
        for instance in session.query(User,User.name).all():
        	print(instance)
        # 切⽚操作
        for instance in session.query(User).order_by(User.id)[1:3]
        	print(instance)
        
    • 过滤条件
  • 过滤 是数据提取的⼀个很重要的功能,以下对⼀些常⽤的过滤条件进⾏解释, 并且这些过滤条件都是只能通过 filter ⽅法实现的。
    • filter_by 和 filter :这两个⽅法都是 ⽤来做过滤的,区别在于,filter_by 是传⼊关键字参数,filter 是传⼊条件判 断,并且 filter 能够传⼊的条件更多更灵活:
    # 第⼀种:使⽤filter_by过滤:
    for name in session.query(User.name).filter_by(fullname='Ed Jones' ):
    	print(name)
    
    # 第⼆种:使⽤filter过滤:
    for name in session.query(User.name).filter(User.fullname=='Ed Jon es'):
    	print(name)
    
  • 等于/不等:query.filter(User.name == ‘ed’) / query.filter(User.name != ‘ed’)
  • like模糊查询:query.filter(User.name.like(’%ed%’))
  • in/not in:
    ------------in----------
    query.filter(User.name.in_(['ed','wendy','jack']))
    # 同时,in也可以作⽤于⼀个Query
    query.filter(User.name.in_(session.query(User.name).filter(User.na me.like('%ed%'))))
    -----------not in----------
    query.filter(~User.name.in_(['ed','wendy','jack']))
    
  • 空与非空:
    ------------空----------
    query.filter(User.name==None)
    # 或者是
    query.filter(User.name.is_(None))
    -----------非空----------
    query.filter(User.name != None)
    # 或者是
    query.filter(User.name.isnot(None)) 
    
  • 与或:
    ----------------and--------------
    from sqlalchemy import and_
    query.filter(and_(User.name=='ed',User.fullname=='Ed Jones'))
    # 或者是传递多个参数,可以直接逗号并列
    query.filter(User.name=='ed',User.fullname=='Ed Jones')
    # 或者是通过多次filter操作
    query.filter(User.name=='ed').filter(User.fullname=='Ed Jones')
    ----------------or--------------
    from sqlalchemy import or_
    query.filter(or_(User.name=='ed',User.name=='wendy'))
    

排序

  • order_by:
    • 可以指定根据这个表中的某个字段进⾏排序,如果在前⾯加了⼀ 个-,代表的是降序排序。
    • 在模型定义的时候指定默认排序:有些时候,不想每次在查询的时候都指定排 序的⽅式,可以在定义模型的时候就指定排序的⽅式。
    • 正向排序和反向排序:默认情况是从⼩到⼤,从前到后排序的,如果想要反向 排序,可以调⽤排序的字段的desc⽅法。
    # 可让⽂章使⽤标题来进⾏排序
    __mapper_args__ = {
    	"order_by": title
    }
    

limit、offset和切片

  • limit:可以限制每次查询的时候只查询⼏条数据。
  • offset:可以限制查找数据的时候过滤掉前⾯多少条。
  • 切⽚:可以对Query对象使⽤切⽚操作,来获取想要的数据。

group_by

  • 根据某个字段进⾏分组。⽐如想要根据性别进⾏分组,来统计每个分组分别有 多少⼈:
    session.query(User.gender,func.count(User.id)).group_by(User.gende r).all()
    

having

  • having 是对查找结果进⼀步过滤。⽐如只想要看未成年⼈的数量,那么可以⾸ 先对年龄进⾏分组统计⼈数,然后再对分组进⾏having过滤。
    result = session.query(User.age,func.count(User.id)).group_by(User .age).having(User.age >= 18).all()
    

join

  • join 查询分为两种,⼀种是 inner join,另⼀种是 outer join。默认的是 inner join,如果指定 left join 或者是 right join 则为 outer join。如果想要查询 User 及 其对应的 Address,则可以通过以下⽅式来实现:
    --------------普通⽅式的实现-------------
    for u,a in session.query(User,Address).filter(User.id==Address.use r_id).all():
    	print(u)
    	print(a) 
    --------------join -------------	
    for u,a in session.query(User,Address).join(Address).all():
    	print(u)
    	print(a) 
    -------------outerjoin -------------	
    # 可以获取所有 user,⽽不⽤在乎这个 user 是否有 address 对象,并且 outerjoin 默认为左外查询:
    for instance in session.query(User,Address).outerjoin(Address).all ():
    	print(instance)
    

别名

  • 当多表查询的时候,有时候同⼀个表要⽤到多次,这时候⽤别名就可以⽅便的 解决命名冲突的问题了:
 from sqlalchemy.orm import aliased
 
 adalias1 = aliased(Address) 
 adalias2 = aliased(Address) 
 for username,email1,email2 in session.query(User.name,adalias1.ema il_address,adalias2.email_address).join(adalias1).join(adalias2).a ll(): 
 	print(username,email1,email2) 

子查询

  • sqlalchemy 也⽀持⼦查询,⽐如现在要查找⼀个⽤户的⽤户名以及该⽤户的邮 箱地址数量。要满⾜这个需求,可以在⼦查询中找到所有⽤户的邮箱数(通过 group by 合并同⼀⽤户),然后再将结果放在⽗查询中进⾏使⽤:
    • 查询某个对象的具体字段:通过 subquery() ⽅法实现,成⼦查询后,通过⼦查询 .c 属性来访问查询出来的列。
    • 查找整个实体,需要通过 aliased 方法
    from sqlalchemy.sql import func
    
    # 构造⼦查询
    stmt = session.query(Address.user_id.label('user_id'),func.count(*).label('address_count')).group_by(Address.user_id).subquery()
    ------------某个对象的具体字段---------
    # 将⼦查询放到父查询中
    for u,count in session.query(User,stmt.c.address_count).outerjoin( stmt,User.id==stmt.c.user_id).order_by(User.id):
    	print u,count 
    ------------查找整个实体---------
    stmt = session.query(Address)
    adalias = aliased(Address,stmt)
    for user,address in session.query(User,stmt).join(stmt,User.addres ses):
    print user,address
    
3)外键
  • 在 Mysql 中,外键可以让表之间的关系更加紧密。⽽ SQLAlchemy 同样也支持外键。
  • 通过 ForeignKey 类来实现,并且可以指定表的外键约束:
class Article(Base):
	__tablename__ = 'article'
	id = Column(Integer,primary_key=True,autoincrement=True)
	title = Column(String(50),nullable=False)
	content = Column(Text,nullable=False)
	uid = Column(Integer,ForeignKey('user.id'))
	def __repr__(self):
		return "" % self.title
		
class User(Base):
	__tablename__ = 'user'
	id = Column(Integer,primary_key=True,autoincrement=True)
	username = Column(String(50),nullable=False) 
  • 外键约束有以下几项:
  • RESTRICT :父表数据被删除,会阻⽌删除。默认就是这⼀项。
  • NO ACTION :在MySQL中,同 RESTRICT 。
  • CASCADE :级联删除。
  • SET NULL :⽗表数据被删除,⼦表数据会设置为 NULL。

表关系:

  • 表之间的关系存在三种:⼀对⼀ 、⼀对多 、多对多 。⽽ SQLAlchemy 中的 ORM 也可以模拟这三种关系。因为⼀对⼀其实在 SQLAlchemy 中底层是通过⼀对多的⽅式模拟的。
  • ⼀对⼀:拿之前的 User 表为例,假如现在要添加⼀个功能,要保存⽤户的邮箱帐号,并 且邮箱帐号可以有多个,这时候就必须创建⼀个新的表,⽤来存储⽤户的邮箱,然后通过 user.id 来作为外键进⾏引⽤:
from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship

class Address(Base):
	__tablename__ = 'address'
	id = Column(Integer,primary_key=True)
	email_address = Column(String,nullable=False)
	user_id = Column(Integer,ForeignKey('users.id'))
	user = relationship('User',backref="addresses")

	def __repr__(self):
		return "" % self.email_addre ss

class User(Base):
	__tablename__ = 'users'
	id = Column(Integer,primary_key=True)
	name = Column(String(50))
	fullname = Column(String(50))
	password = Column(String(100))
	addresses = relationship("Address",backref="user") 
  • 一对一:
    • ⼀对⼀其实就是⼀对多的特殊情况,从以上的⼀对多例⼦中不难发现,⼀对应 的是 User 表,⽽多对应的是 Address ,也就是说⼀个 User 对象有多个 Address 。因此要将⼀对多转换成⼀对⼀,只要设置⼀个 User 对象对应⼀个 Address 对象即可:
    • 从以例⼦可以看到,只要在 User 表中的 addresses 字段上添加 uselist = False 就可以达到⼀对⼀的效果
class User(Base):
	__tablename__ = 'users'
	id = Column(Integer,primary_key=True)
	name = Column(String(50))
	fullname = Column(String(50))
	password = Column(String(100))

	addresses = relationship("Address",backref='addresses',uselis t=False)

class Address(Base):
	__tablename__ = 'addresses'
	id = Column(Integer,primary_key=True)
	email_address = Column(String(50))
	user_id = Column(Integer,ForeignKey('users.id')
	user = relationship('Address',backref='user') 
  • 多对多:多对多需要⼀个中间表来作为连接,同理在 sqlalchemy 中的 orm 也需要⼀个中间表。假如现在有⼀个 Teacher表 和⼀个 Classes表,即⽼师和班级,⼀个⽼师可以教多个班级,⼀个班级有多个⽼师,是⼀种典型的多对多的关系,那 么通过 sqlalchemy 的 ORM 的实现⽅式:
  • 要创建⼀个多对多的关系表,⾸先需要⼀个中间表,通过 Table 来创建⼀个中 间表。上例中 第⼀个 参数 teacher_classes 代表的是中间表的表名,第二个 参数是 Base 的元类,第三 个和 第四 个参数就是要连接的两个表,其中 Column 第⼀个参数是表示的是连接表的外键名,第⼆个参数表示这个外键的类型,第三个参数表示要外键的表名和字段。
association_table = Table(
	'teacher_classes',
	Base.metadata,
	Column('teacher_id',Integer,ForeignKey('teacher.id')),
	Column('classes_id',Integer,ForeignKey('classes.id'))
	)

class Teacher(Base):
	__tablename__ = 'teacher'
	id = Column(Integer,primary_key=True)
	tno = Column(String(10))
	name = Column(String(50))
	age = Column(Integer)
	classes = relationship('Classes',secondary=association_table, backref='teachers')

class Classes(Base):
	__tablename__ = 'classes'
	id = Column(Integer,primary_key=True)
	cno = Column(String(10))
	name = Column(String(50))
	teachers = relationship('Teacher',secondary=association_table ,backref='classes') 
4)Flask-SQLAlchemy插件

Flask-SQLAlchemy插件

  • 另外⼀个框架,叫做 Flask-SQLAlchemy , Flask-SQLAlchemy 是对 SQLAlchemy 进⾏了⼀个简单的封装,使得我们在 Flask 中使⽤ sqlalchemy 更加的简单。可以通过 pip install flask- sqlalchemy 进行包下载。

数据库初始化

  • db = SQLAlchemy(app),数据库初始化不再是通过 create_engine
    from flask import Flask
    from flask_sqlalchemy import SQLAlchemy
    from constants import DB_URI
    
    app = Flask(__name__)
    app.config['SQLALCHEMY_DATABASE_URI'] = DB_URI
    db = SQLAlchemy(app)
    

生成表

  • ORM 类:Flask-SQLAlchemy 所有的类都是继承⾃ db.Model,并且所有的 Column 和 数据类型也都成为 db 的⼀个属性,但是有个好处是不⽤写表名了,Flask-SQLAlchemy 会⾃动将类名小写化,然后映射成表名
  • 之前都是通过 Base = declarative_base() 来初始化⼀个基类,然后再继承,在 Flask-SQLAlchemy 中更加简单了
    class User(db.Model):
    	id = db.Column(db.Integer,primary_key=True)
    	username = db.Column(db.String(80),unique=True)
    	email = db.Column(db.String(120),unique=True)
    
    def __repr__(self):
    	return '' % self.username
    

映射模型到数据库表

  • 创建所有的表db.create_all()到数据库里
  • 写完类模型后,要将模型映射到数据库的表中

处理数据

  • 添加数据:这时候就可以在数据库中看到已经⽣成了⼀个 user 表了。

    • 添加数据和之前的没有区别,只是 session 成为了⼀个 db 的属性。
    admin = User('admin','[email protected]')
    guest = User('guest','[email protected]')
    
    db.session.add(admin)
    db.session.add(guest)
    db.session.commit()
    
  • 查询数据:查询数据不再是之前的 session.query 了,而是将 query 属性放在了 db.Model 上,所以查询就是通过 Model.query 的⽅式进⾏查询了:

    users = User.query.all()
    
  • 删除数据:删除数据跟添加数据类似,只不过 session 是 db 的⼀个属性而已:

    db.session.delete(admin)
    db.session.commit()
    
5)数据库迁移Migrate
  • 在实际的开发环境中,经常会发⽣数据库修改的⾏为。⼀般我们修改数据库不会直接⼿动的去修改,而是去 修改 ORM 对应的模型,然后再把模型 映射 到数据库中。这时候如果有⼀个⼯具能专⻔做这种事情,就显得⾮常有⽤了,⽽ flask-migrate 就是做这个事情的。 flask-migrate 是基于 Alembic 进⾏的⼀个封装,并集成到 Flask 中,而所有的迁移操作其实都是 Alembic 做的,他能跟踪模型的变化,并将变化映射到数据库中。
  • 使⽤ Flask-Migrate 需要安装:pip install flask-migrate
  • 要让 Flask-Migrate 能够管理 app 中的数据库,需要使⽤ Migrate(app,db) 来绑定 app 和数据库:
    from flask import Flask
    from flask_sqlalchemy import SQLAlchemy
    from constants import DB_URI
    from flask_migrate import Migrate
    
    app = Flask(__name__)
    app.config['SQLALCHEMY_DATABASE_URI'] = DB_URI
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
    db = SQLAlchemy(app)
    # 绑定app和数据库
    migrate = Migrate(app,db)
    
    class User(db.Model):
    	id = db.Column(db.Integer,primary_key=True)
    	username = db.Column(db.String(20))
    
    	addresses = db.relationship('Address',backref='user')
    
    class Address(db.Model):
    	id = db.Column(db.Integer,primary_key=True)
    	email_address = db.Column(db.String(50))
    	user_id = db.Column(db.Integer,db.ForeignKey('user.id'))
    
    db.create_all()
    
    @app.route('/')
    def hello_world():
    	return 'Hello World!'
    
    if __name__ == '__main__':
    	app.run() 
    
  • 初始化⼀个迁移⽂件夹:flask db init
  • 然后再把当前的模型添加到迁移⽂件中:flask db migrate
  • 最后再把迁移⽂件中对应的数据库操作,真正的映射到数据库中:flask db upgrade

manage.py文件

  • 这个⽂件⽤来存放映射数据库的命令,MigrateCommand是 flask-migrate 集成的⼀个命令·,因此想要添加到脚本命令中,需要采⽤ manager.add_command('db',MigrateCommand) 的⽅式,以后运⾏python manage.py db xxx的命令,其实就是执⾏ MigrateCommand
from flask_script import Manager
from flask_migrate import MigrateCommand, Migrate
from exts import db
from demo import app
from models import User

manage = Manager(app)
Migrate(app, db)
manage.add_command("db", MigrateCommand)

if __name__ == '__main__':
	manage.run()

(十)Flask-Script

  • Flask-Script 的作⽤是可以通过命令⾏的形式来操作 Flask。例如通过命令跑⼀个开发版本的服务器、设置数据库,定时任务等。要使⽤ Flask-Script ,可以通过 pip install flask-script 安装最新版本
  • 我们把脚本命令代码放在⼀个叫做 manage.py ⽂件中,然后在终端运⾏ python_manage.py 的 hello 命令,就可以看到输出 hello 了。
    from flask_script import Manager
    from your_app import app
    
    manager = Manager(app)
    
    @manager.command
    def hello():
    	print('hello')
    
    if __name__ == '__main__':
    	manager.run() 
    
  • 定义命令的三种方法
    • 1.使⽤ @command 装饰器;
    • 2.使⽤类继承⾃ Command类;使⽤类的⽅式,有三点需要注意:
      • 必须继承⾃ Command 基类;
      • 必须实现 run ⽅法;
      • 必须通过 add_command ⽅法添加命令。
        from flask_script import Command,Manager
        from your_app import app
        
        manager = Manager(app)
        
        class Hello(Command):
        	"prints hello world"
        
        	def run(self):
        		print("hello world")
        
        manager.add_command('hello',Hello())
        
      • 3.使⽤ option 装饰器:如果想要在使⽤命令的时候还传递参数进去,那么使⽤ @option装饰器更加的⽅便:
        @manager.option('-n','--name',dest='name')
        def hello(name):
        	print('hello ',name)
        ## 调⽤ hello 命令:
        python manage.py -n juran
        python manage.py --name juran
        
  • 添加参数到命令中:
  • option 装饰器:以上三种创建命令的⽅式都可以添加参数,@option 装饰器,已经介绍过了:
    @manager.option('-n', '--name', dest='name', default='joe')
    @manager.option('-u', '--url', dest='url', default=None)
    
    def hello(name, url):
    	if url is None:
    		print("hello", name)
    	else:
    		print("hello", name, "from", url)
    
  • command 装饰器:command 装饰器也可以添加参数,但是不能那么的灵活:
    @manager.command
    
    def hello(name="Fred")
    	print("hello", name) 
    
  • 类继承:类继承也可以添加参数
    from flask_Flask import Comman,Manager,Option
    
    class Hello(Command):
    	option_list = (
    		Option('--name','-n',dest='name'),
    	)
    
    	def run(self,name):
    		print("hello %s" % name)
    ----------如果要在指定参数的时候,动态的做⼀些事情,可以使⽤ get_options ⽅法:-----
    class Hello(Command):
    
    	def __init__(self,default_name='Joe'):
    		self.default_name = default_name
    
    	def get_options(self):
    		return [
    		Option('-n','--name',dest='name',default=self.default_n ame),
    		]
    
    	def run(self,name):
    		print('hello',name)
    

(十一)cookie和session

cookie

  • 在⽹站中,http请求是⽆状态的。也就是说即使第⼀次和服务器连 接后并且登录成功后,第⼆次请求服务器依然不能知道当前请求是哪个⽤ 户。cookie的出现就是为了解决这个问题,第⼀次登录后服务器返回⼀些数 据(cookie)给浏览器,然后浏览器保存在本地,当该⽤户发送第⼆次请求 的时候,就会⾃动的把上次请求存储的cookie数据⾃动的携带给服务器,服 务器通过浏览器携带的数据就能判断当前⽤户是哪个了。cookie存储的数据 量有限,不同的浏览器有不同的存储⼤⼩,但⼀般不超过4KB。因此使⽤ cookie只能存储⼀些⼩量的数据。

session

  • session和cookie的作⽤有点类似,都是为了存储⽤户相关的信 息。不同的是,cookie是存储在本地浏览器,session是⼀个思路、⼀个概 念、⼀个服务器存储授权信息的解决⽅案,不同的服务器,不同的框架,不 同的语⾔有不同的实现。虽然实现不⼀样,但是他们的⽬的都是服务器为了 ⽅便存储数据的。session的出现,是为了解决cookie存储数据不安全的问 题的。

cookie和session结合使⽤

  • web开发发展⾄今,cookie和session的使⽤已 经出现了⼀些⾮常成熟的⽅案。在如今的市场或者企业⾥,⼀般有两种存储⽅式:
    • 1)存储在服务端: 通过cookie存储⼀个session_id,然后具体的数据则是保存在session中。如果⽤户已经登录,则服务器会在cookie中保存⼀个 session_id,下次再次请求的时候,会把该session_id携带上来,服务器 根据session_id在session库中获取⽤户的session数据。就能知道该⽤户 到底是谁,以及之前保存的⼀些状态信息。这种专业术语叫做server side session。存储在服务器的数据会更加的安全,不容易被窃取。但存储在 服务器也有⼀定的弊端,就是会占⽤服务器的资源,但现在服务器已经发展⾄今,⼀些session信息还是绰绰有余的。
    • 2)将session数据加密,然后存储在cookie中。这种专业术语叫做client side session。flask采⽤的就是这种⽅式,但是也可以替换成其他形式。

flask中使⽤cookie和session

  • cookies:在Flask中操作cookie,是通过response对象来操作,可以在 response返回之前,通过response.set_cookie来设置,这个⽅法有以下几个参数需要注意:

    • 1)key:设置的cookie的key;
    • 2)value:key对应的value;
    • 3)max_age:改cookie的过期时间,如果不设置,则浏览器关闭后就会⾃ 动过期;
    • 4)expires:过期时间,应该是⼀个datetime类型;
    • 5)domain:该cookie在哪个域名中有效。⼀般设置⼦域名,⽐如 cms.example.com;
    • 6)path:该cookie在哪个路径下有效。
  • session:Flask中的session是通过from flask import session。然后添加值key和value进去即可。并且,Flask中的session机制是将session信息加 密,然后存储在cookie中。专业术语叫做client side session。

SESSION_TYPE = 'redis'  # session类型为redis
SESSION_PERMANENT = False  # 如果设置为True,则关闭浏览器session就失效。
SESSION_USE_SIGNER = False  # 是否对发送到浏览器上session的cookie值进行加密
SESSION_KEY_PREFIX = 'session:'  # 保存到session中的值的前缀

(十一)Flask的上下文

  • Flask 项⽬中有 两个上下文,⼀个是 应用上下文(app),另外⼀个是 请求上下文(request)。请求上下文 request 和 应⽤上下文 current_app 都是⼀个 全局变量,所有请求都共享的。
  • Flask 有特殊的机制 可以保证每次请求的数据都是隔离的,即A请求所产⽣的数据不会影响到B的请求。所以可以直接导⼊ request 对象,也 不会被 一些脏数据影响 了,并且不需要在每个函数中使⽤ request 的时候传⼊ request 对象。
  • 两个上下文 具体的 实现⽅式 和 原理 可以没必要详细了解。仅建议。
    request :请求上下⽂上的对象。这个对象⼀般⽤来保存⼀些请求的变量。比如 method 、 args 、 form 等。
    session :请求上下⽂上的对象。这个对象⼀般⽤来保存⼀些会话信息。
    current_app :返回当前的 app。
    g :应⽤上下⽂上的对象。处理请求时⽤作临时存储的对象
  • request:
  • before_first_request :处理第⼀次请求之前执⾏:
    @app.before_first_request
    def first_request():
    	print 'first time request'
    
  • before_request :在每次请求之前执⾏。通常可以⽤这个装饰器来给视图函 数增加⼀些变量:
    @app.context_processor
    def context_processor():
    	return {'current_user':'xxx'}
    
  • teardown_appcontext :不上下⽂处理器。返回的字典中的键可以在模板上下文中使⽤:
    @app.teardown_appcontext
    def teardown(response):
    	print("teardown 被执⾏")
    	return response
    
  • context_processor :不管是否有异常,注册的函数都会在每次请求之后执⾏:
    @app.teardown_appcontext
    def teardown(response):
    	print("teardown 被执⾏")
    	return respons
    
  • errorhandler :errorhandler 接收状态码,可以⾃定义返回这种状态码的响应的处理⽅法:
    @app.errorhandler(404)
    def page_not_found(error):
    	return 'This page does not exist',404
    

(十二)Restful API规范

restful api

  • restful api 是⽤于在前端与后台进⾏通信的⼀套规范。使⽤这个规范 不仅 可以让前-后端开发变得更加轻松,而且 因为 面向对象 代码每个模块的独立性,方便新老协作者对程序的开发以及版本维护。以下将讨论这套规范的⼀些设计细节。

协议
  采⽤ http 或者 https 协议。
数据传输格式
  数据之间传输的格式应该都使⽤ json ,⽽不使⽤ xml 。
url链接
  url 链接中,不能有动词,只能有名词。并且对于⼀些名词,如果出现复数,那 么应该在后⾯加 s 。
  
HTTP请求的方法

  • GET :从服务器上获取资源;
  • POST :在服务器上新创建⼀个资源;
  • PUT :在服务器上更新资源;(客户端提供
  • 所有改变后的数据)
  • PATCH :在服务器上更新资源;(客户端只提供需要改变的属性)
  • DELETE :从服务器上删除资源。
  • 示例
    • GET /users/ :获取所有⽤户;
      POST /user/ :新建⼀个⽤户;
      GET /user/id/ :根据id获取⼀个⽤户;
      PUT /user/id/ :更新某个id的⽤户的信息(需要提供⽤户的所有信息);
      PATCH /user/id/ :更新某个id的⽤户信息(只需要提供需要改变的信 息);
      DELETE /user/id/ :删除⼀个⽤户。

状态码

状态码 原生描述 描述
200 OK 服务器成功响应客户端的请求
301 临时重定向
302 永久重定向
400 INVALID REQUEST ⽤户发出的请求有错误,服务器没有进⾏新建或修改数据的操作
401 Unauthorized ⽤户没有权限访问这个请求
403 Forbidden 因为某些原因禁⽌访问 这个请求
404 NOT FOUND ⽤户发送的请求的url不存在
406 NOT Acceptable ⽤户请求不被服务器接收(比如服务器期望客 户端发送某个字段,但是没有发送)。
500 Internal server error 服务器内部错误,比如出现了 bug

(十二)Flask-Restful插件

介绍

  • Flask-Restful 是⼀个专⻔⽤来写restful api的⼀个插件。使⽤他可以快速的集 成restful api功能。在app的后台以及纯api的后台中,这个插件可以帮助我们 节省很多时间。当然,如果在普通的⽹站中,这个插件就显得有些鸡肋了,因 为在普通的⽹⻚开发中,是需要去渲染HTML代码的,⽽Flask-Restful在每个请求中都是返回json格式的数据。

安装

  • Flask-Restful需要在Flask 0.8以上的版本,在Python2.6或者Python3.3上运 ⾏。通过pip install flask-restful即可安装。

定义Restful的视图

  • 如果使⽤Flask-Restful,那么定义视图函数的时候,就要继承⾃ flask_restful.Resource类,然后再根据当前请求的method来定义相应的⽅法。⽐如期望客户端是使⽤get⽅法发送过来的请求,那么就定义⼀个get⽅ 法。类似于MethodView。
    from flask import Flask,render_template,url_for
    from flask_restful import Api,Resource
    
    app = Flask(__name__)
    # ⽤Api来绑定app
    api = Api(app)
    
    class IndexView(Resource):
    def get(self):
    return {"username":"juran"}
    
    api.add_resource(IndexView,'/',endpoint='index')
    

注意事项

  • endpoint是⽤来给url_for反转url的时候指定的。如果不写endpoint,那么将会使⽤视图的名字的⼩写来作为endpoint。
    add_resource的第⼆个参数是访问这个视图函数的url,这个url可以跟之前 的route⼀样,可以传递参数。并且还有⼀点不同的是,这个⽅法可以传递多 个url来指定这个视图函数。

参数解析

  • Flask-Restful插件提供了类似WTForms来验证提交的数据是否合法的包,叫做reqparse
  • add_argument 可以指定这个字段的名字,这个字段的数据类型等。
    • default:默认值,如果这个参数没有值,那么将使⽤这个参数指定的值。

    • required:是否必须。默认为False,如果设置为True,那么这个参数就必须 提交上来。

    • type:这个参数的数据类型,如果指定,那么将使⽤指定的数据类型来强制 转换提交上来的值。

    • choices:选项。提交上来的值只有满⾜这个选项中的值才符合验证通过,否 则验证不通过。

    • help:错误信息。如果验证失败后,将会使⽤这个参数指定的值作为错误信 息。

    • trim:是否要去掉前后的空格。

      parser = reqparse.RequestParser()
      parser.add_argument('username',type=str,help='请输⼊⽤户名')
      args = parser.parse_args()
      
    • 输出字段

      • 对于⼀个视图函数,你可以指定好⼀些字段⽤于返回。以后可以使⽤ORM模型 或者⾃定义的模型的时候,他会⾃动的获取模型中的相应的字段,⽣成json数据,然后再返回给客户端。这其中需要导⼊flask_restful.marshal_with装饰器。并且需要写⼀个字典,来指示需要返回的字段,以及该字段的数据类型。
      • 在get⽅法中,返回user的时候,flask_restful会⾃动的读取user模型上的 username以及age还有school属性。组装成⼀个json格式的字符串返回给客户端。
    • 重命名属性

      • 很多时候你⾯向公众的字段名称是不同于内部的属性名。使⽤ attribute可以配 置这种映射。⽐如现在想要返回user.school中的值,但是在返回给外⾯的时 候,想以education返回回去,那么可以这样写
        resource_fields = {
        'education': fields.String(attribute='school')
        }
        
    • 默认值

      • 在返回⼀些字段的时候,有时候可能没有值,那么这时候可以在指定fields的时 候给定⼀个默认值
        resource_fields = {
        'age': fields.Integer(default=18)
        }
        
    • 复杂结构

      • 有时候想要在返回的数据格式中,形成⽐较复杂的结构。那么可以使⽤⼀些特 殊的字段来实现。⽐如要在⼀个字段中放置⼀个列表,那么可以使⽤ fields.List,⽐如在⼀个字段下⾯⼜是⼀个字典,那么可以使⽤ fields.Nested。
        class ProfileView(Resource):
        resource_fields = {
        'username': fields.String,
        'age': fields.Integer,
        'school': fields.String,
        'tags': fields.List(fields.String),
        'more': fields.Nested({
        'signature': fields.String
        })
        }
        

(十三)memcached

什么是memcached

  • memcached之前是dango的⼀个项⽬,最早是为LiveJournal服务的,当初设计师为了加速LiveJournal访问速度⽽开发的,后来被很多⼤型项⽬采⽤。官⽹ 是www.danga.com或者是memcached.org。
  • Memcached是⼀个⾼性能的分布式的内存对象缓存系统,全世界有不少公司 采⽤这个缓存项⽬来构建⼤负载的⽹站,来分担数据库的压⼒。Memcached是 通过在内存⾥维护⼀个统⼀的巨⼤的hash表,memcached能存储各种各样的 数据,包括图像、视频、⽂件、以及数据库检索的结果等。简单的说就是将数 据调⽤到内存中,然后从内存中读取,从⽽⼤⼤提⾼读取速度。
  • 哪些情况下适合使⽤Memcached:存储验证码(图形验证码、短信验证 码)、登录session等所有不是⾄关重要的数据。

memcache特性

  • 1.保存内存中
  • 2.重启服务,数据会丢失
  • 3.LRU算法,根据最近使⽤的变量,将⻓时间没有使⽤的变量删除
  • 4.memcache服务端是不安全的
  • 5.不适合单机使⽤,对内存的消耗⽐较⼤
  • 6.格式简单,不⽀持list数据格式

安装和启动memcached

windows:
安装:memcached.exe -d install。
启动:memcached.exe -d start。

linux(ubuntu):
安装:sudo apt install memcached

启动:
cd /usr/bin/memcached/
memcached -d start 

可能出现的问题:

  • 1.提示你没有权限:在打开cmd的时候,右键使⽤管理员身份运⾏。
  • 2.提示缺少pthreadGC2.dll⽂件:将pthreadGC2.dll⽂件拷⻉到windows/System32.
  • 3.不要放在含有中⽂的路径下⾯

启动 memcached

  • -d:这个参数是让memcached在后台运⾏。
  • -m:指定占⽤多少内存。以M为单位,默认为64M。
  • -p:指定占⽤的端⼝。默认端⼝是11211。
  • -l:别的机器可以通过哪个ip地址连接到我这台服务器。
  • 如果是通过service memcached start的⽅式,那么只能通过本机连接。如果想要让别的机器连接,就必须设置-l 0.0.0. 0 如果想要使⽤以上参数来指定⼀些配置信息,那么不能使⽤service memcached start,⽽应该使⽤/usr/bin/memcached的⽅式来运⾏。⽐ 如/usr/bin/memcached -u memcache -m 1024 -p 11222 start。

telnet操作memcached

  • telnet ip地址 :[11211]

添加数据

  • set和add的区别:add是只负责添加数据,不会去修改数据。如果添加的数据 的key已经存在了,则添加失败,如果添加的key不存在,则添加成功。⽽set不 同,如果memcached中不存在相同的key,则进⾏添加,如果存在,则替换。

删除数据

  • delete key
  • flush_all:删除memcached中的所有数据。

自增自减

  • incr key nums
  • decr key nums

查看 memcached 的当前状态

  • stats

通过 python 操作 memcached

  • 安装 python-memcached :
  • pip install python-memcached

建⽴连接

  • import memcache
    mc = memcache.Client([‘127.0.0.1:11211’,‘192.168.174.130:11211’],debug=True)

三、项⽬结构搭建

(一) 项目介绍

需求分析原因:

  • 可以整体的了解项目的业务流程和主要的业务需求。
    项目中,需求驱动开发。即开发人员需要以需求为目标来实现业务逻辑。

需求分析方式:

  • 企业中,借助 产品原型图 分析需求。
    需求分析完后,前端按照产品原型图开发前端页面,后端开发对应的业务及响应处理。

需求分析内容:

  • 页面及其业务流程和业务逻辑。

需求文档-需求功能

1. 主页
    1.1 最多5个房屋logo图片展示,点击可跳转至房屋详情页面
    1.2 提供登陆/注册入口,登陆后显示用户名,点击可跳转至个人中心
    1.3 用户可以选择城区、入住时间、离开时间等条件进行搜索
    1.4 城区的区域信息需动态加载
    
2. 注册
    2.1 用户账号默认为手机号
    2.2 图片验证码正确后才能发送短信验证码
    2.3 短信验证码每60秒可发送一次
    2.4 每个条件出错时有相应错误提示
    
3. 登陆
    3.1 用手机号与密码登陆
    3.2 错误时有相应提示
    
4. 房屋列表页
    4.1 可根据入住离开时间、区域进行筛选,并可进行排序
    4.2 房屋信息分页加载
    4.3 区域信息动态加载
    4.4 筛选条件更新后,页面立即刷新
    
5. 房屋详情页
    5.1 需展示的详细信息参考设计图
    5.2 提供预定入口
    5.3 若是房东本人查看房屋信息时,预定入口不显示
    
6. 房屋预定
    6.1 由用户确定入住时间
    6.2 根据用户确定的入住离开时间实时显示合计天数与总金额
    
7. 我的爱家
    7.1 显示个人头像、手机号、用户名(用户名未设置时为用户手机号)
    7.2 提供修改个人信息的入口
    7.3 提供作为房客下单的查询入口
    7.4 提供成为房东所需实名认证的入口
    7.5 提供作为房东发布房屋信息的入口
    7.6 提供作为房东查询客户订单的入口
    7.7 提供退出的入口
    
8. 个人信息修改
    8.1 可以修改个人头像
    8.2 可以修改用户名
    8.3 登陆手机号不能修改
    8.4 上传头像与用户名分开保存
    8.5 上传新头像后页面理解显示新头像
    
9. 我的订单(房客)
    9.1 按时间倒序显示订单信息
    9.2 订单完成后提供评价功能
    9.3 已评价的订单能看到评价信息
    9.4 被拒绝的订单能看到拒单原因
    
10. 实名认证
    10.1 实名认证只可进行一次
    10.2 提交认证信息后再次进入只能查看信息,不能修改
    10.3 认证信息包含姓名与身份证号
    
11. 我的房源
    11.1 未实名认证的用户不能发布新房源信息,需引导到实名认证页面
    11.2 按时间倒序显示已经发布的房屋信息
    11.3 点击房屋可以进入详情页面
    11.4 对实名认证的用户提供发布新房屋的入口
    
12. 发布新房源
    12.1 需要用户填写全部房屋信息
    12.2 房屋的文字信息与图片分开操作
    
13. 客户订单(房东)
    13.1 按时间倒序显示用户下的订单
    13.2 对于新订单提供接单与拒单的功能
    13.3 拒单必须填写拒单原因
    13.4 若客户进行了订单评价,需显示
    
14. 退出
    14.1 提供退出功能

项目主要页面介绍

  • 项目主要页面下载链接:https://pan.baidu.com/s/1y78s1Hb0c6Gd-I8ESYgQ8Q
             提取码:h0gi
             
    归纳项目主要模块
  • 为了方便项目管理及多人协同开发,我们根据需求将功能划分为不同的模块。
    将来在项目中,每个模块都会对应一个子应用进行管理和解耦。
  • 模块功能
    • 验证:图形验证、短信验证
    • 用户:注册、登录、用户中心
    • 首页广告:首页广告,城区信息
    • 房源:房源、celery、房屋列表
    • 订单:确认订单、提交订单
    • 支付:支付宝支付、订单商品评价

大厂研发流程

  • https://juejin.im/post/6844904101910675464
    8Flask-----------Flask框架------------安装使用、基本介绍_第9张图片

项目开发模式

选项 技术选型
开发模式 前后端分离
后端框架 Flask
前端框架 HTML、CSS、JS、JQ

说明

  • 前后端不分离的开发模式,是为了提高搜索引擎排名,即SEO。特别是首页,详情页和列表页。
  • 页面需要局部刷新:我们会选择使用AJAX来实现。

(二) 项目搭建

准备工作

  • 创建虚拟环境:pipenv shell
  • 安装Flask框架:pip install flask
  • 创建Flask工程:
    • 确认过 Flask 工程 的创建以及前期的准备没问题后,测试文件 app.py 成功的完成了他的历史使命,可以删除了;接下来我们来创建 Flask 工程 必备的那些文件。
    • 首先我们要创建两夹两包。分别是:
          管理业务逻辑的:lghome 包 ;
          管理项目运行日志的:logs 包 ;
          管理项目配置文件的:config.py 文件 ;
          管理入口文件的:manage.py 文件 。
      8Flask-----------Flask框架------------安装使用、基本介绍_第10张图片
  • 接下来再来介绍一下 管理业务逻辑的 lghome 包里的四个文件负责的功能都是什么:
        api_vi_0 文件夹的主要功能是:v1_0 版本的主要功能业务逻辑存放点 ;
        libs 文件夹的主要功能是:第三方功能存放点 ;
        static 文件夹的主要功能是:静态资源文件存放点 ;
        utils 文件夹的主要功能是:自定义工具文件存放点 ;
        init.py 文件的主要功能是:管理项目运行的业务逻辑的管理文件 ;
        web_html.py 文件的主要功能是:前端资源的入口管理文件。
    8Flask-----------Flask框架------------安装使用、基本介绍_第11张图片

config.py配置开发环境

  • 新建配置文件
  • 准备配置文件目录
  • 准备开发环境配置内容
    • 数据库、 Redis、Flask-Session保存session
    • Post请求(wtf)在项目里,如果提交 POST 请求的话,都会通过这个 csrf 的验证。
      • import flask_wtf import CSRFProtect
        CSRFProtect(app)
      -------------------------home\config.py-------------配置文件-------------------------------
      import redis
      
      class Config(object):
          """配置信息"""
      
          # 数据库 链接配置
          HOSTNAME = '127.0.0.1'
          PORT = 3306
          USERNAME = 'root'
          PASSWORD = 'root'
          DATEBASE = 'homes'
          # 链接 数据库
          SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{}:{}@{}:{}/{}'.format(USERNAME, PASSWORD, HOSTNAME, PORT, DATEBASE)
          SQLALCHEMY_TRACK_MODIFICATIONS = True
      
          # Redis 配置
          REDIS_HOST = '127.0.0.1'
          REDIS_PORT = 6379
      
          # Flask-Session
          SECRET_KEY = 'ASDFFGTRGRVX'				# session 签名定义 的 加盐
          SESSION_TYPE = 'redis'				# session 存储位置
          SESSION_REDIS = redis.Redis(host=REDIS_HOST, port=REDIS_PORT)	# session 链接存储位置
          SESSION_USE_SIGNER = True			# session 签名定义
          PERMANENT_SESSION_LIFETIME = 86400			# session 有效时间定义    单位:s
      
      
      class DevConfig(Config):  # 开发环境  Development
          """开发环境"""
          DEBUG = True
      
      class ProConfig(Config):  # 生产环境  Project
          """生产环境"""
      
      # 工厂模式 的 前期铺垫
      config_map = {
          'dev':DevConfig,
          'pro':ProConfig
      }
      

项目设计模式

  • 什么是设计模式
    • 只要从事计算机开发,有会有一个必须经过的基础性话题 :设计模式。那什么是 设计模式 呢?
      • 简单的介绍一下:设计模式 就是为了 优化代码 ,优化开发 ,优化程序运转 的一种逻辑计算方法。我们统称为 设计模式。
      • 再简单直白的解释:一切为工程开发做优化和简化的方法都叫 设计模式。有多种设计模式
  • 工厂模式
    • 项目的环境分为 开发环境 和 生产环境:
      • 开发环境:用于编写和调试项目代码;
      • 生产环境:用于项目线上部署运行。
    • 前面我们完成了 配置类 Config 的定义,但是项目的环境分为 开发环境 和 生产环境,那我们怎么确定什么时候调用 配置类 Config 呢?这个时候就需要用到 工厂模式 了。
    • 首先,我们需要把 配置类 Config 存放到管理配置信息的 Config.py 文件 中,并且在 Config.py 文件 中创建两个类,分别是管理 开发环境 的 DevConfig(Config) 类 和 管理 生成环境 的 ProConfig(Config) 类,这两个类都是继承 配置类Config 的子类;在最后创建一个管理两个两个类的 字典config_map ,方便后续对他们的调用。
    • 所以在 工厂模式 在 管理业务逻辑的 lghome 包里的 init.py 文件想要调用 不同模式的 Config 类来创建 app,既然,我们现在已经通过 设工程模式 对 app 的创建进行了优化,那么我们直接就完成 管理业务逻辑的 lghome 包里的 init.py 文件的功能定义:
      ----------------------home\lghome\__init__.py------------项目业务相关的初始文件----------------------
      from flask import Flask
      from config import config_map						# 配置信息文件
      from flask_sqlalchemy import SQLAlchemy				# 数据库
      from flask_session import Session					# Session
      from flask_wtf import CSRFProtect          			# 表单验证
      import redis
      
      db = SQLAlchemy()									# 
      
      redis_store = None									# Redis
         
      def create_app(config_name):
          app = Flask(__name__)
          						
          config_class = config_map.get(config_name)		# 工厂模式
          app.config.from_object(config_class) 			# 加载配置文件类
      						
          db.init_app(app)								# 使用 app 初始化 db        什么时候调用,什么时候再绑定
          
          global redis_store      						# 将 redis_store 定义为一个 全局变量
          redis_store = redis.Redis(host=config_class.REDIS_HOST, port=config_class.REDIS_PORT)	# 创建 Redis 链接
      
          Session(app)									# session 绑定
          CSRFProtect(app)								# post 请求   wtf    csrf
          return app
       ---------------------\lghome\__init__.py------------项目业务相关的初始文件----------------------
       
      

配置工程日志 logs 文件

  • 工程日志的功能是为了 记录 项目 自启动后 的 每一步 的 操作 及 反馈结果 。
  • 在 项目目录 下新建一个 logs文件夹 。将 日志 放进项目中
  • 这个部分的代码放到_init_.py中
    ----------------------\lghome\__init__.py------------项目业务相关的初始文件----------------------
    def setup_log():
    # 设置日志的的登记  DEBUG调试级别
    logging.basicConfig(level=logging.DEBUG)
    
    # 创建日志记录器,设置日志的保存路径和每个日志的大小和日志的总大小
    file_log_handler = RotatingFileHandler("logs/log.log", maxBytes=1024*1024*100,backupCount=100)
    
    # 创建日志记录格式,日志等级,输出日志的文件名 行数 日志信息
    formatter = logging.Formatter("%(levelname)s %(filename)s: %(lineno)d %(message)s")
    
    # 为日志记录器设置记录格式
    file_log_handler.setFormatter(formatter)
    
    # 为全局的日志工具对象(flaks app使用的)加载日志记录器
    logging.getLogger().addHandler(file_log_handler)
    

8Flask-----------Flask框架------------安装使用、基本介绍_第12张图片

静态文件

  • 这次项目做的是前后端分离的,所以静态页面的路由我们直接返回静态页面,页面的动态接口再另外设置路由。
  • 配置前端静态文件:项目中需要使用静态文件,比如css,images,js等等
  • 准备静态文件:在项目目录下新建一个 static 目录
  • 指定静态文件加载文件:
    -------------------------home\lghome\web_html.py--------------------------------------------
    from flask import Blueprint, current_app, make_response
    
    # 提供静态文件的蓝图
    html = Blueprint('web_html', __name__)
    
    @html.route("/")
    def get_html(html_file_name):
        """提供HTML文件"""
        
        # 如果html_file_name为"", 表示访问的路径是/ ,请求主页
        if not html_file_name:
            html_file_name = 'index.html'
            
        # 如果资源名不是favicon.ico
        # if html_file_name != "favicon.ico":
        html_file_name = "html/" + html_file_name
        
        return make_response(current_app.send_static_file(html_file_name))
    
  • 静态界面资源及数据库模型下载链接:https://pan.baidu.com/s/1y78s1Hb0c6Gd-I8ESYgQ8Q 提取码:h0gi

总结文件:

-----------------------------home\manage.py 文件-------入口文件---------------------------
from lghome import create_app, db
from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand

app = create_app('dev')

# 数据库迁移
manage = Manager(app)
Migrate(app, db)
manage.add_command('db', MigrateCommand)

if __name__ == '__main__':      # 启动
    manage.run()
---------------------------------------home\config.py 文件 ----------------------------------
import redis

class Config(object):
    """配置信息"""

    # 数据库 链接配置
    HOSTNAME = '127.0.0.1'
    PORT = 3306
    USERNAME = 'root'
    PASSWORD = 'root'
    DATEBASE = 'homes'
    # 链接 数据库
    SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{}:{}@{}:{}/{}'.format(USERNAME, PASSWORD, HOSTNAME, PORT, DATEBASE)
    SQLALCHEMY_TRACK_MODIFICATIONS = True

    # Redis 配置
    REDIS_HOST = '127.0.0.1'
    REDIS_PORT = 6379

    # Flask-Session
    SECRET_KEY = 'ASDFFGTRGRVX'										# session 签名定义 的 加盐
    SESSION_TYPE = 'redis'											# session 存储位置
    SESSION_REDIS = redis.Redis(host=REDIS_HOST, port=REDIS_PORT)	# session 链接存储位置
    SESSION_USE_SIGNER = True										# session 签名定义
    PERMANENT_SESSION_LIFETIME = 86400								# session 有效时间定义    单位:s


class DevConfig(Config):  # 开发环境  Development
    """开发环境"""
    DEBUG = True

class ProConfig(Config):  # 生产环境  Project
    """生产环境"""

# 工厂模式 的 前期铺垫,通过这个选择开发的环境
config_map = {
    'dev':DevConfig,
    'pro':ProConfig
}
---------------------------------------home\lghome\_init_.py ----------------------------------
from flask import Flask
from config import config_map						# 配置信息文件
from flask_sqlalchemy import SQLAlchemy				# 数据库
import redis										# Redis
from flask_session import Session					# Session
from flask_wtf import CSRFProtect          			# 表单验证
import logging	          							# 日志
from logging.handlers import RotatingFileHandler	# 日志用到的方法

db = SQLAlchemy()									# 

redis_store = None									# Redis

def setup_log():									# 日志的创建以及定义
    # 设置日志的的登记  DEBUG调试级别
    logging.basicConfig(level=logging.DEBUG)
    # 创建日志记录器,设置日志的保存路径和每个日志的大小和日志的总大小
    file_log_handler = RotatingFileHandler("logs/log.log", maxBytes=1024*1024*100,backupCount=100)
    # 创建日志记录格式,日志等级,输出日志的文件名 行数 日志信息
    formatter = logging.Formatter("%(levelname)s %(filename)s: %(lineno)d %(message)s")
    # 为日志记录器设置记录格式
    file_log_handler.setFormatter(formatter)
    # 为全局的日志工具对象(flaks app使用的)加载日志记录器
    logging.getLogger().addHandler(file_log_handler)

def create_app(config_name):
	setup_log()
    app = Flask(__name__)
    
    config_class = config_map.get(config_name)		# 工厂模式
    app.config.from_object(config_class) 			# 加载配置文件类
   
    db.init_app(app)								# 使用 app 初始化 db        什么时候调用,什么时候再绑定

    global redis_store      						# 将 redis_store 定义为一个 全局变量
    # 创建 Redis 链接 # 因为可能保存的不在本机的服务器,所以不能写死
    redis_store = redis.Redis(host=config_class.REDIS_HOST, port=config_class.REDIS_PORT)	
	
    Session(app)									# session 绑定

    CSRFProtect(app)								# post 请求   wtf    csrf 保护

    return app

(三)业务逻辑实现

1.用户注册

接口设计

  • 基本思路:对于接口的设计,我们要根据具体的业务逻辑,设计出适合业务逻辑的接口。
    • 具体思路:分析要实现的业务逻辑:
      • 明确在这个业务中涉及到几个相关子业务;
      • 将每个子业务当做一个接口来设计。
      • 分析接口的功能任务,明确接口的访问方式与返回数据:
        1.请求方法(如GET、POST、PUT、DELETE等);
        2.请求地址;
        3.请求参数(如路径参数、查询字符串、表单、JSON等);
        4.响应数据(如HTML、JSON等)。
1.1用户注册表单接口

用户注册表单接口设计

  • csrf验证

    • 浅谈CSEF:https://www.cnblogs.com/hyddd/archive/2009/04/09/1432744.html
  • 用户注册业务逻辑分析
    8Flask-----------Flask框架------------安装使用、基本介绍_第13张图片

  • 接口设计

    • 请求方式:POST
    • 请求地址:/users
    • 请求参数:表单参数
    数名 类型 是否必传 说明
    assword string 密码
    assword2 string 确认密码
    obile string 手机号
    ms_code string 短信验证码
    • 响应结果
    应结果 响应内容
    注册失败 响应错误提示
    注册成功 重定向到首页

用户注册表单接口定义

------------------------------home\lghome\api_1_0\__init__.py--------蓝图定义--------------------------
from flask import Blueprint

api = Blueprint('api_1_0', __name__, url_prefix='/api/v1.0')
------------------------------home\lghome\models.py----------用户模型定义----------------------
from datetime import datetime
from . import db
from werkzeug.security import generate_password_hash, check_password_hash
# from home import constants

class BaseModel(object):
    """模型基类,为每个模型补充创建时间与更新时间"""
    create_time = db.Column(db.DateTime, default=datetime.now)  # 记录的创建时间
    update_time = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)  # 记录的更新时间

class User(BaseModel, db.Model):
    """用户"""

    __tablename__ = "h_user_profile"

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 用户编号
    name = db.Column(db.String(32), unique=True, nullable=False)  # 用户暱称
    password_hash = db.Column(db.String(128), nullable=False)  # 加密的密码
    mobile = db.Column(db.String(11), unique=True, nullable=False)  # 手机号
    real_name = db.Column(db.String(32))  # 真实姓名
    id_card = db.Column(db.String(20))  # 身份证号
    avatar_url = db.Column(db.String(128))  # 用户头像路径
    houses = db.relationship("House", backref="user")  # 用户发布的房屋
    orders = db.relationship("Order", backref="user")  # 用户下的订单

    # 加上property装饰器后,会把函数变为属性,属性名即为函数名
    @property
    def password(self):
        """读取属性的函数行为"""
        # print(user.password)  # 读取属性时被调用
        # 函数的返回值会作为属性值
        # return "xxxx"
        raise AttributeError("这个属性只能设置,不能读取")

    # 使用这个装饰器, 对应设置属性操作
    @password.setter
    def password(self, value):
        """
        设置属性  user.passord = "xxxxx"
        :param value: 设置属性时的数据 value就是"xxxxx", 原始的明文密码
        :return:
        """
        self.password_hash = generate_password_hash(value)

    def check_password(self, passwd):
        """
        检验密码的正确性
        :param passwd:  用户登录时填写的原始密码
        :return: 如果正确,返回True, 否则返回False
        """
        return check_password_hash(self.password_hash, passwd)

    def to_dict(self):
        """将对象转换为字典数据"""
        user_dict = {
            "user_id": self.id,
            "name": self.name,
            "mobile": self.mobile,
            "avatar": constants.QINIU_URL_DOMAIN + self.avatar_url if self.avatar_url else "",
            "create_time": self.create_time.strftime("%Y-%m-%d %H:%M:%S")
        }
        return user_dict

    def auth_to_dict(self):
        """将实名信息转换为字典数据"""
        auth_dict = {
            "user_id": self.id,
            "real_name": self.real_name,
            "id_card": self.id_card
        }
        return auth_dict

------------------------------home\lghome\api_1_0\passport.py----------------注册表单接口定义----------------------
from . import api
from flask import request, jsonify, session
from lghome.response_code import RET
from lghome import redis_store
from lghome.models import User
import re
import logging


@api.route("/users", methods=["POST"])
def register():
    """
    注册
    :param: 手机号 短信验证码  密码 确认密码
    :return: json
    """
    # 接收参数
    request_dict = request.get_json()
    mobile = request_dict.get("mobile")
    sms_code = request_dict.get("sms_code")
    password = request_dict.get("password")
    password2 = request_dict.get("password2")

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

    # 判断手机号格式
    if not re.match(r'1[345678]\d{9}', mobile):
        return jsonify(errno=RET.PARAMERR, errmsg='手机号格式错误')

    if password != password2:
        return jsonify(errno=RET.PARAMERR, errmsg='两次密码不一致')

    # 业务逻辑
    # 从redis取短信验证码
    try:
        real_sms_code = redis_store.get("sms_code_%s" % mobile)
    except Exception as e:
        logging.error(e)
        return jsonify(errno=RET.DBERR, errmsg='读取短信验证码异常')
    # 判断短信验证码是否过期
    if real_sms_code is None:
        return jsonify(errno=RET.NODATA, errmsg='短信验证码失效')

    # 删除redis中的短信验证码
    try:
        redis_store.delete("sms_code_%s" % mobile)
    except Exception as e:
        logging.error(e)
    # 判断用户填写的验证码的正确性
    real_sms_code = real_sms_code.decode()
    if real_sms_code != sms_code:
        return jsonify(errno=RET.DATAERR, errmsg='短信验证码错误')
    # 判断手机号是否存在
    try:
        user = User.query.filter_by(mobile=mobile).first()
    except Exception as e:
        logging.error(e)
    else:
        if user is not None:
            # 表示手机号已经被注册过
            return jsonify(errno=RET.DATAEXIST, errmsg='手机号已经存在')
    # 保存数据
    user = User(name=mobile, mobile=mobile)
    # , password_hash=password 密码需要加密
    # 保存登录状态到session中
    session["name"] = mobile
    session["mobile"] = mobile
    session["user_id"] = user.id

    # 返回结果
    return jsonify(errno=RET.OK, errmsg='注册成功')
1.2图形验证码接口

图形验证码接口设计
8Flask-----------Flask框架------------安装使用、基本介绍_第14张图片

  • 接口设计

    • 请求方式:GET
    • 请求地址:/image_codes/
    • 请求参数:
    数名 类型 是否必传 说明
    uid string 唯一编号
    • 响应结果
      在这里插入图片描述

图形验证码接口定义

  • 准备captcha扩展包:captcha扩展包用于后端生成图形验证码
-------------------home\lghome\api_1_0\verify_code.py----后端图形验证码接口实现----------------------
@api.route('/image_codes/')
def get_image_code(image_code_id):
    """
    获取图片验证码
    :param image_code_id: 图片验证码编号
    :return: 返回验证码图片
    """
    text, image_data = captcha.generate_captcha()
    try:
        redis_store.setex("image_code_%s" % image_code_id, constants.IMAGE_CODE_REDIS_EXPIRES, text)
    except Exception as e:
        # 记录日志
        logging.error(e)
        # return jsonify(errno=RET.DBERR,  errmsg="save image code id failed")
        return jsonify(errno=RET.DBERR, errmsg="保存图片验证码失败")
    # 返回值
    response = make_response(image_data)
    response.headers["Content-Type"] = "image/jpg"
    return response
-------------------前端触发访问接口----------------------
function generateImageCode() {
    // 形成图片验证码的后端地址, 设置到页面中,让浏览请求验证码图片
    // 1. 生成图片验证码编号
    imageCodeId = generateUUID();
    // 是指图片url
    var url = "/api/v1.0/image_codes/" + imageCodeId;
    $(".image-code img").attr("src", url);
}
1.3短信验证码接口

短信验证码接口设计

  • 保存短信验证码是为注册做准备的。
  • 为了避免用户使用图形验证码恶意测试,后端提取了图形验证码后,立即删除图形验证码。
  • Flask 不具备发送短信的功能,所以我们借助第三方的容联云通讯短信平台来帮助我们发送短信验证码。
  • 容联云通讯短信平台:第三方发送短信的
      容联云通讯网址:https://www.yuntongxun.com/
    -------------------home/lghome/libs/ronglianyun-----------容联云业务连接-----------
    from ronglian_sms_sdk import SmsSDK
    	 
    accId = '容联云通讯分配的主账号ID'
    accToken = '容联云通讯分配的主账号TOKEN'
    appId = '容联云通讯分配的应用ID'
    
    class CCP(object):
    """发送短信的单例类"""
    
    def __new__(cls, *args, **kwargs):
        # 判断是否存在类属性_instance,_instance是类CCP的唯一对象,即单例
        if not hasattr(cls, "_instance"):
            cls._instance = super(CCP, cls).__new__(cls, *args, **kwargs)
            cls._instance.rest = SmsSDK(accId, accToken, appId)
        return cls._instance
       
    def send_message(self, mobile, datas, tid):
    	"""
    	tid = '容联云通讯创建的模板ID'
        mobile = '手机号1,手机号2'
        datas = ('变量1', '变量2')
    	"""
        sdk = self._instance.rest
        resp = sdk.sendMessage(tid, mobile, datas)
        result = json.loads(resp)
        if result['statusCode'] == '000000':
            return 0
        else:
            return 1
            
    # 测试单例类发送模板短信结果
    if __name__ == '__main__':
    	d = CCP()
    	d.send_message('18xxxxxxx', ('1234', 5), 1)
    -------------------home/lghome/tasks/sms/tasks-------定义发短信任务-----------------
    from lghome.tasks.main import celery_app
    from lghome.libs.ronglianyun.ccp_sms import CCP
    
    
    @celery_app.task
    def send_sms(mobile, datas, tid):
        """发送短信的异步任务"""
        ccp = CCP()
        ccp.send_message(mobile, datas, tid)
    -------------------home/lghome/tasks/main-------注册任务到Celery-------------------
    from celery import Celery
    
    celery_app = Celery("home")
    
    # 加载配置文件
    celery_app.config_from_object("lghome.tasks.config")
    
    # 注册任务
    celery_app.autodiscover_tasks(["lghome.tasks.sms"])
    
    # celery 启动
    # celery -A lghome.tasks.main worker -l info -P eventlet
    
    -------------------home/lghome/tasks/config-------配置文件---------------
    BROKER_URL = 'redis://127.0.0.1:6379/1'
    CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/2'
    

8Flask-----------Flask框架------------安装使用、基本介绍_第15张图片

  • 接口设计

    • 请求方式:GET
    • 请求地址:/sms_codes/
    • 请求参数:
    数名 类型 是否必传 说明
    obile string 手机号
    mage_code string 图形验证码
    mage_code_id string 唯一编号
    • 响应结果:JSON
    字段 说明
    code 状态码
    rrmsg 错误信息

图形验证码接口定义

  • 准备captcha扩展包:captcha扩展包用于后端生成图形验证码
  • 存在的问题难点:
    • 避免频繁发送短信
      • 原因:虽然我们在前端界面做了60秒倒计时功能。但是恶意用户可以绕过前端界面向后端频繁请求短信验证码。
      • 解决办法:在后端也要限制用户请求短信验证码的频率。60秒内只允许一次请求短信验证码。在Redis数据库中缓存一个数值,有效期设置为60秒。
        8Flask-----------Flask框架------------安装使用、基本介绍_第16张图片
    • pipeline 操作 Redis 数据库
      • 存在的问题:如果 Redis 服务端 需要同时处理多个请求,加上网络延迟,那么服务端利用率不高,效率降低。
      • 解决的办法:管道 pipeline,可以一次性发送多条命令并在执行完后一次性将结果返回。pipeline 通过减少客户端与 Redis 的通信次数来实现降低往返延时时间。实现的原理是队列。Client 可以将三个命令放到一个 tcp 报文一起发送。Server 则可以将三条命令的处理结果放到一个 tcp 报文返回。队列是先进先出,这样就保证数据的顺序性。
    • 异步方案 Celery
      • 存在的问题:我们的代码是自上而下同步执行的。发送短信是耗时的操作。如果短信被阻塞住,用户响应将会延迟。响应延迟会造成用户界面的倒计时延迟。
        8Flask-----------Flask框架------------安装使用、基本介绍_第17张图片
      • 解决的办法:异步发送短信,发送短信和响应分开执行,将发送短信从主业务中解耦出来。
        8Flask-----------Flask框架------------安装使用、基本介绍_第18张图片

        生产者消费者设计模式介绍:为了将发送短信从主业务中解耦出来,我们引入生产者消费者设计模式。它是最常用的解耦方式之一,寻找中间人(broker)搭桥,保证两个业务没有直接关联
        8Flask-----------Flask框架------------安装使用、基本介绍_第19张图片

      • 发送短信设计模式:
        任务:发送短信
        生成者:flask项目
        消费者:celery
        中间人:消息队列 redis
        -------------home\lghome\api_1_0\verify_code.py------发短信接口实现-------
        from lghome.libs.ronglianyun.ccp_sms import CCP
        from lghome.tasks.sms.tasks import send_sms
        
        @api.route("/sms_codes/")
        def get_sms_code(mobile):
          """获取短信验证码"""
          # 获取参数
          # 图片验证码
          image_code = request.args.get('image_code')
          # UUID
          image_code_id = request.args.get('image_code_id')
        
          # 校验参数
          if not all([image_code, image_code_id]):
              return jsonify(errno=RET.PARAMERR, errmsg='参数不完整')
        
          # 业务逻辑
          # 从redis中取出验证码
          try:
              real_image_code = redis_store.get('image_code_%s' % image_code_id)
          except Exception as e:
              logging.error(e)
              return jsonify(errno=RET.DBERR, errmsg='redis数据库异常')
        
          # 判断图片验证码是否过期
          if real_image_code is None:
              return jsonify(errno=RET.NODATA, errmsg='图片验证码失效')
        
          # 删除redis中的图片验证码
          try:
              redis_store.delete('image_code_%s' % image_code_id)
          except Exception as e:
              logging.error(e)
        
          # print(real_image_code)  b'RVMJ'
          # 与用户填写的图片验证码对比
          real_image_code = real_image_code.decode()
          if real_image_code.lower() != image_code.lower():
              return jsonify(errno=RET.DATAERR, errmsg='图片验证码错误')
        
          # 判断手机号的操作
          try:
              send_flag = redis_store.get('send_sms_code_%s' % mobile)
          except Exception as e:
              logging.error(e)
          else:
              if send_flag is not None:
                  return jsonify(errno=RET.REQERR, errmsg='请求过于频繁')
        
          # 判断手机号是否存在
          try:
              user = User.query.filter_by(mobile=mobile).first()
          except Exception as e:
              logging.error(e)
          else:
              if user is not None:
                  # 表示手机号已经被注册过
                  return jsonify(errno=RET.DATAEXIST, errmsg='手机号已经存在')
        
          # 生成短信验证码
          sms_code = "%06d" % random.randint(0, 999999)
        
          # 保存真实的短信验证码到redis
          try:
              # redis管道
              pl = redis_store.pipeline()
              pl.setex("sms_code_%s" % mobile, constants.SMS_CODE_REDIS_EXPIRES, sms_code)
              # 保存发送给这个手机号的记录
              pl.setex('send_sms_code_%s' % mobile, constants.SNED_SMS_CODE_EXPIRES, 1)
              pl.execute()
        
          except Exception as e:
              logging.error(e)
              return jsonify(errno=RET.DBERR, errmsg='保存短信验证码异常')
        
          # 发短信 放到tasks异步进行
          send_sms.delay(mobile, (sms_code, int(constants.SMS_CODE_REDIS_EXPIRES/60)), 1)
        
          # 返回值 短信发送失败没必要传到前端,因为用户会直接点重新发送
          return jsonify(errno=RET.OK, errmsg='发送成功')
        

2.用户登录

2.1用户登录接口

用户登录接口设计
8Flask-----------Flask框架------------安装使用、基本介绍_第20张图片

  • 接口定义

    • 请求方式:POST
    • 请求地址:/sessions
    • 请求参数:
    数名 类型 是否必传 说明
    mobile string 手机号
    password string 密码
    • 响应结果:HTML
    字段 说明
    登录失败 响应错误提示
    登录成功 重定向到首页

用户登录接口定义

  • 1.获取参数、2.校验参数、3.手机号的格式
    4.从数据库中根据手机号查询用户的数据对象
    5.用数据库的密码与用户填写的密码进行对比验证
    6.如果验证相同成功,保存登录状态, 在session中
    -------------------home\lghome\api_1_0\passport.py---------用户登录接口定义-------------
    from . import api
    from flask import request, jsonify, session
    from lghome import constants
    from lghome.response_code import RET
    from lghome import redis_store
    import re
    
    @api.route("/sessions", methods=["POST"])
    def login():
        """
        用户登录
        :param: 手机号,密码
        :return: json
        """
        # 接收参数
        request_dict = request.get_json()
        mobile = request_dict.get('mobile')
        password = request_dict.get('password')
        # 校验参数
        if not all([mobile, password]):
            return jsonify(errno=RET.PARAMERR, errmsg='参数不完整')
    
        # 判断手机号格式
        if not re.match(r'1[345678]\d{9}', mobile):
            return jsonify(errno=RET.PARAMERR, errmsg='手机号格式错误')
        # 业务逻辑处理
        # 判断错误次数是否超过限制,如果超过限制直接返回
        # redis 用户IP地址:次数
        user_ip = request.remote_addr
        try:
            # bytes
            access_nums = redis_store.get("access_nums_%s" % user_ip)
        except Exception as e:
            logging.error(e)
        else:
            if access_nums is not None and int(access_nums) >= constants.LOGIN_ERROR_MAX_TIMES:
                return jsonify(errno=RET.REQERR, errmsg='错误次数太多,请稍后重试')
    
        # 从数据库中查询手机号是否存在
        try:
            user = User.query.filter_by(mobile=mobile).first()
        except Exception as e:
            logging.error(e)
            # 查询时候的异常
            return jsonify(errno=RET.DBERR, errmsg='获取用户信息失败')
        # 验证密码
        # pwd = check_password_hash(user.password_hash, password)
        # if pwd != user.password_hash:
        if user is None or not user.check_pwd_hash(password):
            try:
                redis_store.incr("access_nums_%s" % user_ip)
                redis_store.expire("access_nums_%s" % user_ip, constants.LOGIN_ERROR_FORBID_TIME)
            except Exception as e:
                logging.error(e)
    
            return jsonify(errno=RET.DATAERR, errmsg='账号密码不匹配')
        # 保存登录状态
        session['name'] = user.name
        session['mobile'] = user.mobile
        session['user_id'] = user.id
    
        # 返回
        return jsonify(errno=RET.OK, errmsg='登录成功')
    ---------------------home\lghome\onstants-----------------
    # 登录次数的最大值
    LOGIN_ERROR_MAX_TIMES = 5
    
    # 登录次数验证时间间隔
    LOGIN_ERROR_FORBID_TIME = 600
    
2.2退出登录接口

退出登录接口设计

  • 清除session数据:session.clear()

  • 接口定义

    • 请求方式:DELETE
    • 请求地址:/sessions
    • 请求参数:
    • 响应结果:JSON
    字段 说明
    code 状态码
    errmsg 错误信息

退出登录接口定义

-------------------home\lghome\api_1_0\passport.py---------退出登录接口定义-------------
@api.route("/session", methods=["DELETE"])
def logout():
    """退出登录"""
    # 清空session
    session.clear()
    return jsonify(errno=RET.OK, errmsg='OK')
2.3检查登录状态接口

检查登录状态接口设计

  • 接口定义

    • 请求方式:GET
    • 请求地址:/sessions
    • 请求参数:
    数名 类型 是否必传 说明
    mobile string 手机号
    password string 密码
    • 响应结果:JSON
    字段 说明
    code 状态码
    errmsg 错误信息

检查登录状态接口定义

-------------------home\lghome\api_1_0\passport.py---------用户登录接口定义-------------
@api.route("/session", methods=["GET"])
def check_login():
    """
    检查登录状态
    :return: 用户的信息或者返回错误信息
    """
    name = session.get('name')
    # print(name)

    if name is not None:
        return jsonify(errno=RET.OK, errmsg='true', data={"name": name})
    else:
        return jsonify(errno=RET.SESSIONERR, errmsg='false')
2.4 登入装饰器

登入装饰器设计

  • Python装饰器(decorator)在实现的时候,被装饰后的函数其实已经是另外一个函数了(函数名等函数属性会发生改变),为了不影响,Python的functools包中提供了一个叫wraps的decorator来消除这样的副作用。写一个decorator的时候,最好在实现之前加上functools的wrap,它能保留原有函数的名称和函数属性

登入装饰器定义

-------------------home\lghome\api_1_0\passport.py---------用户登录接口定义-------------

from werkzeug.routing import BaseConverter
from flask import session, jsonify, g
from lghome.response_code import RET
import functools


class ReConverter(BaseConverter):
    def __init__(self, map, regex):
        super().__init__(map)
        # super(ReConverter, self).__init__(map)
        self.regex = regex


# view_func 被装饰的函数
def login_required(view_func):

    @functools.wraps(view_func)
    def wrapper(*args, **kwargs):
        # 判断用户的登录状态
        user_id = session.get('user_id')

        if user_id is not None:
            # 已登录
            g.user_id = user_id
            return view_func(*args, **kwargs)
        else:
            # 未登陆
            return jsonify(errno=RET.SESSIONERR, errmsg='用户未登陆')

    return wrapper

3.个人信息

3.1 获取个人信息

获取个人信息接口设计

  • 接口定义

    • 请求方式:GET
    • 请求地址:/user
    • 请求参数:
    • 响应结果:JSON
    字段 说明
    存失败 响应错误提示
    存成功 返回用户名

获取个人信息定义

  • 获取用户提交的用户名、修改用户名、更新session数据
    ---------------home\lghome\api_1_0\profile.py---------获取个人信息接口-----------
    @api.route("/user", methods=["GET"])
    @login_required
    def get_user_profile():
        """
        获取个人信息
        :return: 用户名,用户头像URL
        """
        user_id = g.user_id
        try:
            user = User.query.get(user_id)
        except Exception as e:
            logging.error(e)
            return jsonify(errno=RET.DBERR, errmsg='获取用户信息失败')
    
        if user is None:
            return jsonify(errno=RET.NODATA, errmsg='获取用户信息失败')
    
        return jsonify(errno=RET.OK, errmsg='ok', data=user.to_dict())
    
3.2 用户上传头像

用户上传头像设计

  • 存储方案

    • 问题:保存到程序本地,磁盘满了要扩容、备份的问题、多机存储、多用户保存同一张图片,名字不一样,造成空间浪费、多用户上传同名文件,会出现覆盖之前的内容
    • 文件存储解决方案:
      • 自己搭建文件存储系统
      • FastDFS 快速分布式文件存储系统(电商)
      • HDFS hadoop分布式文件系统
      • 选择第三方服务七牛云:开发者中心:https://developer.qiniu.com/kodo/sdk/1242/python
  • 接口定义

    • 请求方式:POST
    • 请求地址:/users/avatar
    • 请求参数:
    数名 类型 是否必传 说明
    avatar file 用户头像
    • 响应结果:HTML
    字段 说明
    登录失败 响应错误提示
    登录成功 重定向到首页

用户上传头像定义

  • 获取图片、图片转化成二进制数据、上传到七牛云、保存到数据库中
    -------------------home\lghome\libs\image_storage.py--------连接千牛云-------------
    from qiniu import Auth, put_file, etag, put_data
    import qiniu.config
    
    # 需要填写你的 Access Key 和 Secret Key
    access_key = 'Pq9naz_G-zMSGq0SzRkKgb9au1puctwOTzJ9yHqo'
    secret_key = 'jvgL4iJl45XhSIFzieZPlS4rNwPOIBKOhwvps2mO'
    
    
    def storage(file_data):
        # 构建鉴权对象
        q = Auth(access_key, secret_key)
        # 要上传的空间
        bucket_name = 'home-image-flask'
        # 上传后保存的文件名 key = 'my-python-logo.png'
        # 生成上传 Token,可以指定过期时间等
        token = q.upload_token(bucket_name, None, 3600)
    
        ret, info = put_data(token, None, file_data)
        if info.status_code == 200:
            return ret.get('key')
        else:
            raise Exception('上传图片失败')
            
    
    if __name__ == '__main__':
        with open("target.txt", 'rb') as f:
            storage(f.read())
    -------------------home\lghome\api_1_0\profile.py---------用户登录接口定义-------------
    from . import api
    from lghome.utils.commons import login_required
    from flask import g, request, jsonify, session
    from lghome.response_code import RET
    from lghome.libs.image_storage import storage
    import logging
    from lghome.models import User
    from lghome import db
    from lghome import constants
    from sqlalchemy.exc import IntegrityError
    
    
    @api.route("/users/avatar", methods=["POST"])
    @login_required
    def set_user_avatar():
        """
        设置用户的头像
        :param: 图片
        :return: avatar_url 头像的地址
        """
        user_id = g.user_id
    
        # 获取图片
        image_file = request.files.get('avatar')
        # print(image_file)
        # print(type(image_file))
    
        if image_file is None:
            return jsonify(errno=RET.PARAMERR, errmsg='未上传图片')
    
        # 图片的二进制数据
        image_data = image_file.read()
    
        # 上传到七牛云
        try:
            file_name = storage(image_data)
        except Exception as e:
            logging.error(e)
            return jsonify(errno=RET.THIRDERR, errmsg='上传图片失败')
    
        # 保存到数据库中  保存file_name = FoRZHZTBj2xRSDa_Se0Rvx26qm0C
        # URL  http://qkgi1wsiz.hd-bkt.clouddn.com/FoRZHZTBj2xRSDa_Se0Rvx26qm0C
        try:
            User.query.filter_by(id=user_id).update({"avatar_url": file_name})
            db.session.commit()
    
        except Exception as e:
            # 出现异常回滚
            db.session.rollback()
            logging.error(e)
            return jsonify(errno=RET.DBERR, errmsg='保存图片信息失败')
    
        avatar_url = constants.QINIU_URL_DOMAIN + file_name
        return jsonify(errno=RET.OK, errmsg='保存成功', data={"avatar_url": avatar_url})
    ---------------------home\lghome\onstants-----------------
    # 七牛云域名
    QINIU_URL_DOMAIN = 'http://qkgi1wsiz.hd-bkt.clouddn.com/'
    
3.3 修改用户名接口设计

修改用户名接口设计

  • 接口定义

    • 请求方式:PUT
    • 请求地址:/users/name
    • 请求参数:
    数名 类型 是否必传 说明
    name string 用户名
    • 响应结果:HTML
    字段 说明
    存失败 响应错误提示
    存成功 返回用户名

修改用户名定义

  • 获取用户提交的用户名、修改用户名、更新session数据
    ---------------home\lghome\api_1_0\profile.py---------修改用户名接口-----------
    @api.route("/users/name", methods=["PUT"])
    @login_required
    def change_user_name():
        """
        修改用户名
        :return: 修改用户名成功或者失败
        """
        user_id = g.user_id
    
        # 获取用户提交的用户名
        request_data = request.get_json()
    
        if not request_data:
            return jsonify(errno=RET.PARAMERR, errmsg='参数不完整')
    
        name = request_data.get("name")
    
        user = User.query.filter_by(name=name).first()
        if user:
            return jsonify(errno=RET.DBERR, errmsg='用户名已经存在')
    
        # 修改用户名
        try:
            User.query.filter_by(id=user_id).update({"name": name})
            db.session.commit()
        # except IntegrityError as e:
        #     db.session.rollback()
        #     logging.error(e)
        #     return jsonify(errno=RET.DATAEXIST, errmsg='用户名已经存在')
        except Exception as e:
            logging.error(e)
            db.session.rollback()
            return jsonify(errno=RET.DBERR, errmsg='设置用户名错误')
    
        # 更新session数据
        session["name"] = name
    
        return jsonify(errno=RET.OK, errmsg='OK')
    
3.4 获取用户的实名认证信息

获取用户的实名认证信息接口设计

  • 接口定义

    • 请求方式:GET
    • 请求地址:/users/name
    • 请求参数:
    数名 类型 是否必传 说明
    name string 用户名
    • 响应结果:HTML
    字段 说明
    存失败 响应错误提示
    存成功 返回用户名

修改用户名定义

  • 获取用户提交的用户名、修改用户名、更新session数据
    ---------------home\lghome\api_1_0\profile.py---------修改用户名接口-----------
    @api.route("/users/name", methods=["PUT"])
    @login_required
    def change_user_name():
        """
        修改用户名
        :return: 修改用户名成功或者失败
        """
        user_id = g.user_id
    
        # 获取用户提交的用户名
        request_data = request.get_json()
    
        if not request_data:
            return jsonify(errno=RET.PARAMERR, errmsg='参数不完整')
    
        name = request_data.get("name")
    
        user = User.query.filter_by(name=name).first()
        if user:
            return jsonify(errno=RET.DBERR, errmsg='用户名已经存在')
    
        # 修改用户名
        try:
            User.query.filter_by(id=user_id).update({"name": name})
            db.session.commit()
        # except IntegrityError as e:
        #     db.session.rollback()
        #     logging.error(e)
        #     return jsonify(errno=RET.DATAEXIST, errmsg='用户名已经存在')
        except Exception as e:
            logging.error(e)
            db.session.rollback()
            return jsonify(errno=RET.DBERR, errmsg='设置用户名错误')
    
        # 更新session数据
        session["name"] = name
    
        return jsonify(errno=RET.OK, errmsg='OK')
    
3.5 保存实名认证信息

保存实名认证信息接口设计

  • 接口定义
    • 请求方式:POST
    • 请求地址:/users/auth
    • 请求参数:
    • 响应结果:

修改用户名定义

  • 获取用户提交的用户名、修改用户名、更新session数据
    ---------------home\lghome\api_1_0\profile.py---------保存实名认证信息-----------
    @api.route("/users/auth", methods=["POST"])
    @login_required
    def set_user_auth():
        """
        保存实名认证信息
        :return:
        """
        user_id = g.user_id
        # 第一次修改
        # User.query.filter_by(id=user_id, real_name=None, id_card=None).update({"real_name": real_name, "id_card": id_card})
    

4. 发布房源

4.1 城区信息数据

城区信息接口设计

  • 区域数据先在mysql服务器保留,如果本地有缓存的话就直接访问缓存的数据,创建好mysql模型迁移后就可以将城区数据保存到mysql里面
  • 缓存区域数据
    • 区域数据是我们动态查询的结果。但是区域数据不是频繁变化的数据,所以没有必要每次都重新查询。所以我们可以选择对区域数据进行缓存处理。
    • 缓存工具:Redis
      8Flask-----------Flask框架------------安装使用、基本介绍_第21张图片
  • 前端模板:
    -------------------newhouse.html-------------------
    
    	// 使用js模板
    var html = template("areas-tmpl", {areas: areas})
    $("#area-id").html(html);
    

城区信息接口定义

  • 接口定义

    • 请求方式:GET
    • 请求地址:/areas
    • 请求参数:
    • 响应结果:JSON
    应结果 响应内容
    响应结果 响应内容
    json字符串 城区信息
    状态码 200
    类型 json类型
    ----------------------------home\lghome\models.py----------------------------mysql城区的模型----------------------
    class Area(BaseModel, db.Model):
    """城区"""
    
    __tablename__ = "h_area_info"
    
    id = db.Column(db.Integer, primary_key=True)  # 区域编号
    name = db.Column(db.String(32), nullable=False)  # 区域名字
    houses = db.relationship("House", backref="area")  # 区域的房屋
    
    def to_dict(self):
        """将对象转换为字典"""
        d = {
            "aid": self.id,
            "aname": self.name
        }
        return d
    -----------------------------------home\lghome\api_1_0\houses.py-----------------------------------------------------
    @api.route("/areas")
    def get_area_info():
        """获取城区信息"""
    
        # 用redis中读取数据
        try:
            response_json = redis_store.get("area_info")
        except Exception as e:
            logging.error(e)
        else:
            # redis有缓存数据
            if response_json is not None:
                response_json = json.loads(response_json)
                # print(response_json)
                logging.info('redis cache')
                # return response_json, 200, {"Content-Type": "application/json"}
                return jsonify(errno=RET.OK, errmsg='OK', data=response_json['data'])
    
        # 查询数据库,读取城区信息
        try:
            area_li = Area.query.all()
        except Exception as e:
            logging.error(e)
            return jsonify(errno=RET.DBERR, errmsg='数据库异常')
    
        area_dict_li = []
        # print(area_li)
        for area in area_li:
            area_dict_li.append(area.to_dict())
    
        # 将数据转成json字符串  {key:value} <= dict(key=value)
        # response_dict = dict(errno=RET.OK, errmsg='OK', data=area_dict_li)
        response_dict = dict(data=area_dict_li)
        response_json = json.dumps(response_dict)
    	
    	# 将数据缓存到redis中
        try:
            redis_store.setex("area_info", constants.AREA_INFO_REDIS_CACHE_EXPIRES, response_json)
        except Exception as e:
            logging.error(e)
    
        # return response_json, 200, {"Content-Type": "application/json"}
        return jsonify(errno=RET.OK, errmsg='OK', data=area_dict_li)
    
4.2 保存房屋的基本信息

城区信息接口定义

  • 接口定义

    • 请求方式:POST
    • 请求地址:/houses/info
    • 请求参数:
    • 响应结果:JSON
    应结果 响应内容
    errno 错误编号
    errmsg 错误信息
    data 房屋信息
  • 接收参数:房屋名称标题、房屋单价、房屋所属城区的编号…

    ----------------------------home\lghome\models.py----------------------------房屋的模型----------------------
    # 房屋设施表,建立房屋与设施的多对多关系
    house_facility = db.Table(
        "h_house_facility",
        db.Column("house_id", db.Integer, db.ForeignKey("h_house_info.id"), primary_key=True),  # 房屋编号
        db.Column("facility_id", db.Integer, db.ForeignKey("h_facility_info.id"), primary_key=True)  # 设施编号
    )
    
    
    class House(BaseModel, db.Model):
        """房屋信息"""
    
        __tablename__ = "h_house_info"
    
        id = db.Column(db.Integer, primary_key=True)  # 房屋编号
        user_id = db.Column(db.Integer, db.ForeignKey("h_user_profile.id"), nullable=False)  # 房屋主人的用户编号
        area_id = db.Column(db.Integer, db.ForeignKey("h_area_info.id"), nullable=False)  # 归属地的区域编号
        title = db.Column(db.String(64), nullable=False)  # 标题
        price = db.Column(db.Integer, default=0)  # 单价,单位:分
        address = db.Column(db.String(512), default="")  # 地址
        room_count = db.Column(db.Integer, default=1)  # 房间数目
        acreage = db.Column(db.Integer, default=0)  # 房屋面积
        unit = db.Column(db.String(32), default="")  # 房屋单元, 如几室几厅
        capacity = db.Column(db.Integer, default=1)  # 房屋容纳的人数
        beds = db.Column(db.String(64), default="")  # 房屋床铺的配置
        deposit = db.Column(db.Integer, default=0)  # 房屋押金
        min_days = db.Column(db.Integer, default=1)  # 最少入住天数
        max_days = db.Column(db.Integer, default=0)  # 最多入住天数,0表示不限制
        order_count = db.Column(db.Integer, default=0)  # 预订完成的该房屋的订单数
        index_image_url = db.Column(db.String(256), default="")  # 房屋主图片的路径
        facilities = db.relationship("Facility", secondary=house_facility)  # 房屋的设施
        images = db.relationship("HouseImage")  # 房屋的图片
        orders = db.relationship("Order", backref="house")  # 房屋的订单
    
        def to_basic_dict(self):
            """将基本信息转换为字典数据"""
            house_dict = {
                "house_id": self.id,
                "title": self.title,
                "price": self.price,
                "area_name": self.area.name,
                "img_url": constants.QINIU_URL_DOMAIN + self.index_image_url if self.index_image_url else "",
                "room_count": self.room_count,
                "order_count": self.order_count,
                "address": self.address,
                "user_avatar": constants.QINIU_URL_DOMAIN + self.user.avatar_url if self.user.avatar_url else "",
                "ctime": self.create_time.strftime("%Y-%m-%d")
            }
            return house_dict
    
        def to_full_dict(self):
            """将详细信息转换为字典数据"""
            house_dict = {
                "hid": self.id,
                "user_id": self.user_id,
                "user_name": self.user.name,
                # "user_avatar": constants.QINIU_URL_DOMAIN + self.user.avatar_url if self.user.avatar_url else "",
                "title": self.title,
                "price": self.price,
                "address": self.address,
                "room_count": self.room_count,
                "acreage": self.acreage,
                "unit": self.unit,
                "capacity": self.capacity,
                "beds": self.beds,
                "deposit": self.deposit,
                "min_days": self.min_days,
                "max_days": self.max_days,
            }
    
            # 房屋图片 不止一张而且需要给诶长照片url加上地址
            img_urls = []
            # for image in self.images:
                # img_urls.append(constants.QINIU_URL_DOMAIN + image.url)
            house_dict["img_urls"] = img_urls
    
            # 房屋设施
            facilities = []
            for facility in self.facilities:
                facilities.append(facility.id)
            house_dict["facilities"] = facilities
    
            # 评论信息
            comments = []
            orders = Order.query.filter(Order.house_id == self.id, Order.status == "COMPLETE", Order.comment != None)\
                # .order_by(Order.update_time.desc()).limit(constants.HOUSE_DETAIL_COMMENT_DISPLAY_COUNTS)
            for order in orders:
                comment = {
                    "comment": order.comment,  # 评论的内容
                    "user_name": order.user.name if order.user.name != order.user.mobile else "匿名用户",  # 发表评论的用户
                    "ctime": order.update_time.strftime("%Y-%m-%d %H:%M:%S")  # 评价的时间
                }
                comments.append(comment)
            house_dict["comments"] = comments
            return house_dict
    
    
    class Facility(BaseModel, db.Model):
        """设施信息"""
    
        __tablename__ = "h_facility_info"
    
        id = db.Column(db.Integer, primary_key=True)  # 设施编号
        name = db.Column(db.String(32), nullable=False)  # 设施名字
    -----------------------------------home\lghome\api_1_0\houses.py-----------------------------------------------------
    from datetime import datetime
    from . import db
    from lghome import constants
    
    
    @api.route("/houses/info", methods=["POST"])
    @login_required
    def save_house_info():
        """
        保存房屋的基本信息
        :return: 保存失败或者保存成功
        {
        "title":"1",
        "price":"1",
        "area_id":"8",
        "address":"1",
        "room_count":"1",
        "acreage":"1",
        "unit":"1",
        "capacity":"1",
        "beds":"1",
        "deposit":"1",
        "min_days":"1",
        "max_days":"1",
        "facility":["2","4"]
        }
        """
        # 发布房源的用户
        user_id = g.user_id
        house_data = request.get_json()
        
        title = house_data.get("title")  # 房屋名称标题
        price = house_data.get("price")  # 房屋单价
        area_id = house_data.get("area_id")  # 房屋所属城区的编号
        address = house_data.get("address")  # 房屋地址
        room_count = house_data.get("room_count")  # 房屋包含的房间数目
        acreage = house_data.get("acreage")  # 房屋面积
        unit = house_data.get("unit")  # 房屋布局(几室几厅)
        capacity = house_data.get("capacity")  # 房屋容纳人数
        beds = house_data.get("beds")  # 房屋卧床数目
        deposit = house_data.get("deposit")  # 押金
        min_days = house_data.get("min_days")  # 最小入住天数 2
        max_days = house_data.get("max_days")  # 最大入住天数 1
        # facility = house_data.get("facility")  # 设备信息
    
        # 校验参数
        if not all([title, price, area_id, address, room_count, acreage, unit]):
            return jsonify(errno=RET.PARAMERR, errmsg='参数不完整')
    
        # 判断价格是否正确
        try:
            price = int(float(price)*100)    # 分
            deposit = int(float(deposit)*100)    # 分
        except Exception as e:
            logging.error(e)
            return jsonify(errno=RET.PARAMERR, errmsg='参数错误')
    
        # 判断区域ID
        try:
            area = Area.query.get(area_id)
        except Exception as e:
            logging.error(e)
            return jsonify(errno=RET.PARAMERR, errmsg='数据库异常')
    
        if area is None:
            return jsonify(errno=RET.PARAMERR, errmsg='城区信息有误')
    
        # 保存房屋信息
        house = House(
            user_id=user_id,
            area_id=area_id,
            title=title,
            price=price,
            address=address,
            room_count=room_count,
            acreage=acreage,
            unit=unit,
            capacity=capacity,
            beds=beds,
            deposit=deposit,
            min_days=min_days,
            max_days=max_days
        )
        # 设施信息
        facility_ids = house_data.get("facility")
    
        if facility_ids:
            # ["23", "24"]
            # 查看设置是否存在
            # SELECT * FROM h_facility_info WHERE id IN (1,3, 5);
            try:
                facilities = Facility.query.filter(Facility.id.in_(facility_ids)).all()
            except Exception as e:
                logging.error(e)
                return jsonify(errno=RET.DBERR, errmsg='数据库异常')
    
            if facilities:
                # 表示有合法的设备
                house.facilities = facilities
        try:
            db.session.add(house)
            db.session.commit()
        except Exception as e:
            logging.error(e)
            db.session.rollback()
            return jsonify(errno=RET.DBERR, errmsg='保存数据失败')
    
        # 返回结果
        return jsonify(errno=RET.OK, errmsg='OK', data={"house_id": house.id})
    ---------------------home\lghome\onstants-----------------
    # 七牛云域名
    QINIU_URL_DOMAIN = 'http://qkgi1wsiz.hd-bkt.clouddn.com/'
    
4.3 保存房屋的图片

保存房屋的图片接口定义

  • 接口定义

    • 请求方式:POST
    • 请求地址:/houses/image
    • 请求参数:
    数名 类型 是否必传 说明
    house_image files 房屋图片
    house_id string 房屋ID
    • 响应结果:JSON
    应结果 响应内容
    errno 错误编号
    errmsg 错误信息
    data 房屋图片URL地址
    ----------------------------home\lghome\models.py----------------------------房屋的模型----------------------
    class HouseImage(BaseModel, db.Model):
        """房屋图片"""
    
        __tablename__ = "h_house_image"
    
        id = db.Column(db.Integer, primary_key=True)
        house_id = db.Column(db.Integer, db.ForeignKey("h_house_info.id"), nullable=False)  # 房屋编号
        url = db.Column(db.String(256), nullable=False)  # 图片的路径
    -----------------------------------home\lghome\api_1_0\houses.py-----------------------------------------------------
    @api.route("/houses/image", methods=["POST"])
    @login_required
    def save_house_image():
        """
        保存房屋的图片
        :param:house_id 房屋的ID   house_image 房屋的图片
        :return: image_url 房屋图片地址
        """
        # 接收参数
        image_file = request.files.get("house_image")
        house_id = request.form.get("house_id")
    
        # 校验
        try:
            house = House.query.get(house_id)
        except Exception as e:
            logging.error(e)
            return jsonify(errno=RET.DBERR, errmsg='数据库异常')
    
        if house is None:
            return jsonify(errno=RET.NODATA, errmsg='房屋不存在')
    
        # 图片上传到七牛云
        image_data = image_file.read()
        try:
            filename = storage(image_data)
        except Exception as e:
            logging.error(e)
            return jsonify(errno=RET.THIRDERR, errmsg='保存图片失败')
    
        # 保存图片信息到数据库(图片的名字)
        house_image = HouseImage(house_id=house_id, url=filename)
        db.session.add(house_image)
    
        # 处理房屋的主图
        if not house.index_image_url:
            house.index_image_url = filename
            db.session.add(house)
    
        try:
            db.session.commit()
        except Exception as e:
            logging.error(e)
            db.session.rollback()
            return jsonify(errno=RET.DBERR, errmsg='保存数据失败')
    
        image_url = constants.QINIU_URL_DOMAIN + filename
        return jsonify(errno=RET.OK, errmsg='OK', data={"image_url": image_url})
    ---------------------home\lghome\onstants-----------------
    # 七牛云域名
    QINIU_URL_DOMAIN = 'http://qkgi1wsiz.hd-bkt.clouddn.com/'
    
4.4 获取房东发布的房源

获取房东发布的房源接口定义

  • 接口定义

    • 请求方式:GET
    • 请求地址:/user/houses
    • 请求参数:g 对象中
    参数名 类型 是否必传 说明
    user_id string 用户ID
    • 响应结果:JSON
    应结果 响应内容
    houses 房屋列表信息
    -----------------------------------home\lghome\api_1_0\houses.py-----------------------------------------------------
    @api.route("/user/houses", methods=["GET"])
    @login_required
    def get_user_houses():
        """
        获取用户发布的房源
        :return: 发布的房源信息
        """
        # 获取当前的用户
        user_id = g.user_id
    
        try:
            user = User.query.get(user_id)
            houses = user.houses
        except Exception as e:
            logging.error(e)
            return jsonify(errno=RET.DBERR, errmsg='获取数据失败')
    
        # 转成字典存放到列表中
        houses_list = []
        if houses:
            for house in houses:
                houses_list.append(house.to_basic_dict())
    
        return jsonify(errno=RET.OK, errmsg="OK", data={"houses": houses_list})
    
4.5 获取主页展示的房屋基本信息

获取主页展示的房屋基本信息接口定义

  • 接口定义

    • 请求方式:GET
    • 请求地址:/houses/index
    • 请求参数:
    • 响应结果:JSON
    应结果 响应内容
    houses 房屋列表信息
    -----------------------------------home\lghome\api_1_0\houses.py-----------------------------------------------------
    @api.route("/houses/index", methods=["GET"])
    def get_house_index():
        """
        获取首页房屋信息
        :return: 排序后的房屋信息
        """
    
        # 先查询缓存数据
        try:
            result = redis_store.get("home_page_data")
        except Exception as e:
            logging.error(e)
            result = None
    
        if result:
            # print("redis")
            return result.decode(), 200, {"Content-Type": "application/json"}
        else:
            try:
                # 查询数据库,房屋订单最多的5条
                houses = House.query.order_by(House.order_count.desc()).limit(constants.HOME_PAGE_MAX_NUMS).all()
            except Exception as e:
                return jsonify(errno=RET.DBERR, errmsg="查询数据库失败")
            # [, ]
            if not houses:
                return jsonify(errno=RET.NODATA, errmsg="查询没有数据")
    
            houses_list = []
            for house in houses:
                houses_list.append(house.to_basic_dict())
    
            house_dict = dict(errno=RET.OK, errmsg="OK", data=houses_list)
            json_houses = json.dumps(house_dict)
    
            try:
                redis_store.setex("home_page_data", constants.HOME_PAGE_DATA_REDIS_EXPIRES, json_houses)
            except Exception as e:
                logging.error(e)
    
            # [, ]
            return json_houses, 200, {"Content-Type": "application/json"}
            # return jsonify(errno=RET.OK, errmsg="OK", data=houses_list)
    
4.6 获取房源详情接口定义

获取房源详情接口定义

  • 接口定义

    • 请求方式:GET
    • 请求地址:/houses/
    • 请求参数:
    参数名 类型 是否必传 说明
    house_id 整形 房屋ID
    • 响应结果:JSON
    应结果 响应内容
    houses 房屋列表信息
    -----------------------------------home\lghome\api_1_0\houses.py-----------------------------------------------------
    @api.route("/houses/", methods=["GET"])
    def get_house_detail(house_id):
        """
        获取房屋详情
        :param house_id: 房屋的ID
        :return: 房屋的详细信息
        """
        # 当前用户
        # g对象中
        user_id = session.get("user_id", "-1")
    
        # 校验参数
        if not house_id:
            return jsonify(errno=RET.PARAMERR, errmsg='参数错误')
    
        # 先从缓存中查询数据
        try:
            result = redis_store.get("house_info_%s" % house_id)
        except Exception as e:
            logging.error(e)
            result = None
    
        if result:
            # dict(name=12)  {"name": 12}
            return '{"errno":%s, "errmsg":"OK", "data":{"house": %s, "user_id": %s}}' % (RET.OK, result.decode(), user_id), 200, {"Content-Type": "application/json"}
    
        # 查询数据库
        try:
            house = House.query.get(house_id)
        except Exception as e:
            logging.error(e)
            return jsonify(errno=RET.DBERR, errmsg='查询数据库失败')
    
        if not house:
            return jsonify(errno=RET.NODATA, errmsg='房屋不存在')
    
        # print(house)
        house_data = house.to_full_dict()
    
        # 存入redis
        json_house = json.dumps(house_data)
        try:
            redis_store.setex("house_info_%s" % house_id, constants.HOUSE_DETAIL_REDIS_EXPIRE, json_house)
        except Exception as e:
            logging.error(e)
    
        # print(house_data)
        return jsonify(errno=RET.OK, errmsg='OK', data={"house": house_data, "user_id": user_id})
    
4.7 房屋的搜索页面

房屋的搜索页面接口设计

  • 搜索的数据放到redis中
  • 选择什么数据类型:字符串

房屋的搜索页面接口定义

  • 接口定义

    • 请求方式:GET
    • 请求地址:/houses
    • 请求参数:http://127.0.0.1:5000/api/v1.0/houses?aid=&sd=&ed=&sk=new&p=1
    • 响应结果:JSON
    应结果 响应内容
    errno	|错误编号
    

    errmsg |错误信息
    data | 搜出来的总页数和房屋信息

    -----------------------------------home\lghome\api_1_0\houses.py-----------------------------------------------------
    @api.route("/houses", methods=["GET"])
    def get_house_list():
        """
        房屋的搜索页面
        :param: aid   区域的id   sd  开始时间  ed    结束时间  sk    排序  p  页码
        :return: 符合条件的房屋
        """
        # 接收参数
        start_date = request.args.get('sd')
        end_date = request.args.get('ed')
        area_id = request.args.get('aid')
        sort_key = request.args.get('sk')
        page = request.args.get('p')
    
        # 校验参数
        # 2020-12-04 字符串
        try:
            if start_date:
                start_date = datetime.strptime(start_date, "%Y-%m-%d")
    
            if end_date:
                end_date = datetime.strptime(end_date, "%Y-%m-%d")
    
            # if start_date >= end_date:
            #     return jsonify(errno=RET.PARAMERR, errmsg='日期参数有误')
            if start_date and end_date:
                # 05 <= 04
                assert start_date <= end_date
        except Exception as e:
            logging.error(e)
            return jsonify(errno=RET.PARAMERR, errmsg='日期参数有误')
    
        # if start_date and end_date:
        #     assert start_date <= end_date
    
        if area_id:
            try:
                area = Area.query.get(area_id)
            except Exception as e:
                logging.error(e)
                return jsonify(errno=RET.PARAMERR, errmsg='区域参数有误')
    
        # if sort_key in ["new"]:
        #     pass
        try:
            # 1.2
            page = int(page)
        except Exception as e:
            logging.error(e)
            page = 1
    
        # 查询缓存数据
        redis_key = "house_%s_%s_%s_%s" % (start_date, end_date, area_id, sort_key)
        try:
            resp_json = redis_store.hget(redis_key, page)
        except Exception as e:
            logging.error(e)
        else:
            if resp_json:
    
                return resp_json.decode(), 200, {"Content-Type": "application/json"}
    
        # 查询数据库
        conflict_orders = None
        # 过滤条件
        filter_params = []
    
        try:
            if start_date and end_date:
                # 查询冲突的订单
                # order.begin_data  <= end_date and
                # order.end_date >= start_date
                conflict_orders = Order.query.filter(Order.begin_date <= end_date, Order.end_date >= start_date).all()
            elif start_date:
                conflict_orders = Order.query.filter(Order.end_date >= start_date).all()
            elif end_date:
                conflict_orders = Order.query.filter(Order.begin_date <= end_date).all()
        except Exception as e:
            logging.error(e)
            return jsonify(errno=RET.DBERR, errmsg='数据库异常')
    
        # print(conflict_orders)
        if conflict_orders:
            # 从订单中获取冲突的房屋ID
            conflict_house_id = [order.house_id for order in conflict_orders]
    
            if conflict_house_id:
                # house = House.query.filter(House.id.notin_(conflict_house_id))
                filter_params.append(House.id.notin_(conflict_house_id))
    
        if area_id:
            # 查询的条件
            filter_params.append(House.area_id == area_id)
    
        # 排序
        if sort_key == "booking":
            house_query = House.query.filter(*filter_params).order_by(House.order_count.desc())
        elif sort_key == "price-inc":
            house_query = House.query.filter(*filter_params).order_by(House.price.asc())
        elif sort_key == "price-des":
            house_query = House.query.filter(*filter_params).order_by(House.price.desc())
        else:
            house_query = House.query.filter(*filter_params).order_by(House.create_time.desc())
    
        # 处理分页
        page_obj = house_query.paginate(page=page, per_page=constants.HOUSE_LIST_PAGE_NUMS, error_out=False)
    
        # 总页数
        total_page = page_obj.pages
    
        # 获取数据
        house_li = page_obj.items
    
        houses = []
        for house in house_li:
            houses.append(house.to_basic_dict())
    
        resp_dict = dict(errno=RET.OK, errmsg="OK", data={"total_page": total_page, "houses": houses})
        resp_json = json.dumps(resp_dict)
    
        # 将数据保存到redis中
        redis_key = "house_%s_%s_%s_%s" % (start_date, end_date, area_id, sort_key)
        try:
            # redis管道
            pipeline = redis_store.pipeline()
    
            pipeline.hset(redis_key, page, resp_json)
            pipeline.expire(redis_key, constants.HOUSE_LIST_PAGE_REDIS_CACHE_EXPIRES)
            pipeline.execute()
    
        except Exception as e:
            logging.error(e)
    
        return jsonify(errno=RET.OK, errmsg="OK", data={"total_page": total_page, "houses": houses})
        # house = House.query.filter(*filter_params)
        # print(house)
    

5.订单

5.1 保存订单

保存订单接口定义

  • 接口定义

    • 请求方式:POST
    • 请求地址:/orders
    • 请求参数:
    参数名 类型 是否必传 说明
    house_id 整形 房屋ID
    start_date 日期 预订的起始时间
    end_date 日期 预订的结束时间
    • 响应结果:JSON
    响应结果 响应内容
    errno 错误编号
    errmsg 错误信息
    -----------------------------home\lghome\api_1_0\orders.py-----------------------------------------------------
    @api.route("/orders", methods=["POST"])
    @login_required
    def save_order():
        """
        保存订单
        :param: start_date  end_date house_id
        :return: 保存订单的状态
        """
        # 接收参数
        user_id = g.user_id
    
        order_data = request.get_json()
        if not order_data:
            return jsonify(errno=RET.PARAMERR, errmsg="参数错误")
    
        start_date = order_data.get('start_date')
        end_date = order_data.get('end_date')
        house_id = order_data.get('house_id')
    
        # 校验参数
        if not all([start_date, end_date, house_id]):
            return jsonify(errno=RET.PARAMERR, errmsg="参数错误")
    
        try:
            start_date = datetime.strptime(start_date, "%Y-%m-%d")
            end_date = datetime.strptime(end_date, "%Y-%m-%d")
            assert start_date <= end_date
            # 预定的天数
            days = (end_date - start_date).days + 1
    
        except Exception as e:
            logging.error(e)
            return jsonify(errno=RET.PARAMERR, errmsg="日期格式错误")
    
        try:
            house = House.query.get(house_id)
        except Exception as e:
            logging.error(e)
            return jsonify(errno=RET.DBERR, errmsg="获取房屋信息失败")
    
        if not house:
            return jsonify(errno=RET.NODATA, errmsg="房屋不存在")
    
        # 预定的房屋是否是房东自己
        if user_id == house.user_id:
            # 说明是房东自己
            return jsonify(errno=RET.ROLEERR, errmsg="不能预定自己的房间")
    
        # 查询时间冲突的订单数量
        try:
            count = Order.query.filter(Order.begin_date <= end_date, Order.end_date >= start_date, Order.house_id == house_id).count()
        except Exception as e:
            logging.error(e)
            return jsonify(errno=RET.DBERR, errmsg="订单数据有误")
    
        if count > 0:
            return jsonify(errno=RET.DATAERR, errmsg="房屋已经被预定")
    
        # 订单总金额
        amount = days * house.price
    
        # 保存订单数据
        order = Order(
            user_id=user_id,
            house_id=house_id,
            begin_date=start_date,
            end_date=end_date,
            days=days,
            house_price=house.price,
            amount=amount
        )
    
        try:
            db.session.add(order)
            db.session.commit()
        except Exception as e:
            logging.error(e)
            db.session.rollback()
            return jsonify(errno=RET.DBERR, errmsg="保存订单失败")
    
        return jsonify(errno=RET.OK, errmsg="OK")
    
5.2 查询用户的订单信息

查询用户的订单信息接口定义

  • 接口定义

    • 请求方式:GET
    • 请求地址:/user/orders
    • 请求参数:
    参数名 类型 是否必传 说明
    role string custom landlord 访问的用户类型
    • 响应结果:JSON
    响应结果 响应内容
    errno 错误编号
    errmsg 错误信息
    orders 查询到的订单
    -----------------------------home\lghome\api_1_0\orders.py-----------------------------------------------------
    @api.route("/user/orders", methods=["GET"])
    @login_required
    def get_user_orders():
        """
        查询用户的订单信息
        :param: role 角色   custom  landlord
        :return: 订单的信息
        """
        user_id = g.user_id
    
        role = request.args.get("role", "")
    
        try:
            if role == "landlord":
                # 房东
                # 先查询属于自己的房子
                houses = House.query.filter(House.user_id == user_id).all()
                houses_id = [house.id for house in houses]
    
                # 根据房子的ID 查询预定了自己房子的订单
                orders = Order.query.filter(Order.house_id.in_(houses_id)).order_by(Order.create_time.desc()).all()
    
            else:
                # 客户的身份
                orders = Order.query.filter(Order.user_id == user_id).order_by(Order.create_time.desc()).all()
        except Exception as e:
            logging.error(e)
            return jsonify(errno=RET.DBERR, errmsg="查询订单失败")
    
        orders_dict_list = []
        if orders:
            for order in orders:
                orders_dict_list.append(order.to_dict())
    
        return jsonify(errno=RET.OK, errmsg="OK", data={"orders": orders_dict_list})
    

6.支付平台–支付宝

6.1 发起支付宝支付

发起支付宝支付接口设计

  • 支付宝开放平台入口:https://open.alipay.com/platform/home.html
  • 沙箱环境:支付宝提供给开发者的模拟支付的环境。跟真实环境是分开的。
    沙箱应用:https://open.alipay.com/platform/appDaily.htm?tab=info
    沙箱账号:https://openhome.alipay.com/platform/appDaily.htm?tab=account
    支付宝开发文档
    文档主页:https://openhome.alipay.com/developmentDocument.htm
    电脑网站支付产品介绍:https://docs.open.alipay.com/270
    电脑网站支付快速接入:https://docs.open.alipay.com/270/105899/
    API列表:https://docs.open.alipay.com/270/105900/
    SDK文档:https://docs.open.alipay.com/270/106291/
    Python支付宝SDK:https://github.com/fzlee/alipay/blob/master/README.zh-hans.md
    SDK安装:pip install python-alipay-sdk --upgrade
    
    • 电脑网站支付流程
      8Flask-----------Flask框架------------安装使用、基本介绍_第22张图片
  • 配置RSA2公私钥:
    • 支付宝开放平台密钥工具:https://opendocs.alipay.com/open/291/105971
    • 提示:商城私钥加密数据,商城公钥解密数据。支付宝私钥加密数据,支付宝公钥解密数据。
    • 生成商城公私钥:
      $ openssl
      $ OpenSSL> genrsa -out app_private_key.pem 2048  # 制作私钥RSA2
      $ OpenSSL> rsa -in app_private_key.pem -pubout -out app_public_key.pem # 导出公钥
      $ OpenSSL> exit
      
    • 配置商城私钥:1.在该子应用下新建文件夹 keys 用于存储公私钥;2.将制作的商城私钥 app_private_key.pem 拷贝到 keys 文件夹中。
      配置商城公钥:将 keys.app_public_key.pem 文件中内容上传到支付宝。
      配置支付宝公钥:将支付宝公钥内容拷贝到keys.alipay_public_key.pem文件中。
      8Flask-----------Flask框架------------安装使用、基本介绍_第23张图片
      8Flask-----------Flask框架------------安装使用、基本介绍_第24张图片
      8Flask-----------Flask框架------------安装使用、基本介绍_第25张图片

对接支付平台接口定义

  • 接口定义

    • 请求方式:POST
    • 请求地址:/orders//payment
    • 请求参数:
    参数名 类型 是否必传 说明
    order_id 整型 订单ID
    • 响应结果:JSON
    响应结果 响应内容
    errno 错误编号
    errmsg 错误信息
    orders 查询到的订单
    ---------------------home\lghome\api_1_0\keys\app_private_key.pem---------------------------
    ----------BEGIN RSA PRIVATE KEY----------
    应用私钥
    -----END RSA PRIVATE KEY-----
    ----------------------------home\lghome\api_1_0\keys\alipay_public_key.pem-------------------
    -----BEGIN PUBLIC KEY-----
    支付宝公钥
    -----END PUBLIC KEY-----
    -------------------home\lghome\api_1_0\pay.py--------------------------------------------------------
    from . import api
    from lghome.utils.commons import login_required
    from flask import g, request, jsonify
    from lghome.response_code import RET
    import logging
    from lghome.models import Order
    from lghome import db
    from alipay import AliPay
    import os
    
    
    @api.route("/orders//payment", methods=["POST"])
    @login_required
    def order_pay(order_id):
        """
        发起支付宝支付
        :param order_id: 订单ID
        :return:
        """
        user_id = g.user_id
        try:
            order = Order.query.filter(Order.id == order_id, Order.status == "WAIT_PAYMENT", Order.user_id == user_id).first()
        except Exception as e:
            logging.error(e)
            return jsonify(errno=RET.DBERR, errmsg='数据库异常')
    
        if order is None:
            return jsonify(errno=RET.NODATA, errmsg='订单数据有误')
    
        app_private_key_string = open(os.path.join(os.path.dirname(__file__), "keys/app_private_key.pem")).read()
        alipay_public_key_string = open(os.path.join(os.path.dirname(__file__), "keys/alipay_public_key.pem")).read()
    
        alipay = AliPay(
            appid="2016091200492699",
            app_notify_url=None,  # 默认回调url
            app_private_key_string=app_private_key_string,
            # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
            alipay_public_key_string=alipay_public_key_string,
            sign_type="RSA2",  # RSA 或者 RSA2
            debug=False,  # 默认False
        )
    
        # https://openapi.alipaydev.com/gateway.do
        # 电脑网站支付,需要跳转到 https://openapi.alipay.com/gateway.do? + order_string
        order_string = alipay.api_alipay_trade_page_pay(
            out_trade_no=order.id,    # 订单编号
            total_amount=order.amount/100,      # 总金额
            subject='租房 %s' % order.id,           # 订单的标题
            return_url="http://127.0.0.1:5000/payComplete.html",       # 返回的连接地址
            notify_url=None  # 可选, 不填则使用默认notify url
        )
        pay_url = "https://openapi.alipaydev.com/gateway.do?" + order_string
    
        return jsonify(errno=RET.OK, errmsg='OK', data={"pay_url": pay_url})
    
6.2 保存订单结果

查询用户的订单信息接口定义

  • 接口定义

    • 请求方式:PUT
    • 请求地址:/order/payment
    • 请求参数:
    • 响应结果:JSON
    响应结果 响应内容
    errno 错误编号
    errmsg 错误信息
    @api.route("/order/payment", methods=["PUT"])
    def save_order_payment_result():
        """
        保存订单结果
        :return: json
        """
        data = request.form.to_dict()
        # sign 不能参与签名验证
        signature = data.pop("sign")
    
        app_private_key_string = open(os.path.join(os.path.dirname(__file__), "keys/app_private_key.pem")).read()
        alipay_public_key_string = open(os.path.join(os.path.dirname(__file__), "keys/alipay_public_key.pem")).read()
    
        alipay = AliPay(
            appid="2016091200492699",
            app_notify_url=None,  # 默认回调url
            app_private_key_string=app_private_key_string,
            # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
            alipay_public_key_string=alipay_public_key_string,
            sign_type="RSA2",  # RSA 或者 RSA2
            debug=False,  # 默认False
        )
    
        success = alipay.verify(data, signature)
        if success:
            order_id = data.get('out_trade_no')
            trade_no = data.get('trade_no')     # 支付宝的交易号
    
            try:
                Order.query.filter(Order.id == order_id).update({"status": "WAIT_COMMENT", "trade_no": trade_no})
                db.session.commit()
            except Exception as e:
                logging.error(e)
                db.session.rollback()
    
        return jsonify(errno=RET.OK, errmsg='OK')
    

你可能感兴趣的:(web全栈开发知识,flask,python,django)