当前位置:首页 > 技术 > 正文内容

Flask 房屋租赁系统:首页轮播与房源搜索实现方案

访客 技术 2026年6月15日 1

项目背景与需求概述

在本模块中,我们需要构建租房平台的两大核心页面:首页与搜索页。首页主要负责展示登录用户信息以及热门推荐的房源图片;搜索页则需支持多维度筛选(如区域、日期、排序),并具备无限滚动加载的能力。为了优化性能,后端将引入 Redis 缓存机制来存储热点数据。

首页功能开发

进入门户页面的主要交互目标是展示当前用户的状态(未登录显示登录框,已登录显示昵称),同时在首屏通过轮播图形式呈现预订量最高的前 5 套房源。

![首页界面布局示意](#)

服务端接口设计

在 `api_1_0/houses.py` 路由文件中定义获取首页房源的逻辑。该接口需要优先从缓存读取数据,若缓存失效则查询数据库并更新缓存。

from flask import jsonify, current_app
import json
import redis # 假设已有连接对象 conn
from models import Property, constants
from utils.ret_code import RET, ret_msg

def fetch_homepage_listings():
    """获取首页热门房源列表"""
    cache_slot = 'hot_listings_cache'
    cached_data = None
    
    # 尝试从 Redis 获取缓存
    try:
        raw_data = conn.get(cache_slot)
        if raw_data:
            cached_data = raw_data.decode()
    except Exception as err:
        current_app.logger.error(f"Redis 读取错误: {err}")

    if not cached_data:
        # 缓存未命中,查询数据库
        try:
            properties = Property.query \
                .filter(Property.main_image != None) \
                .order_by(Property.booking_count.desc()) \
                .limit(constants.HOME_MAX_ITEMS).all()
            
            result_set = []
            for prop in properties:
                result_set.append({
                    'prop_id': prop.id,
                    'name': prop.title,
                    'thumb': prop.main_image,
                    'cost': prop.price
                })
            
            cached_data = json.dumps(result_set)
            # 写入缓存,设置过期时间
            conn.setex(cache_slot, constants.CACHE_EXPIRE_TIME, cached_data)
            
        except Exception as err:
            current_app.logger.error(f"数据库查询异常: {err}")
            return jsonify(code=RET.DBERR, msg='房源数据加载失败'), 500

    return jsonify(code=RET.SUCCESS, msg=ret_msg[RET.SUCCESS], result=cached_data)

客户端交互逻辑

前端页面 `index.html` 对应的脚本需处理三个独立的数据请求:会话校验、房源列表、城区数据。由于依赖异步数据,DOM 操作必须放置在回调函数内部。

$(function() {
    // 1. 验证用户登录状态
    $.get('/api/v1.0/user/current', function(resp) {
        if (resp.code === 0) {
            $('.user-nick').text(resp.result.name);
            $('.login-module').hide();
            $('.profile-module').fadeIn();
        } else {
            $('.login-entry').show();
        }
    });

    // 2. 加载热门房源并初始化轮播
    $.get('/api/v1.0/homes/recommend', function(data) {
        if (data.code === 0) {
            const listContainer = $('#slider-content');
            listContainer.html(template('listing-card', { items: data.result }));
            
            // 此时 DOM 已渲染完成,再初始化插件
            const swiperInstance = new Swiper('.main-slider', {
                loop: true,
                autoplay: { delay: 2500 },
                pagination: '.swiper-pagination'
            });
        } else {
            alert(data.msg);
        }
    });

    // 3. 加载区域下拉菜单
    $.get('/api/v1.0/zones/list', function(zones) {
        if (zones.code === 0) {
            const zoneHtml = template('zone-select', { zones: zones.result });
            $('#city-selector ul').html(zoneHtml);
            
            // 绑定选择事件
            $('#city-selector li').on('click', function() {
                const selectedName = $(this).text();
                const selectedId = $(this).attr('data-id');
                
                // 更新按钮显示及隐藏属性
                $('#search-trigger').html(selectedName);
                $('#search-trigger').attr('data-area-id', selectedId);
                $('#search-trigger').attr('data-area-name', selectedName);
                
                $('#city-popup').modal('hide');
            });
        }
    });
});

// 跳转至搜索入口
function navigateToSearch(btnElement) {
    const baseUrl = '/search.html';
    let params = '?area=' + $(btnElement).attr('data-area-id');
    params += '&areaName=' + ($(btnElement).attr('data-area-name') || '');
    params += '&checkIn=' + $(btnElement).attr('data-start-date');
    params += '&checkOut=' + $(btnElement).attr('data-end-date');
    
    window.location.href = baseUrl + params;
}

搜索结果页实现

当用户执行搜索后,URL 携带筛选参数,例如:/search.html?aid=1&aname=朝阳区&sd=2023-10-01&ed=2023-10-05。页面顶部保留筛选器以便二次调整。

![搜索结果页面示意](#)

后端搜索算法与缓存策略

搜索接口 /search/houses 需要处理复杂的排他逻辑(排除已被预订的时间段)。为提高响应速度,采用 Redis Hash 结构存储分页结果,以"查询条件组合"作为 Key,"页码"作为 Field。

@app.route('/api/v1.0/search/houses')
def search_available_properties():
    # 解析请求参数
    req_area_id = request.args.get('aid')
    req_start = request.args.get('sd')
    req_end = request.args.get('ed')
    req_page = int(request.args.get('page', 1))
    req_sort = request.args.get('sorted_by', 'latest')

    # 构建唯一的缓存键值,不包含页码以保证分页数据原子过期
    cache_hash_key = f"search:{req_area_id}:{req_start}:{req_end}:{req_sort}"
    
    try:
        cached_res = conn.hget(cache_hash_key, str(req_page)).decode()
    except Exception:
        cached_res = None

    if not cached_res:
        # --- 数据库查询逻辑 ---
        # 1. 验证基础数据类型
        try:
            if req_start: req_start_date = datetime.strptime(req_start, '%Y-%m-%d').date()
            else: req_start_date = None
            
            if req_end: req_end_date = datetime.strptime(req_end, '%Y-%m-%d').date()
            else: req_end_date = None
        except ValueError:
            return jsonify(code=RET.PARAMERR, msg='日期格式无效'), 400
            
        # 2. 构造排除订单的条件列表
        exclude_filters = [Order.status == 'completed']
        
        # 如果存在时间范围,需排除冲突订单
        if req_start_date and req_end_date:
            # 时间段重叠判断逻辑
            exclude_filters.append(Order.start_time <= req_end_date)
            exclude_filters.append(Order.end_time >= req_start_date)
        elif req_start_date:
            exclude_filters.append(Order.end_time >= req_start_date)
        elif req_end_date:
            exclude_filters.append(Order.start_time <= req_end_date)

        # 3. 先查被占用的房源 ID
        booked_ids = Order.query.filter(*exclude_filters).with_entities(Order.prop_id).all()
        booked_ids = [bid[0] for bid in booked_ids] if booked_ids else []

        # 4. 主查询:过滤可用房源
        prop_query = Property.query.filter(
            Property.area_id == (int(req_area_id) if req_area_id else None),
            ~Property.id.in_(booked_ids)
        )

        # 5. 应用排序规则
        if req_sort == 'price_asc':
            prop_query = prop_query.order_by(Property.cost.asc())
        elif req_sort == 'newest':
            prop_query = prop_query.order_by(Property.created_at.desc())
        # 默认按热度降序
        
        # 6. 执行分页
        try:
            page_obj = prop_query.paginate(page=req_page, per_page=constants.LIST_PER_PAGE)
            total_pages = page_obj.pages
            items = [item.to_search_dict() for item in page_obj.items]
        except Exception:
            return jsonify(code=RET.DBERR, msg='分页服务异常'), 500

        # 组装返回数据结构
        final_result = {'items': items, 'current': req_page, 'total': total_pages}
        cached_res = json.dumps(final_result)

        # 使用 Pipeline 批量写入 Redis
        pipe = conn.pipeline()
        pipe.hset(cache_hash_key, str(req_page), cached_res)
        pipe.expire(cache_hash_key, constants.SEARCH_CACHE_TTL)
        pipe.execute()

    return jsonify(code=RET.SUCCESS, msg=ret_msg[RET.SUCCESS], data=cached_res)

前端分页与交互控制

搜索页的核心在于滚动监听与状态管理。利用全局变量记录当前页和加载状态,防止重复请求。

var SearchState = {
    currPage: 1,
    nextPage: 2,
    totalPages: 1,
    isLoading: false
};

function loadListings(mode) {
    var filters = {
        aid: $('.filter-active[data-type=area]').attr('data-id'),
        sd: $('#start-picker').val(),
        ed: $('#end-picker').val(),
        sort: $('.filter-active[data-type=sort]').attr('data-key')
    };

    var pageNum = (mode === 'append') ? SearchState.nextPage : 1;
    
    // 避免重复点击触发多次请求
    if (SearchState.isLoading) return;
    SearchState.isLoading = true;

    var urlParams = new URLSearchParams({
        aid: filters.aid,
        sd: filters.sd,
        ed: filters.ed,
        sorted_by: filters.sort || 'latest',
        page: pageNum
    });

    $.get('/api/v1.0/search/houses?' + urlParams.toString(), function(resp) {
        SearchState.isLoading = false;
        if (resp.code === 0 && resp.data) {
            var parsedData = JSON.parse(resp.data);
            SearchState.totalPages = parsedData.total;
            
            var htmlContent = template('house-row', { houses: parsedData.items });
            
            if (mode === 'refresh') {
                $('#listing-container').html(htmlContent);
                SearchState.currPage = 1;
                SearchState.nextPage = 2;
            } else {
                $('#listing-container').append(htmlContent);
                SearchState.currPage++;
                SearchState.nextPage++;
            }
        }
    }).fail(function() {
        SearchState.isLoading = false;
    });
}

// 窗口初始化
$(document).ready(function() {
    // 解析 URL 参数恢复筛选状态
    var params = decodeURI(window.location.search);
    // ...省略参数回填 DOM 的代码...
    
    // 首次加载
    loadListings('refresh');

    // 监听滚动实现懒加载
    $(window).scroll(function() {
        var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
        var clientHeight = document.documentElement.clientHeight || document.body.clientHeight;
        var bodyHeight = document.documentElement.scrollHeight || document.body.scrollHeight;

        // 距离底部不足 50px 且未加载完所有数据
        if ((scrollTop + clientHeight) >= (bodyHeight - 50) && !SearchState.isLoading) {
            if (SearchState.currPage < SearchState.totalPages) {
                loadListings('append');
            }
        }
    });
    
    // 筛选控件改变后的处理
    function applyFilter() {
        loadListings('refresh');
        hideDropdowns();
    }
    // 绑定点击事件到筛选项...
});

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。