引入
Flask流⾏原因
1.安装pipenv
2.创建虚拟环境
[[source]]
name = "pypi"
url = "https://pypi.org/simple" # 指定国内 pip 源,不然下载库会很慢
verify_ssl = true # 安全验证信息
[dev-packages] # 开发环境
[packages] # ⽣产环境
django = "*" # * 表示最新版本
[requires] # Python 的版本号
python_version = "3.6"
3.创建系统环境变量
5.进入 / 退出 / 删除 虚拟环境
pipenv shell # 进⼊虚拟环境
exit # 退出虚拟环境
pipenv --rm # 删除整个环境 不会删除 pipfile
pipenv install --dev itchat #安装在开发环境下
freeze > c:/xxx/requirements.txt # requirements.txt 多人开发使用包的整合:
6.下载 Flask 框架
1.初始化Flask实例:
2.创建视图函数:给函数加上app.route装饰器,就是一个视图函数
3.运行项目app.run()
点击运⾏,在浏览器中输⼊ http://127.0.0.1:5000 就能看到视图函数的运行结果
app.run 这种⽅式只适合于开发,如果在⽣产环境中, 应该使⽤ Gunicorn 或者 uWSGI 来启动。如果是在终端运⾏的,可以按 ctrl + c 来让服务停⽌。
开启debug方法
注意:
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(self, rule, **options)
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)
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)
引入
定义
特点
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 "⾸⻚"
重定向定义
设置重定向
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
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')
Flask 渲染 Jinja模板
# 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')
模板的参数传递
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'})
配置文件
# 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)
# 默认
# 指定路径
app = Flask(__name__,static_folder='C:\static')
1.标准类视图
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.基于调度方法的视图
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]
...
-----------------------------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 '这是新闻首页'
寻找静态文件
寻找模板文件
url_for 生成 url
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 上出现。
过滤器 | 作用 |
---|---|
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 | 将字符串转换为⼩、大写。 |
定义
1.if
{% if kenny.sick %}
Kenny is sick.
{% elif kenny.dead %}
You killed Kenny! You bastard!!!
{% else %}
Kenny looks okay --- so far
{% endif %}
2.for…in…
# 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 %}
3.宏macro
定义
# 抽取出了⼀个 标签,指定了⼀些 默认参数。
{% macro input(name, value='', type='text') %}
{% endmacro %}
# 那么我们以后创建 标签的时候,可以通过他快速的创建:
{{ input('username') }}
{{ input('password', type='password') }}
导入宏
# 假设现在有⼀个⽂件,叫做 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 'header.html' %}
主体内容
{% include 'footer.html' %}
4.赋值(set)语句
# 那么以后就可以使⽤ name 来代替 juran 这个值了;
{% set name='juran' %}
# 同时,也可以给他赋值为列表 和 元组:
{% set navigation = [('index.html', 'Index'), ('about.html', 'Abou t')] %}
with 代码块
# 这两种⽅式都是等价的,⼀旦超出 with 代码块,就不能再使⽤ foo 这个变量 了
{% with %}
{% set foo = 42 %}
{{ foo }} foo is 42 here
{% endwith %}
# 或
{% with foo = 42 %}
{{ foo }}
{% endwith %}
5.模板继承block
# 假设现在有⼀个 base.html 这个⽗模板:
---------------------------base.html--------------父模板----------
{% block title %}{% endblock %}
{% block head %}{% endblock %}
{% block body %}{% endblock %}
# 以上base.html⽗模板中,抽取了所有模板都需要⽤到的元素 、 等
# 并且对于⼀些所有模板都要⽤到的样式⽂件 style.css 也进⾏了抽取,
# 同时对于⼀些⼦模板需要重写的地⽅,⽐如 、 、 都定义成了 ,然后⼦模板可以根据⾃⼰的需要,再具体的实现
---------------------------index.html--------------子模板----------
# ⾸先第⼀⾏就定义了 ⼦模板 继承的⽗模板,并且可以看到 ⼦模板 实现了 这个 ,并填充了⾃⼰的内容.
{% extends "base.html" %}
{% block title %}⾸⻚{% endblock %}
{% block head %}
{{ super() }}
{% endblock %}
{% block content %}
这⾥是⾸⻚
⾸⻚的内容
{% endblock %}
self.blockname
的⽅式进⾏引⽤:
{% block title %}
这是标题
{% endblock %}
{{ self.title() }}
MySQL 简介
特点
操作数据库
SQLAlchemy 是⼀个数据库的 ORM框架
,我们在后⾯会⽤到。安装命令为:pip3 install SQLAlchemy。学习这个通过 SQLAlchemy 连接数据库 (固定格式,必须紧记!)
⾸先从 sqlalchemy 中导⼊ create_engine
,⽤这个函数来创建引擎,然后⽤ engine.connect() 来连接数据库
。其中⼀个⽐较重要的⼀点是,通过 create_engine 函数的时候,需要传递⼀个满⾜某种格式的字符串,对这个字符串的格式来进⾏解释:
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())
操作数据库方法
用 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)
引入原因
ORM介绍:
易⽤性
:使⽤ORM做数据库的开发可以有效的减少SQL语句,写出来的模型也更加直观
设计灵活
:可以轻松写出来复杂的查询可移植性
:SQLAlchemy封装了底层的数据库实现,⽀持多个关系型数据库,包括MySQL,SQLite使⽤ORM来操作数据库
1.创建表
。
declarative_base
这个函数⽣成的基类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.添加数据
。在创建完数据表,并且做完和数据库的映射后,接下来让我们添加数据进去:
数据添加到 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)
将事务中的操作真正的映射到数据库中
4.修改数据
:ed_user.name = ‘Edwardo’5.查询数据
:session.query()
返回 Query对象
。Query对象相当于⼀个数组,装载了查找出来的数据,并且可以进⾏迭代。具体⾥⾯装的什么数据,就要看向 session.query() ⽅法传的什么参数了,如果只是传⼀个ORM的类名作为参数, 那么提取出来的数据就是都是这个类的实例:
query可用参数
User
。指定查找这个模型中所有的对象;User.name
。可以指定只查找某个模型的其中⼏个属性;聚合函数
:
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_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)
------------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'))
排序
# 可让⽂章使⽤标题来进⾏排序
__mapper_args__ = {
"order_by": title
}
limit、offset和切片
group_by
session.query(User.gender,func.count(User.id)).group_by(User.gende r).all()
having
result = session.query(User.age,func.count(User.id)).group_by(User .age).having(User.age >= 18).all()
join
--------------普通⽅式的实现-------------
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)
子查询
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
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)
表关系:
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")
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')
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')
Flask-SQLAlchemy插件
对 SQLAlchemy 进⾏了⼀个简单的封装
,使得我们在 Flask 中使⽤ sqlalchemy 更加的简单。可以通过 pip install flask- sqlalchemy 进行包下载。数据库初始化
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)
生成表
Flask-SQLAlchemy 所有的类都是继承⾃ db.Model
,并且所有的 Column 和 数据类型也都成为 db 的⼀个属性,但是有个好处是不⽤写表名了,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 表了。
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()
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()
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
。例如通过命令跑⼀个开发版本的服务器、设置数据库,定时任务等。要使⽤ Flask-Script ,可以通过 pip install flask-script 安装最新版本from flask_script import Manager
from your_app import app
manager = Manager(app)
@manager.command
def hello():
print('hello')
if __name__ == '__main__':
manager.run()
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())
@manager.option('-n','--name',dest='name')
def hello(name):
print('hello ',name)
## 调⽤ hello 命令:
python manage.py -n juran
python manage.py --name juran
@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)
@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和session结合使⽤
flask中使⽤cookie和session
cookies:在Flask中操作cookie,是通过response对象来操作,可以在 response返回之前,通过response.set_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中的值的前缀
@app.before_first_request
def first_request():
print 'first time request'
@app.context_processor
def context_processor():
return {'current_user':'xxx'}
@app.teardown_appcontext
def teardown(response):
print("teardown 被执⾏")
return response
@app.teardown_appcontext
def teardown(response):
print("teardown 被执⾏")
return respons
@app.errorhandler(404)
def page_not_found(error):
return 'This page does not exist',404
restful api
协议
采⽤ http 或者 https 协议。
数据传输格式
数据之间传输的格式应该都使⽤ json ,⽽不使⽤ xml 。
url链接
url 链接中,不能有动词,只能有名词。并且对于⼀些名词,如果出现复数,那 么应该在后⾯加 s 。
HTTP请求的方法
状态码
状态码 | 原生描述 | 描述 |
---|---|---|
200 | OK | 服务器成功响应客户端的请求 |
301 | 临时重定向 | |
302 | 永久重定向 | |
400 | INVALID REQUEST | ⽤户发出的请求有错误,服务器没有进⾏新建或修改数据的操作 |
401 | Unauthorized | ⽤户没有权限访问这个请求 |
403 | Forbidden | 因为某些原因禁⽌访问 这个请求 |
404 | NOT FOUND | ⽤户发送的请求的url不存在 |
406 | NOT Acceptable | ⽤户请求不被服务器接收(比如服务器期望客 户端发送某个字段,但是没有发送)。 |
500 | Internal server error | 服务器内部错误,比如出现了 bug |
介绍
安装
定义Restful的视图
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')
注意事项
参数解析
default:默认值,如果这个参数没有值,那么将使⽤这个参数指定的值。
required:是否必须。默认为False,如果设置为True,那么这个参数就必须 提交上来。
type:这个参数的数据类型,如果指定,那么将使⽤指定的数据类型来强制 转换提交上来的值。
choices:选项。提交上来的值只有满⾜这个选项中的值才符合验证通过,否 则验证不通过。
help:错误信息。如果验证失败后,将会使⽤这个参数指定的值作为错误信 息。
trim:是否要去掉前后的空格。
parser = reqparse.RequestParser()
parser.add_argument('username',type=str,help='请输⼊⽤户名')
args = parser.parse_args()
输出字段
重命名属性
resource_fields = {
'education': fields.String(attribute='school')
}
默认值
resource_fields = {
'age': fields.Integer(default=18)
}
复杂结构
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
memcache特性
安装和启动memcached
windows:
安装:memcached.exe -d install。
启动:memcached.exe -d start。
linux(ubuntu):
安装:sudo apt install memcached
启动:
cd /usr/bin/memcached/
memcached -d start
可能出现的问题:
启动 memcached
telnet操作memcached
添加数据
删除数据
自增自减
查看 memcached 的当前状态
通过 python 操作 memcached
建⽴连接
需求分析原因:
需求分析方式:
需求分析内容:
需求文档-需求功能
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 提供退出功能
项目主要页面介绍
大厂研发流程
项目开发模式
选项 | 技术选型 |
---|---|
开发模式 | 前后端分离 |
后端框架 | Flask |
前端框架 | HTML、CSS、JS、JQ |
说明
准备工作
config.py配置开发环境
-------------------------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 # 数据库
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 文件
----------------------\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)
静态文件
-------------------------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))
总结文件:
-----------------------------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.1用户注册表单接口
用户注册表单接口设计
csrf验证
接口设计
数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
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图形验证码接口
图形验证码接口定义
-------------------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短信验证码接口
短信验证码接口设计
-------------------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'
接口设计
数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
obile | string | 是 | 手机号 |
mage_code | string | 是 | 图形验证码 |
mage_code_id | string | 是 | 唯一编号 |
字段 | 说明 |
---|---|
code | 状态码 |
rrmsg | 错误信息 |
图形验证码接口定义
生产者消费者设计模式介绍:为了将发送短信从主业务中解耦出来,我们引入生产者消费者设计模式。它是最常用的解耦方式之一,寻找中间人(broker)搭桥,保证两个业务没有直接关联
-------------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用户登录接口
接口定义
数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
mobile | string | 是 | 手机号 |
password | string | 是 | 密码 |
字段 | 说明 |
---|---|
登录失败 | 响应错误提示 |
登录成功 | 重定向到首页 |
用户登录接口定义
-------------------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()
接口定义
字段 | 说明 |
---|---|
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检查登录状态接口
检查登录状态接口设计
接口定义
数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
mobile | string | 是 | 手机号 |
password | string | 是 | 密码 |
字段 | 说明 |
---|---|
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 登入装饰器
登入装饰器设计
登入装饰器定义
-------------------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 获取个人信息
获取个人信息接口设计
接口定义
字段 | 说明 |
---|---|
存失败 | 响应错误提示 |
存成功 | 返回用户名 |
获取个人信息定义
---------------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 用户上传头像
用户上传头像设计
存储方案
接口定义
数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
avatar | file | 是 | 用户头像 |
字段 | 说明 |
---|---|
登录失败 | 响应错误提示 |
登录成功 | 重定向到首页 |
用户上传头像定义
-------------------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 修改用户名接口设计
修改用户名接口设计
接口定义
数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
name | string | 是 | 用户名 |
字段 | 说明 |
---|---|
存失败 | 响应错误提示 |
存成功 | 返回用户名 |
修改用户名定义
---------------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 获取用户的实名认证信息
获取用户的实名认证信息接口设计
接口定义
数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
name | string | 是 | 用户名 |
字段 | 说明 |
---|---|
存失败 | 响应错误提示 |
存成功 | 返回用户名 |
修改用户名定义
---------------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 保存实名认证信息
保存实名认证信息接口设计
修改用户名定义
---------------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 城区信息数据
城区信息接口设计
-------------------newhouse.html-------------------
// 使用js模板
var html = template("areas-tmpl", {areas: areas})
$("#area-id").html(html);
城区信息接口定义
接口定义
应结果 | 响应内容 |
---|---|
响应结果 | 响应内容 |
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 保存房屋的基本信息
城区信息接口定义
接口定义
应结果 | 响应内容 |
---|---|
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 保存房屋的图片
保存房屋的图片接口定义
接口定义
数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
house_image | files | 是 | 房屋图片 |
house_id | string | 是 | 房屋ID |
应结果 | 响应内容 |
---|---|
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 获取房东发布的房源
获取房东发布的房源接口定义
接口定义
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
user_id | string | 是 | 用户ID |
应结果 | 响应内容 |
---|---|
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 获取主页展示的房屋基本信息
获取主页展示的房屋基本信息接口定义
接口定义
应结果 | 响应内容 |
---|---|
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 获取房源详情接口定义
获取房源详情接口定义
接口定义
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
house_id | 整形 | 是 | 房屋ID |
应结果 | 响应内容 |
---|---|
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 房屋的搜索页面
房屋的搜索页面接口设计
房屋的搜索页面接口定义
接口定义
应结果 | 响应内容 |
---|
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 保存订单
保存订单接口定义
接口定义
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
house_id | 整形 | 是 | 房屋ID |
start_date | 日期 | 是 | 预订的起始时间 |
end_date | 日期 | 是 | 预订的结束时间 |
响应结果 | 响应内容 |
---|---|
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 查询用户的订单信息
查询用户的订单信息接口定义
接口定义
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
role | string | 是 | custom landlord 访问的用户类型 |
响应结果 | 响应内容 |
---|---|
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/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
$ 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
对接支付平台接口定义
接口定义
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
order_id | 整型 | 是 | 订单ID |
响应结果 | 响应内容 |
---|---|
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 保存订单结果
查询用户的订单信息接口定义
接口定义
响应结果 | 响应内容 |
---|---|
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')