爱家租房项目③

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">&times;</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')

在这里插入图片描述
更新数据库版本
在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值