文章目录
12-celery
设置celery脚本
from celery import Celery
from ihome.libs.cloudcommunication.sms import CCP
celery_app = Celery('ihome', broker='redis://127.0.0.1:6379/2')
@celery_app.task
def send_sms(to, datas, expire, temp_id):
"""发送短信的celery异步任务"""
ccp = CCP()
status = ccp.send_template_sms(to, [datas, expire], temp_id)
添加到任务里
# celery 发布异步任务 发送手机验证码
send_sms.delay(mobile_num, sms_code, str(
constants.SMS_CODE_REDIS_EXPIRES//60), 1)
执行
celery -A ihome.tasks.task_sms worker --loglevel=info --pool=solo
13-房屋管理
01_房屋管理后端接口后端说明
获取用户房源
@api.route('/user/houses', methods=['GET'])
@login_required
def get_user_houses():
"""获取用户房源"""
user_id = g.user_id
try:
houses = House.query.filter_by(user_id=user_id).all()
except Exception as e:
current_app.logger.error(e)
return jsonify(errno=RET.DBERR, errmsg='数据库查询异常')
house_list = []
for house in houses:
house_list.append(house.to_basic_dict())
# 将数据转换成json字符串
# rsp_dict = dict(errno=RET.OK, errmsg='OK', data=house_list)
# rsp_json = json.dumps(rsp_dict)
# return rsp_json, 200, {'Content-Type': 'application/json'}
return jsonify(errno=RET.OK, errmsg='OK', data={'houses': house_list})
获取主页幻灯片的基本房屋信息
与查询租房区域一样的逻辑 ,先判断是否在redis中缓存
@api.route('/houses/index', methods=['GET'])
def get_house_index():
"""获取主页幻灯片的基本房屋信息"""
# 1. 尝试从 redis 数据库获取主页房
houses = None
try:
houses = redis_store.get('home_page_data')
except Exception as e:
current_app.logger.error(e)
if houses:
current_app.logger.info('hit house index info redis')
# return jsonify(errno=RET.OK, errmsg='OK', data={'houses': houses})
return '{"errno": 0, "errmsg": "OK", "data": %s}' % (
houses.decode()), 200, {'Content-Type': 'application/json'}
# 2. 如果没有从 mysql 数据库查询获得, 并将查询的数据放入redis中
try:
houses = House.query.order_by(
House.order_count.desc()).limit(
constants.HOME_PAGE_MAX_HOUSE).all()
except Exception as e:
current_app.logger.error(e)
return jsonify(errno=RET.DBERR, errmsg='查询数据失败')
if not houses:
return jsonify(errno=RET.NODATA, errmsg='查询无数据')
# 把house对象转换成字符串,存入redis缓存
# [<house1>, <house2>, ...]
house_list = []
for house in houses:
if house.index_image_url:
house_list.append(house.to_basic_dict())
house_json = json.dumps(house_list) # '[{}, {}, {}]'
try:
redis_store.setex('home_page_data',
constants.HOME_PAGE_REDIS_CACHE_EXPIRES, house_json)
except Exception as e:
current_app.logger.error(e)
return '{"errno": 0, "errmsg": "OK", "data": %s}' % house_json, 200, {
'Content-Type': 'application/json'}
获取房源详情
同样的道理,将房屋详情信息存入redis,
@api.route('/house/<int:house_id>', methods=['GET'])
def get_house_detail(house_id):
"""获取房源详情"""
if not house_id:
return jsonify(errno=RET.PARAMERR, errmsg='参数缺失')
user_id = session.get('user_id', '-1')
house_info = None
try:
house_info = redis_store.get(f'house_info_{house_id}').decode()
except Exception as e:
current_app.logger.error(e)
if house_info:
current_app.logger.info('hit house info redis')
return '{"errno": "0", "errmsg": "OK", "data": {"house_info": %s, ' \
'"user_id": %s}}' % (house_info, user_id), \
200, \
{'Content-Type': 'application/json'}
try:
house = House.query.get(int(house_id))
except Exception as e:
current_app.logger.error(e)
return jsonify(errno=RET.DBERR, errmsg='数据库异常')
if not house:
return jsonify(errno=RET.NODATA, errmsg='房屋不存在')
try:
house_data = house.to_full_dict()
except Exception as e:
current_app.logger.error(e)
return jsonify(errno=RET.DATAERR, errmsg='数据出错')
house_json = json.dumps(house_data)
try:
redis_store.setex(f'house_info_{house_id}',
constants.HOUSE_DETAIL_REDIS_CACHE_EXPIRES,
house_json)
except Exception as e:
current_app.logger.error(e)
resp = '{"errno": "0", "errmsg": "OK", "data": {"house_info": %s,' \
'"user_id": %s}}' % (house_json, user_id), \
200, \
{'Content-Type': 'application/json'}
return resp
model.py
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") # 房屋的订单
def __repr__(self):
return f'<House: {self.title}>'
def to_basic_dict(self):
return {
'id': self.id,
'user_avatar': constants.QINIU_URL_DOMAIN + self.user.avatar_url if self.user.avatar_url else "",
'area_name': self.area.name,
'address': self.address,
'title': self.title,
'room_count': self.room_count,
'order_count': self.order_count,
'price': '%.2f' % (self.price/100),
'create_time': self.create_time.strftime('%Y-%m-%d %H:%M:%S'),
'update_time': self.update_time.strftime('%Y-%m-%d %H:%M:%S'),
'index_image_url': constants.QINIU_URL_DOMAIN + self.index_image_url if self.index_image_url else ""
}
def to_full_dict(self):
house_dict = {
'hid': self.id,
'uid': self.user_id,
'uname': self.user.name,
'user_avatar': constants.QINIU_URL_DOMAIN + self.user.avatar_url if self.user.avatar_url else "",
'title': self.title,
'price': '%.2f' % (self.price/100),
'address': self.address,
'room_count': self.room_count,
'acreage': self.acreage,
'unit': self.unit,
'capacity': self.capacity,
'beds': self.beds,
'deposit': '%.2f' % (self.deposit/100),
'min_days': self.min_days,
'max_days': self.max_days,
}
# 获取房屋图片url
image_urls = []
for image in self.images:
image_urls.append(constants.QINIU_URL_DOMAIN + image.url)
house_dict['image_urls'] = image_urls
# 设备信息
facilities = []
for facility in self.facilities:
facilities.append(facility.id)
house_dict['facilities'] = facilities
# 评论信息
# 订单完成,且评论不为空的最新20跳评论
comments = []
orders = Order.query.filter(Order.house_id == self.id,
Order.status == 'COMPLETE',
Order.comment != None).order_by(
Order.create_time.desc()).limit(constants.HOUSE_DETAIL_COMMENTS_DISPLAY_COUNT)
for order in orders:
comment = {
'user_name': order.user.name if order.user.name != order.user.mobile else '匿名用户',
'comment': order.comment,
'ctime': order.update_time.strftime('%Y-%m-%d %H:%M:%S')
}
comments.append(comment)
house_dict['comments'] = comments
return house_dict
02_房屋管理前端代码说明
房源显示
查询我的房源时,判断是否认证
myhouse.js
$(document).ready(function () {
/**
* 检查用户是否已经实名认证
* 只有完成了实名认证才能发布新房源,才能查询自己的房源
*/
$.ajax({
type: "get",
url: "/api/v1.0/user/auth",
dataType: "json",
success: function (rsp) {
if ('0' == rsp.errno) {
$('.auth-warn').hide();
get_houses()
$('#houses-list').show();
} else if ('4004' == rsp.errno) {
$('.auth-warn').show();
$('#houses-list').hide();
} else if ('4101' == rsp.errno) {
location.href = '/login.html'
} else {
alert(rsp.errmsg)
}
}
});
})
/** 查询我的房源 */
function get_houses () {
$.get("/api/v1.0/user/houses",
function (rsp) {
if ('0' == rsp.errno) {
html = template('houses-tmpl', { houses: rsp.data.houses })
$('#houses-list').append(html)
} else if ('4101' == rsp.errno) {
location.href = '/login.html'
} else {
alert(rsp.errmsg)
}
},
"json"
);
}
展示房源
<div class="houses-con">
<ul class="houses-list auth-warn">
<li>
<div class="house-title">
<h3>尚未进行实名认证,无法发布房屋信息!</h3>
</div>
<div class="house-content">
<a href="/auth.html" class="btn btn-success">去实名认证</a>
</div>
</li>
</ul>
<ul id="houses-list" class="houses-list">
<li>
<div class="new-house">
<a href="/newhouse.html">发布新房源</a>
</div>
</li>
</ul>
<script type="text/html" id="houses-tmpl">
{{ each houses as house }}
<li>
<a href="/detail.html?id={{house.id}}&f=my">
<div class="house-title">
<h3>房屋ID:{{ house.id }} —— {{ house.title }}</h3>
</div>
<div class="house-content">
<img src="{{ house.index_image_url }}">
<div class="house-text">
<ul>
<li>位于:{{ house.area }}</li>
<li>价格:¥{{ house.price }}/晚</li>
<li>发布时间:{{ house.update_time }}</li>
</ul>
</div>
</div>
</a>
</li>
{{ /each }}
</script>
</div>
首页显示
index.js
$(document).ready(function () {
// 根据登录状态显示按钮还是名称
$.get("/api/v1.0/session", function (rsp) {
// console.log(data)
if ("0" == rsp.errno) {
$('.register-login').hide();
$(".top-bar>.user-info>.user-name").html(rsp.data.name);
$(".top-bar>.user-info").show();
} else {
$(".top-bar>.register-login").show();
}
}, "json");
// 获取幻灯片要展示的房屋基本信息
$.get("/api/v1.0/houses/index", function (resp) {
if ("0" == resp.errno) {
$(".swiper-wrapper").html(template("swiper-houses-tmpl", { houses: resp.data }));
// 设置幻灯片对象,开启幻灯片滚动
var mySwiper = new Swiper('.swiper-container', {
loop: true,
autoplay: 2000,
autoplayDisableOnInteraction: false,
pagination: '.swiper-pagination',
paginationClickable: true
});
}
}, "json");
// 获取城区信息
$.get("/api/v1.0/areas", function (resp) {
if ("0" == resp.errno) {
$(".area-list").html(template("area-list-tmpl", { areas: resp.data }));
$(".area-list a").click(function (e) {
$("#area-btn").html($(this).html());
$(".search-btn").attr("area-id", $(this).attr("area-id"));
$(".search-btn").attr("area-name", $(this).html());
$("#area-modal").modal("hide");
});
}
}, "json");
$('.modal').on('show.bs.modal', centerModals);
//当模态框出现的时候
$(window).on('resize', centerModals);
//当窗口大小变化的时候
$("#start-date").datepicker({
language: "zh-CN",
keyboardNavigation: false,
startDate: "today",
format: "yyyy-mm-dd"
});
$("#start-date").on("changeDate", function () {
var date = $(this).datepicker("getFormattedDate");
$("#start-date-input").val(date);
});
})
轮播图插件
在index.html 中作用
<script src="/static/plugins/swiper/js/swiper.jquery.min.js"></script>
<div class="swiper-container">
<div class="swiper-wrapper">
<!-- <div class="swiper-slide">
<a href="/detail.html?id=1"><img src="/static/images/home01.jpg"></a>
<div class="slide-title">房屋标题1</div>
</div>
<div class="swiper-slide">
<a href="/detail.html?id=2"><img src="/static/images/home02.jpg"></a>
<div class="slide-title">房屋标题2</div>
</div>
<div class="swiper-slide">
<a href="/detail.html?id=3"><img src="/static/images/home03.jpg"></a>
<div class="slide-title">房屋标题3</div>
</div> -->
</div>
<script type="text/html" id="swiper-houses-tmpl">
{{ each houses as house }}
<div class="swiper-slide">
<a href="/detail.html?id={{ house.id }}"><img src="{{ house.index_image_url }}"></a>
<div class="slide-title">{{ house.title }}</div>
</div>
{{ /each }}
</script>
<div class="swiper-pagination"></div>
</div>
房屋详情页
$(document).ready(function () {
house_id = decodeQuery()[ 'id' ]
$.get("/api/v1.0/house/" + house_id,
function (resp) {
if ('0' == resp.errno) {
var house = resp.data.house_info
// console.log(house)
$('.swiper-container').html(template('house-image-tmpl', { image_urls: house.image_urls, price: house.price }))
$('.detail-con').html(template('house-detail-tmpl', { house: house, fas: house.facilities }))
// 预订按钮是否显示
if (house.uid != resp.data.user_id) {
$('.book-house').attr('href', '/booking.html?hid=' + house.hid)
$('.book-house').show()
} else {
$('.book-house').hide()
}
// 设置幻灯片对象,开启幻灯片滚动
var mySwiper = new Swiper('.swiper-container', {
loop: true,
autoplay: 2000,
autoplayDisableOnInteraction: false,
pagination: '.swiper-pagination',
paginationClickable: true
});
} else {
alert(resp.errmsg)
}
},
"json"
);
var mySwiper = new Swiper('.swiper-container', {
loop: true,
autoplay: 2000,
autoplayDisableOnInteraction: false,
pagination: '.swiper-pagination',
paginationType: 'fraction'
})
$(".book-house").show();
})
房屋详情轮播图
<div class="swiper-container"></div>
<script type="text/html" id="house-image-tmpl">
<ul class="swiper-wrapper">
{{ each image_urls as image_url }}
<li class="swiper-slide"><img src="{{ image_url }}"></li>
{{ /each }}
</ul>
<div class="swiper-pagination"></div>
<div class="house-price">¥<span>{{ (price/1.0).toFixed(0) }}</span>/晚</div>
</script>
评价
<div class="house-info layout-style">
<h3>评价信息</h3>
<ul class="house-comment-list">
{{ each house.comments }}
<li>
<p>{{$value.user_name}}<span class="fr">{{$value.ctime}}</span></p>
<p>{{$value.comment}}</p>
</li>
{{ /each }}
</ul>
</div>
14-房屋列表
03_构造时间条件
04_房屋列表页分页补充与测试
# GET /api/v1.0/houses?sd=2019-09-10&ed=2019-09-12&aid=1&sk=new&p=1
@api.route('/houses', methods=['GET'])
def get_house_list():
"""获取房屋列表信息,房屋搜索页面"""
# 1. 获取参数
start_date = request.args.get('sd', '')
end_date = request.args.get('ed', '')
area_id = request.args.get('aid', '')
sort_key = request.args.get('sk', 'new')
page = request.args.get('p')
# 2. 检查参数
# 检查时间
try:
if start_date:
start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
if end_date:
end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
if start_date and end_date:
assert start_date <= end_date
except Exception as e:
current_app.logger.error(e)
return jsonify(errno=RET.PARAMERR, errmsg='日期参数错误')
# 检查区域id是否存在
try:
area = Area.query.get(area_id)
except Exception as e:
current_app.logger.error(e)
return jsonify(errno=RET.PARAMERR, errmsg='区域参数错误')
# 页面参数
try:
page = int(page)
except Exception as e:
current_app.logger.error(e)
page = 1
# 优先在redis数据库中查询
redis_key = 'houses_%s_%s_%s_%s' % (
start_date, end_date, area_id, sort_key)
try:
resp = redis_store.hget(redis_key, page)
except Exception as e:
current_app.logger.error(e)
if resp:
current_app.logger.info('hit houses list in redis')
return resp, 200, {'Content-Type': 'application/json'}
# 3. 查询数据库
# 过滤条件的参数列表容器
filter_param = []
# 填充过滤条件
# 区域
if area:
filter_param.append(House.area_id == area_id)
# 时间
confilct_orders = []
try:
if start_date and end_date:
confilct_orders = Order.query.filter(
Order.begin_date <= end_date, Order.end_date >= start_date).all()
elif start_date:
confilct_orders = Order.query.filter(
Order.end_date >= start_date).all()
elif end_date:
confilct_orders = Order.query.filter(Order.start <= end_date).all()
except Exception as e:
current_app.logger.error(e)
confilct_house_ids = [order.house_id for order in confilct_orders]
filter_param.append(House.id.notin_(confilct_house_ids))
# 排序
# 查询数据库
houses_query = House.query.filter(*filter_param)
if 'booking' == sort_key:
houses_query = houses_query.order_by(House.room_count.desc())
elif 'price-inc' == sort_key:
houses_query = houses_query.order_by(House.price.asc())
elif 'price-des' == sort_key:
houses_query = houses_query.order_by(House.price.desc())
else:
houses_query = houses_query.order_by(House.create_time.desc())
# 分页
try:
# 获取数据时才真正与数据库交互,前面都是构建查询条件
page_obj = houses_query.paginate(
page=page,
per_page=constants.HOUSE_LIST_PAGE_CAPACITY,
error_out=False)
except Exception as e:
current_app.logger.error(e)
return jsonify(errno=RET.DBERR, errmsg='数据库异常')
# 4. 组建返回数据
houses = [house.to_basic_dict() for house in page_obj.items]
total_page = page_obj.pages
resp_dict = dict(errno=RET.OK, errmsg='OK', data={
'houses': houses, 'page': page, 'total_page': total_page})
resp_json = json.dumps(resp_dict)
# 5. 添加到redis缓存
if page <= total_page:
try:
pipeline = redis_store.pipeline()
pipeline.multi()
pipeline.hset(redis_key, page, resp_json)
pipeline.expire(
redis_key, constants.HOUSE_LIST_REDIS_CACHE_EXPIRES)
pipeline.execute()
except Exception as e:
current_app.logger.error(e)
return resp_json, 200, {'Content-Type': 'application/json'}
05_等号参数的说明
07redis的pipeline使用
创建redis管道,可以一次执行多个语句,避免执行一条语句后,另一条挂掉
# 5. 添加到redis缓存
if page <= total_page:
try:
# 创建redis管道,可以一次执行多个语句
pipeline = redis_store.pipeline()
# 开启多个语句记录
pipeline.multi()
pipeline.hset(redis_key, page, resp_json)
pipeline.expire(
redis_key, constants.HOUSE_LIST_REDIS_CACHE_EXPIRES)
# 执行语句
pipeline.execute()
except Exception as e:
current_app.logger.error(e)
08_房屋列表页前端代码
/**
* 更新房源列表信息
* action表示后端发送的数据在前端的展示方式
* 默认采取追加模式
* action=renew清空后再重新展示
*/
function updateHouseData (action) {
var areaId = $(".filter-area>li.active").attr("area-id");
if (undefined == areaId) areaId = "";
var startDate = $("#start-date").val();
var endDate = $("#end-date").val();
var sortKey = $(".filter-sort>li.active").attr("sort-key");
var params = {
aid: areaId,
sd: startDate,
ed: endDate,
sk: sortKey,
p: next_page
};
$.get("/api/v1.0/houses", params, function (resp) {
house_data_querying = false;
if ("0" == resp.errno) {
if (0 == resp.data.total_page) {
$(".house-list").html("暂时没有符合您查询的房屋信息。");
} else {
total_page = resp.data.total_page;
if ("renew" == action) {
cur_page = 1;
$(".house-list").html(template("house-list-tmpl", { houses: resp.data.houses }));
} else {
cur_page = next_page;
$(".house-list").append(template("house-list-tmpl", { houses: resp.data.houses }));
}
}
}
})
}
$(document).ready(function () {
var queryData = decodeQuery();
var startDate = queryData[ "sd" ];
var endDate = queryData[ "ed" ];
$("#start-date").val(startDate);
$("#end-date").val(endDate);
updateFilterDateDisplay();
var areaName = queryData[ "aname" ];
var areaId = queryData[ 'aid' ]
if (!areaName) areaName = "位置区域";
$(".filter-title-bar>.filter-title").eq(1).children("span").eq(0).html(areaName);
// 获取筛选条件中的城市区域信息
$.get("/api/v1.0/areas", function (data) {
if ("0" == data.errno) {
// 用户从首页跳转到这个搜索页面时可能选择了城区,所以尝试从url的查询字符串参数中提取用户选择的城区
var areaId = queryData[ "aid" ];
// 如果提取到了城区id的数据
if (areaId) {
// 遍历从后端获取到的城区信息,添加到页面中
for (var i = 0; i < data.data.length; i++) {
// 对于从url查询字符串参数中拿到的城区,在页面中做高亮展示
// 后端获取到城区id是整型,从url参数中获取到的是字符串类型,所以将url参数中获取到的转换为整型,再进行对比
areaId = parseInt(areaId);
if (data.data[ i ].aid == areaId) {
$(".filter-area").append('<li area-id="' + data.data[ i ].aid + '" class="active">' + data.data[ i ].aname + '</li>');
} else {
$(".filter-area").append('<li area-id="' + data.data[ i ].aid + '">' + data.data[ i ].aname + '</li>');
}
}
} else {
// 如果url参数中没有城区信息,不需要做额外处理,直接遍历展示到页面中
for (var i = 0; i < data.data.length; i++) {
$(".filter-area").append('<li area-id="' + data.data[ i ].aid + '">' + data.data[ i ].aname + '</li>');
}
}
// 在页面添加好城区选项信息后,更新展示房屋列表信息
updateHouseData("renew");
// 获取页面显示窗口的高度
var windowHeight = $(window).height();
// 为窗口的滚动添加事件函数
window.onscroll = function () {
// var a = document.documentElement.scrollTop==0? document.body.clientHeight : document.documentElement.clientHeight;
var b = document.documentElement.scrollTop == 0 ? document.body.scrollTop : document.documentElement.scrollTop;
var c = document.documentElement.scrollTop == 0 ? document.body.scrollHeight : document.documentElement.scrollHeight;
// 如果滚动到接近窗口底部
if (c - b < windowHeight + 50) {
// 如果没有正在向后端发送查询房屋列表信息的请求
if (!house_data_querying) {
// 将正在向后端查询房屋列表信息的标志设置为真,
house_data_querying = true;
// 如果当前页面数还没到达总页数
if (cur_page < total_page) {
// 将要查询的页数设置为当前页数加1
next_page = cur_page + 1;
// 向后端发送请求,查询下一页房屋数据
updateHouseData();
} else {
house_data_querying = false;
}
}
}
}
}
});
15-订单
01_订单模块后端代码
保存订单
@api.route("/orders", methods=["POST"])
@login_required
def save_order():
"""保存订单"""
user_id = g.user_id
# 获取参数
order_data = request.get_json()
if not order_data:
return jsonify(errno=RET.PARAMERR, errmsg="参数错误")
house_id = order_data.get("house_id") # 预订的房屋编号
start_date_str = order_data.get("start_date") # 预订的起始时间
end_date_str = order_data.get("end_date") # 预订的结束时间
# 参数检查
if not all((house_id, start_date_str, end_date_str)):
return jsonify(errno=RET.PARAMERR, errmsg="参数错误")
# 日期格式检查
try:
# 将请求的时间参数字符串转换为datetime类型
start_date = datetime.datetime.strptime(start_date_str, "%Y-%m-%d")
end_date = datetime.datetime.strptime(end_date_str, "%Y-%m-%d")
assert start_date <= end_date
# 计算预订的天数
days = (end_date - start_date).days + 1 # datetime.timedelta
except Exception as e:
current_app.logger.error(e)
return jsonify(errno=RET.PARAMERR, errmsg="日期格式错误")
# 查询房屋是否存在
try:
house = House.query.get(house_id)
except Exception as e:
current_app.logger.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.house_id == house_id, Order.begin_date <= end_date,
Order.end_date >= start_date).count()
# select count(*) from order where ....
except Exception as e:
current_app.logger.error(e)
return jsonify(errno=RET.DBERR, errmsg="检查出错,请稍候重试")
if count > 0:
return jsonify(errno=RET.DATAERR, errmsg="房屋已被预订")
# 订单总额
amount = days * house.price
# 保存订单数据
order = Order(
house_id=house_id,
user_id=user_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:
current_app.logger.error(e)
db.session.rollback()
return jsonify(errno=RET.DBERR, errmsg="保存订单失败")
return jsonify(errno=RET.OK, errmsg="OK", data={"order_id": order.id})
查询用户的订单信息
# /api/v1.0/user/orders?role=custom role=landlord
@api.route("/user/orders", methods=["GET"])
@login_required
def get_user_orders():
"""查询用户的订单信息"""
user_id = g.user_id
# 用户的身份,用户想要查询作为房客预订别人房子的订单,还是想要作为房东查询别人预订自己房子的订单
role = request.args.get("role", "")
# 查询订单数据
try:
if "landlord" == role:
# 以房东的身份查询订单
# 先查询属于自己的房子有哪些
houses = House.query.filter(House.user_id == user_id).all()
houses_ids = [house.id for house in houses]
# 再查询预订了自己房子的订单
orders = Order.query.filter(Order.house_id.in_(
houses_ids)).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:
current_app.logger.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})
接单、拒单
@api.route("/orders/<int:order_id>/status", methods=["PUT"])
@login_required
def accept_reject_order(order_id):
"""接单、拒单"""
user_id = g.user_id
# 获取参数
req_data = request.get_json()
if not req_data:
return jsonify(errno=RET.PARAMERR, errmsg="参数错误")
# action参数表明客户端请求的是接单还是拒单的行为
action = req_data.get("action")
if action not in ("accept", "reject"):
return jsonify(errno=RET.PARAMERR, errmsg="参数错误")
try:
# 根据订单号查询订单,并且要求订单处于等待接单状态
order = Order.query.filter(
Order.id == order_id, Order.status == "WAIT_ACCEPT").first()
house = order.house
except Exception as e:
current_app.logger.error(e)
return jsonify(errno=RET.DBERR, errmsg="无法获取订单数据")
# 确保房东只能修改属于自己房子的订单
if not order or house.user_id != user_id:
return jsonify(errno=RET.REQERR, errmsg="操作无效")
if action == "accept":
# 接单,将订单状态设置为等待评论
order.status = "WAIT_PAYMENT"
elif action == "reject":
# 拒单,要求用户传递拒单原因
reason = req_data.get("reason")
if not reason:
return jsonify(errno=RET.PARAMERR, errmsg="参数错误")
order.status = "REJECTED"
order.comment = reason
try:
db.session.add(order)
db.session.commit()
except Exception as e:
current_app.logger.error(e)
db.session.rollback()
return jsonify(errno=RET.DBERR, errmsg="操作失败")
return jsonify(errno=RET.OK, errmsg="OK")
保存订单评论信息
@api.route("/orders/<int:order_id>/comment", methods=["PUT"])
@login_required
def save_order_comment(order_id):
"""保存订单评论信息"""
user_id = g.user_id
# 获取参数
req_data = request.get_json()
comment = req_data.get("comment") # 评价信息
# 检查参数
if not comment:
return jsonify(errno=RET.PARAMERR, errmsg="参数错误")
try:
# 需要确保只能评论自己下的订单,而且订单处于待评价状态才可以
order = Order.query.filter(Order.id == order_id, Order.user_id == user_id,
Order.status == "WAIT_COMMENT").first()
house = order.house
except Exception as e:
current_app.logger.error(e)
return jsonify(errno=RET.DBERR, errmsg="无法获取订单数据")
if not order:
return jsonify(errno=RET.REQERR, errmsg="操作无效")
try:
# 将订单的状态设置为已完成
order.status = "COMPLETE"
# 保存订单的评价信息
order.comment = comment
# 将房屋的完成订单数增加1
house.order_count += 1
db.session.add(order)
db.session.add(house)
db.session.commit()
except Exception as e:
current_app.logger.error(e)
db.session.rollback()
return jsonify(errno=RET.DBERR, errmsg="操作失败")
# 因为房屋详情中有订单的评价信息,为了让最新的评价信息展示在房屋详情中,所以删除redis中关于本订单房屋的详情缓存
try:
redis_store.delete("house_info_%s" % order.house.id)
except Exception as e:
current_app.logger.error(e)
return jsonify(errno=RET.OK, errmsg="OK")
02_订单模块前端
function hrefBack () {
history.go(-1);
}
function getCookie (name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[ 1 ] : undefined;
}
function decodeQuery () {
var search = decodeURI(document.location.search);
return search.replace(/(^\?)/, '').split('&').reduce(function (result, item) {
values = item.split('=');
result[ values[ 0 ] ] = values[ 1 ];
return result;
}, {});
}
function showErrorMsg () {
$('.popup_con').fadeIn('fast', function () {
setTimeout(function () {
$('.popup_con').fadeOut('fast', function () { });
}, 1000)
});
}
$(document).ready(function () {
// 判断用户是否登录
$.get("/api/v1.0/session", function (resp) {
if ("0" != resp.errno) {
location.href = "/login.html";
}
}, "json");
$(".input-daterange").datepicker({
format: "yyyy-mm-dd",
startDate: "today",
language: "zh-CN",
autoclose: true
});
$(".input-daterange").on("changeDate", function () {
var startDate = $("#start-date").val();
var endDate = $("#end-date").val();
if (startDate && endDate && startDate > endDate) {
showErrorMsg("日期有误,请重新选择!");
} else {
var sd = new Date(startDate);
var ed = new Date(endDate);
days = (ed - sd) / (1000 * 3600 * 24) + 1;
var price = $(".house-text>p>span").html();
var amount = days * parseFloat(price);
$(".order-amount>span").html(amount.toFixed(2) + "(共" + days + "晚)");
}
});
var queryData = decodeQuery();
var houseId = queryData[ "hid" ];
// 获取房屋的基本信息
$.get("/api/v1.0/house/" + houseId, function (resp) {
if (0 == resp.errno) {
$(".house-info>img").attr("src", resp.data.house.img_urls[ 0 ]);
$(".house-text>h3").html(resp.data.house.title);
$(".house-text>p>span").html((resp.data.house.price / 100.0).toFixed(0));
}
});
// 订单提交
$(".submit-btn").on("click", function (e) {
if ($(".order-amount>span").html()) {
$(this).prop("disabled", true);
var startDate = $("#start-date").val();
var endDate = $("#end-date").val();
var data = {
"house_id": houseId,
"start_date": startDate,
"end_date": endDate
};
$.ajax({
url: "/api/v1.0/orders",
type: "post",
data: JSON.stringify(data),
contentType: "application/json",
dataType: "json",
headers: {
"X-CSRFTOKEN": getCookie("csrf_token"),
},
success: function (resp) {
if ("4101" == resp.errno) {
location.href = "/login.html";
} else if ("4004" == resp.errno) {
showErrorMsg("房间已被抢定,请重新选择日期!");
} else if ("0" == resp.errno) {
location.href = "/orders.html";
}
}
});
}
});
})
房客前端
order.js
//模态框居中的控制
function centerModals () {
$('.modal').each(function (i) { //遍历每一个模态框
var $clone = $(this).clone().css('display', 'block').appendTo('body');
var top = Math.round(($clone.height() - $clone.find('.modal-content').height()) / 2);
top = top > 0 ? top : 0;
$clone.remove();
$(this).find('.modal-content').css("margin-top", top - 30); //修正原先已经有的30个像素
});
}
function getCookie (name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[ 1 ] : undefined;
}
$(document).ready(function () {
$('.modal').on('show.bs.modal', centerModals); //当模态框出现的时候
$(window).on('resize', centerModals);
// 查询房客订单
$.get("/api/v1.0/user/orders?role=custom", function (resp) {
if ("0" == resp.errno) {
$(".orders-list").html(template("orders-list-tmpl", { orders: resp.data.orders }));
$(".order-pay").on("click", function () {
var orderId = $(this).parents("li").attr("order-id");
$.ajax({
url: "/api/v1.0/orders/" + orderId + "/payment",
type: "post",
dataType: "json",
headers: {
"X-CSRFToken": getCookie("csrf_token"),
},
success: function (resp) {
if ("4101" == resp.errno) {
location.href = "/login.html";
} else if ("0" == resp.errno) {
// 引导用户跳转到支付宝连接
location.href = resp.data.pay_url;
}
}
});
});
$(".order-comment").on("click", function () {
var orderId = $(this).parents("li").attr("order-id");
$(".modal-comment").attr("order-id", orderId);
});
$(".modal-comment").on("click", function () {
var orderId = $(this).attr("order-id");
var comment = $("#comment").val()
if (!comment) return;
var data = {
order_id: orderId,
comment: comment
};
// 处理评论
$.ajax({
url: "/api/v1.0/orders/" + orderId + "/comment",
type: "PUT",
data: JSON.stringify(data),
contentType: "application/json",
dataType: "json",
headers: {
"X-CSRFTOKEN": getCookie("csrf_token"),
},
success: function (resp) {
if ("4101" == resp.errno) {
location.href = "/login.html";
} else if ("0" == resp.errno) {
$(".orders-list>li[order-id=" + orderId + "]>div.order-content>div.order-text>ul li:eq(4)>span").html("已完成");
$("ul.orders-list>li[order-id=" + orderId + "]>div.order-title>div.order-operate").hide();
$("#comment-modal").modal("hide");
}
}
});
});
}
});
});
order.html
<div class="orders-con">
<ul class="orders-list">
</ul>
<script id="orders-list-tmpl" type="text/html">
{{if orders}}
{{each orders as order}}
<li order-id={{order.order_id}}>
<div class="order-title">
<h3>订单编号:{{order.order_id}}</h3>
{{ if "WAIT_COMMENT" == order.status }}
<div class="fr order-operate">
<button type="button" class="btn btn-success order-comment" data-toggle="modal" data-target="#comment-modal">发表评价</button>
</div>
{{ else if "WAIT_PAYMENT" == order.status }}
<div class="fr order-operate">
<button type="button" class="btn btn-success order-pay">去支付</button>
</div>
{{/if}}
</div>
<div class="order-content">
<img src="{{order.img_url}}">
<div class="order-text">
<h3>{{order.title}}</h3>
<ul>
<li>创建时间:{{order.ctime}}</li>
<li>入住日期:{{order.start_date}}</li>
<li>离开日期:{{order.end_date}}</li>
<li>合计金额:¥{{(order.amount/100.0).toFixed(0)}}(共{{order.days}}晚)</li>
<li>订单状态:
<span>
{{if "WAIT_ACCEPT" == order.status}}
待接单
{{else if "WAIT_PAYMENT" == order.status}}
待支付
{{else if "WAIT_COMMENT" == order.status}}
待评价
{{else if "COMPLETE" == order.status}}
已完成
{{else if "REJECTED" == order.status}}
已拒单
{{/if}}
</span>
</li>
{{if "COMPLETE" == order.status}}
<li>我的评价: {{order.comment}}</li>
{{else if "REJECTED" == order.status}}
<li>拒单原因: {{order.comment}}</li>
{{/if}}
</ul>
</div>
</div>
</li>
{{/each}}
{{else}}
暂时没有订单。
{{/if}}
</script>
<div class="modal fade" id="comment-modal" tabindex="-1" role="dialog" aria-labelledby="comment-label">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span
aria-hidden="true">×</span></button>
<h4 class="modal-title">发表评价</h4>
</div>
<div class="modal-body">
<textarea class="form-control" rows="3" id="comment" placeholder="请在此处填写评价"></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary modal-comment">确定</button>
</div>
</div>
</div>
</div>
</div>
房东前端逻辑
lorder.js
//模态框居中的控制
function centerModals(){
$('.modal').each(function(i){ //遍历每一个模态框
var $clone = $(this).clone().css('display', 'block').appendTo('body');
var top = Math.round(($clone.height() - $clone.find('.modal-content').height()) / 2);
top = top > 0 ? top : 0;
$clone.remove();
$(this).find('.modal-content').css("margin-top", top-30); //修正原先已经有的30个像素
});
}
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
$(document).ready(function(){
$('.modal').on('show.bs.modal', centerModals); //当模态框出现的时候
$(window).on('resize', centerModals);
// 查询房东的订单
$.get("/api/v1.0/user/orders?role=landlord", function(resp){
if ("0" == resp.errno) {
$(".orders-list").html(template("orders-list-tmpl", {orders:resp.data.orders}));
$(".order-accept").on("click", function(){
var orderId = $(this).parents("li").attr("order-id");
$(".modal-accept").attr("order-id", orderId);
});
// 接单处理
$(".modal-accept").on("click", function(){
var orderId = $(this).attr("order-id");
$.ajax({
url:"/api/v1.0/orders/"+orderId+"/status",
type:"PUT",
data:'{"action":"accept"}',
contentType:"application/json",
dataType:"json",
headers:{
"X-CSRFTOKEN":getCookie("csrf_token"),
},
success:function (resp) {
if ("4101" == resp.errno) {
location.href = "/login.html";
} else if ("0" == resp.errno) {
$(".orders-list>li[order-id="+ orderId +"]>div.order-content>div.order-text>ul li:eq(4)>span").html("已接单");
$("ul.orders-list>li[order-id="+ orderId +"]>div.order-title>div.order-operate").hide();
$("#accept-modal").modal("hide");
}
}
})
});
$(".order-reject").on("click", function(){
var orderId = $(this).parents("li").attr("order-id");
$(".modal-reject").attr("order-id", orderId);
});
// 处理拒单
$(".modal-reject").on("click", function(){
var orderId = $(this).attr("order-id");
var reject_reason = $("#reject-reason").val();
if (!reject_reason) return;
var data = {
action: "reject",
reason:reject_reason
};
$.ajax({
url:"/api/v1.0/orders/"+orderId+"/status",
type:"PUT",
data:JSON.stringify(data),
contentType:"application/json",
headers: {
"X-CSRFTOKEN":getCookie("csrf_token")
},
dataType:"json",
success:function (resp) {
if ("4101" == resp.errno) {
location.href = "/login.html";
} else if ("0" == resp.errno) {
$(".orders-list>li[order-id="+ orderId +"]>div.order-content>div.order-text>ul li:eq(4)>span").html("已拒单");
$("ul.orders-list>li[order-id="+ orderId +"]>div.order-title>div.order-operate").hide();
$("#reject-modal").modal("hide");
}
}
});
})
}
});
});
设置支付宝沙箱环境
具体流程可参考天天生鲜付款
后端支付宝订单支付逻辑
@api.route('/orders/<int:order_id>/payment', methods=['POST'])
@login_required
def order_pay(order_id):
"""支付宝订单支付
@param order_id: 订单编号
"""
user_id = g.user_id
# 创建支付宝对象
alipay = AliPay(
appid="你的appid",
app_private_key_path=os.path.join(os.path.dirname(__file__),
'keys/app_private_key.pem'),
alipay_public_key_path=os.path.join(os.path.dirname(__file__),
'keys/alipay_public_key.pem'),
sign_type="RSA",
app_notify_url=None,
debug=True
)
# 判断订单状态
try:
order = Order.query.filter(
Order.id == order_id, Order.user_id == user_id, Order.status == "WAIT_PAYMENT").first()
except Exception as e:
current_app.logger.error(e)
return jsonify(errno=RET.DBERR, errmsg='数据库异常')
if order is None:
return jsonify(errno=RET.NODATA, errmsg='订单不存在')
# 手机网站支付,需要跳转到https://openapi.alipay.com/gateway.do? + order_string
order_string = alipay.api_alipay_trade_wap_pay(
out_trade_no=order.id,
total_amount=str(order.amount/100.0),
subject=f'爱家租房-{order.id}',
timeout_express='15m',
return_url="http://127.0.0.1:5000/payComplete.html",
notify_url=None # 可选, 不填则使用默认notify url
)
# 构建支付宝支付链接, 发送给用户
pay_url = constants.ALIPAY_URL_PREFIX + order_string
return jsonify(errno=RET.OK, errmsg='OK', data={'pay_url': pay_url})
跳转时到订单页面时,前端调用保存订单信息
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[ 1 ] : undefined;
};
var alipayData = document.location.search.substr(1)
$.ajax({
type: "put",
url: "/api/v1.0/order/payment",
data: alipayData,
headers: {
"Access-Control-Allow-Orgin": "*",
"X-CSRFToken": getCookie('csrf_token')
},
dataType: 'json'
});
后端保存订单结果
@api.route('/order/payment', methods=['PUT'])
def save_order_payment_result():
"""保存订单支付结果"""
data = request.form.to_dict()
# sign 不能参与签名验证
signature = data.pop("sign")
# 创建支付宝对象
alipay = AliPay(
appid="你的appid",
app_private_key_path=os.path.join(os.path.dirname(__file__),
'keys/app_private_key.pem'),
alipay_public_key_path=os.path.join(os.path.dirname(__file__),
'keys/alipay_public_key.pem'),
sign_type="RSA",
app_notify_url=None,
debug=True
)
# verify
success = alipay.verify(data, signature)
if success:
# 修改订单状态
order_id = data.get('out_trade_no')
trade_no = data.get('trade_no')
try:
Order.query.filter_by(id=order_id).update(
{'status': 'WAIT_COMMENT', 'trade_no': trade_no})
db.session.commit()
except Exception as e:
current_app.logger.error(e)
db.session.rollback()
return jsonify(errno=RET.OK, errmsg='OK')
数据库迁移 Flask-Migrate
实际操作顺序:
1.python 文件 db init
2.python 文件 db migrate -m"版本名(注释)"
3.python 文件 db upgrade 然后观察表结构
4.根据需求修改模型
5.python 文件 db migrate -m"新版本名(注释)"
6.python 文件 db upgrade 然后观察表结构
7.若返回版本,则利用 python 文件 db history查看版本号
8.python 文件 db downgrade(upgrade) 版本号
在支付宝返回数据,提取其订单流水号,存入数据库
# 修改订单状态
order_id = data.get('out_trade_no')
trade_no = data.get('trade_no')
更新数据库版本