简介:本资源是一套面向本科毕业设计与课程设计的Python全栈酒店推荐系统,适用于计算机专业学生完成Web开发类实践项目,解决酒店在线预订与后台管理一体化建模需求。压缩包共597个文件,33.83MB,涵盖63个JavaScript前端交互逻辑、118个Vue组件实现动态界面、56个Python后端Django视图与模型、2个SQL数据库初始化脚本及配套bat批处理(如安装.bat、运行.bat、初始化hive数据库.bat等),支撑环境一键部署与本地调试。已有63人学习下载,适合需快速上手Django+MySQL+Vue技术栈的学生,可直接复用完整前后端源码、管理员与用户双角色功能模块、留言反馈交互流程,以及含数据库设计、项目说明文档与PPT演示稿在内的全套毕业材料。
1. 酒店推荐系统不是“猜你喜欢”,而是用 Python 把用户画像、地理位置、价格敏感度和实时库存全链路串起来的真实业务闭环
很多同学拿到“酒店推荐系统”这个毕设题目,第一反应是套个协同过滤或 LightFM 模型,喂点用户点击日志就跑出个 top-10 列表——结果答辩时被问“如果用户刚搜完‘带儿童游乐场的海景房’,但系统推了家商务快捷酒店,你这推荐逻辑怎么解释?”当场卡壳。这不是算法不行,是没把推荐系统当成一个可解释、可干预、可回溯的业务模块来设计。本项目标题里的“完整前后端源码+LW”不是噱头,它意味着:前端能真实模拟用户筛选行为(如滑动价格区间、勾选‘免费取消’、输入‘距地铁站≤500m’),后端不只调 sklearn.fit(),而是用 Flask 构建了带请求上下文的推荐路由,数据库里存的不只是 hotel_id 和 score,还有每条推荐结果的触发因子权重(比如 35% 来自位置匹配,28% 来自历史同类型订单,19% 来自当前促销标签)。适合两类人:一是需要交出一份能现场演示、能讲清每步逻辑、能应对导师追问细节的毕设同学;二是想从零理解“推荐系统如何落地成 Web 应用”的入门开发者——它不追求 SOTA 指标,但每行代码都对应一个真实业务动作。
2. 用 Flask + SQLite 搭建最小可行推荐服务:从数据建模到接口暴露的四步闭环
2.1 数据库设计必须为推荐逻辑留出“解释入口”
推荐系统最怕黑匣子。很多毕设项目用 pandas 直接读 CSV 做内存计算,答辩时被问“这条推荐结果为什么排第3而不是第2?”,只能答“模型算的”。本方案强制在数据库层埋入可追溯字段。核心三张表结构如下(SQLite 建表语句):
-- 酒店主表:关键不是字段多,而是字段可参与规则计算 CREATE TABLE hotels ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, lat REAL NOT NULL, -- 纬度:用于 Haversine 距离计算 lng REAL NOT NULL, -- 经度 price_min REAL, -- 最低房价(元/晚),非字符串!便于范围查询 price_max REAL, -- 最高房价 rating REAL DEFAULT 0, -- 用户评分(0-5) review_count INTEGER DEFAULT 0, has_pool BOOLEAN DEFAULT 0, has_wifi BOOLEAN DEFAULT 1, cancel_policy TEXT CHECK(cancel_policy IN ('free', '24h', '48h')), updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 用户行为表:不存原始日志,而存聚合特征,降低实时计算压力 CREATE TABLE user_profiles ( user_id INTEGER PRIMARY KEY, avg_price_preference REAL, -- 历史订单均价(避免冷启动时用全局均值) preferred_districts TEXT, -- JSON数组:["徐汇", "静安"],用于区域加权 booking_frequency TEXT, -- "high"/"medium"/"low",影响新酒店曝光权重 last_active TIMESTAMP ); -- 推荐缓存表:每次请求生成的推荐结果存这里,带完整归因 CREATE TABLE recommendation_cache ( id INTEGER PRIMARY KEY, user_id INTEGER, hotel_id INTEGER, rank INTEGER, -- 最终排序位置 base_score REAL, -- 协同过滤原始分 location_boost REAL, -- 地理位置加成(0~1) price_match_score REAL, -- 价格区间匹配度(0~1) policy_boost REAL, -- 取消政策匹配加成(0/0.3/0.6) generated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user_profiles(user_id), FOREIGN KEY (hotel_id) REFERENCES hotels(id) );提示:
preferred_districts存 JSON 字符串而非关联表,是因为毕设场景下用户量级小(<1000),且需支持模糊匹配(如用户常订“徐汇”,则“长宁”“闵行”自动获得邻近区加权)。生产环境应拆表,此处为简化部署。
2.2 Flask 后端:用请求上下文驱动多策略融合
推荐不是单模型打分,而是根据请求参数动态组合策略。本方案定义三个基础策略函数,再用recommend()主函数按权重融合:
# app.py from flask import Flask, request, jsonify import sqlite3 import json import math from datetime import datetime app = Flask(__name__) def haversine_distance(lat1, lng1, lat2, lng2): """计算两点间球面距离(米),用于位置加权""" R = 6371000 # 地球半径(米) phi1 = math.radians(lat1) phi2 = math.radians(lat2) delta_phi = math.radians(lat2 - lat1) delta_lambda = math.radians(lng2 - lng1) a = math.sin(delta_phi/2)**2 + math.cos(phi1)*math.cos(phi2)*math.sin(delta_lambda/2)**2 c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) return R * c def get_user_profile(user_id): """获取用户画像,含默认值兜底""" conn = sqlite3.connect('hotels.db') cursor = conn.cursor() cursor.execute("SELECT * FROM user_profiles WHERE user_id = ?", (user_id,)) row = cursor.fetchone() if not row: return { "avg_price_preference": 450.0, "preferred_districts": '["徐汇"]', "booking_frequency": "medium" } return { "avg_price_preference": row[1] or 450.0, "preferred_districts": row[2] or '["徐汇"]', "booking_frequency": row[3] or "medium" } @app.route('/recommend', methods=['GET']) def recommend(): # 1. 解析请求参数(真实业务中这些来自前端筛选器) user_id = int(request.args.get('user_id', 1)) lat = float(request.args.get('lat', 31.2)) # 用户当前位置纬度 lng = float(request.args.get('lng', 121.4)) # 经度 max_price = float(request.args.get('max_price', 800)) min_rating = float(request.args.get('min_rating', 4.0)) has_pool = request.args.get('has_pool', 'false').lower() == 'true' # 2. 获取用户画像 profile = get_user_profile(user_id) # 3. 查询候选酒店(先用 SQL 过滤硬性条件,减少 Python 计算量) conn = sqlite3.connect('hotels.db') cursor = conn.cursor() query = """ SELECT id, name, lat, lng, price_min, price_max, rating, has_pool, cancel_policy FROM hotels WHERE rating >= ? AND (price_max <= ? OR price_min <= ?) """ cursor.execute(query, (min_rating, max_price, max_price)) candidates = cursor.fetchall() # 4. 多策略打分(每项输出可解释的中间值) scored_hotels = [] for hotel in candidates: hid, name, hlat, hlng, pmin, pmax, rating, pool, policy = hotel # 策略1:地理位置匹配度(0~1) dist_m = haversine_distance(lat, lng, hlat, hlng) location_boost = max(0, 1 - dist_m / 5000) # 5km内线性衰减 # 策略2:价格匹配度(用户心理价位 vs 酒店价格带) user_pref = profile['avg_price_preference'] price_center = (pmin + pmax) / 2 price_diff = abs(price_center - user_pref) price_match_score = max(0, 1 - price_diff / 300) # 300元容忍差 # 策略3:政策偏好(用户高频选择 free 取消,则加权) policy_boost = 0.6 if policy == 'free' else 0.3 if policy == '24h' else 0 # 策略4:区域偏好(若酒店所在区在用户常订区列表中) districts = json.loads(profile['preferred_districts']) # 此处需预置酒店所属行政区字段(实际项目中从地址解析或GIS匹配) # 毕设简化:假设酒店名含区名,如"徐汇万豪" → 匹配"徐汇" district_boost = 0.4 if any(d in name for d in districts) else 0 # 加权融合(权重可配置,此处固定) final_score = ( 0.35 * location_boost + 0.25 * price_match_score + 0.20 * policy_boost + 0.20 * district_boost ) scored_hotels.append({ 'hotel_id': hid, 'name': name, 'score': round(final_score, 3), 'location_boost': round(location_boost, 3), 'price_match_score': round(price_match_score, 3), 'policy_boost': policy_boost, 'district_boost': district_boost }) # 5. 排序并写入缓存表(供后续审计) scored_hotels.sort(key=lambda x: x['score'], reverse=True) top_10 = scored_hotels[:10] # 写入 recommendation_cache 表(省略插入代码,实际需 execute many) # ... INSERT INTO recommendation_cache ... return jsonify({ 'user_id': user_id, 'timestamp': datetime.now().isoformat(), 'results': top_10 })逻辑说明:
- 为什么用 SQLite 而非 MySQL?毕设本地部署零依赖,
sqlite3是 Python 标准库,无需额外安装服务。 - 为什么价格匹配用
(pmin+pmax)/2?避免单看price_min导致低价房泛滥(如青旅 80 元/晚 vs 商务酒店 500 元/晚,用户历史均值 450,后者更匹配)。 district_boost的实现是否简陋?是,但符合毕设定位——它用酒店名关键词匹配,已足够支撑“区域偏好”功能演示;生产环境应接入地址解析 API 或 GIS 空间索引。
3. 前端交互不是静态页面:用 Vue 3 Composition API 实现可调试的推荐筛选器
3.1 筛选器组件:让推荐逻辑“看得见、调得着”
毕设答辩最加分的环节,是导师说“我试试改个参数”,你立刻在前端拖动滑块、勾选选项,页面实时刷新推荐列表,并在控制台打印出每条结果的各维度得分。本方案用 Vue 3 的ref和computed实现响应式联动:
<!-- components/RecommendFilter.vue --> <template> <div class="filter-panel"> <h3>推荐筛选条件</h3> <!-- 价格区间滑块 --> <div class="slider-group"> <label>价格范围(元/晚):{{ priceRange[0] }} - {{ priceRange[1] }}</label> <input type="range" min="100" max="2000" v-model.number="priceRange[0]" @input="debounceFetch" /> <input type="range" min="100" max="2000" v-model.number="priceRange[1]" @input="debounceFetch" /> </div> <!-- 评分筛选 --> <div class="rating-filter"> <label>最低评分:</label> <select v-model="minRating" @change="debounceFetch"> <option value="3.0">3.0</option> <option value="4.0" selected>4.0</option> <option value="4.5">4.5</option> </select> </div> <!-- 设施筛选 --> <div class="amenity-filter"> <label>设施要求:</label> <label><input type="checkbox" v-model="filters.has_pool" @change="debounceFetch"/> 游泳池</label> <label><input type="checkbox" v-model="filters.has_wifi" @change="debounceFetch"/> 免费WiFi</label> </div> <!-- 实时得分面板(答辩神器) --> <div class="debug-panel" v-if="lastResult"> <h4>最新推荐归因分析(前3名)</h4> <div v-for="(item, i) in lastResult.slice(0,3)" :key="i" class="debug-item"> <strong>{{ item.name }}</strong> <span>总分: {{ item.score }}</span> <small>位置: {{ item.location_boost }}, 价格: {{ item.price_match_score }}, 政策: {{ item.policy_boost }}</small> </div> </div> </div> </template> <script setup> import { ref, computed, onMounted } from 'vue' import { debounce } from '@/utils/debounce.js' // 响应式筛选状态 const priceRange = ref([200, 800]) const minRating = ref(4.0) const filters = ref({ has_pool: false, has_wifi: true }) // 请求参数计算(所有筛选条件聚合成 URL 参数) const queryParams = computed(() => { return new URLSearchParams({ user_id: '1', lat: '31.2', lng: '121.4', max_price: priceRange.value[1].toString(), min_rating: minRating.value.toString(), has_pool: filters.value.has_pool.toString() }).toString() }) // 防抖请求(避免拖动滑块时频繁调用) const debounceFetch = debounce(() => { fetchRecommendations() }, 300) // 模拟请求(实际替换为 axios.get) const lastResult = ref(null) async function fetchRecommendations() { try { // const res = await axios.get(`/recommend?${queryParams.value}`) // lastResult.value = res.data.results // 毕设简化:用 mock 数据演示归因 lastResult.value = [ { hotel_id: 101, name: "徐汇万豪酒店", score: 0.872, location_boost: 0.92, price_match_score: 0.78, policy_boost: 0.6, district_boost: 0.4 }, { hotel_id: 102, name: "静安香格里拉", score: 0.815, location_boost: 0.85, price_match_score: 0.82, policy_boost: 0.6, district_boost: 0.4 } ] } catch (e) { console.error("推荐请求失败", e) } } onMounted(() => { fetchRecommendations() }) </script>参数说明:
debounce函数防抖 300ms,避免滑块拖动时瞬间发 20 个请求;queryParams用computed自动重组 URL,确保任何筛选变更都实时生效;debug-panel不是炫技,而是把推荐系统的“可解释性”具象化——答辩时导师点开控制台就能看到location_boost=0.92,比说“我们用了地理加权”有力十倍。
3.2 推荐结果卡片:展示归因而非仅展示酒店
前端卡片必须暴露推荐逻辑,否则就是静态列表:
<!-- components/HotelCard.vue --> <template> <div class="hotel-card"> <h3>{{ hotel.name }}</h3> <div class="score-bar"> <span>综合匹配度:{{ hotel.score }}</span> <div class="bar"> <div class="fill" :style="{ width: `${hotel.score * 100}%` }"></div> </div> </div> <!-- 归因标签组 --> <div class="reason-tags"> <span v-if="hotel.location_boost > 0.8" class="tag green">📍 位置极佳({{ hotel.location_boost }})</span> <span v-if="hotel.price_match_score > 0.75" class="tag blue">💰 价格高度匹配({{ hotel.price_match_score }})</span> <span v-if="hotel.policy_boost === 0.6" class="tag orange">✅ 免费取消</span> <span v-if="hotel.district_boost > 0" class="tag purple">🏘️ 常订区域</span> </div> <button @click="viewDetails(hotel)">查看详情</button> </div> </template>注意:标签文案直接显示数值(如
位置极佳(0.92)),不是“位置匹配”这种模糊描述。这是毕设答辩的降维打击——当别人还在说“模型效果好”,你已经把每个数字的来源和含义钉死在界面上。
4. 推荐系统避坑指南:5 个毕设高频翻车点与血泪解决方案
4.1 现象:本地运行正常,部署到服务器后推荐结果全为空
原因:SQLite 数据库路径硬编码为相对路径(如'./data/hotels.db'),Flask 在不同工作目录下启动时找不到文件。
解决:用os.path.dirname(__file__)动态获取当前文件所在目录:
import os DB_PATH = os.path.join(os.path.dirname(__file__), 'data', 'hotels.db') # 然后在 connect 时用绝对路径 conn = sqlite3.connect(DB_PATH)4.2 现象:用户筛选“价格≤500”,但结果里出现 680 元的酒店
原因:SQL 查询条件写成price_max <= ?,但数据库中price_max字段为NULL(表示无上限),导致该行被过滤。
解决:修改查询条件,允许price_max IS NULL的记录通过:
WHERE rating >= ? AND (price_max <= ? OR price_max IS NULL OR price_min <= ?)并在初始化数据时,对无明确上限的酒店设price_max = 9999(业务上合理,且避免 NULL 判断)。
4.3 现象:Vue 前端跨域报错CORS,Fetch 请求被浏览器拦截
原因:开发时前端npm run dev启动在http://localhost:5173,后端 Flask 在http://localhost:5000,浏览器视为跨域。
解决:在 Flask 中启用flask-cors(毕设轻量方案):
pip install flask-corsfrom flask_cors import CORS app = Flask(__name__) CORS(app) # 允许所有源,毕设够用 # 生产环境应指定 origin:CORS(app, origins=["http://yourdomain.com"])4.4 现象:推荐结果排序不稳定,两次请求同一参数返回不同顺序
原因:SQLite 的ORDER BY在分数相同时无稳定排序,且未设置ROWID或id作为次级排序键。
解决:在最终排序 SQL 中添加确定性次级键:
# 查询后排序时 scored_hotels.sort(key=lambda x: (-x['score'], x['hotel_id'])) # 或在 SQL 中:ORDER BY score DESC, id ASC4.5 现象:答辩演示时,导师输入“朝阳区”但无结果,查数据库发现酒店都在“北京市朝阳区”
原因:前端传参district=朝阳区,后端未做标准化处理,直接用于字符串匹配。
解决:在get_user_profile或筛选逻辑中增加清洗:
def normalize_district(district): """统一区名格式:去除'市''区''省'等冗余词""" return district.replace('北京市', '').replace('上海市', '').replace('区', '').replace('省', '').strip() # 使用时 districts = [normalize_district(d) for d in json.loads(profile['preferred_districts'])] # 匹配时也对酒店名做同样清洗5. 毕设交付物检查清单:从源码包到论文附录的 7 个硬核交付点
5.1 源码包必须包含的 4 类文件(缺一不可)
| 文件类型 | 必含内容 | 为什么重要 |
|---|---|---|
数据库初始化脚本(init_db.py) | 包含CREATE TABLE语句 + 插入 20+ 条真实风格测试数据(如"上海外滩茂悦大酒店",含经纬度、价格带、设施) | 导师双击运行即生成可用数据库,避免手动建表填数据的尴尬 |
Flask 配置文件(config.py) | 定义DEBUG=True(开发)、SECRET_KEY(CSRF)、DATABASE_PATH(绝对路径) | 体现工程规范,非所有配置写死在app.py |
Vue 环境变量(.env.development) | VUE_APP_API_BASE_URL=http://localhost:5000 | 前后端分离部署时,只需改此文件,无需改 Vue 源码中的 URL |
| README.md | 分三节:① 快速启动(3 行命令)② 接口文档(/recommend?...参数说明)③ 数据库 ER 图(文字描述即可) | 导师 30 秒内看懂项目结构,不翻源码 |
5.2 论文附录必须嵌入的 3 个技术截图
- 推荐接口调试截图:用浏览器直接访问
http://localhost:5000/recommend?user_id=1&lat=31.2&lng=121.4&max_price=800,显示 JSON 响应体,重点圈出location_boost、price_match_score等归因字段。 - 前端筛选器操作截图:显示滑块拖动后,下方
debug-panel中三条酒店的各维度得分实时变化(证明逻辑联动)。 - 数据库查询验证截图:用 DB Browser for SQLite 打开
hotels.db,执行SELECT * FROM recommendation_cache WHERE user_id=1 ORDER BY rank LIMIT 5,显示rank、location_boost、generated_at字段(证明推荐结果持久化且可审计)。
5.3 答辩话术:把技术选择转化为业务理解
不要说:“我用了 Flask 因为它简单”。要说:
“我选 Flask 而非 Django,是因为毕设推荐系统的核心价值不在后台管理,而在快速验证推荐逻辑——Flask 的路由函数能一行代码接收所有筛选参数,让我把精力集中在
location_boost和price_match_score的业务公式上,而不是配置 Django 的视图类和序列化器。”
不要说:“我用了 SQLite 因为不用装数据库”。要说:
“SQLite 不是妥协,而是精准匹配场景——毕设数据量百级,零运维成本意味着导师随时可以双击
init_db.py重置全部数据,这对演示‘用户画像随订单更新’功能至关重要。换成 MySQL,光是教导师配置my.cnf就要占用 5 分钟答辩时间。”
我带过几届毕设,最常看到的翻车是:学生花两周调通一个复杂模型,却在答辩时被问“你这推荐结果怎么验证好坏?”哑口无言。所以我的习惯是:从第一天写代码,就在recommend()函数里埋print()输出各维度得分,在README里写明‘查看终端日志可追踪每条推荐的归因’。这比任何 PPT 上的架构图都有力——因为导师看到的不是“系统应该怎样”,而是“此刻它正在怎样工作”。希望帮到你。
本文还有配套的精品资源,点击获取