1. 项目全貌与基础认知
1.1 先搞清楚它到底是个什么东西
第一次看到“旅游指南系统”这个题目的人,十有八九会以为这就是个景点介绍网页。真上手做起来才发现,它背后是一整套前后端分离的完整链路,Spring Boot 负责后端接口和数据逻辑,Vue 负责前端页面渲染和交互,再加上 MySQL 数据库存储,三个部分拼在一起才能跑起来一个真正能用的系统。
我做毕业设计指导和全栈项目带教这些年,遇到过不少学生拿着这个题目来问我,开口第一句往往是“这个系统是不是很简单”。我的回答一般都是:如果你只是想在本地跑起来截几张图,那确实不难,照着文档一步步来就行;但如果你想在答辩的时候讲清楚每一个模块为什么这么设计、每个接口的返回结构为什么这么定,那需要下的功夫一点都不少。
这个系统的基本盘其实很典型:前端用户登录注册、景点列表展示、景点详情、酒店民宿信息、美食推荐、交通路线规划、个人收藏、订单管理,后端就是响应的数据表设计、CRUD 接口、登录鉴权、文件上传(景点图片)、搜索筛选分页。说白了,它是一套标准的管理信息系统加一个偏展示型的前端页面,这种组合在真实企业项目里也特别常见——后台管理系统加门户网站,或者小程序加运营后台,都是类似的套路。
正因为它的技术栈足够主流、功能边界足够清晰,所以用 Spring Boot + Vue 来做“旅游指南系统”几乎是所有高校软件工程、计算机科学与技术专业学生在毕业设计阶段的“必选题”之一。这也就意味着,你做出来的东西大概率会被拿出来跟网上其他版本对比。同样是景点列表,有的人做出来就是一个表格套模板,有的人做出来会有瀑布流卡片、地域筛选、搜索热度排行、线路推荐联动,差距直接就拉开了。
1.2 谁适合读这篇,读完能拿到什么
这篇文章的定位不是那种“三个命令跑起来完事”的快速上手教程,而是从零到一的完整拆解。适合三类人看:第一类是正在做这个题目的在校学生,需要的是一套能讲清楚原理、能自己改代码扩展功能的系统;第二类是打算把前后端分离项目作为简历项目的自学者,需要的是一份能经得起面试官追问的完整实战经验;第三类是已经做完基础功能,想让项目在功能深度和代码规范上再往上走一个台阶的人。
文章里不会有模拟数据那种糊弄人的做法,所有功能都是真实可运行的。我下面会从架构选型、数据库设计、后端接口实现、前端页面搭建、部署调试五个大块来拆,每一块都带上我在实际开发过程中踩过的坑和总结下来的经验。
2. 架构设计与技术选型背后的为什么
2.1 前后端分离到底解放了什么
做旅游指南系统,第一件要决定的事就是采用前后端分离架构,而不是像早年那种用 Thymeleaf 做模板渲染的传统单体方式。这个问题看起来好像没什么好讨论的——现在网上教程不都是这么教的吗?但只有把前后端分离带来的好处想清楚了,你在答辩的时候才不会被问倒。
前后端分离的本质,是把“数据怎么取”和“数据怎么展示”这两件事彻底拆开。后端只负责按照约定好的接口规范返回 JSON 数据,至于这些数据是在电脑浏览器上展示,还是被做成小程序,又或者被哪个第三方平台调用,后端完全不用关心。这在旅游指南这个场景里特别合适,因为同一个景点数据,用户在 PC 端看到的列表页是一套样式,在移动端看到的卡片流是另一套样式,但底层接口完全可以复用同一个。
举个例子,景点列表接口/api/scenic/list,返回的是景点 ID、名称、封面图、评分、简介、所属城市、热度值这些结构化数据。PC 端 Vue 页面拿到这份数据,可以把它渲染成带侧边筛选栏的宽屏布局;移动端拿到同样的数据,可以渲染成上下滑动的竖屏卡片流。前端的同事想怎么改视觉效果,后端一点都不用跟着动。
同样的道理,数据库里的酒店、美食、政策公告这些数据,将来要做推荐系统或者大数据分析,也是直接调接口就行,不用重新写一套数据访问层。这就是为什么现在几乎所有项目都默认前后端分离——它解放的不只是开发时的工作量,更是后续迭代和维护的灵活性。
2.2 Spring Boot 版本的纠结与选择
Spring Boot 的版本选择是个老生常谈但永远有人踩坑的问题。以这个旅游指南系统为例,我会直接建议用 Spring Boot 2.7.x,而不是最新的 3.x 版本。原因很简单:大量第三方集成组件,特别是 MyBatis 相关的各种增强插件、代码生成工具,对 Spring Boot 3 的 Jakarta EE 规范迁移还不够完全平滑。
但这个建议并不是一成不变的。如果你不是用 MyBatis,而是用 Spring Data JPA,那 Spring Boot 3 反而是个更接近未来主流的选择,因为 JPA 本身就跟着 Spring 生态走,兼容性几乎不需要操心。所以核心原则是:选版本不是选数字最大的,而是要看你整个技术栈里其他组件的兼容情况。
我一般会建议学生把项目里的 Spring Boot 版本固定下来之后,就不要再反复升级了。明明系统跑得好好的,看到一个大版本更新就想去试试新特性,结果经常是依赖冲突一堆,花一个下午调环境,最后又回滚回原来的版本。做毕设阶段,稳定性永远大于新特性。
2.3 Vue 2 还是 Vue 3,以及 UI 组件库怎么选
前端这块的版本纠结程度比后端还严重。如果我只针对这个旅游指南系统给一个标准答案,那就是:Vue 2 + Element UI 这套组合现在还是最稳妥的选择。这不是说 Vue 3 + Element Plus 不好,而是考虑到大部分学生是照着网上的教程一步一步做的,而全网旅游指南类的教程里,Vue 2 + Element UI 的案例存量最大,遇到问题能搜到的解决方案最多。
Vue 2 的 Options API 对初学者来说也确实更友好,data 里声明数据、methods 里写方法、mounted 里调接口,思维模型非常直观,不需要先理解组合式 API 那一套 setup 语法糖的逻辑。如果你本身已经是写了几个项目的“老手”,那直接用 Vue 3 + Vite + Element Plus 当然体验更好,构建速度快很多,组合式 API 写复杂逻辑的时候也更顺手。
这里我还想展开说一下 UI 组件库的事。Element UI 帮我们解决了后台管理类页面的 80% 需求,但旅游指南系统的前端不只是后台管理,它还有一个面向普通用户的展示端。这一部分如果也用 Element UI 那种方方正正的管理风格,观感会很生硬。更好的做法是展示端自己写 CSS,用卡片、轮播图、瀑布流这些自定义布局,后台管理端用 Element UI 快速搭表格和表单。两种风格各有定位,不再一个组件库上吊死。
3. 数据库设计——系统好不好的分水岭
3.1 从需求反推表结构
数据库设计是整个系统里最不能跳步的环节。很多人的做法是一上来就建表,边写代码边改字段,最后表结构乱七八糟,外键关系理不清不说,原来的数据还可能因为改字段而损坏。正确做法是先梳理需求,把所有需要支撑的功能列出来,再反推每张表要存什么。
旅游指南系统的核心功能可以分成三块:内容展示类、用户行为类、管理维护类。内容展示类需要景点、酒店、美食、交通信息、攻略文章这些数据实体;用户行为类需要用户表、收藏表、浏览记录表;管理维护类则需要管理员表、操作日志表、评论审核表。把这些实体和它们之间的关联关系理清楚,一个基础的表结构设计就出来了。
再往下就要处理字段的粒度。比如景点表,除了名称、简介、图片这些直观字段之外,经纬度、开放时间、门票价格区间、建议游玩时长、热度值这些辅助字段,在后续做线路推荐和搜索排序的时候都非常有用。很多人在设计阶段觉得“反正数据先填上,以后要用再加字段不就行了”,这个想法是最大的坑。MySQL 的 ALTER TABLE 在数据量小的时候不觉得慢,但表一多、数据一多,改表结构的成本和风险都会成倍上升。
3.2 核心核心表字段示例
下面这张是景点表的简洁版本,实际项目中我会加上 create_time、update_time、deleted 等通用审计字段。先关注最核心的信息:
| 字段名 | 类型 | 说明 | 备注 |
|---|---|---|---|
| id | bigint | 主键ID | 自增,唯一 |
| name | varchar(100) | 景点名称 | 必填 |
| province | varchar(50) | 所在省份 | 用于区域筛选 |
| city | varchar(50) | 所在城市 | 同上 |
| address | varchar(255) | 详细地址 | 展示用 |
| longitude | decimal(10,6) | 经度 | 定位和路线规划用 |
| latitude | decimal(10,6) | 纬度 | 同上 |
| cover_image | varchar(255) | 封面图URL | 列表页缩略图 |
| gallery | text | 图集,JSON数组 | 详情页轮播图 |
| description | text | 景点介绍 | 富文本 |
| open_time | varchar(50) | 开放时间 | 如“08:00-17:30” |
| ticket_price | decimal(10,2) | 票价 | 0表示免费 |
| recommend_duration | varchar(20) | 建议游玩时长 | 如“半天” |
| popularity | int | 热度值 | 默认0,越高排序越靠前 |
| status | tinyint | 上下架状态 | 0下架 1上架 |
这里有两个字段值得展开说:gallery用 JSON 数组存一个景点的多张图片,这个设计会让很多习惯了做一对一数据表的人产生疑惑——图片不应该单独建一张图集表吗?理论上是可以的,但考虑到景点图片一旦上传之后几乎不会单独更新,一张 JSON 字段直接解决读取和存储,能省掉一次联表查询,性能也更好。这是典型的“按业务复杂度取舍”的设计策略,不是所有一对多关系都必须拆表。
另外一个字段是popularity。这个热度值不是业务数据自动算出来的,而是管理员在后台上手动维护的一个权重值。这样做的好处是,运营者可以通过调整热度值来干预排序结果,把当季主推的景点顶到前面。这种可人工干预的排序设计,在真实的运营后台里非常常见,比单纯按点赞数或浏览量排序灵活得多。
3.3 用户行为和订单流程的难点
用户部分的核心表是用户表、收藏表、浏览记录表和订单表。用户表字段比较常规,需要注意的一点是密码不能用明文存储,用 BCrypt 加密之后落库。收藏表要建立联合唯一索引,防止同一个用户对同一个景点重复收藏;浏览记录表则要控制数据量,最多保留每个人最近 50 条,超过就删除最旧的。
订单表是这个系统里最容易出问题的表,因为它牵扯到一个流程状态流转的概念:已创建、待支付、已支付、已取消、已完成、退款中、已退款。用数据表来表示流程状态,最常见的坑是用字符串随便存状态名,“支付完成”“交易成功”“已完成”这种口径不一致的写法,会在后期统计时制造很多麻烦。
正确做法是定义一组常量枚举,接口里只传固定的数字状态码,展示端再把状态码翻译成中文。比如 0 表示待支付、1 表示已支付、2 表示已取消、3 表示已完成,这样在业务逻辑里面判断和处理都非常干净。后面如果要做运营统计,比如“已支付订单的转化率”,直接按状态码过滤即可。这个规则放在任何带订单的系统里都适用,不只是旅游指南项目特有。
4. 后端接口设计与关键实现
4.1 RESTful 接口怎么定才不乱
接口设计这件事,表面上看是“前端要什么接口就写什么接口”,实际做起来如果没有一套统一规范,项目写着写着就会变成灾难。我带的项目里,接口统一按 RESTful 风格来命名,资源用名词复数,操作通过 HTTP 方法区分。
比如景点模块的接口是这样的:
GET /api/scenic/list 景点分页+关键字+筛选 GET /api/scenic/{id} 景点详情 POST /api/scenic/admin 新增景点(需管理员权限) PUT /api/scenic/{id} 修改景点信息(需管理员权限) DELETE /api/scenic/{id} 下架/删除景点(需管理员权限)这组接口最值得注意的地方是:C 端用户查询走的是GET /api/scenic/list,管理端的增删改走的是同一个资源路径下的不同方法,只是权限校验逻辑不同。有同学问为什么查询不是/api/scenic/query或者/api/scenic/search。答案很简单:查询参数再多,只要语义上还是“获取景点资源列表”,路径就保持一致,参数全部放在 query string 里。
这里还有一个初创项目容易忽视的边界问题:内网接口和外网接口要不要分开。如果系统规模小,所有接口走同一个前缀/api就行;但一旦考虑到接入小程序或第三方平台,最好一开始就按/api(Web端)和/open(开放接口)分开设计。后者的签名鉴权逻辑跟前端登录态完全不同,提前分开能省去后面大量的返工。
4.2 统一返回结构与全局异常处理
接口返回结构如果不统一,前端写起来会非常痛苦。有的接口返回一个 JSON 对象,有的返回一个数组,有的把错误信息放在 message 字段里,有的放在 msg 字段里——前端每个接口都要单独适配,代码能写成千层饼。
我的做法是封装一个统一的返回类Result,结构固定为 code、message、data 三个字段。code 为 0 表示成功,非 0 表示业务错误码;message 是人类可读的描述信息;data 是实际数据,可以是对象、数组或者 null。
{ "code": 0, "message": "success", "data": { "total": 100, "records": [...] } }有了这个统一结构,前端就能封装一个通用的 request 拦截器,所有请求进来先判断 code 是不是 0,不是就弹错误提示,是就直接把 data 抛给页面。这个拦截器封装好之后,写新页面只需要关心业务数据,错误处理完全不用重复写。
全局异常处理也要配套做。Spring Boot 里建议用@RestControllerAdvice+@ExceptionHandler统一捕获异常,业务代码里不要到处 try-catch 之后自己吞掉异常,而是通过抛自定义业务异常让全局处理器统一返回。对于数据库异常、空指针这类未知异常,服务端要打日志保留现场,但返回给前端的 message 不能暴露内部堆栈信息,避免信息泄露。
4.3 登录鉴权用 JWT 比 Session 好在哪
旅游指南系统涉及用户登录,必然要选一套登录态方案。早年 Java 项目爱用 Session + Cookie,同一个 Session 对象在服务器内存里存着,用户量一上来就得考虑 Session 共享、粘性会话、Redis 存储这些问题,复杂度会逐渐失控。
现在的主流做法是用 JWT,无状态鉴权。用户登录成功后,后端生成一个带签名信息的 token(包含用户 ID、用户名、过期时间),前端把它存到 localStorage,每次请求在请求头加Authorization: Bearer <token>。后端用一个拦截器解析 token、校验有效性、取出当前用户名,然后放行到具体的 Controller。
JWT 要特别注意的两个点,一个是密钥管理,签名密钥不能直接硬编码在代码里写死,至少要做到配置文件外置、不同环境用不同的密钥。另一个是过期时间,有些人不设置过期时间,token 永久有效,等于给系统挂了一颗永久后门。比较合适的做法是 token 有效期 2 小时,用户操作时无感续签,超过 2 小时没有活跃操作就需要重新登录。
管理端的鉴权比用户端多一层角色判断。最简单的做法是用户表加一个role字段,普通用户和管理员用不同的 int 值区分,被拦截器解析出角色后再跟接口要求的角色比对。如果想要更灵活,可以用 Spring Security 或 Sa-Token;但就这个项目的复杂度而言,写一个自定义拦截器完全够用,不必被框架束缚住。
4.4 文件上传与静态资源映射
景点图片的上传是这类系统的标配功能。常规做法是后端接收 MultipartFile,保存到服务器本地磁盘的某个目录,然后把访问 URL 返回给前端。注意这里有一个最容易犯的错:图片存到了本地磁盘,但前端请求地址直接写/upload/xxx.png,结果一直 404,因为根本没有把磁盘目录映射成 URL 访问路径。
在 Spring Boot 里,需要显式配置静态资源映射:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB web: resources: static-locations: - file:/www/wwwroot/tour-upload/这样配置之后,访问http://localhost:8080/upload/xxx.png就会映射到服务器的/www/wwwroot/tour-upload/xxx.png。实际项目部署时不要用默认的static目录来存用户上传的图片,因为代码重新打包发布时,静态目录文件会被一并清理,辛辛苦苦传上去的图片就全丢了。独立目录存业务文件,这是从设计上就要记住的红线。
5. 前端页面搭建与交互实现
5.1 展示端和后台管理的页面规划
前端页面按角色可以分成两块:面向普通用户的展示端和面向管理员的后台管理端。展示端的风格自由度很高,是整份代码里最能体现个人审美的部分,我建议至少有以下这些首页模块:顶部导航、搜索框、景点推荐轮播、热门城市入口、按省份筛选、最近浏览记录。
首屏尤其重要。旅游类网站是强视觉驱动的,如果首页第一屏就是一堆列表文字,用户大概率不会多看一秒。所以我在做主推景点轮播时,背景会直接用大图铺满,标题叠一层半透明渐变蒙层,下面的导航栏悬浮在半透明背景上。这些全用原生 CSS 写就行,不需要引入任何复杂组件库,每个学生都能自己做出来。
后台管理端就没必要在视觉上花太多心思了。用 Element UI 的布局组件搭出侧边栏加顶栏的结构,菜单按模块分组:内容管理(景点、酒店、美食、攻略)、用户管理、订单管理、系统管理。每个页面就是一个表格加一个弹窗表单,表格负责展示数据,弹窗负责新增和编辑,这是 Element UI 最标准也最高效的使用方式。
5.2 路由与状态管理的正确组织方式
Vue 项目一跑起来,路由设计就得跟上。我的习惯是把页面路由分为两层:公开页面和管理页面。公开页面直接用router/index.js里注册,包括首页、景点详情、搜索结果、登录注册;管理页面统一挂在同一个父路由下,加一个嵌套布局组件 Layout,把侧边栏和顶栏固定在这个 Layout 里,子路由是各个管理功能页面。
这样做的好处是管理端的公共框架只渲染一次,切换子页面时只替换中间内容区,而不是整个页面重新加载,在开发时也不用每个管理页面重复引入侧边栏组件,省掉大量重复代码。
权限控制方面,前端路由需要一个全局前置守卫。逻辑不复杂:进入任何路由之前,先判断目标路由是否需要登录才能访问,如果需要且本地没有存 token,就强制跳转到登录页并带上回跳路径,登录成功后可以自动跳回来。这个细节非常提升用户体验,很多毕设项目都没做,导致用户逛一圈后点登录,进来又变成首页,之前想看的页面还得重新搜一遍。
数据请求这块,统一把 axios 实例封装在utils/request.js里,设置 baseURL、超时时间、请求拦截器加 token、响应拦截器统一处理 code 和错误提示。业务页面里尽量不要直接 import axios,而是从封装后的 request 模块导入,确保全局只有一套请求配置、一套拦截器。
5.3 景点详情的核心交互落地
景点详情页是整个前端里交互最丰富的一页,也是最容易做砸的一页。这一页通常需要同时加载景点基础信息、图集、评论列表、相似推荐,如果按最直接的方式写,在 mounted 里依次调四五个接口,每个接口都独立处理 loading 状态,代码冗余不说,页面还会因为接口返回速度不同出现内容跳动的“闪烁感”。
更优雅的方案是用 Promise.all 并行发起请求,所有数据全部到位后再一次性赋值:
async loadDetail() { this.loading = true try { const [detailRes, commentRes, recommendRes] = await Promise.all([ getScenicDetail(this.id), getCommentList({ scenicId: this.id }), getRecommendList({ city: this.city }) ]) this.detail = detailRes.data this.commentList = commentRes.data.records this.recommendList = recommendRes.data } catch (e) { this.$message.error('详情加载失败,请稍后重试') } finally { this.loading = false } }使用 Promise.all 会让页面等待时间取决于最慢的那个请求,但在 99% 的情况下,这些数据都是同一个后端服务返回的,响应时间差距一般不会超过几百毫秒,用户体验反而是更好的——不需要看到一块一块的数据慢慢蹦出来。
收藏按钮的交互也要注意防重复操作。按钮点击之后要立刻进入 loading 状态或禁用状态,等后端返回结果后再恢复。不然用户手速一快,连续点了两三次,就会给后端发出多个重复的收藏请求。后端虽然也可以靠联合唯一索引兜底,但从用户角度看,明明看到“已收藏”按钮状态却还来回跳变,体验会很差。
6. 核心功能扩展——超出普通毕设的那一层
6.1 搜索排序、智能推荐和日志埋点
很多教程版本的旅游指南系统,搜索功能就是一个模糊查询WHERE name LIKE '%x%',排序就是一个单一字段 order by。能做,但撑不起“指南”这两个字。
我的建议是至少在搜索模块做到两级:基础搜索用关键字匹配景点名称和简介,结果按其热度值排序;进阶一点可以加入城市筛选和分类标签的组合条件,比如“杭州 + 美食 + 按评分排序”,后端用 MyBatis-Plus 的 LambdaQueryWrapper 动态拼条件,完全不用手写复杂 SQL。
推荐模块是体现差异化的大亮点。最简单的推荐策略是“同城推荐”和“看过又看”——用户访问完某个城市的景点,详情页下方就展示同一个城市的其他景点。如果要更进一步,可以在用户表里加兴趣标签字段,登录时或浏览时更新,比如用户收藏了“自然风光”分类的景点,推荐列表优先展示同分类的高热度内容,这就是最基础的人肉版协同过滤。
日志埋点也是我一直强调的内容。不做埋点,热门景点、常用搜索词这些数据永远只能靠管理员手动猜。在景点详情页加载时向后端上报一个浏览记录接口,在用户点击搜索按钮时记录搜索关键词,后端把这些数据落库。一个月之后你就能看到完整的统计报表,哪个景点最受欢迎、哪些关键词是高频搜索,一清二楚。这份数据不仅是功能迭代的依据,还是论文里面数据分析章节的素材,价值很高。
6.2 评论与后台审核的闭环设计
景点评论功能看起来只是加一张评论表和一个提交接口,但真做起来会牵出审核、排序、重复提交好几个问题。评论表的核心字段包括:评论人、所属景点、评论内容、评分(1到5星)、审核状态、置顶标记、创建时间。
用户提交评论之后,建议不要直接显示出来。因为网上的系统只要开放评论,几乎一定会遇到垃圾广告内容,所以这里的闭环设计是:用户提交 -> 后台管理员审核 -> 审核通过后前端可见。审核状态用 tinyint 存储,0 待审核、1 已通过、2 已拒绝。
列表加载时前端只请求审核状态为 1 的评论。这里有个防刷的关键点:状态为 0 的评论,用户在前台反复提交接口不会重复入表,接口层要做幂等控制,校验同一个用户对同一个景点是否已经存在待审核状态的评论,存在就直接返回“已提交,请等待审核”,把重复提交挡在最外层。
7. 实际部署与运维调试记录
7.1 本地跑通需要的最小环境
很多人都卡在“代码明明是对的但就是跑不起来”这关。给大家一个本地环境的最小清单:JDK 1.8(或 11)、Maven 3.6 以上、MySQL 5.7 以上、Node.js 14 以上。四个环境变量配好,按顺序执行以下步骤就能把项目跑起来:
# 1. 导入数据库文件,执行 sql 初始化脚本 mysql -u root -p < tour_guide.sql # 2. 配置后端 application.yml 中的数据库连接信息 # 3. 启动后端服务 mvn spring-boot:run # 4. 进入前端目录,安装依赖并启动开发服务 cd frontend npm install npm run serve本地联调时,前后端端口不同会触发浏览器的跨域拦截。最简单的方式是在 Vue 的vue.config.js里配置一个 devServer 代理,把所有/api开头的前端请求转发到后端的 8080 端口:
devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }代理配好之后,前端写接口时就直接写/api/xxx,不用写http://localhost:8080/api/xxx那种硬编码全路径,以后部署上线只需要改一处代理配置。
7.2 线上部署环境的安全与性能考虑
项目如果要在服务器上正式部署,推荐的方式不是 Spring Boot 内置的 Tomcat 直接暴露给公网,而是前面再加一层 Nginx 做反向代理。Nginx 负责监听 80/443 端口、处理静态资源、转发 API 请求到后端服务,这样既减轻了后端压力,又可以在 Nginx 层统一处理 HTTPS 证书。
线上操作的时候有几个地方最容易顺手犯懒,但必须提醒你有坑:第一个是数据库密码,有的人直接写在配置文件里提交到代码仓库,等于把数据库密码公开了。正确做法是部署时通过环境变量注入,配置文件里只写环境变量占位符。第二个是前后端分离部署时的跨域问题,线上不要用 Vue devServer 代理,直接用 Nginx 根据路径前缀把请求分流:
server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }第三个是前端代码的构建,上线前一定要执行npm run build生成 dist 目录,把 dist 目录扔到服务器上由 Nginx 托管。用npm run serve这种开发模式部署到线上服务器,是很典型的门外汉做法,性能差还没法配 HTTPS。
我还想特别提醒一件事:部署之前,最好把 Spring Boot 的 MySQL 连接池配置调好。默认配置在并发上来之后很容易报连接池耗尽,HikariCP的 maximum-pool-size 建议设到 20 左右,数据库连接超时时间设到 30000ms,这样至少不会在用户量稍微大一点的时候直接挂掉。
7.3 毕业设计/课程报告的文档组织逻辑
源码和数据库都有了之后,文档也值得好好讲一下。很多项目做完了但论文或报告没东西写,问题不是没做功能,而是整个开发过程没有记录。建议从一开始就按照“需求分析、系统设计、数据库设计、核心功能实现、系统测试、总结展望”这个章节框架来积累素材。
核心功能的实现章节,不要长篇大论的贴代码,而是选一两个能体现设计思想的功能点重点讲。以旅游指南系统为例,登录鉴权模块的 JWT 无状态方案、订单模块的状态流转设计、搜索模块的动态条件拼装,都是非常适合展开讲的素材。在代码里把关键方法的注释写好,报告里直接提炼成文字,效率会高很多。
测试章节不能只写“系统能正常运行”。设计一份完整的黑盒测试用例表,覆盖正常流程(注册-登录-搜索-查看-收藏-下单)、异常流程(密码错误、token过期、重复收藏)、边界情况(空列表、超长文本、必填项缺失),表格列清楚用例编号、操作步骤、预期结果、实际结果,这一章节的完成度立刻就拉满了。
8. 常见问题排查与避坑速查
8.1 后端启动阶段的高频报错
先看一个出现频率高到离谱的问题:启动 Spring Boot 项目时报数据库连接失败,就那些CannotGetJdbcConnectionException、Access denied for user 'root'@'localhost'。排查思路很简单,第一步看用户名密码是否真的正确,第二步看localhost和127.0.0.1的区别——MySQL 的授权表里如果只授权了root@localhost,用127.0.0.1去连是会被拒的。
还有一个 2022 年后爆发的新问题:新版本 MySQL 8+ 默认使用 caching_sha2_password 加密方式,而旧版 MySQL 驱动不认识这个协议。解决方法是把 MySQL Connector 驱动升级到 8.x,或者用驱动版本对应的连接 URL 后缀参数。这个报错信息往往很隐晦,会绕掉很多人半天时间。
Mapper 文件路径配错也是高频问题。MyBatis 在 Spring Boot 中整合时,mybatis-plus.mapper-locations如果写的是classpath*:mapper/*.xml,但你的 XML 文件实际放在 resources 下的mapper目录,没问题;但如果放在了多级子目录,通配符就要相应调整。报 BindingException 时优先查这里。为了避免这个问题的出现,我的习惯是把 XML 放在和实体类的包路径一致的目录下,用多级通配符配置,这样代码和 XML 天然就近,维护起来也不会乱。
8.2 前端常见报错排查思路
前端第一高频报错非跨域莫属,浏览器控制台出现blocked by CORS policy,或者访问接口直接变成http://localhost:8080/api/xxx跑到前端自己的 devServer 上了。看到这种,第一反应检查 Vue 的 proxy 配置,确认 target 端口跟后端实际端口一致,然后确认代理路径前缀写对了没有。
第二高频是页面白屏,可能的原因有两个:路由模式用 history 之后,刷新非首页路径时 Nginx 没配try_files回退到 index.html;另一种是打包后静态资源路径不对,build 之后 css/js 文件找不到。解决办法是vue.config.js里publicPath配置成相对路径./,这样不管部署到哪个子路径下都不会挂掉。
接口 401 报错多半是 token 丢了或者过期了。排查看请求头里有没有带上Authorization,再看后端的拦截器是不是把不需要拦截的登录注册接口也拦了。我见过一个项目,注册接口也被拦截器要求带 token,结果用户注册时请求直接 401,前端报“请先登录”,这就很尴尬。用配置放行列表把白名单接口全部排掉,这是拦截器设计里必须做的事。
8.3 一份实用的问题排查速查表
| 现象 | 排查方向 | 常见原因 | 解决办法 |
|---|---|---|---|
| 后端启动报数据库连接失败 | 配置、网络、授权 | 密码错误 / 驱动与MySQL版本不匹配 / 网络不通 | 逐项核对配置,升级驱动,用 ping 测网络 |
| 接口返回 404 | 路由、Mapper、路径 | XML 路径不对 / 接口路径拼写错 | 看控制台日志的 mapping 信息,再到前端核对 URL |
| 接口返回 500 | 日志、数据 | 空指针 / 参数不匹配 / SQL 语法问题 | 看服务端堆栈日志,定位到具体行 |
| 前端页面白屏 | 路由、部署、依赖 | history 路由无回退 / 静态资源路径错误 / 组件加载崩溃 | 配 try_files,改 publicPath,看控制台报错 |
| 图片显示不出来 | 路径、授权 | 磁盘映射不对 / URL 含中文未编码 / 权限不足 | 检查静态资源映射配置,用浏览器直开图片地址定位 |
| 搜索无结果 | 关键词、编码 | 中文乱码 / 字段名不匹配 | 检查 URL 参数编码,核对数据库排序规则,统一用 utf8mb4 |
| 登录成功但访问受限接口报 401 | token、拦截器 | token 过期 / 请求头没带 token / 白名单没配 | 换新 token 测试,排查请求头,补充放行路径 |
| 本地开发正常线上异常 | 环境差异 | 数据库版本不同 / 文件路径写死 / Nginx 配置缺失 | 对比环境变量、检查路径,参考上文 Nginx 配置 |
这个速查表里的每一条,都是我实际帮别人排错时真真切切遇到过的问题,尤其是图片显示和静态资源映射那条,十个项目能有八个踩坑。很多人本地跑好好的,一部署到服务器图片全挂,就是因为服务器上目标磁盘目录不存在或者没有写权限。先把 mkdir 目录建好,再给足权限,这个坎就过去了。
8.4 汇报演示时的几个演示陷阱
每次看到同学在答辩现场或演示录屏时翻车,我心里都会先默哀三秒。提前把你把演示流程走两遍,避开这些已知的坑。
视频演示或现场演示时用的数据,一定要提前检查一遍。图片不能有防盗链,比如从别的站点复制过来链接的图,加载不出来就是一个大黑框,观感很差。演示时数据库里的数据尽量丰富一些,每个分类至少保证 10 条以上,因为一旦现场要演示搜索、筛选、排序这些功能,数据太少一眼就露怯,甚至显得功能没做全。
现场演示的电脑上要提前配好环境,如果现场用 U 盘拷代码再打开项目,报几个依赖下载失败的错,基本就没法收场了。最稳的做法是演示前在目标电脑上完整跑一遍本地项目的启动流程,确认后端能起、前端能开、数据库能连,再进入正式演示。
演示顺序上也有讲究,把最好看的页面留在最前面,比如带轮播图、瀑布流、地图标注的首页或详情页,开场吸引眼球。然后再进入到功能演示:登录注册、搜索筛选、景点收藏、下单支付、后台管理。最后收尾时展示一下数据库设计或者代码结构,体现你的工程规范性。这个顺序能帮助听众建立对项目的整体好感,比一上来就贴 ER 图讲表结构要舒服得多。
9. 写在最后的心得
做完旅游指南系统,你会发现这类以 Spring Boot + Vue 为核心的项目,真正拉开差距的地方往往不是框架本身,而是你对业务需求的理解深度和数据模型的设计能力。景点、酒店、评论、订单这些实体谁都会建表,但能不能把热度值、审核状态、幂等控制这些细节做进去,做出来的系统给人的感觉完全是两回事。
我个人的建议是,如果时间和精力允许,在基础功能都跑通之后,挑一个方向做深化。比如把搜索模块升级成全文检索引擎(Elasticsearch 或简单的数据库全文索引),或者把推荐模块升级成带用户画像的推荐策略,再或者把统计模块做成一整套可视化看板。这个深化的方向不一定多前沿,但按“调研 -> 设计 -> 落地 -> 验证”的路子独立走完,你的收获会远超照搬一个成品项目。
还有一句老话想说:项目里的代码要尽量自己敲一遍,哪怕是在网上下载的源码,也要一行一行看懂、亲手改一改。我见过太多同学下载了项目答辩时支支吾吾,被问到一个最简单的字段含义都答不上来,给老师留下很差的印象。代码可以借鉴,但原理和思路,只有自己消化过才是自己的。