基于Django与协同过滤的湖北旅游景点推荐系统设计实现
拿到这个题目的时候我第一反应是终于有个不是“图书管理系统”或者“学生信息管理系统”的课设选题了。12429这批编号在很多课程设计网站上都能见到标题写着湖北旅游景点推荐系统网上有人把它挂成付费项目但其实完整源码包括SQL脚本早就被整理出来了属于可以白嫖源码、自己动手跑起来的典型项目。这篇文章我不打算只把源码链接甩给你就完事而是从头到尾把这个系统拆开讲清楚——技术栈怎么选、推荐算法怎么写、数据库几张表怎么建、跑起来会踩哪些坑。你看完再拿源码心里有底得多。这类“景点推荐系统”本质上不复杂但该有的东西一样不少前台展示、用户登录、景点搜索、评分收藏、推荐算法、后台管理。作为课程设计或者毕业设计覆盖面够难度适中最关键的是它有个实实在在的推荐逻辑在里面不是纯粹堆页面。对于一个想入门Web开发和推荐系统的同学来说这套代码的性价比非常高。1. 项目概述与核心需求拆解1.1 这个系统到底在解决什么问题游客去湖北旅游打开搜索引擎或者旅游App面对的是浩如烟海的景点信息。武当山、神农架、恩施大峡谷、三峡人家、黄鹤楼、东湖……几百个景点游客根本不知道哪些适合自己。有人喜欢自然风光有人偏爱人文古迹有人只想找个适合带娃的公园。传统的门户网站把所有信息平铺在首页上用户筛选成本极高。这个系统的价值就在这里通过收集用户的评分、收藏、浏览行为构建用户兴趣画像然后主动把用户可能感兴趣的景点推到面前。它本质上和电商平台“猜你喜欢”的逻辑一脉相承只是把商品换成了景区。1.2 项目定位与适用人群从项目编号和命名方式看这是一个典型的“课程设计/毕业设计”项目目标场景非常明确。给计算机相关专业做课程设计的学生需要一个能讲清楚原理、能演示、能答辩的完整系统给想练手Web开发的新手需要一套结构清晰、代码量适中、能跑通全流程的参考项目给对推荐系统好奇但不想一上来就啃深度学习论文的同学需要一个能落地的推荐算法实践案例。这个项目正好卡在“简单”和“复杂”的中间地带。技术上不涉及高深的机器学习框架用纯Python就能实现一个可解释的协同过滤推荐业务上覆盖了完整的用户交互链路足够支撑一场答辩。1.3 功能需求清单梳理拆解标题里的关键词“推荐系统”“设计与实现”拿到手的功能需求大致可以理成下面的列表功能模块具体功能角色用户模块注册、登录、个人信息维护游客景点模块景点列表、景点详情、按城市/类型筛选、关键词搜索游客互动模块景点评分、收藏、评论游客推荐模块首页热门推荐、基于协同过滤的个性化推荐游客后台管理景点信息增删改、用户管理、评论管理管理员这个功能清单基本就是市面上旅游类Web应用的“最低可行版本”。该有的闭环都有数据流清晰适合作为教学案例。2. 技术选型与系统架构设计2.1 技术栈选型与理由源码用的技术栈非常有代表性Python生态下最成熟的Web框架搭配最常用的数据库开发语言Python 3.x推荐算法写起来舒服字符串和集合操作非常顺手Web框架Django自带ORM、Admin后台、用户认证体系开发效率高适合这种中小型项目前端Bootstrap jQuery 原生HTML/CSS/JS没有引入复杂的前端工程化降低上手门槛数据库MySQL 5.7或8.0关系型数据库对景点、用户、评分这类结构化数据天然友好缓存Django自带Cache框架可选Redis但对这个项目体量来说不配也不会影响运行。说句实话很多同学一开始纠结要不要用前后端分离弄个Vue加Spring Boot显得“高级”。但实际上课程设计和毕业设计的核心目标是快速开发、稳定运行、答辩能讲清楚。Django的“大而全”在这里反而成了优势——用户认证和Admin后台都不用自己写省下的时间全砸在推荐算法上。2.2 整体架构的分层思考系统采用经典的B/S三层架构浏览器端负责页面展示和交互服务端Django负责业务逻辑和数据处理MySQL负责数据存储。视图层Templates接收用户请求提交表单和Ajax调用业务层Views处理推荐计算、用户校验、数据组装模型层Models通过ORM映射操作数据库表。推荐算法的调用放在业务层中。用户登录后进入“为我推荐”页面时视图函数实时调用推荐模块从数据库取出用户的评分记录构建评分矩阵计算相似度筛选出候选景点最终生成推荐列表传入模板渲染。这里有一个很实际的设计考量实时计算还是离线计算。这个项目的推荐算法是在请求时实时计算的数据量小的时候响应很快而且用户每次评分后重新进入推荐页结果都会更新产品上叫作“增量反馈”。如果以后景点和用户数据膨胀到千万级别再改成离线预计算推荐结果存入缓存这是后话但思路要留好余地。2.3 推荐算法选型为什么用ItemCF而不是UserCF推荐系统里最经典的两个协同过滤算法UserCF基于用户的协同过滤找到和你兴趣相似的用户把他们喜欢的景点推荐给你ItemCF基于物品的协同过滤找到和你之前喜欢过的景点相似的景点推荐给你。两个都能做但旅游景点推荐这个场景更适合ItemCF原因有三个第一用户的兴趣相对稳定。一个喜欢自然风光的人今天喜欢恩施大峡谷明天很大概率喜欢神农架这类偏好是长尾且持久的适合用“物品相似度”来聚合。第二推荐结果可解释性强。ItemCF给出的理由可以是“因为你喜欢武当山所以推荐神农架”用户一眼就能看懂产品的接受度高。第三实现复杂度低。UserCF需要维护用户相似度矩阵当用户数量增长时矩阵膨胀很快itemCF的相似度计算发生在景点之间景点数量在整个系统里是相对稳定的。对应到代码里ItemCF的计算过程就三句话找出用户评分过的景点找出这些景点的相似景点按照相似度和评分的加权和排序。3. 数据库设计与核心数据模型3.1 表结构设计思路数据表设计是这套源码里最值得看的部分。设计得好后面写推荐算法会非常顺。整个系统围绕三个核心实体展开用户、景点、评分记录。再加上收藏表和评论表一共五张核心表。表名字段说明tb_userid, username, password, nickname, avatar, create_time用户表Django自带User模型可扩展tb_scenicid, name, city, category, address, price, open_time, star, intro, image, heat景点主表tb_ratingid, user_id, scenic_id, score, comment, create_time评分评论表tb_favoriteid, user_id, scenic_id, create_time收藏表tb_feedbackid, user_id, content, contact, create_time留言反馈表所有业务表都保留创建时间字段后面做数据统计和分析时非常有用。3.2 景点表字段详解tb_scenic这张表是整个系统的数据基础字段设计很实用city字段用于按地市筛选湖北下面武汉、宜昌、十堰、恩施这样分类很清楚category字段是景点类型自然风光、历史古迹、主题乐园、城市公园、博物馆price是门票价格0表示免费景区这个字段在推荐结果里可以做价格区间筛选star是景点基础评分可以在用户没有评分记录时作为冷启动兜底推荐依据heat是热度值可以手动维护也可以统计浏览量、收藏量、评论数加权计算。3.3 核心模型代码解读源码里Django的models.py定义大概是这个风格from django.db import models class Scenic(models.Model): name models.CharField(max_length100, verbose_name景点名称) city models.CharField(max_length50, verbose_name所在城市) category models.CharField(max_length50, verbose_name景点类型) address models.CharField(max_length200, verbose_name详细地址) price models.DecimalField(max_digits10, decimal_places2, default0, verbose_name门票价格) open_time models.CharField(max_length100, verbose_name开放时间) star models.FloatField(default4.5, verbose_name评分) intro models.TextField(verbose_name景点简介) image models.ImageField(upload_toscenic/, blankTrue, verbose_name景点图片) heat models.IntegerField(default0, verbose_name热度) class Meta: db_table tb_scenic verbose_name 景点信息 class Rating(models.Model): user models.ForeignKey(UserProfile, on_deletemodels.CASCADE, verbose_name用户) scenic models.ForeignKey(Scenic, on_deletemodels.CASCADE, verbose_name景点) score models.IntegerField(verbose_name评分1-5) comment models.TextField(blankTrue, verbose_name评论内容) create_time models.DateTimeField(auto_now_addTrue) class Meta: db_table tb_rating评分表用外键关联用户和景点同时通过联合唯一约束保证一个用户对一个景点只能评一次分class Meta: db_table tb_rating unique_together (user, scenic)这个联合唯一约束很关键。如果允许多次评分推荐算法读取评分记录时会碰到同一个用户对同一个景点的多条记录矩阵构建就乱了。3.4 初始数据湖北旅游景点的填充源码的SQL脚本里带了一批湖北景点的初始数据。以我实际看到的数据为例大致包括以下几个有代表性的景点景点名称城市类型票价黄鹤楼武汉历史古迹70元东湖风景区武汉城市公园免费武汉大学樱花大道武汉城市公园免费三峡大坝旅游区宜昌水利工程免费三峡人家宜昌自然风光180元武当山十堰道教古迹140元神农架神农架林区自然风光120元恩施大峡谷恩施自然风光155元清江画廊宜昌自然风光100元荆州古城荆州历史古迹免费这份数据量不算大但对一个课设项目来说完全够用。如果自己想扩展市面上各大旅游网站的景点数据都可以做爬虫抓取但注意别滥用课设的话自己构造个二三十条假数据跑通逻辑也足够了。4. 推荐算法核心实现这是一篇讲“推荐系统”文章的核心也是这套源码里含金量最高的一部分。我把它单独拿来拆解。4.1 基于物品的协同过滤原理拆解ItemCF的思想用大白话讲就是武当山和神农架都经常被同一批游客评分说明它们之间存在某种关联所以喜欢武当山的人大概率也会喜欢神农架。具体分三步根据用户的评分记录构建“景点——评分用户”的倒排表两两计算景点之间的相似度常用余弦相似度公式用户对某个景点的偏好程度乘以景点和候选景点的相似度累加起来得到候选景点的预测评分取TopN推荐。公式上余弦相似度的定义是相似度 (A景区和B景区共同被评分的用户数) / (sqrt(A景区被评分的人数) * sqrt(B景区被评分的人数))公式简单但理解清楚很重要。分母是归一化避免热门景点天然占便宜。比如“东湖”因为是免费景点评分人数远多于“荆州古城”如果不做归一化东湖就会和所有景点看起来都“相似”这显然不合理。4.2 完整推荐代码实现源码里推荐算法的Python实现大概是下面这个思路我用尽量完整的代码还原def build_scenic_similarity(): # 1. 从数据库读取所有评分记录 ratings Rating.objects.all().values(user_id, scenic_id, score) # 2. 构建 景点 - 用户集合 的倒排表 scenic_users {} for r in ratings: scenic_users.setdefault(r[scenic_id], set()).add(r[user_id]) # 3. 计算景点两两之间的余弦相似度 similar_matrix {} scenic_list list(scenic_users.keys()) for i in range(len(scenic_list)): for j in range(i 1, len(scenic_list)): si scenic_list[i] sj scenic_list[j] users_i scenic_users[si] users_j scenic_users[sj] # 交集大小 common_users users_i users_j if len(common_users) 0: continue # 余弦相似度 sim len(common_users) / (len(users_i) ** 0.5 * len(users_j) ** 0.5) similar_matrix.setdefault(si, {})[sj] sim similar_matrix.setdefault(sj, {})[si] sim return similar_matrix def recommend_for_user(user, top_n10): # 1. 取用户评过分的景点和分数 user_ratings Rating.objects.filter(useruser).values(scenic_id, score) if not user_ratings: # 冷启动按热度排序返回 return Scenic.objects.order_by(-heat)[:top_n] user_scenic_ids {r[scenic_id] for r in user_ratings} avg_score sum(r[score] for r in user_ratings) / len(user_ratings) # 2. 加载景点的相似度矩阵 similarity_matrix build_scenic_similarity() # 3. 计算候选景点的预测得分 scores {} for r in user_ratings: scenic_id r[scenic_id] score r[score] sim_list similarity_matrix.get(scenic_id, {}) for sim_scenic_id, sim_val in sim_list.items(): # 跳过已经评过分的景点 if sim_scenic_id in user_scenic_ids: continue # 预测分累加以当前用户评分作为权重乘以相似度 scores.setdefault(sim_scenic_id, 0) scores[sim_scenic_id] sim_val * (score - avg_score) # 4. 按预测得分排序取评分记录没有排除过的景点 sorted_scenics sorted(scores.items(), keylambda x: x[1], reverseTrue)[:top_n] recommended_ids [s[0] for s in sorted_scenics] return Scenic.objects.filter(id__inrecommended_ids)注意一个细节预测得分我用了score - avg_score做中心化处理。为什么要这样因为用户打分的尺度可能不一样有人习惯给高分有人习惯给3分就算不错。把评分减去用户平均分就能消除个人评分习惯的偏差这是评分预测里一个很小但很关键的优化点。如果数据集变大了这个用纯字典写的算法会慢可以换成pandas DataFrame和矩阵运算import pandas as pd from sklearn.metrics.pairwise import cosine_similarity def build_similarity_pandas(): df pd.DataFrame(list(Rating.objects.all().values(user_id, scenic_id, score))) matrix df.pivot_table(indexuser_id, columnsscenic_id, valuesscore, fill_value0) sim_matrix cosine_similarity(matrix.T) # 景点之间的相似度 return sim_matrix但我更推荐把第一版纯Python实现吃透因为所有推荐系统的变体——SVD矩阵分解也好、图神经网络也好——底层都是这套“评分矩阵→相似度/隐向量→排序推荐”的思维框架。课设答辩时老师让你讲原理你能不看代码把逻辑画出来才是真懂了。4.3 冷启动问题与兜底方案推荐系统的经典难题有一个叫冷启动。新用户没有任何评分记录协同过滤算法直接歇菜。源码里做了两个兜底策略第一个是热门推荐用户没有评分记录时按热度值倒序返回。热度值可以手写SQL计算浏览总量、收藏数、评论数的加权和保证推给新用户的都是“大多数人去过的安全选择”。第二个是分类偏好推荐如果用户只注册没打分但关注了某个类型的景点可以用用户对景点类型的偏好计算相似度用类型标签代替评分行为。这个在源码里是选做功能但很值得自己加上练手。5. 系统功能模块实现解析5.1 用户注册登录模块Django自带的用户认证体系简化了大量工作源码在原生User模型之上做了扩展增加了昵称、头像字段。密码使用PBKDF2算法加密存储不是明文这一点很关键。很多课程设计项目密码明文存数据库答辩时被老师一问就露怯。登录态管理依赖Django的Session机制默认存在数据库session表中。用户登录后推荐模块需要读取当前用户ID通过request.user获取。5.2 景点列表与多维筛选景点列表页支持按城市、按类型筛选同时支持关键字搜索和价格排序。这部分可以用Django的ORM非常优雅地完成链式过滤def scenic_list(request): scenic_queryset Scenic.objects.all() city request.GET.get(city) category request.GET.get(category) keyword request.GET.get(keyword) sort request.GET.get(sort) if city: scenic_queryset scenic_queryset.filter(citycity) if category: scenic_queryset scenic_queryset.filter(categorycategory) if keyword: scenic_queryset scenic_queryset.filter(name__icontainskeyword) if sort price: scenic_queryset scenic_queryset.order_by(price) elif sort star: scenic_queryset scenic_queryset.order_by(-star) ...这里ORM的链式filter写得很典范。面试和答辩的时候能够解释清楚QuerySet是惰性求值的只在真正遍历时才执行SQL也会是加分项。5.3 评分与收藏模块用户可以在景点详情页打分1到5分和收藏。评分提交后页面通过Ajax刷新该景点的平均分展示区不需要整页刷新。评分数据写入后系统要重新计算景点的平均分def update_scenic_star(scenic_id): scenic Scenic.objects.get(idscenic_id) ratings Rating.objects.filter(scenicscenic) avg ratings.aggregate(avg_scoreAvg(score))[avg_score] scenic.star round(avg, 1) scenic.save()这一步看起来简单但很多新手会漏掉“评分改变后景点总分要联动更新”的逻辑。如果不在景点表里维护一个冗余字段每次展示景点列表都要实时去评分表里聚合查询数据量大时性能隐患明显。5.4 推荐结果展示页推荐页的核心逻辑在视图中def recommend(request): if not request.user.is_authenticated: return redirect(/login/) recommended_scenics recommend_for_user(request.user, top_n10) return render(request, recommend.html, {scenics: recommended_scenics})页面展示上要保留“推荐理由”的提示语比如“因为你看过武当山推荐你去神农架”。我在实际改造过程中把推荐理由写了进去直接调用算法过程中的相似度信息。这个体验细节看起来没啥但对于演示和答辩来说非常抓眼球。6. 前端页面设计与交互体验6.1 页面整体结构布局这个系统的前端是典型的服务端渲染多页面结构整体风格清爽。导航栏、横幅区、景点卡片栅格、分页器、用户下拉菜单这些元素在Bootstrap框架下搭建非常快。核心页面包括首页横幅轮播图、热门景点推荐、湖北地市入口景点列表页城市标签、分类按钮、关键字搜索框、排序筛选、卡片网格景点详情页大图展示、基本信息表、评分区域、评论区登录注册页表单校验个人中心我的评分、我的收藏、退出登录推荐页面Top10推荐结果卡片6.2 推荐页面的展示逻辑推荐页的布局有讲究单纯把10个景点平铺是很浪费的。我建议改造为三段式展示第一屏“为你精选”展示Top3推荐卡片更大带“推荐指数”的星星装饰第二屏是“根据你的喜好发现更多”展示Top10中的后七个第三屏“大家都在看”调用热门兜底逻辑给推荐结果做交叉补充。前端注意事项有一个很容易踩坑Django模板渲染图片时如果settings.py里没有配置MEDIA_URL和MEDIA_ROOT图片会404。很多小白拿到源码一跑发现图片全挂就是这个原因。需要在settings.py加MEDIA_URL /media/ MEDIA_ROOT os.path.join(BASE_DIR, media)然后在主urls.py里加from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)7. 部署与运行从源码到跑通全流程拿到源码后怎么运行这里我按实际踩过的流程一步步来截图就不放了但每一步能遇到什么问题都给你说清楚。7.1 环境准备第一步是装Python。推荐用Python 3.8到3.10之间的版本Django 3.x或4.x都兼容。不要一上来装最新版Python 3.12部分老源码的依赖包可能还没适配。创建虚拟环境是必须的一步它能帮你隔离项目依赖mkdir hbtourism cd hbtourism python -m venv venv venv\Scripts\activate # Windows环境 source venv/bin/activate # Mac/Linux环境7.2 安装依赖包源码里如果带了requirements.txt那就简单pip install -r requirements.txt如果没带手工安装最核心的依赖pip install django3.2 mysqlclient这里我特意提一个坑mysqlclient在Windows上经常装不上。这种情况可以退一步用pymysql然后在项目init.py里写两行import pymysql pymysql.install_as_MySQLdb()这个方法能解决Windows环境下一大半MySQL连接报错问题。7.3 数据库配置和初始化数据库这块源码一般带的是tb_scenic.sql这样的文件需要手动导入CREATE DATABASE hbtourism CHARACTER SET utf8mb4; USE hbtourism; SOURCE tb_scenic.sql;然后在settings.py修改数据库配置把账号密码改成自己的DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: hbtourism, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, } }数据库来源有两种方式如果是源码自带整个SQL文件就导入整个文件如果只有Django的models定义没有数据那就跑迁移命令python manage.py makemigrations python manage.py migrate7.4 启动项目python manage.py runserver浏览器访问http://127.0.0.1:8000就可以看到系统界面了。管理员后台访问http://127.0.0.1:8000/admin需要先创建超级管理员python manage.py createsuperuser7.5 部署到云服务器的可选方案如果想把系统部署到公网方便演示答辩最简单的方案是租一台云服务器装宝塔面板把代码传上去Python项目管理器里直接配置gunicorn启动。域名和HTTPS这些都可以在宝塔面板上配置比手工配Nginx反向代理省力太多。当然这只是捷径想深入学习服务器部署还是建议手动搞一遍。8. 常见问题与排查技巧实录这部分干货比较碎但都是我实际跑这个项目过程中的血泪经验。做成表格方便你快速定位。问题现象可能原因解决办法页面能打开但图片不显示缺少MEDIA_ROOT配置或静态文件路径错误检查settings的MEDIA_ROOTurls里配置static映射登录后推荐页一直转圈推荐算法实时计算太慢或数据库没数据确认tb_rating有数据或者优化算法内部循环减少重复构建相似度矩阵的次数数据库sql导入报语法错误MySQL版本过高或文件中文编码问题检查文件编码为utf8mb4用source命令导入而非粘贴执行注册时提示密码字段超长Django新版本对密码字段有更长密的hash需求确认使用Django3.x自带User模型否则要迁移默认表结构mysqlclient安装失败Windows缺编译环境使用pymysql替代并install_as_MySQLdb()推荐结果全是热门景点没有个性评分数据太少矩阵稀疏多造一些评分数据模拟不同画像的用户行为用Ubuntu系统跑代码时图片上传保存不了静态目录权限不足chmod -R 755 media目录Django版本过高老代码兼容性报错源码基于旧版本Django锁版本尽量使用源码对应版本或按报错信息逐个替换deprecated函数再提一个性能层面的杀手级问题如果你决定手动扩充景点数据到两三百个每次访问推荐页都实时算相似度矩阵会非常痛苦。解决思路是加缓存把矩阵序列化后缓存到内存或者Redis里评分记录变动时才重新计算。Django自带的cache框架完全够用from django.core.cache import cache def get_similarity_matrix(): matrix cache.get(scenic_similarity_matrix) if matrix is None: matrix build_scenic_similarity() cache.set(scenic_similarity_matrix, matrix, 60 * 30) # 缓存30分钟 return matrix最后的实操心得跑完这套源码再把推荐算法从原理到代码都自己手写了一遍之后我的体会是这个项目的最大价值不在代码量而在它把一个完整的推荐链路从零到一串了起来。网上很多免费源码只是给了你一套能跑的页面推荐接口根本没实现而这个项目是真的把ItemCF算法落地了。想把这个项目吃透建议你自己做三件事第一把数据库里的评分数据改成你自造的测试数据模拟三个完全不同偏好类型的用户看推荐结果是否有区分度第二在原算法上加上“推荐理由”功能把相似景点的名字拼进推荐卡片第三把算法缓存加上测试一下响应时间的变化。这三件事做完下次面试聊推荐系统项目你会发现自己能讲的东西比想象中多很多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →