1. 这个旅游出行指南系统到底在做什么做毕业设计的选题很多人容易走进一个误区要么选一个听起来高大上但自己完全驾驭不了的方向要么选一个纯增删改查的管理系统做完之后自己都觉得拿不出手。今天拆解的这套基于SpringBootVueMySQL的旅游出行指南系统恰好踩在两者中间——它不引入微服务、分布式这些对本科阶段明显超纲的技术但也不是一个只写几个 CRUD 接口就能糊弄过去的玩具。我见过不少同学选这个方向最后答辩效果都还不错。核心原因在于这套系统的业务链条天然完整用户要注册登录、要浏览景点、要看路线、要收藏要评论管理员要维护景点、管理路线、审核内容。这些需求每一条都能在真实产品里找到对应物做出来的东西不是纸面蓝图而是能跑、能演示、能讲清楚设计思路的完整项目。先说具体的功能边界。用户端这块登录注册是基础核心页面有这么几类景点列表页支持按城市、主题分类筛选也支持关键词搜索、景点详情页图片轮播、开放时间、门票价格、详细介绍、路线推荐管理员维护好的多日行程方案、个人中心收藏列表、评论记录、昵称头像修改。管理端相对固定景点管理、路线管理、用户管理、评论管理四个模块就是日常维护后台。这套功能拆开看不复杂但真正把它组织得结构清晰、可扩展比想象中更花时间。我拆过不少类似的代码最大的问题往往不是某个功能写不出来而是模块耦合太死。比如把景点列表、搜索、分页全部堆在一个控制器里前端再把所有请求写在同一个组件里最后改一处崩三处。如果你正在准备这个方向的毕业设计或者想拿一个完整项目充实简历这篇文章会把我在实操中踩过的坑、验证过的方案、整理过的细节都列出来。从数据库建表到后端接口设计从前端页面组件到部署上线按一条主线走完。最后还会分享答辩准备的思路和一些可以让项目加分的扩展方向免得你在演示现场被老师问住。2. 整体思路与技术选型为什么是这套组合2.1 选型的三个理由最后一个最容易被忽略SpringBoot Vue MySQL 这三件套在毕业设计作品里出场率极高不是没有道理的。第一学习成本和资料丰富度平衡得好。SpringBoot 把后端配置做得极轻不用再折腾一堆繁琐的 XMLVue 的上手曲线平缓组件化思路和现代前端开发一致MySQL 是应用最广泛的开源关系型数据库不管遇到什么问题网上基本都能搜到成熟解法。第二这套技术在真实中小型项目中大量存在把它吃透写简历时可以理直气壮地说自己熟悉完整业务开发流程。第三点经常被忽略它的分工边界非常清晰。后端只负责业务逻辑和数据处理前端只负责页面和交互数据库只负责存储三者之间通过约定好的 JSON 接口通信。这种职责分离恰好符合软件工程项目的展示需求答辩时可以很顺畅地讲清楚每个模块的职责边界。也有人会问为什么不直接做前后端不分离用 Thymeleaf 模板引擎渲染页面如果单纯为了交差模板引擎确实更省事但它少了一个极其重要的技术环节——前后端分离的接口交互。现在招聘市场上几乎所有初级岗位都要求候选人理解 RESTful API、JSON 数据传输、跨域处理这些概念。做毕设本来就是向面试官证明具备实际开发能力的过程选这套方案等于把入门训练场搬到了自己手里。2.2 功能模块拆解与业务流转从角色维度看系统就两类人普通用户和管理员。用户端的操作流是注册登录 → 进入景点列表 → 按城市或主题筛选 → 点进详情 → 收藏或评论 → 去个人中心查看收藏和评论记录。首页还会展示推荐路线路线详情页按天展示行程安排和预算这些数据都由管理员在后台维护。管理员的日常是登录后进入管理面板左侧菜单列出景点管理、路线管理、用户管理、评论管理。景点管理支持分页列表、新增编辑、删除、图片上传路线管理同样有完整 CRUD用户管理主要做启用和禁用评论管理可以审核删除。这里有一个业务流程细节容易被忽略删除景点时必须连带清理该景点下的评论和收藏记录否则会出现脏数据——景点列表正常但详情页评论数对不上用户收藏列表里残留着已删除景点的历史记录。2.3 数据库设计建表是地基省一步后面十倍来还数据库设计是整个项目最先开工、也最影响后期开发效率的部分。我建议按五张核心表加一张中间表来设计。用户表字段id、username、password、nickname、avatar、phone、create_time。password 字段必须用加密算法存储哪怕只在本地演示这也是基本职业习惯。景点表字段id、name、city、category、description、image_url、images、ticket_price、open_time、address、view_count、status、create_time。其中 images 存的是详情图组的路径列表用逗号分隔的字符串存储就够用不必单独建图集表。路线表id、title、days、budget、cover_image、description、create_time。评论表id、user_id、scenic_id、content、rating、create_time。收藏表id、user_id、scenic_id、create_time并且要加唯一约束避免同一用户对同一景点重复收藏。景点与路线的多对多关系需要一张中间表id、scenic_id、route_id、day_number、sort_order。为什么不能把景点直接塞进路线表的一个字段里因为一条路线包含多个景点一个景点也可能出现在多条路线中不拆中间表后续要按天分组、做行程排序就会非常痛苦SQL 写起来又长又乱。CREATE TABLE scenic ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, city VARCHAR(50), category VARCHAR(20), description TEXT, image_url VARCHAR(255), images TEXT, ticket_price DECIMAL(10,2), open_time VARCHAR(100), address VARCHAR(255), view_count INT DEFAULT 0, status TINYINT DEFAULT 1, create_time DATETIME ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时时间字段统一用 datetime别混着用 timestamp。还有一个高频坑后端 LocalDateTime 默认序列化格式带字母 T如果不做配置前端拿到的是 2025-06-01T10:30:00 这种带字母的格式页面显示还得再处理一层。这个坑我在每次联调时几乎都会遇到解决方案后文专门讲。3. 后端落地SpringBoot 骨架到业务接口3.1 项目初始化和分层目录创建项目时选择 Spring Web、MyBatis-Plus、MySQL Driver、Lombok 这几个依赖。我推荐直接用 MyBatis-Plus 而不是原生 MyBatis原因很实际内置分页插件单表 CRUD 几乎不用手写 SQL毕设时间能省出来打磨功能细节。如果担心答辩时被追问底层原理MP 的底层本来就是 MyBatis把 SQL 映射机制讲清楚就行这个不难补。目录按常见分层组织清晰是第一位的com.example.travel ├── controller # 接口层 ├── service # 业务逻辑层 ├── mapper # 数据访问层 ├── entity # 实体类 ├── dto # 数据传输对象 ├── config # 配置类 ├── common # 通用返回结果、异常处理、工具类 └── TravelApplication.java统一返回结果这个类一定不要省略。我见过很多毕设代码接口返回格式五花八门有的直接返回实体有的返回 HashMap前端解析一片混乱。花十分钟写一个 Result 类定义 code、message、data 三个字段所有接口都用它包装前端拦截器再统一处理整个项目的交互规范一下子就立住了。3.2 JWT 登录认证的实现路径登录方案我直接用 JWT思路很直白用户登录成功后后端生成 token 返回前端把 token 存在 localStorage每次请求在请求头里带上 Authorization后端写拦截器校验 token解析出用户 id 放入请求上下文。具体分四步走。第一步引入 JWT 相关依赖。第二步写 JwtUtil 工具类包含生成和解析两个方法。生成时把用户 id 放进 payload过期时间设 24 小时毕设项目不用做刷新机制。第三步行登录逻辑验证用户名密码通过后生成 token连同用户基本信息一起返回。第四步写拦截器实现 HandlerInterceptor 接口在 preHandle 中读取请求头、解析校验 token。注册拦截器时配置排除路径登录、注册、景点列表和详情这些不需登录就能访问的接口全部放行。这里有个很容易踩的坑在 WebMvcConfigurer 里配置拦截路径如果不小心把静态资源也拦截了前端图片全部 404。我的经验是图片上传后存放在服务器本地目录通过后端资源映射暴露成 /images/** 路径拦截器对这种静态路径也要放行。排查时如果发现页面布局正常但图片不加载第一反应先去检查拦截器的排除列表。3.3 景点模块接口设计和分页实现景点模块是整套系统最核心的部分接口按这个清单来设计GET /api/scenic/list 分页查询支持 keyword、city、category 参数GET /api/scenic/{id} 景点详情POST /api/scenic 新增景点管理员PUT /api/scenic/{id} 修改景点管理员DELETE /api/scenic/{id} 删除景点管理员POST /api/user/favorite 收藏或取消收藏GET /api/user/favorite/list 当前用户收藏列表分页用 MyBatis-Plus 的 Page 对象非常省事查询条件用 LambdaQueryWrapper 动态拼接。keyword 走 name 的模糊匹配city 和 category 做等值匹配用 StringUtils 判断空值后再决定是否拼接条件。这里注意条件构造的顺序先判断再拼接不然生成的 SQL 可读性很差排查问题时也难一眼看出逻辑。景点详情的浏览量自增值得单说。我的方案是在 service 层提供一个方法每次调用详情接口就执行一次自增更新。注意不要先 select 再 update那是先查后改多一次查询而且并发场景下数据容易丢失。正确做法是直接用 SQL 原子自增UPDATE scenic SET view_count view_count 1 WHERE id #{id}MySQL 对这种自增操作是原子的并发请求也不会互相覆盖。答辩时能主动讲出这个细节比背一百个概念都加分。3.4 图片上传和静态资源映射景点图片上传是毕设里最容易出问题的功能之一。我的方案前端用 FormData 携带文件后端接收 MultipartFile校验文件类型和大小jpg/png/webp5MB 以内然后保存到服务器本地 uploads 目录文件名用 UUID 重生成避免中文名和重名问题。保存完返回一个访问路径存数据库。访问路径的设计要特别提醒不能返回本地绝对路径比如 D:/travel-upload/xxx.jpg。把服务器目录结构暴露给用户不说换台机器部署路径就全废了。正确做法是返回相对路径前端用接口域名拼接完整 URL。还有一个常见的坑SpringBoot 默认不把本地目录映射成可访问的静态资源。需要在配置类里重写 addResourceHandlers把 /images/** 映射到本地磁盘目录。上传目录的位置建议放在项目外部比如 D:/travel-upload别放在项目内部不然重新部署打包时用户上传的图片容易被覆盖丢失。4. 前端实现Vue 页面组件和接口对接4.1 前端骨架、路由设计和状态管理前端项目我推荐用 Vite 创建启动速度快对毕设这种中小项目绰绰有余。目录按功能模块组织src ├── api # 接口请求封装一个模块一个文件 ├── components # 通用组件 ├── router # 路由配置 ├── store # 状态管理 ├── views # 页面组件分 user 和 admin 两套 ├── utils # 工具方法 ├── App.vue └── main.js路由设计要区分用户端和管理端。用户端包括首页、景点列表、景点详情、路线详情、登录注册、个人中心管理端所有页面都要求当前用户是管理员。实现方式是在路由配置的 meta 里标记所需角色全局前置守卫读取 token 和用户信息做判断。状态管理用 PiniaVue 3 项目或 VuexVue 2 习惯都行。需要全局保留的数据主要是用户信息和 token。景点列表、详情这类数据没有跨页面共享的强需求每次进页面重新请求就好放进全局状态反而增加维护负担。4.2 Axios 封装拦截器决定联调顺不顺前端和后端通信统一走 Axios必须做封装。请求拦截器负责从 localStorage 取 token有就加到请求头的 Authorization 字段。响应拦截器检查返回结构里的 code成功就直接抽出 data 返回未登录的错误码就清掉本地用户信息并跳登录页其他错误码统一弹提示。// 响应拦截器核心逻辑 service.interceptors.response.use( response { const res response.data if (res.code 200) { return res.data } if (res.code 401) { localStorage.removeItem(token) router.push(/login) return Promise.reject(未登录) } ElMessage.error(res.message || 请求失败) return Promise.reject(res) }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } )跨域问题基本在联调第一天就会遇到。开发环境前端地址是 localhost:5173后端是 localhost:8080端口不同就触发跨域。解决方式有两种后端配置 CorsFilter或者前端在 vite.config.js 里配 proxy。我推荐开发环境用 proxy上线用 Nginx 反向代理。但要注意一个隐藏问题后端配了 CORS、前端又配了 proxy两套机制同时生效时可能冲突表现是请求打到了代理但响应头里缺少允许跨域的字段排查起来非常费劲。选一种方式并坚持用一种别叠加。4.3 核心页面实现的三个关键点景点列表页是用户端最核心的页面。布局上顶部是搜索框和城市选择器中间是分类筛选标签下方是景点卡片列表。卡片展示封面图、名称、城市、简介摘要、门票价格和查看详情按钮。分页组件用组件库的 Pagination页码变化就重新请求数据。列表页还有一个容易被忽略的体验细节筛选条件变化时要重置页码到第一页不然用户在第 5 页切换城市后可能看到空列表。景点详情页要处理的内容更多。顶部图片轮播把 images 字段按逗号拆分成数组循环渲染中间是基本信息区下面是评论区。评论区加载该景点下的评论列表每条评论展示昵称、头像、评分、内容和时间。头像这里有一个细节用户没有上传头像时要给默认图不然页面上会出现一片破图图标观感非常差。管理端方面景点管理页面是最典型的表格页。表格列包括景点名称、城市、分类、价格、状态、创建时间、操作按钮。编辑和新增共用一个弹窗表单组件图片上传控件放在表单里上传成功返回 URL 后写进表单字段。删除操作必须加二次确认防止管理员手滑误删。管理员登录和用户登录可以共用一个登录页接口返回 token 的同时返回用户角色前端根据角色决定跳转首页还是管理面板。5. 联调阶段高频问题与排查思路5.1 字段对不上联调第一天的头号杀手前后端联调最常见的问题就是字段名不一致。后端实体类叫 scenicName前端接口文档写的是 name结果列表页名字全为空。这个问题的根源是前后端各写各的缺少一份统一约定的接口文档。我的经验是第一次联调前先花半小时做一件事把后端所有接口返回的 JSON 用 Postman 全部跑一遍对照前端页面的数据结构找差异统一改掉再开始联调能省下一整天的时间。还有一个隐蔽的坑实体类字段是驼峰命名数据库字段是下划线命名MyBatis-Plus 默认开启了下划线到驼峰的映射常规操作没问题。但如果你手写自定义 SQL查询结果的列名不会自动映射到实体字段表现就是查询返回的对象全是空值。解决方式是在自定义 SQL 中给查询列显式起别名或者在配置里设置 mapUnderscoreToCamelCase 为 true。5.2 中文乱码两个环节分别根治乱码在前后端分离项目里会出现在两个环节。第一个是请求乱码前端发过来的 JSON 中文在接收后变成问号。SpringBoot 里处理方式是确保 server.servlet.encoding 配置生效强制 UTF-8。第二个是数据库乱码插入数据后查出来是乱码。这个问题根源几乎都是数据库或表的字符集不是 utf8mb4。建库时统一使用 utf8mb4这个字符集能正确存储表情符号和生僻字兼容性最好。如果数据库已经建好但字符集不对用一条 SQL 就能查出所有表的字符集SELECT table_name, table_collation FROM information_schema.TABLES WHERE table_schema travel_db发现问题后用 ALTER TABLE 语句批量转换就行不用推倒重建库。这套排查方法我不止一次分享过每次都能帮人省下半天调试时间。5.3 登录失效和越权问题两道防线不能省JWT 失效的典型表现是用户操作到一半突然被拦截器拦下来提示未登录。排查顺序很固定先确认 token 是否过期再看前端有没有在请求拦截器里把 token 加到请求头。我见过很多次用户明明登录成功但因为 Axios 封装里忘了写请求拦截器每次请求都没有 token等于一直以匿名身份在访问。管理员接口的权限校验也常出问题。如果拦截器只校验 token 有效性不校验用户角色那普通用户也能通过直接调用接口的方式操作管理端数据。正确的做法是两道防线拦截器层面做粗粒度角色校验根据 token 解析出的用户 id 查询角色核心管理接口的 service 层再做一次细粒度校验。双保险的好处是即使拦截器配置漏了某个路径service 层的校验仍然能兜底。5.4 部署上线别在最后一步翻车毕设演示一般在本地但很多学校要求能通过公网或现场服务器访问部署这关躲不掉。前端打包出来的 dist 是纯静态文件夹放到 Nginx 就行后端打包成 jar 用 java -jar 启动。部署的关键点是 Nginx 反向代理把 /api 前缀的请求转发到后端 8080 端口根路径指向 dist 目录。还有一个重点后端配置文件里的数据库连接、文件上传路径部署环境往往和本地不一样。我建议把配置写进 application.yml用 dev 和 prod 两个 profile 区分环境。部署时指定 prod profile改掉数据库密码和路径。如果图省事直接沿用本地配置一部署就是一堆 500 错误排查半天发现只是数据库连不上非常浪费时间。6. 答辩准备与项目深度扩展6.1 答辩时怎么把项目讲清楚答辩的核心不是功能堆叠而是把每个决策背后的考虑讲明白。我建议按这个逻辑准备幻灯片先讲痛点为什么需要这个系统再讲技术选型为什么是 SpringBoot Vue MySQL 而不是其他组合接着展示数据库设计把表结构关系和中间表讲清楚然后演示典型流程用户端走一遍完整操作管理端走一遍完整操作最后讲两个实际遇到的技术问题和解决过程。演示环节有一个很加分的小技巧每演示完一个功能主动抛出为什么这样做并现场解释。比如演示景点分页查询时解释 MyBatis-Plus 分页插件做了什么演示图片上传时解释为什么不返回绝对路径而要返回相对路径。把这些提前写进讲解稿演示节奏会非常稳。反过来如果演示只是机械地点击按钮画面再流畅答辩老师也很难判断你是否真的理解了这套系统。6.2 让项目加分的三个扩展方向时间充裕的话我建议在基础功能上再扩展一两个亮点。第一个方向是简化版推荐逻辑根据当前用户的收藏记录找出收藏偏好相似的其它用户把他们收藏的但你没收藏的景点推荐出来。这种基于用户的协同过滤思路用 SQL 加少量业务代码就能实现能展示业务理解力。第二个方向是数据可视化管理后台加一个统计面板用 ECharts 展示各城市景点数量柱状图、用户增长折线图、热门景点 Top10。第三个方向是定时任务通知收藏的景点价格变动时通过邮件通知用户用一个定时任务扫描即可实现。选扩展方向有一条原则不要为了炫技引入自己没完全弄懂的技术。答辩时老师顺着你的演示深问两句答不上来反而减分。我最推荐数据可视化因为 ECharts 配置资料多、效果直观、半天就能做出相当不错的面板性价比最高。最后分享一个技术之外的体会。我做这套系统时也崩溃过联调那几天全在跟乱码和跨域较劲一度觉得哪哪都是问题。但冷静下来把报错一条一条搜解决方案发现所有错误都能定位到根源没有任何一个真正无解。毕业设计最大的收获不是做出一个系统而是完整走了一遍发现问题、定位问题、解决问题的过程。这个思路对以后任何开发工作都用得上。希望这篇实战拆解能帮你在动手之前看清全貌少走一些我已经替你走过的弯路。