这个选题在毕业设计里算是常青树了——文化旅游类管理系统,需求明确、技术栈主流、演示效果直观。不过正因为做的人多,想拿高分就不能只停留在CRUD层面。我结合自己带项目和学生做毕设的经验,把SpringBoot古镇旅游路线规划网站从需求拆解到算法实现完整梳理一遍,包括那些踩过的坑和评委会追问的点。这个项目不仅能覆盖SpringBoot、MyBatis Plus、Vue等主流技术栈的考核点,还天然自带一个技术亮点——路线规划算法,做好了就是答辩时的加分项。
1. 项目需求分析与功能拆解
先说说这个项目到底解决什么问题。历史文化名镇的旅游资源往往散落在各个角落,游客到了之后面对一堆景点容易懵,要么走回头路,要么时间分配不合理。传统的导览方式又停留在纸质地图和讲解器阶段,信息更新慢、交互体验差。这个系统的核心价值就是把“人找信息”变成“信息找人”,通过智能推荐和路线规划,让游客在有限时间内看到最想看的风景。
1.1 用户痛点与核心业务逻辑
从用户侧来看,痛点其实很明确。第一次去某个古镇的游客,根本不知道哪些景点值得看、顺序怎么安排最合理、每个景点大概要停留多久。跟团游受制于固定路线,自由行又缺乏专业导览能力。从景区管理侧来看,缺乏游客行为数据,热门景点拥堵情况无法预判,也不能根据淡旺季动态调整推荐策略。
系统需要解决的核心问题有三个:
- 信息整合:把分散的景点介绍、开放时间、门票价格、餐饮住宿信息集中起来
- 路线优化:根据游客的时间预算和兴趣偏好,生成合理的游览顺序
- 行程管理:支持用户保存、调整、分享自己的定制行程
这套逻辑做下来,其实就是一个小型化的“文旅推荐引擎”。所以在设计的时候不要只盯着CRUD,要把推荐和规划这块当成创新点来打磨。
1.2 角色权限与功能模块划分
系统分成三个端来看:
- 游客端(前台):景点检索与详情浏览、路线智能推荐、行程定制与收藏、在线评论与评分
- 管理端(后台):景点信息管理、路线管理、用户管理、评论审核、数据统计
- 系统端(公共服务):登录认证、权限校验、文件上传(景点图片)、日志记录
管理端和游客端最好是前后端分离部署,管理端单独走一套后台界面。虽然会增加工作量,但是答辩的时候老师问到“为什么这么设计”,你就可以从职责分离、安全隔离、扩展性的角度去解释,比“别人都这么做”要有说服力得多。
2. 技术架构与工具选型
这个项目的技术选型,原则就一个:主流的、自己熟练的、能讲清楚的。别整花活,毕设的核心是展示你掌握了多少主流技术,而不是冒险用一堆冷门框架。
2.1 后端技术栈详解
后端技术选型如下:
| 技术组件 | 选择方案 | 选择理由 |
|---|---|---|
| 核心框架 | SpringBoot 2.7.x | 生态成熟、资料多、自动配置省去大量样板代码 |
| ORM框架 | MyBatis Plus | 单表操作零SQL,复杂查询用XML,适合提升开发效率 |
| 权限认证 | Spring Security + JWT | 无状态认证,适应前后端分离场景 |
| 数据库 | MySQL 8.x | 稳定可靠,InnoDB支持事务 |
| 缓存 | Redis | 缓存景点热度信息、验证码、会话状态 |
| 接口文档 | Swagger/Knife4j | 自动生成接口文档,调试方便 |
版本这里强调一下:SpringBoot选2.7.x而不是3.x,主要考虑兼容性问题。3.x要求JDK17及以上,很多学校的毕设环境还是JDK8,而且一些老版本的依赖在3.x下会报错。用自己熟悉的版本,稳才是王道。启动类上加一个@EnableScheduling开启定时任务,可以用来定期更新景点热度排行。
2.2 前端技术栈与交互方案
前端有两条路可选:
- 纯Vue3 + Element Plus + Vite,前后端完全分离
- 服务端渲染模板(Thymeleaf)+ Bootstrap,单体架构
我更推荐前者,虽然有跨域问题要处理,但是分离架构对毕设来说是加分项。Vite比Webpack启动快很多,调试体验好。地图展示那块,用开源的Leaflet就够用了,做一些标注点、路线连线都没问题。
游客端页面设计要做到“三屏内能看懂”——首页、景点列表页、行程定制页是核心页面。导航要清晰,行程定制用步骤条组件分三步走:选景点、调顺序、定时间。这个交互模式参考携程的行程规划逻辑,用户接受度很高。
3. 数据库设计与核心表结构
数据库设计是整个项目的基石,也是答辩时老师大概率会看的东西。我建议按“基础数据+业务数据+扩展数据”三层来设计表结构。
3.1 核心数据表设计
至少要有这么几张表:
t_user:用户表,含用户名、密码(BCrypt加密存储)、手机号、角色标识t_scenic:景点表,含名称、简介、历史背景、开放时间、建议游玩时长、经纬度坐标、封面图、热度值t_route:路线表,含名称、描述、总时长、创建人t_route_detail:路线明细表,含路线ID、景点ID、顺序号、停留时间t_comment:评论表,含用户ID、景点ID、评分、内容、状态t_favorite:收藏表,用户和景点的多对多关联t_file:文件表,统一管理上传的图片资源
景点表里给建议游玩时长和经纬度留字段,这是后面算法要用到的关键数据,很多做这个题目的学生容易忽略。
主要表结构示例:
CREATE TABLE `t_scenic` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '景点名称', `history` text COMMENT '历史背景介绍', `open_time` varchar(50) DEFAULT '08:00-17:30' COMMENT '开放时间', `visit_duration` int DEFAULT 90 COMMENT '建议游玩时长(分钟)', `longitude` decimal(10,6) DEFAULT NULL COMMENT '经度', `latitude` decimal(10,6) DEFAULT NULL COMMENT '纬度', `cover_image` varchar(255) DEFAULT NULL COMMENT '封面图URL', `hot_value` int DEFAULT 0 COMMENT '热度值', `status` tinyint DEFAULT 1 COMMENT '状态 1上架 0下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;3.2 字段设计的经验要点
有几点经验要分享一下:
- 所有表都要带
create_time和update_time字段,这是审计需要,MyBatis Plus自动填充就能搞定,不用手动维护 - 经纬度用
decimal(10,6),不要用float,精度差一个量级 - 热度值
hot_value建议定时从评论数、收藏数、点击量计算更新,而不是实时计算,性能好得多 - 逻辑删除用
deleted字段配合MyBatis Plus的@TableLogic注解,防止误删数据
4. 路线规划核心算法与实现
这个部分是项目的技术亮点,也是决定你是“及格”还是“优秀”的分水岭。普通的毕设做CRUD,聪明的学生会给系统装上“大脑”。
4.1 路线推荐算法的设计思路
路线规划本质上是一个带约束的路径优化问题。体量不需要做到商飞那种级别,但可以用一种工程化的贪心策略来解决问题。
核心思路是这样:
- 用户输入总游玩时间(比如半天/一天/两天)
- 结合用户选择的兴趣标签(古迹/美食/自然风光/博物馆)
- 从所有开放景点中筛选候选集
- 基于景点间的地理距离和游玩时长,用贪心策略生成一条时间约束内的最优路线
- 按顺序输出路线,并计算总路程和总时长
贪心策略的具体细节:从游客设定的起点出发,每次从候选景点中选择“最值得去且距离当前点最近”的景点加入路线,直到剩余时间无法容纳下一个景点为止。虽然贪心不能保证全局最优,但在景点数量有限的场景下已经能得出非常合理的方案。
“最值得去”的评分公式可以设计成:
score = 景点热度值 * 0.6 + 兴趣标签匹配度 * 0.4如果用户勾选了“古迹”标签,那么历史背景类型的景点匹配度就设为1,其他为0.5。“距离最近”则用Haversine公式计算两点的球面距离。
4.2 核心代码实现
public RoutePlanResult planRoute(RoutePlanRequest request) { // 1. 根据兴趣标签筛选候选景点 List<Scenic> candidates = scenicMapper.selectList( new LambdaQueryWrapper<Scenic>() .eq(Scenic::getStatus, 1) .like(StrUtil.isNotBlank(request.getInterest()), Scenic::getTags, request.getInterest())); // 2. 按热度值与标签匹配度计算评分 candidates.forEach(s -> { double score = s.getHotValue() * 0.6; if (matchInterest(s.getTags(), request.getInterest())) { score += 40; } s.setScore(score); }); // 3. 按评分降序排序 candidates.sort((a, b) -> Double.compare(b.getScore(), a.getScore())); // 4. 贪心生成路线 List<Scenic> route = new ArrayList<>(); int totalTime = 0; double totalDistance = 0; double currentLon = request.getStartLon(); double currentLat = request.getStartLat(); for (Scenic scenic : candidates) { double dist = GeoUtil.distance(currentLon, currentLat, scenic.getLongitude(), scenic.getLatitude()); // 交通时间按步行速度5km/h折算 int travelTime = (int) Math.ceil(dist / 5000 * 60); int needTime = travelTime + scenic.getVisitDuration(); if (totalTime + needTime <= request.getAvailableTime()) { route.add(scenic); totalTime += needTime; totalDistance += dist; currentLon = scenic.getLongitude(); currentLat = scenic.getLatitude(); } } // 5. 封装结果 RoutePlanResult result = new RoutePlanResult(); result.setScenicList(route); result.setTotalTime(totalTime); result.setTotalDistance(totalDistance); return result; }代码里GeoUtil.distance封装了Haversine公式,这个算法答辩的时候要能默写出来原理。交通时间的折算,按步行速度5km/h算是一个合理的默认值,如果景区内允许骑行可以调整参数。
4.3 算法优化与参数调整
有两点可以进一步拿来做文章:
- 引入“早鸟/晚归”优化。把开放时间纳入约束,太早或太晚到的景点自动跳过。这需要景点表有更细粒度的时段配置。
- 加入“热度阈值过滤”。热度值低于某个阈值的景点不进推荐候选集,避免冷门景点被反复推荐。
答辩的时候如果老师问“为什么用贪心而不用动态规划”,你要能答上来:状态空间规模庞大,动态规划求解带时间窗的路径问题复杂度太高,在景点数量小于50的场景下,贪心策略的耗时几乎为0,并且生成的路线具备可解释性——每一步都是“当前最优”。动态规划适合追求全局最优,但用户体感差异极小,性价比不高。
5. 核心功能模块的实现细节
功能模块很多,挑三个最有代表性的展开讲一下实现细节。这三个模块都是能直接展示的项目亮点。
5.1 JWT认证与权限控制的完整实现
Spring Security集成JWT这个点,几乎是必问的。流程是这样的:用户登录成功后,后端生成一个包含用户ID和角色信息的JWT令牌,前端存储到localStorage并在每次请求的Authorization头带上。后端写一个过滤器,校验JWT的合法性并解析出用户信息填充到上下文。
核心配置类是周边近100行,关键逻辑就三点:
- 自定义
OncePerRequestFilter,在SecurityFilterChain里加入过滤器链 AuthenticationManager负责登录认证,成功之后签发JWT- 用
@PreAuthorize("hasRole('ADMIN')")注解做接口级别的权限控制
这里有个坑:JWT的密钥不要硬编码在代码里,一定要放到application.yml配置文件里通过@ConfigurationProperties注入。答辩的时候说到安全性,能提到这一点说明你考虑过生产环境的问题。
5.2 景点检索与热度排行实现
景点列表页的搜索功能,不要只做分页查询。要做成“关键字+分类+排序”三合一:关键字匹配景点名称和简介,分类是古迹/美食/自然/展馆,排序支持按热度、按评分、按距离。
距离排序需要拿到当前用户的经纬度,前端用浏览器定位API获取后传到后端。SQL里用Haversine公式做排序条件:
SELECT *, (6371000 * acos( cos(radians(#{lat})) * cos(radians(latitude)) * cos(radians(longitude) - radians(#{lng})) + sin(radians(#{lat})) * sin(radians(latitude)) )) AS distance FROM t_scenic ORDER BY distance ASC虽然每组坐标都算余弦有一定开销,但景点数量级在几百的时候MySQL扛得住。这一切的前提是给latitude和longitude加联合索引。
热度排行这个功能,我在项目里用Redis的ZSET实现。key是scenic:hot,score就是热度值,每次用户浏览详情时incrementScore。定时任务每天凌晨从Redis同步到MySQL的hot_value字段。这么设计的好处是热点数据的读写都在内存,不会压到数据库。
5.3 行程定制与前端交互逻辑
行程定制页面是游客端的核心交互场景。我建议做成三步式向导:
第一步选择日期和总时长(半天/全天/两天一夜) 第二步勾选兴趣标签(多选) 第三步系统推荐路线
第三步是重点。前端拿到推荐路线后,把景点以卡片形式列出来,每个卡片可以拖拽调整顺序。调整后重新计算总时长和预计路程,实时展示在地图组件上连线。这个拖拽排序的功能,前端用Vue的vuedraggable库非常方便。
后端保存行程,走t_route和t_route_detail两张表,事务保证一致性。一个路线头对应多个明细行,明细行里的sort_order字段记录顺序,这个字段在保存之前要做重排序校验。
6. 常见问题与排查技巧实录
做这类项目,有几个问题几乎所有人都会踩。提前写在这里,就当给你们排雷了。
6.1 跨域请求与Session失效问题
前后端分离模式下,最常见的报错就是跨域。排查思路很简单:先确认后端的CorsFilter有没有正确注册,再看前端请求有没有带withCredentials需要设置allowCredentials(true)。开发阶段可以在前端的Vite配置文件里配个代理,把/api前缀的请求全部转发到后端端口,这样浏览器就不会有跨域问题了。
Session失效的问题是JWT方案的一个经典误解。因为JWT是无状态的,后端不会主动记住你的登录状态,所以只要前端不清理localStorage里的令牌,登录状态就一直有效。前端在axios的响应拦截器里要统一处理401状态码,跳转到登录页并清掉失效令牌。
6.2 MyBatis Plus分页查询失效的坑
分页插件突然不生效,十有八九是配置问题。确保两件事:第一,MybatisPlusInterceptor这个Bean有正确注册,并且PaginationInnerInterceptor设置了大页数限制;第二,mapper-locations路径对得上XML文件的实际位置。
这里有个很容易被忽略的细节——分页插件要放在责任链的最后,否则会被其他拦截器影响导致失效。调试的时候如果你发现SQL语句没有LIMIT关键字,先查这两个配置。
6.3 前端地图加载慢与坐标偏移
Google地图在国内无法使用,百度地图又存在坐标偏移问题,所以我的推荐是Leaflet + 高德地图瓦片。高德用的是GCJ-02坐标,如果是从第三方数据源拿的WGS-84坐标,要先做坐标转换,否则地图上的标注会偏差几百米。
如果用户访问地图时加载慢,别急着优化后端,先看静态资源有没有走CDN。Leaflet的JS和CSS文件可以本地化部署,不要每次加载都从官方CDN拉取,国内访问CDN延迟高且不稳定。
6.4 常见报错速查表
| 问题现象 | 排查思路 | 解决方案 |
|---|---|---|
| 启动报Bean重复类异常 | 检查是否同时引入了多个同类的自动配置类 | 排除冗余依赖,确认版本兼容 |
| 登录后接口返回403 | 权限注解配置有误 | 检查@PreAuthorize角色字符串是否一致 |
| 中文乱码 | 数据库连接未配置UTF-8 | 连接串加characterEncoding=utf8 |
| 部署服务器内存不足 | JVM参数配置过大 | 启动参数加-Xmx256m |
| 上传图片后访问404 | 静态资源映射未配置 | 注册WebMvcConfigurer设置资源映射路径 |
6.5 部署与演示环节的关键准备
答辩要现场演示的话,有几个细节值得提前准备:
- 演示数据要真实丰富,至少20个景点、5条预设路线、每个景点配图片和详细介绍。别用测试数据糊弄,评委能看出来
- 做一个“一键初始化”的API,方便部署时快速导入演示数据
- 提前准备好局域网联调方案,防止现场网络出问题
- 前端构建产物(
dist目录)可以放到后端静态资源目录下,这样无需单独部署前端服务器,一台机器就能跑起来整个项目
最后的部署,推荐用java -jar xxx.jar的方式跑,别用IDE跑。一是演示环境不一定有IDE,二是打包部署更符合生产习惯。
这个项目做完,除了拿到毕业设计和答辩的分数,你还会收获一套完整的全栈开发方法论——从前端交互设计到后端接口规划、从数据库设计到算法实现、从开发调试到部署上线,这是一条完整的链路。记得一个原则:系统是死的,场景是活的。你把用户场景想得越清楚,设计出来的系统就越有说服力。