ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Django智能旅游推荐系统全栈毕设项目深度拆解

Django智能旅游推荐系统全栈毕设项目深度拆解

简介:这是一份基于Python的智能旅游推荐系统毕业设计完整项目,面向计算机相关专业学生在毕业设计、课程设计或期末大作业中快速落地实践。项目基于Django框架构建,前端以HTML、CSS、Vue、JavaScript为主,后端由Python实现,开发环境为PyCharm,数据库可视化使用Navicat,并附有SQL脚本与安装运行批处理,真正做到开箱即用。压缩包内共797个文件,涵盖45个Python源码文件、164个JavaScript脚本、53个Vue组件、53个HTML页面、53个CSS样式表,以及图片、字体、SVG图标等静态资源,整体约20.19MB,目录结构清晰,便于二次开发与系统学习。项目功能完善、界面美观,经过严格调试确保可运行,目前已有227人学习下载,是毕业设计答辩和课程实践的高性价比参考方案。

1. 智能旅游推荐系统:能直接跑起来的 Django 全栈毕设长什么样

一个 python 智能旅游推荐系统,放在毕业设计里到底能做到什么程度?这份资源给我的答案是:Django 做后端,Vue 写法的前端页面,配好数据库脚本和三四个一键启动的 .bat 文件,解压之后按顺序点两下就能在本地跑起来。它不是那种只给你一段算法代码的玩具,而是完整的、能演示、能答辩的全栈项目。对正在找 python 毕业设计、课程设计或期末大作业的人来说,这意味着你不需要从零搭框架,也不用到处拼代码,拿到手先跑通,再按自己的思路改。全文我会把项目结构、数据表设计、推荐逻辑、运行方式和常见报错都拆开讲清楚。

2. Django 数据模型与数据库初始化:先把景点和用户表建对

2.1 项目结构与前后端两层代码怎么摆

这个项目的目录和普通 Django 教程项目不太一样。常见教学项目是纯 Django + templates 模板渲染,而这份资源里出现了 index.html.bak、IndexMain.vue.bak、IndexAsideStatic.vue.bak 这类文件。.vue后缀说明前端原本是用 Vue 写的,.bak是备份文件,对应的是 index.html、IndexMain.vue、IndexAsideStatic.vue、BreadCrumbs.vue、IndexHeader.vue 等原始组件。典型做法是前端用 Vue 开发后台管理界面,build 打包后把静态文件交给 Django 托管,所以最终你看到的 index.html 其实是打包产物。

这种结构的直接好处是前后端职责清晰:Django 只管接口和数据,页面交互全交给 Vue。你改后端逻辑不用动页面,改页面样式也不用碰 Python 代码。对毕业设计来说,答辩时老师问你"前后端怎么交互的",你直接说"Vue 通过 axios 调 Django 的 REST 接口,数据以 JSON 格式返回",这句话本身就值不少分。

2.2 核心数据表设计与 Navicat 导入

旅游推荐系统的核心表,按我一贯拆项目的经验,至少会包含这几张:用户表、景点表、收藏表、评论表。用户表可以直接用 Django 自带的 auth_user,也可以自己建。景点表是重头戏,字段一般包含景点名称、所在城市、景点类别、评分、热度、门票价格、介绍、封面图。收藏表和评论表用来支撑推荐逻辑和用户行为分析。

用 Navicat 导入数据库脚本时,我习惯先看一下 SQL 文件里的建表语句,确认表名和字段再导入。下面是一份简化版景点表的建表参考,和这个项目的设计思路基本一致:

CREATE TABLE `scenic_spot` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '景点名称', `city` varchar(50) DEFAULT NULL COMMENT '所在城市', `category` varchar(50) DEFAULT NULL COMMENT '景点类别', `score` float DEFAULT '0' COMMENT '评分', `heat` int DEFAULT '0' COMMENT '热度值', `price` decimal(8,2) DEFAULT '0.00' COMMENT '门票价格', `description` text COMMENT '景点介绍', `image` varchar(200) DEFAULT NULL COMMENT '封面图路径', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

建表语句里utf8mb4很重要。很多人在 Navicat 里导入 SQL 报错,就是因为脚本里写的是utf8,而数据库默认字符集不匹配。utf8mb4能完整存下 Emoji 和生僻字,景点介绍里偶尔会出现特殊符号,用这个字符集最稳。另外heat字段我建议用int而不是varchar,因为后面排序推荐要靠它。

2.3 从 .bak 备份文件看前端组件结构

文件列表里那串.bak不是没用的垃圾文件,它们暴露了前端页面的组织方式。update-password.vue 是修改密码页,IndexMain.vue 是主内容区,IndexAsideStatic.vue 是左侧静态菜单栏,BreadCrumbs.vue 是面包屑导航,IndexHeader.vue 是顶部头部。这是一个标准后台管理布局:左边菜单、右边内容、顶部导航、底部面包屑。

为什么会有 .bak?我推测是作者改版时保留了旧版本,方便回退。你拿到项目后,如果改前端改坏了,把 .bak 复制一份改名回去就能恢复,相当于自带后悔药。这提醒我一件事:动手改任何文件之前,先复制一份 .bak 出来,哪怕项目里没有,自己也该养成这个习惯。

数据库层面,除了景点表,还要看收藏表和评论表是否导入了。推荐功能依赖收藏关系,如果没有收藏数据,推荐逻辑跑起来就是空的。我一般会在 Navicat 里手动插入几条测试收藏数据,比如让用户 A 收藏"故宫"和"颐和园",用户 B 收藏"故宫",这样调试推荐接口时能立刻看到效果。

3. 核心业务与推荐逻辑:登录、景点管理和推荐结果怎么串起来

3.1 登录鉴权与会话控制

后台管理系统的第一道门是登录。这个项目前端有 update-password.vue,说明它至少包含登录和修改密码两个基础功能。Django 生态里做登录鉴权,主流方案有两种:一是用 Django 自带的django.contrib.auth配合 session,二是用django-rest-framework的 JWT 认证。毕业设计项目用前者居多,因为代码量少、依赖少、调通容易。

典型登录接口的写法大致是这个思路:

from django.contrib.auth import authenticate, login from django.http import JsonResponse def user_login(request): if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') user = authenticate(request, username=username, password=password) if user is not None: login(request, user) return JsonResponse({'code': 0, 'msg': '登录成功', 'data': {'username': user.username}}) return JsonResponse({'code': 1, 'msg': '用户名或密码错误'})

authenticate负责校验用户名和密码是否匹配,login负责把登录状态写进 session。这里有个细节:Django 的authenticate默认只对is_active为 True 的用户返回有效对象,所以如果你在数据库里直接插用户记录而没设置is_active,密码对也登不进去。我遇到不少人在这一步翻车。

修改密码逻辑也很直接,拿到当前用户对象,调用set_password再save即可。注意set_password之后必须调用save(),否则密码只存在内存里,没写进数据库,刷新页面又变回老密码。这个坑我帮别人排查过好几回,现象就是"密码改成功了,但退出登录后用新密码登不上"。

3.2 景点信息的增删改查与检索

后台管理的核心价值是把景点数据管起来。所谓数据库增删改查,在 Django 里就是 ORM 的create、filter、update、delete。景点管理页面对应前端 IndexMain.vue 里的内容区域,通常会有一个表格,支持按名称、城市、类别搜索,支持分页,支持编辑和删除。

一个完整的景点列表接口,考虑分页和搜索,写法大致如下:

from django.core.paginator import Paginator from django.http import JsonResponse from .models import ScenicSpot def spot_list(request): keyword = request.GET.get('keyword', '') city = request.GET.get('city', '') spots = ScenicSpot.objects.all() if keyword: spots = spots.filter(name__icontains=keyword) if city: spots = spots.filter(city=city) paginator = Paginator(spots, 10) page = request.GET.get('page', 1) page_obj = paginator.get_page(page) data = [{ 'id': s.id, 'name': s.name, 'city': s.city, 'score': s.score, 'heat': s.heat, 'price': str(s.price), } for s in page_obj] return JsonResponse({'code': 0, 'data': data, 'total': paginator.count})

name__icontains是 Django ORM 里的模糊查询,i表示忽略大小写。这里字段名直接取列表里的字段,接口返回给前端,前端在表格里渲染。分页参数page由前端传入,Paginator每页 10 条。你拿到源码后想改每页条数,直接改Paginator(spots, 10)里的 10 就行。

新增和更新景点可以用 Django 的ModelForm或者直接objects.update_or_create,视具体实现而定。删除我更建议用软删除,即给表加一个is_delete字段,删除时把状态置为 1,列表查询时过滤掉。这是血泪经验:管理员手滑删错景点,如果是物理删除,数据直接没了,想恢复只能靠数据库备份;软删除至少还能捞回来。

3.3 推荐逻辑:基于热度和用户行为排序

"智能推荐"是这份资源的卖点。真正的推荐系统会用协同过滤、内容推荐、甚至深度学习模型,但毕业设计在演示层面跑得通、讲得清才是第一位的。我看了这个项目的文件结构,没有额外的算法库依赖,所以最可能的实现是基于收藏行为和景点热度做排序。

一种常见且合理的简化方案是:用户收藏过某些类别的景点,就给用户推荐同类别里热度高、评分高的其它景点。这样既利用了用户行为数据,又不需要复杂的矩阵计算,答辩时也容易解释。

from .models import ScenicSpot, Favorite def recommend_for_user(user): # 找出用户收藏过的景点类别 fav_categories = ( Favorite.objects.filter(user=user) .values_list('spot__category', flat=True) .distinct() ) if not fav_categories: # 没有收藏记录,就按全站热度推 return ScenicSpot.objects.order_by('-heat', '-score')[:10] # 排除已收藏的,按热度和评分排序 recommended = ( ScenicSpot.objects .exclude(favorite__user=user) .filter(category__in=fav_categories) .order_by('-heat', '-score')[:10] ) return recommended

这段逻辑的关键是exclude(favorite__user=user),它把用户已经收藏过的景点过滤掉,避免"推荐了用户早就收藏过的地方"。.distinct()防止用户收藏同一个类别的多个景点导致重复推荐。order_by('-heat', '-score')先按热度排,热度相同再按评分排,负号表示倒序。

如果你想升级推荐效果,一个低成本的改法是把"只看类别"改成"类别 + 城市"双维度加权。比如用户收藏了北京的自然风光,推荐时就优先推北京其他自然风光,其次才推其他城市的。这个改动不需要动数据表,只是在 filter 条件上多加一个 OR 条件。

前端拿到推荐接口返回的列表后,渲染成卡片或者列表,图片和介绍从对应字段取值。推荐接口调用时机一般是用户登录后进入首页时触发,所以前端会有一次初始请求,后端根据当前 session 里的用户计算推荐结果。

4. 环境搭建与一键运行:PyCharm 里把项目拉起来

4.1 Python 环境与依赖安装

拿到项目第一步,是确认本机 Python 版本。Django 项目对 Python 版本有要求,Django 2.x 支持 Python 3.5 到 3.8,Django 3.x 要求 Python 3.6 以上。这份资源自带"安装.bat",点开会执行依赖安装,我建议不要直接双击,而是先右键编辑,看看里面的命令是什么。

常见的安装脚本内容是:

@echo off pip install -r requirements.txt echo 依赖安装完成 pause

requirements.txt里一般会列 Django、mysqlclient 或 pymysql、djangorestframework 等。如果你用的是 Python 3.10 以上版本,注意mysqlclient可能装不上,常见做法是改装pymysql,然后在项目__init__.py里加上一句:

import pymysql pymysql.install_as_MySQLdb()

这句的作用是把 PyMySQL 伪装成 MySQLdb,Django 才能正常连 MySQL。不装或者不导入这行,运行迁移命令时会直接报ModuleNotFoundError: No module named 'MySQLdb'。我自己习惯用 PyMySQL,因为它纯 Python 实现,不需要编译 C 扩展,Windows 上省心很多。

4.2 数据库连接配置与迁移

打开项目的 settings.py,找到 DATABASES 配置块,确认连接参数和本地一致。最常见的配置是:

DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'travel_db', 'USER': 'root', 'PASSWORD': '123456', 'HOST': '127.0.0.1', 'PORT': '3306', } }

NAME是数据库名,必须在 Navicat 里先创建好,Django 不会自动建库。USER和PASSWORD按你本机的 MySQL 账号改。这里有一个高频报错:如果 MySQL 密码里有特殊字符比如@或#,直接在配置里写字符串容易解析出错,解决办法是在 PyCharm 里检查字符串有没有被正确转义。

配置改好后,打开终端执行:

python manage.py makemigrations python manage.py migrate

makemigrations会根据 models.py 生成迁移文件,migrate把表建到数据库里。如果项目自带了数据库脚本,你会发现在脚本导入后,表结构和 models.py 是对得上的,migrate 只会生成少量系统表。如果对不上,migrate 可能报错,需要根据报错信息判断是缺字段还是多字段。

4.3 一键脚本各自干什么

资源里有两个运行相关的脚本,2-run.bat和运行.bat,还有一个3-build.bat。它们的分工,我按一般项目习惯推断一下:3-build.bat是前端构建,作用是执行npm install和npm run build,把 Vue 源码打包成静态文件;2-run.bat和运行.bat很可能一个是启动 Django 后端、一个是启动前端开发服务器的。

运行脚本最常见的形态是:

@echo off python manage.py runserver 0.0.0.0:8000

如果你不想用脚本,直接在 PyCharm 终端里手动执行这行命令也行。0.0.0.0表示监听所有网卡,这样同一局域网下其他设备也能访问,演示时方便用手机连接看效果。如果只想本机访问,用python manage.py runserver 8000就够了。

我这里要强调一个点:运行.bat 里如果写的是npm run dev,说明前端是开发模式启动,和 Django 是两个端口,比如 Django 占 8000,Node 占 8080。开发模式下前端通过代理转发请求到后端,这种配置在 Vue 的vue.config.js里会有一段 devServer.proxy。如果你直接打开 index.html 而不是走前端服务器,接口请求会 404。所以拿到项目后,先确认你要用哪种方式演示,不要混着来。

5. 避坑指南:毕设跑不通时先查这五处

5.1 数据库连接失败,Navicat 却明明能连上

现象:双击运行.bat 启动后,浏览器访问页面报OperationalError: (1045, "Access denied for user 'root'@'localhost'"),但用 Navicat 用同一个账号密码却能连上。

原因:settings.py 里的密码和本机 MySQL 实际密码不一致,或者密码包含特殊字符被 Python 转义问题干扰。另一个可能,是项目里用的数据库名不存在,报错会变成Unknown database 'xxx'。

解决:先在 Navicat 里用配置的账号密码测试连接,确认能通。然后在 PyCharm 里单独跑一行命令验证 Django 侧连接:

python -c "import django; import os; os.environ.setdefault('DJANGO_SETTINGS_MODULE', '你的项目名.settings'); django.setup(); from django.db import connection; connection.ensure_connection(); print('连接成功')"

提示:这段命令里的"你的项目名"要换成实际的 settings 模块名。如果能输出"连接成功",说明配置没问题。

5.2 页面能打开但样式全丢,图片全是裂图

现象:页面结构在,但 CSS、JS、图片都加载不出来,控制台一堆 404。

原因:Django 没有正确配置静态文件路径。前端 build 出来的静态文件没有放到 Django 能识别的位置,或者 settings.py 里STATIC_URL、STATICFILES_DIRS没配对。

解决:打开 settings.py 检查静态文件配置:

STATIC_URL = '/static/' STATICFILES_DIRS = [ BASE_DIR / 'static', ]

如果前端文件已经 build 到了 static 目录,再执行一次收集:

python manage.py collectstatic --noinput

注意:开发环境这个命令不是必需的,Django 会自动找STATICFILES_DIRS里的文件。

5.3 端口被占用导致启动失败

现象:运行.bat 执行后立刻退出,终端提示Error: That port is already in use。

原因:上次启动的服务没有关干净,端口被残留进程占住。

解决:Windows 下用命令查占用:

netstat -ano | findstr :8000

看到 LISTENING 行后面对应的 PID,到任务管理器里结束对应进程,或者用命令强杀:

taskkill /PID 那个PID /F

我建议每次演示完都正常按 Ctrl + C 关闭服务,避免残留进程。

5.4 导入数据库脚本时中途报错

现象:Navicat 导入 .sql 文件,执行到一半报Invalid utf8 character string或Unknown collation。

原因:SQL 文件里的字符集或排序规则和当前 MySQL 版本不兼容。常见是脚本里写了utf8mb4_unicode_ci,而本地 MySQL 版本较老不支持,或者脚本文件本身编码不是 UTF-8。

解决:用 Notepad++ 或 VS Code 打开 .sql 文件,确认右下角编码是 UTF-8,如果不是先转码保存。把文件开头的SET NAMES utf8改成SET NAMES utf8mb4,再去掉或替换掉不支持的排序规则。改完再重新导入。

5.5 前端 build 后接口请求 404

现象:执行 3-build.bat 打包后,打开 index.html 页面,登录接口请求地址是http://localhost:8080/api/login,但后端跑在 8000 端口,请求打到前端服务器上直接 404。

原因:前端代码里接口地址写的是相对路径,本地开发依赖 devServer 代理转发,打包后没有正确配置接口域名。

解决:打开前端源码里的请求配置文件,常见是src/utils/request.js或.env.production,把baseURL改成后端地址:

import axios from 'axios' const service = axios.create({ baseURL: 'http://127.0.0.1:8000', // 改成 Django 实际地址 timeout: 10000 })

改完重新 build,或者用开发模式启动前端。这个坑几乎每个前后端分离项目都会遇到,提前改好能省很多时间。

6. 答辩与二次开发:把推荐结果做出差异化

6.1 把推荐逻辑升级成协同过滤

基础版推荐按类别和热度排序,够用但不够"智能"。如果想让答辩更有亮点,可以把它升级成基于用户的协同过滤。原理不复杂:找到与当前用户行为最相似的其他用户,把那些用户收藏过而当前用户没收藏过的景点推出来。用 Django ORM 写一个简化版:

def recommend_collaborative(user, top_n=10): # 找到用户收藏过的景点 my_favs = set( Favorite.objects.filter(user=user) .values_list('spot_id', flat=True) ) if not my_favs: return ScenicSpot.objects.order_by('-heat')[:top_n] # 找到收藏过这些景点的其他用户 similar_users = ( Favorite.objects .filter(spot_id__in=my_favs) .exclude(user=user) .values_list('user_id', flat=True) .distinct() ) # 把这些用户收藏的其他景点统计出来,按出现次数排序 from django.db.models import Count candidates = ( Favorite.objects .filter(user_id__in=similar_users) .exclude(spot_id__in=my_favs) .values('spot_id') .annotate(cnt=Count('spot_id')) .order_by('-cnt')[:top_n] .values_list('spot_id', flat=True) ) return ScenicSpot.objects.filter(id__in=candidates)

这个算法用annotate(cnt=Count('spot_id'))统计候选景点的支持人数,支持人数越多说明"和你相似的人越喜欢"。答辩时你要能说清楚:这不是基于内容的推荐,而是基于用户行为的协同过滤。这一句话,就能把你和只会抄 CRUD 的同学区分开。

6.2 用 ECharts 给后台加一个数据看板

旅游推荐系统如果只有表格和列表,视觉上会显得单薄。一个性价比极高的加分项,是在首页加一个统计看板,展示城市景点分布、类别占比、热度榜前五。前后端分离的架构下,前端用 ECharts 是非常成熟的方案。

后端只需提供一个汇总接口:

from django.db.models import Count from django.http import JsonResponse from .models import ScenicSpot def spot_stats(request): category_stats = ( ScenicSpot.objects .values('category') .annotate(count=Count('id')) .order_by('-count') ) return JsonResponse({ 'code': 0, 'data': { 'categories': [item['category'] for item in category_stats], 'counts': [item['count'] for item in category_stats], } })

前端在 IndexMain.vue 里引入 ECharts,请求这个接口,渲染一个饼图出来。整个改动约等于新增一个接口加一个组件,但演示效果提升非常明显。老师看到的不再是干巴巴的数据,而是可视化呈现,这个印象分很重要。

6.3 答辩演示的完整路径

答辩演示环节,我建议你提前走一遍完整流程,别等到现场才开项目。顺序可以固定为:第一步展示登录页,讲清楚认证方式;第二步进入首页,讲解推荐结果,说明推荐依据是什么,最好提前准备好几条收藏数据,让推荐结果有内容可讲;第三步演示景点管理,现场新增一条景点记录,刷新列表让老师看到数据变化;第四步打开 Navicat,对照数据库表讲解数据结构。这四个环节串起来,从功能到数据到算法逻辑,全部覆盖到了。

演示前强制检查三件事:数据库服务有没有启动,Django 服务有没有启动,前端页面能不能正常打开。很多现场翻车都出在"我昨天还能跑"这句话上,因为 MySQL 服务没开机自启,换了一台电脑就忘了装依赖。从那以后我每次演示前都会强制走一遍这套检查流程,再拿一份离线备份放到 U 盘里,万一环境坏了还有后悔药。希望帮到你,祝答辩顺利。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进