做旅游景点加酒店推荐的网站很多朋友一开始都把这个项目当成一个前端展示在做结果忙活半个月做出来的东西就是一个带搜索框的静态页面完全没有推荐的影子。其实这类项目的核心难点不在页面而在三个地方数据怎么整理、推荐逻辑怎么落地、前后端怎么把数据串联起来。这篇博客我会直接以Python生态里最常用的Flask和Django两个框架展开把一套能跑通的景点酒店推荐完整方案讲清楚。适合正在做课程设计、毕业设计或者想沉淀一个完整Web项目经验的开发者参考也适合刚开始接触Flask和Django、想搞明白框架到底在项目里干什么的新手去理解。先说一个反直觉的结论这个项目最值钱的部分不是网页做得多好看而是你那个推荐接口返回的数据够不够像样。很多同学把大量时间花在改CSS和调试轮播图上最后答辩时老师问一句你这个推荐结果是怎么算出来的直接就卡住了。所以这篇文章我会把重量压在推荐机制的实现上前后端反而会讲得比较节制。1. 先想清楚景点酒店推荐到底要解决什么问题动手写代码之前我先问过自己一个问题用户打开这个网站他的完整操作路径是什么把这个路径想明白了功能边界就清晰了。在我看来一个最小可用版的路径应该是这样的用户输入目的地或者景点名称网站返回这个城市的景点列表再根据用户选择的景点推荐周边合适的酒店。中间可以穿插关键词搜索、筛选、排序这些常规操作但核心链路就是找景点、看景点、订酒店。把这个路径拆开功能清单基本就固定了。景点模块需要支持按城市浏览、按关键词搜索、查看景点详情酒店模块需要支持按城市列表、按关联景点推荐、按评分和价格排序推荐模块则是整个网站的灵魂负责把冷冰冰的数据库记录变成用户觉得有用的排序。如果还要加一点个性化元素可以记录用户的浏览记录在用户第二次访问时优先展示他看过的那类景点和酒店。技术栈上之所以纠结Flask和Django是因为这两个框架都能完成上述功能但完成方式差异很大。Flask是典型的轻量微框架路由、视图、请求响应全靠自己搭灵活程度高适合我自己清楚每一步在干什么的开发者Django则是全家桶自带ORM、Admin后台、表单处理、认证体系适合我想快速把后台管理做出来的场景。我的建议是如果这个项目的重点是推荐算法和大胆的数据处理逻辑Flask会让你更舒服如果项目还要求做一个能管理景点、酒店、用户评论的管理后台Django的Admin能帮你省下大量时间。我最终选择用Django作为主线来写这篇博客原因是这个项目太适合用Django的Admin来管理景点和酒店数据了。你爬完数据、清洗完入库之后打开Admin后台就能直接修改景点标签、调整酒店价格不需要为管理功能单独写任何代码。Flask当然也能做但你需要手动搭一个后台页面工作量直接翻倍。1.1 推荐系统的真实地位这里我特别想强调一点旅游推荐网站里的推荐和电商网站那种猜你喜欢不是一回事。电商推荐讲究个性化要看用户历史行为、偏好建模、协同过滤但旅游推荐的场景通常是用户有明确目的地然后才产生决策。所以这个项目里推荐的核心是召回排序而不是预测用户想看什么。具体来说召回环节是把符合用户目的地条件的所有景点和酒店找出来排序环节则是按照关键词匹配度、景点评分、酒店价格与景点的距离、热度等因素计算出一个综合得分最后按得分降序返回。这个思路在头条、美团、携程这些网站的内部推荐系统里都是基础形态把它做透比盲目去调一个机器学习模型更符合项目定位。2. 数据是推荐系统的地基采集思路与数据库建模不做数据清洗和表结构设计推荐逻辑写得再漂亮都没有意义。一个推荐网站如果把景点、酒店、城市、标签全塞在一张表里那后面做筛选和匹配的时候每一条SQL都像在倒腾垃圾堆。2.1 数据来源爬虫补充还是手动整理旅游数据获取无非三种途径爬虫采集、公开数据集、手动整理。我建议先把手动整理做起来再考虑爬虫。原因是这个项目的数据量级不需要很大50个城市、300个景点、200家酒店已经足够支撑完整的推荐效果手动整理完全能够覆盖。等你把核心逻辑跑通了再引入爬虫去扩充数据量效果会非常好。如果确实想用爬虫我个人的做法是拿Python的requests加BeautifulSoup去抓公开的旅游信息站点比如景点名称、简介、评分、经纬度这一类基础公开数据。但这里必须提醒几句要控制请求频率设置合理的间隔时间要优先抓取公开接口或者静态页面不要碰有明显反爬标识和数据加密的站点抓到的数据只用于学习和演示不要直接商用。我在实际项目中吃过亏爬虫速度没控制好IP被对方封了连带整个开发机的外网访问都受影响非常耽误事。2.2 三张核心表加两张关系表数据库设计上我强烈建议用拆分清晰的多表结构而不是宽表。我这里给出一个最少三张核心表加若干关系表的方案表名核心字段作用cityid, name, province, pinyin城市维度景点和酒店都归属城市scenicid, name, city_id, description, rating, heat_score, price, cover_url景点基础信息hotelid, name, city_id, address, rating, avg_price, distance_to_city_center, lat, lng酒店基础信息scenic_tagid, scenic_id, tag景点标签一对多hotel_tagid, hotel_id, tag酒店标签一对多这个结构最重要的地方在于城市独立成表景点和酒店通过city_id关联城市标签不直接塞在景点字段里而是单独用子表存储。为什么标签要单独拆表因为一个景点往往有多个标签比如亲子历史文化网红打卡地如果用逗号拼在字段里SQL查询时做模糊匹配会非常痛苦计算相似度时也要反复做字符串切分拆成子表之后一行一个标签做匹配直接连表JOIN或者内存里集合运算干净利落。2.3 多对多关系里的落地技巧如果你用的是先建模型再自动生成表的方式那么Django的ManyToManyField可以直接省去手动建关系表的工作。举个例子景点表里直接写一行tags models.ManyToManyField(Tag)Tag表单独定义那么访问某个景点的标签时直接调用spot.tags.all()就行Django会自动维护中间表。这个机制在Flask加SQLAlchemy里也有对应实现但Django的ORM更省心新手不容易写错。我的实际经验是在开发初期不要迷信自动建表先把模型字段写清楚然后用Django的makemigrations和migrate生成数据库中间如果出现字段漏加、类型不匹配还能随时修改再迁移。等你习惯了这个节奏再去考虑复杂的数据模型设计。另外如果要用到经纬度计算酒店与景点的距离一定要在数据库里保留lat和lng字段后面推荐排序要用千万别省。3. 推荐逻辑的核心从关键词匹配到综合评分推荐逻辑听起来玄乎但真正落到代码上没有多难本质上就是先召回再打分最后排序三步走。3.1 召回阶段城市筛选加标签匹配当用户提交一个查询词比如北京故宫亲子我们的程序先要做的是拆解意图。最简单的拆解方式就是分词把查询词切分成词项然后与城市表、景点名称、景点标签做匹配。中文分词的技术栈比较多用jieba是最省事的方案比如jieba.lcut(北京故宫亲子)得到[北京, 故宫, 亲子]然后逐个词去遍历。召回代码如下import jieba def recall_spots(query, city_id): words jieba.lcut(query) # 先按城市筛出基础景点集合 spots ScenicSpot.objects.filter(city_idcity_id) matched_spots set() for word in words: # 匹配景点名称 name_match spots.filter(name__icontainsword) # 匹配标签 tag_match spots.filter(tags__name__icontainsword) matched_spots.update(name_match) matched_spots.update(tag_match) return list(matched_spots)这里用Django ORM的filter直接查数据库层面就完成了大部分筛选。如果数据量大到需要更精确的相似度可以给景点描述接一个TF-IDF向量化匹配但项目初期用关键词就够了。关键词匹配的缺点也很明显——词序和语义会被忽略比如亲子游故宫和故宫亲子游效果一样这完全可以接受我们的目标是保证召回率排序的精确性靠下一步的评分来兜底。3.2 评分公式把不同维度折算成可比分数召回得到的候选集往往是几十条用户不可能全看必须排序。排序的依据我建议用一个多因子加权公式def score_spot(spot, keywords, city_id): tag_score 0.0 for kw in keywords: if spot.tags.filter(name__icontainskw).exists(): tag_score 1.0 rating_score (spot.rating - 3.0) / 2.0 # 假设评分1-5映射到0-1区间 heat_score min(spot.heat_score / 100, 1.0) # 热度归一化假设最高100 final_score ( 0.4 * tag_score 0.3 * rating_score 0.3 * heat_score ) return final_score这个公式里标签匹配占0.4的权重评分和热度各占0.3。为什么这么设因为用户带了明确目的地之后最关心的是这个景点到底符不符合自己的偏好这是标签匹配要解决的随后参考景点的口碑评分和人气热度)。这三个维度组合起来才能把评分极高但毫无关联的景点压下去把匹配度高且口碑好的景点顶上来。实际开发中权重怎么调是个经验活。我常用的办法是写一个排序开关把权重参数放到一个字典里比如WEIGHTS {tag: 0.4, rating: 0.3, heat: 0.3}然后反复用不同查询词去测试看结果是否符合直觉。如果发现某类查询明显偏向高评分老景点而无视新热门就把热度权重调高一些如果发现用户搜安静但出来一堆嘈杂的热门景点那就是标签匹配的权重没给够。3.3 酒店推荐距离优先还是评分优先酒店推荐的逻辑和景点略有不同。用户在查看某个景点详情时我们推荐酒店核心因素应该是距离近和价格适中。距离怎么算如果景点和酒店都有经纬度可以用球面距离公式import math def haversine(lat1, lng1, lat2, lng2): R 6371.0 dlat math.radians(lat2 - lat1) dlng math.radians(lng2 - lng1) a math.sin(dlat/2)**2 math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlng/2)**2 c 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) return R * c拿到距离之后推荐公式就和景区类似了只是因子换成distance_score和price_score。价格分数不是越低越好而是与景点门票价格所在的消费档位匹配才行。比如你推一个每晚50元的招待所给一个正在看奢华度假酒店的用户体验就很差反过来也一样。我建议把酒店价格映射成一个消费区间与景点消费水平形成对照这样推荐出来更像人话。4. 后端落地Django项目结构与推荐接口实现逻辑想清楚了接下来就是代码落地。我按照Django的标准结构来搭建项目整个项目的app划分是这样的一个travel主应用负责景点、酒店、城市和推荐逻辑一个accounts应用负责用户和浏览记录如果后续想加个性化这个应用会派上用场。如果你用Flask则不需要这么重的目录划分但路由视图还是要独立成模块不要全部堆在一个入口文件里。4.1 模型与视图的设计Django里模型就是数据库表的映射。我定义一个精简版模型如下from django.db import models class City(models.Model): name models.CharField(max_length50, uniqueTrue) pinyin models.CharField(max_length50, blankTrue) def __str__(self): return self.name class ScenicSpot(models.Model): name models.CharField(max_length100) city models.ForeignKey(City, on_deletemodels.CASCADE, related_namespots) description models.TextField(blankTrue) rating models.FloatField(default4.0) heat_score models.IntegerField(default0) cover_url models.URLField(blankTrue) tags models.ManyToManyField(Tag, blankTrue, related_namespots) class Hotel(models.Model): name models.CharField(max_length100) city models.ForeignKey(City, on_deletemodels.CASCADE, related_namehotels) address models.CharField(max_length200, blankTrue) rating models.FloatField(default4.0) avg_price models.DecimalField(max_digits7, decimal_places2) lat models.FloatField(nullTrue, blankTrue) lng models.FloatField(nullTrue, blankTrue) tags models.ManyToManyField(Tag, blankTrue, related_namehotels) class Tag(models.Model): name models.CharField(max_length50, uniqueTrue)视图函数的职责要单一。我通常把逻辑放在独立的service.py文件里视图只负责接收请求和返回响应。Django的视图写法很多人习惯用def函数也可以用class但论简洁程度函数视图够用了而且和Flask的写法更接近跨框架迁移时不至于割裂。4.2 推荐接口开发与返回格式约定前端项目最怕后端接口返回格式不统一。我从一开始就约定一个固定的JSON格式{ code: 0, msg: success, data: { spots: [...], hotels: [...] } }前端拿到code0才往下走否则直接弹错误提示省去大量联调沟通成本。推荐接口用Django JSON响应实现from django.http import JsonResponse from .service import recommend_for_query def search_api(request): query request.GET.get(q, ) city_id request.GET.get(city_id, None) if not query: return JsonResponse({code: 1, msg: query is empty, data: {}}) data recommend_for_query(query, city_id) return JsonResponse({code: 0, msg: success, data: data})如果你用Flask这段代码的写法和Django几乎同构只要把request.GET换成flask.request.args把JsonResponse换成flask.jsonify就行其他业务逻辑完全可以复制。这恰好说明框架只是壳真正的业务逻辑是平台无关的。4.3 用户行为记录让推荐越来越准只做基于关键词的静态推荐网站会显得一次性。我加了一个简单的行为记录表把用户的搜索词和点击的景点、酒店保存下来下次进入时优先展示和过去行为匹配的同类内容。class BehaviorRecord(models.Model): session_key models.CharField(max_length100) query models.CharField(max_length100, blankTrue) clicked_spot models.ForeignKey(ScenicSpot, nullTrue, blankTrue, on_deletemodels.SET_NULL) clicked_hotel models.ForeignKey(Hotel, nullTrue, blankTrue, on_deletemodels.SET_NULL) created_at models.DateTimeField(auto_now_addTrue)视图里记录一条点击行为就是新建一行记录的事十几行代码解决。真正个性化推荐还需要分析行为序列、构建用户画像这个项目阶段不用做太深但表结构先预留后期扩展空间就有保障。5. 前端展示与静态资源容易卡壳的两个细节后端接口好了前端就要把推荐结果展示出来。我习惯用Django模板加简单的前端框架比如Bootstrap来做不搞前后端分离。因为项目重点不在前端工程化模板渲染最快、最稳妥而且避免跨域问题。5.1 页面规划与推荐位设计页面我分成四个首页、搜索结果页、景点详情页、城市列表页。首页放一个大搜索框搜索框下面是热门城市入口搜索结果页左侧是景点列表右侧是当前城市的热门酒店景点详情页上面是景点图片和介绍下面推荐附近酒店和相似景点。推荐位一定要放在用户操作的自然延伸处比如用户搜索完景点之后页面底部露出酒店推荐要比弹窗和侧边栏的效果好很多。Django模板里展示推荐列表的方式很简单{% for hotel in hotels %} div classcard div classcard-body h5{{ hotel.name }}/h5 p评分{{ hotel.rating }} | 价格{{ hotel.avg_price }}元/p /div /div {% endfor %}5.2 Django静态文件不显示的排查思路很多新手在页面里引用CSS和图片时发现静态文件怎么都出不来。这个问题的根源通常是STATIC_URL和STATICFILES_DIRS配置不匹配。我给的排查顺序是先检查settings.py里是否配置了STATIC_URL /static/再确认模板里是否用了{% load static %}并写成{% static css/style.css %}的方式引用最后打开浏览器开发者工具看网络请求里静态文件的URL是否返回404。还有一个高频坑在DEBUG True时Django会直接服务静态文件但一旦上线把DEBUG改成False静态文件立刻全部消失。这是因为生产模式下Django不再托管静态文件必须靠Nginx或者collectstatic收集处理。我建议在开发阶段一张表记清楚这三个配置STATIC_URL、STATICFILES_DIRS和STATIC_ROOT。前两个是开发用的第三个是生产环境执行python manage.py collectstatic之后把静态文件汇总的目录。5.3 用AJAX局部刷新推荐结果如果不想每次点筛选条件都整页刷新可以用一个小的AJAX请求去调用推荐接口局部更新推荐位。这段代码不复杂function loadRecommendations(cityId) { fetch(/api/recommend/?city_id${cityId}) .then(res res.json()) .then(data { if (data.code 0) { renderHotelList(data.data.hotels); } }); }选中城市后调用loadRecommendations几行JavaScript就能让页面交互的智能感上一个档次。很多同学在这里习惯把推荐逻辑放在后端模板里渲染导致每次切换城市都要整页刷新其实交互体验远不如AJAX方案。6. 部署上线与实战踩坑别在最后一步垮掉项目开发完最怕的就是部署环节出问题。我把自己多次部署Python Web项目遇到的坑集中列出来这些教训都是用时间换来的。6.1 本地能跑服务器上跑不了是怎么回事最常见的翻车原因是环境不一致。刚在Windows本地跑通的项目传到Linux服务器后接连报错检查下来发现Python版本不同本地3.10服务器3.6数据库版本不同本地SQLite线上MySQL依赖包缺失一堆。我的建议是项目一开始就做好两件事用虚拟环境管理依赖并生成requirements.txt上线前在服务器上新建虚拟环境重新安装所有依赖数据库方面如果线上用MySQL本地也尽量用MySQL避免因为SQLite和MySQL差异导致SQL语句不兼容。Linux服务器上装Python我自己偏爱的方案是pyenv它能管理多版本切换比直接编译源码省心很多。6.2 Gunicorn加Nginx的经典部署组合Django或Flask自带的开发服务器只能用于本地调试线上必须用专业的WSGI服务器最常用的是Gunicorn。启动命令很简单gunicorn config.wsgi:application -b 127.0.0.1:8000 -w 4然后再配置Nginx反向代理让外部请求走到80端口后转发给Gunicorn。需要同时配置好静态文件目录server { listen 80; server_name yourdomain.com; location /static/ { alias /path/to/project/staticfiles/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }部署过程中我踩过最典型的坑是ALLOWED_HOSTS没配好导致的400错误以及忘记执行python manage.py collectstatic导致的样式丢失。这两个问题在浏览器端表现得都非常隐蔽前者直接拒绝访问后者页面结构全乱但控制台没有任何报错排查起来异常费劲。所以我的部署检查清单是DEBUG改为False、ALLOWED_HOSTS里加上服务器IP或域名、执行数据库迁移、收集静态文件、正确启动Gunicorn最后再一步步检查Nginx配置和日志。6.3 数据库迁移与数据备份上线之后最忌讳的是手动改数据库结构。我见过太多人直接登录服务器数据库执行SQL改表结果和模型不同步后续做迁移时直接卡死。正确做法是任何表结构变更都走Django的迁移机制本地改模型生成迁移文件提交到代码仓库服务器上拉取代码再执行migrate。另外景点和酒店数据如果靠爬虫获取建议定期把SQLite或MySQL数据导出备份可以用django.core.management写一个自定义命令定时导出为JSON文件防止数据丢失。写在最后的一点体会这个项目做下来我最大的感受是一个推荐网站能不能让人用着舒服关键不在于模型多复杂、页面多华丽而在于你愿不愿意把推荐背后的逻辑掰开揉碎讲清楚。把数据表设计好把评分公式做稳把接口格式固定住剩下的一切都水到渠成。如果你也是刚开始做这类项目建议先把手动整理的一小批数据跑通整条链路再考虑扩大数据量、接爬虫、加个性化。小步快跑比一开始就追求完美要靠谱得多。后续如果想扩展可以给酒店价格做聚类分析、用协同过滤挖掘相似用户、甚至接入地图API展示位置——这个项目能往上长的方向非常多前提是你得先把地基打扎实。
企业数字化 ERP 产品动态
相关推荐
Jev模型:不做自然语言生成的System One决策模型解析 1. 这个叫 Jev 的模型到底是个什么东西第一次看到 Jev 这个名字,是在几个技术群里有人转了一张截图,配文是"又一个不做自然语言生成的模型,但这次有点意思"。当时我的第一反应是:不做文本生成的大模型,那它做… · 2026/9/26 18:36:24
纳米CT:电子断层扫描如何重塑半导体三维失效分析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 18:36:24
个税APP年度汇算实操指南:从查收入到退税一次讲清 三四月的个税话题,每年都会准时热起来。“你退税了吗”成了办公室里的固定开场白。不少人第一次打开个人所得税APP,直接冲进“综合所得年度汇算”,结果有人顺利退到钱,也有人对着页面上的“应补税额”发懵。同一个APP,… · 2026/9/26 18:36:17
机器学习期末大作业合集:六次实验从跑通到拿满分的路径 简介:这份资源是机器学习期末大作业的六次项目合集,面向高校学生、课程设计者及需要快速完成高分作业的自学者,覆盖从数据预处理到模型评估的完整流程。包内包含基于KNN的手写数字识别、回归模型、参数估计与非参数估计、朴素贝叶斯分类器、层… · 2026/9/26 19:04:07
中药研发数据库:从立项到处方的数据支撑与落地实践 1. 中药研发立项:数据库先把“做不做”的数据功课做足1.1 立项调研离不开的四类数据做中药研发的人都会有同感:一个项目能不能往前走,很多时候不是卡在实验室的瓶瓶罐罐里,而是卡在信息手里。立项要查政策法规和临床需求ÿ… · 2026/9/26 19:04:01
Higgsfield开源视频生成模型:因果注意力与动态掩码核心原理及部署实践 1. 项目概述与核心思路拆解1.1 higgsfield 是什么:开源视频生成模型里的“种子选手”higgsfield 这个名字第一次出现的时候,大多数人第一反应都是“这是不是和粒子物理有什么关系”。实际上它在 AI 视频生成圈里已经火了一段时间,被很多人直接… · 2026/9/26 19:04:01
PASICALvoc格式IP102数据集:开箱即用的VOC标准昆虫检测数据 简介:本资源为IP102昆虫图像识别任务适配的PASCAL VOC格式标注数据集,面向计算机视觉方向的学习者、算法工程师及农业AI应用开发者,解决昆虫细粒度分类与目标检测模型训练中高质量标注数据稀缺的问题。数据集包含9997张原始高清昆虫图像及其对… · 2026/9/26 19:04:01
京东JData高潜用户购买意向预测源码拆解:从特征工程到模型训练 简介:这份资源是京东JData算法大赛中「高潜用户购买意向预测」赛题的完整项目包,面向计算机、人工智能、数据科学等相关专业的学生、教师与从业者,可用于毕业设计、课程设计、竞赛复现或机器学习入门进阶。压缩包共24个文件,约92K… · 2026/9/26 19:04:01
Windows安全中心打不开?根源是内存完整性拦截华为USB驱动 1. 问题现象还原:不是“打不开”,而是系统在悄悄拒绝服务我第一次遇到这个情况是在帮客户做Win11 23H2系统健康检查时。客户说“Windows安全中心打不开”,我过去一看,界面确实空白,但更奇怪的是——任务管理器里根本找… · 2026/9/26 19:04:01
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46