目录
需求文档
需求功能
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配置
在项目目录下创建空目录iHome-python04作为flask项目的主工程目录,在ihome-python04目录下创建以下:
ihome(python包)
--init.py
--api_1_0(python 包)
--init.py
config.py(主配置文件)
首先编写config.py配置文件,主要是配置环境,包括基础环境、开发者模式环境以及生产(线上)环境:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import redis
class Config(object):
"""基础配置信息"""
SECRET_KEY = "js@dhfkjbkjfbsjdfg2" # 内容随便填
# 数据库
SQLALCHEMY_DATABASE_URI = "mysql+pymysql://root:root3306@127.0.0.1:3306/ihome-python04"
SQLALCHEMY_TRACK_MODIFICATIONS = True
# redis
REDIS_HOST = "127.0.0.1"
REDIS_PORT = 6379
# flask-session 配置 详细参见:https://pythonhosted.org/Flask-Session/
SESSION_TYPE = "redis"
SESSION_REDIS = redis.StrictRedis(host=REDIS_HOST, port=REDIS_PORT)
SESSION_USE_SIGNER = True # 对cookie中的session_id设置隐藏处理
PERMANENT_SESSION_LIFETIME = 86400 # session数据的有效期,单位:秒
class DevelopmentConfig(Config):
"""开发者模式的配置信息"""
DEBUG = True
class ProductionConfig(Config):
"""生产环境的配置信息"""
pass
# 映射关系
config_map = {
"develop": DevelopmentConfig,
"product": ProductionConfig,
}
编写ihome下的init.py文件
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import redis
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_session import Session
from flask_wtf import CSRFProtect
from config import config_map
# 数据库
db = SQLAlchemy()
# 创建redis连接对象
redis_store = None
# 工厂模式
def create_app(config_name):
"""
创建flask的应用对象
:param config_name: str 配置模式的名字 ("develop", "product")
:return: app
"""
# 根据配置模式的名字获取模式的类
app = Flask(__name__)
config_class = config_map.get(config_name)
app.config.from_object(config_class)
# 使用app初始化db
db.init_app(app)
# 初始化redis
global redis_store
redis_store = redis.StrictRedis(host=config_class.REDIS_HOST, port=config_class.REDIS_PORT)
# 利用flask-session,将session数据保存到redis中
Session(app)
# 为flask添加CSRF防护
CSRFProtect(app)
return app
编写api_1_0下的init.py文件,创建蓝图
from flask import Blueprint
# 创建蓝图对象
api = Blueprint("api_1_0", __name__)
创建完蓝图后需要在ihome下的init.py中注册
def create_app(config_name):
app = Flask(__name__)
config_class = config_map.get(config_name)
app.config.from_object(config_class)
# 使用app初始化db
db.init_app(app)
...
# 注册蓝图
from ihome import api_1_0
app.register_blueprint(api_1_0.api, url_prefix="/api/v1.0")
return app
编写启动文件manage.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from ihome import create_app, db
from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand
# 创建flask应用
app = create_app("develop")
manage = Manager(app)
Migrate(app, db)
manage.add_command("db", MigrateCommand)
if __name__ == '__main__':
manage.run()
在api_1_0目录下创建测试文件demo.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 导入蓝图对象
from . import api
@api.route("/index")
def index():
return "index page"
在api_1_0下的init.py文件中导入蓝图的视图demo
# 导入蓝图的视图
from . import demo
在终端进入到ihome-python04,即manage.py所在的目录,输入python manage.py runserver启动项目
在不报错的情况下,打开浏览器,地址栏输入127.0.0.1:5000/index
页面打印“index page”
如果报错了,可根据错误提示自行百度
配置日志信息
flask貌似没有提供日志的模块,不过 python 提供了,即 logging 模块
在工程根目录下(ihome-python04)创建logs目录,用来存放日志文件
在ihome下的init.py文件中配置日志模块
# 创建redis连接对象
redis_store = None
import logging
from logging.handlers import RotatingFileHandler
# 设置日志的记录等级
logging.basicConfig(level=logging.DEBUG) # 调试debug级
# 创建日志记录器,指明日志的保存路径,每个日志文件的最大大小,保存的日志文件个数上限
file_log_handler = RotatingFileHandler("logs/log", maxBytes=1024*1024*100, backupCount=10)
# 创建日志记录的格式 等级 输入日志信息的文件名 行数 日志信息
formatter = logging.Formatter('%(levelname)s %(filename)s:%(lineno)d %(message)s')
# 为刚创建的日志记录器设置日志记录格式
file_log_handler.setFormatter(formatter)
# 为全局的日志工具对象(current_app)添加日志记录器
logging.getLogger().addHandler(file_log_handler)
在api_1_0下的demo.py文件中写日志测试
# 导入蓝图对象
from . import api
from flask import current_app
@api.route("/index")
def index():
current_app.logger.error('dmngdcfnf')
current_app.logger.warn('dmngdcfnf')
current_app.logger.info('dmngdcfnf')
current_app.logger.debug('dmngdcfnf')
return "index page"
重新启动一下工程,刷新浏览器,看logs目录下应该会生成一个log文件,内容大致如下:
INFO _internal.py:122 * Restarting with stat WARNING _internal.py:122 * Debugger is active! INFO _internal.py:122 * Debugger PIN: 872-898-311 INFO _internal.py:122 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) ERROR demo.py:11 dmngdcfnf WARNING demo.py:12 dmngdcfnf INFO demo.py:13 dmngdcfnf DEBUG demo.py:14 dmngdcfnf INFO _internal.py:122 127.0.0.1 - - [09/Aug/2019 19:36:41] "GET /api/v1.0/index HTTP/1.1" 200 -
数据库设计与迁移
根据前端的需求,可先设计数据库的e-r(如下)图,然后根据e-r图编写models
在ihome目录下创建models.py
# -*- coding:utf-8 -*-
from datetime import datetime
from . import db
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__ = "ih_user_profile"
id = db.Column(db.Integer, primary_key=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") # 用户下的订单
class Area(BaseModel, db.Model):
"""城区"""
__tablename__ = "ih_area_info"
id = db.Column(db.Integer, primary_key=True) # 区域编号
name = db.Column(db.String(32), nullable=False) # 区域名字
houses = db.relationship("House", backref="area") # 区域的房屋
# 房屋设施表,建立房屋与设施的多对多关系
house_facility = db.Table(
"ih_house_facility",
db.Column("house_id", db.Integer, db.ForeignKey("ih_house_info.id"), primary_key=True), # 房屋编号
db.Column("facility_id", db.Integer, db.ForeignKey("ih_facility_info.id"), primary_key=True) # 设施编号
)
class House(BaseModel, db.Model):
"""房屋信息"""
__tablename__ = "ih_house_info"
id = db.Column(db.Integer, primary_key=True) # 房屋编号
user_id = db.Column(db.Integer, db.ForeignKey("ih_user_profile.id"), nullable=False) # 房屋主人的用户编号
area_id = db.Column(db.Integer, db.ForeignKey("ih_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") # 房屋的订单
class Facility(BaseModel, db.Model):
"""设施信息"""
__tablename__ = "ih_facility_info"
id = db.Column(db.Integer, primary_key=True) # 设施编号
name = db.Column(db.String(32), nullable=False) # 设施名字
class HouseImage(BaseModel, db.Model):
"""房屋图片"""
__tablename__ = "ih_house_image"
id = db.Column(db.Integer, primary_key=True)
house_id = db.Column(db.Integer, db.ForeignKey("ih_house_info.id"), nullable=False) # 房屋编号
url = db.Column(db.String(256), nullable=False) # 图片的路径
class Order(BaseModel, db.Model):
"""订单"""
__tablename__ = "ih_order_info"
id = db.Column(db.Integer, primary_key=True) # 订单编号
user_id = db.Column(db.Integer, db.ForeignKey("ih_user_profile.id"), nullable=False) # 下订单的用户编号
house_id = db.Column(db.Integer, db.ForeignKey("ih_house_info.id"), nullable=False) # 预订的房间编号
begin_date = db.Column(db.DateTime, nullable=False) # 预订的起始时间
end_date = db.Column(db.DateTime, nullable=False) # 预订的结束时间
days = db.Column(db.Integer, nullable=False) # 预订的总天数
house_price = db.Column(db.Integer, nullable=False) # 房屋的单价
amount = db.Column(db.Integer, nullable=False) # 订单的总金额
status = db.Column( # 订单的状态
db.Enum(
"WAIT_ACCEPT", # 待接单,
"WAIT_PAYMENT", # 待支付
"PAID", # 已支付
"WAIT_COMMENT", # 待评价
"COMPLETE", # 已完成
"CANCELED", # 已取消
"REJECTED" # 已拒单
),
default="WAIT_ACCEPT", index=True)
comment = db.Column(db.Text) # 订单的评论信息或者拒单原因
在控制台先初始化models(自行创建数据库)
python manage.py db init
初始化后会在根目录下生成migrations目录
执行命令,提交这些文件
python manage.py db migrate -m 'db tables'
如果报如下错误
说明models没有被引用,可以在demo.py中引用一下
from ihome import models
再执行以下以上命令,会出现以下结果,表示执行完成
完成之后执行命令进行数据库的迁移
python manage.py db upgrade
可在数据库中查看,是否迁移成功
重新启动一下,浏览器刷新,可正常显示页面“index page”
接下来就开始项目的正式工作了,
首先让浏览器可以正常的访问页面,并通过静态文件的配置,让浏览时更加民主化。
静态文件的配置,使浏览器可以正常访问文件
首先在ihome目录下创建static静态目录,用来存放css、js、html以及images等静态资源。
为什么要配置静态文件?
如果不配置的话,浏览器访问页面时需要输入“127.0.0.1:5000/static/html/index.html”,这样有点太不友好了,我们需要的是输入“127.0.0.1:5000”或者“127.0.0.1:5000/index.html”,就可以访问主页面,因此,配置静态文件还是需要的。
我们可以自定义一个蓝图,用来提供静态文件的资源
首先在ihome目录下创建python包utils,用来存放自己编写的工具类。在utils目录下创建commoms.py文件
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from werkzeug.routing import BaseConverter
# 定义正则转换器
class ReConverter(BaseConverter):
def __init__(self, url_map, regex):
# 调用父类的初始化方法
super(ReConverter, self).__init__(url_map)
# 保存正则表达式
self.regex = regex
为flask添加自定义的转换器,在ihome目录下的init.py文件中添加:
from ihome.utils.commons import ReConverter
# 为flask添加自定义的转换器
app.url_map.converters['re'] = ReConverter
在ihome目录下创建一个web_html.py文件
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from flask import Blueprint, current_app
# 提供静态文件的蓝图
html = Blueprint("web_html", __name__)
# 127.0.0.1:5000/()
# 127.0.0.1:5000/(index.html)
# 127.0.0.1:5000/(register.html)
# 127.0.0.1:5000/(favico.ico)
@html.route("/<re(r'.*'):html_file_name>")
def get_html(html_file_name):
"""提供html文件"""
# 如果html_file_name为空,表示访问的路径为/ , 请求的是主页
if not html_file_name:
html_file_name = 'index.html'
# 如果html_file_name不是favicon.ico
if html_file_name != 'favicon.ico':
html_file_name = 'html/' + html_file_name
# flask 提供的返回静态文件的方法
return current_app.send_static_file(html_file_name)
使用正则表达式来匹配 “127.0.0.1:5000/” 后边的参数
共分为三种,分别是:
- 空---->主页
- xxx.html
- favicon.ico----->标识
返回时使用的是send_static_file,默认会去static目录下找,但是文件有可能在子目录html目录下,所以作出了以上两种判断。
编写了蓝图后需要注册
在ihome目录写的init.py中完成html蓝图的注册
from ihome import web_html
app.register_blueprint(web_html.html)
从新启动,浏览器清除一下缓存,并重新输入“127.0.0.1:5000”或者“127.0.0.1:5000/index.html”或者“127.0.0.1:5000/favicon.ico”等看是否可以正常访问,并可以正常跳转至指定的页面(页面中点击登陆,则跳转至登陆界面,点击注册,则跳转至注册界面)
csrf 防护机制
为静态路由添加csrf_token的cookie值
在web_html.py文件中添加或修改以下内容
from flask import Blueprint, current_app, make_response
from flask_wtf import csrf
def get_html(...):
...
# 创建一个csrf_token的值
csrf_token = csrf.generate_csrf()
# flask 提供的返回静态文件的方法
resp = make_response(current_app.send_static_file(html_file_name))
# 设置cookie值
resp.set_cookie('csrf_token', csrf_token)
return resp
重新启动程序,浏览器清除之前的cookie值,并刷新浏览器
本节完