最近不少准备做 Java Web 毕业设计的同学找我聊同一个困境:网上各种 SpringBoot+Vue 的旅游网站源码下载一抓一大把,但要么缺 SQL 脚本、要么接口文档只有两页 PPT、要么代码结构乱到根本没法在答辩时讲清楚。这批人里有一部分是真心想做一个能跑通、能演示、能扛住评委提问的项目,而不是只想交一份"看起来差不多"的东西。这次我围绕一套典型的 SpringBoot+Vue 旅游网站平台项目来拆解,从源码结构、SQL 脚本设计、接口文档规范到前端后端的协作思路,把这类项目从"能跑"带到"能讲明白、能改、能扩展"的层面,给准备拿它当毕设或者练手的人一条真正可以跟着走的路。
先说说我对这种项目的总体判断:旅游网站平台是所有 Java Web 毕设选题里功能边界最清晰的一种,因为它既不涉及电商那种超复杂的订单状态机,也不需要做社交产品那样高并发的实时消息体系,它的核心就是"资源展示+信息检索+订单流转"这三个能力。这样的业务复杂度恰好卡在"能体现完整开发链路"和"不会把自己埋进泥潭"之间的黄金位置。你自己上手做完一遍,能把 SpringBoot 的后端分层、Vue 的组件化开发、关系型数据库的表设计、RESTful 接口的规范全部串起来,这正是评委想看到的东西。
1. 为什么旅游网站适合做 SpringBoot+Vue 毕设:选型逻辑与功能边界
1.1 前后端分离的架构选择不是跟风,是必然趋势
在拿到项目源码之后,你首先要理解一个问题:为什么现在的毕设几乎清一色是 SpringBoot 后端加 Vue 前端,而不是以前那种传统的 JSP+Servlet 项目?这背后不是单纯的技术潮流问题,而是开发模式和团队协作方式的变化。SpringBoot 负责提供纯 RESTful API,Vue 负责通过 Axios 这类 HTTP 客户端去消费这些 API,两者之间只通过 JSON 数据交互,前后端可以完全并行开发。理论上两个人在同一份接口文档的约束下,前端不需要等后端写完 Controller,后端也不需要等前端画好页面才启动联调。
这种架构给你带来的最大好处是:你可以在答辩时清晰地画出一条数据链路,从用户在页面上点击一个按钮,到 Vue 组件事件触发、Axios 发送请求、后端 Controller 接收参数、Service 处理业务逻辑、Mapper 操作数据库、再通过 JSON 原路返回、最终由 Vue 渲染到页面上。评委最喜欢问的就是这条链路,因为它能一眼看出你是真的做过还是只会照着教程敲。而传统 JSP 项目里,页面和 Java 代码缠在一起,别说画链路了,自己调个 Bug 都会迷路。
1.2 功能模块怎么划分才不会被评委追问到崩溃
旅游网站最容易犯的错误是功能堆砌。很多参考源码里恨不得把"订酒店、买机票、约导游、租车"全塞进去,看起来功能很多,但实际上每个模块都做得很浅,反而会在答辩时被评委抓住其中一个细节连续追问,一追问就露馅。我建议把功能边界收敛成两条主线:用户端和管理端。用户端围绕"找景点、看详情、下订单、发评论"这条完整用户体验链路,管理端围绕"维护数据、处理订单、管理内容"这条运营链路,两头合起来正好闭环。
完整的模块划分大概是这样:
| 端 | 模块 | 核心职责 | 关键表 |
|---|---|---|---|
| 用户端 | 用户认证 | 注册、登录、个人信息维护 | t_user |
| 用户端 | 景点展示 | 景点列表、详情、图片轮播、视频 | t_scenic |
| 用户端 | 线路规划 | 多日游线路、行程安排、价格展示 | t_route, t_route_item |
| 用户端 | 订单系统 | 选择日期和人数、生成订单、支付模拟 | t_order |
| 用户端 | 互动功能 | 评论、收藏、评分 | t_comment, t_favorite |
| 管理端 | 景区管理 | 景点的增删改查、上下架 | t_scenic |
| 管理端 | 线路管理 | 线路设计和价格调整 | t_route |
| 管理端 | 订单处理 | 查看订单、确认/取消订单 | t_order |
| 管理端 | 数据概览 | 访问量、订单量简单统计 | 聚合查询 |
这个划分方法的好处在于每个模块都能用一两句话说清楚它解决了什么问题,不会出现那种"一个页面管八件事"的混沌状态。你在理解源码或者自己改代码的时候,每动一个模块,都能明确知道自己改的是哪一层、影响的是哪张表,这种可控感在答辩前非常重要。
1.3 这套源码的目录组织方式决定了它好不好扩展
我压箱底的建议是:拿到源码后先别急着跑起来,先花一个小时把目录结构捋清楚。一个设计良好的 SpringBoot 项目,根目录下应该按职责而不是按技术栈划分包结构。比如你看到这样的包结构:controller、service、mapper、entity、config、common、util,比看到一堆散落的类文件要靠谱得多。common通常放统一返回体和异常处理,config放跨域配置、拦截器配置、MyBatis-Plus 配置,util放 JWT 工具类、日期工具类。前端 Vue 项目里,views放页面组件,router放路由表,api目录里的每个 JS 文件对应后端一个模块的接口,store放 Vuex 或 Pinia 的全局状态。这个组织方式本身就是一个潜在答辩亮点,因为大部分人的项目目录是随便堆的,你能把结构讲清楚,评委的第一印象就不一样。
前端侧的目录组织细节往往被非重点思考,但这段恰恰容易出彩。你可以在前端api目录下看到类似scenic.js、order.js、user.js这种按业务模块拆分的接口定义文件,每个文件导出一个或多个封装好的请求函数。这样前端调用接口时不用在页面组件里直接写一堆 Axios 裸请求,而是统一调用函数。你讲解的时候就说:"我把接口层做了统一封装,页面组件不直接关心请求细节,只调用对应模块的接口函数,这样后端接口 URL 变了,我只需要改动 api 目录下的一个文件。"这话一出口,评委就知道你不是完全不懂工程化。
2. 数据库设计这一环决定后续开发效率:核心表结构与 SQL 脚本组织方案
2.1 SQL 脚本不是"建几张表"这么简单,它承载的是整个业务语义
很多人打开 SQL 脚本只看建表语句,忽视了脚本本身的分段组织方式。一套完整的旅游网站数据库脚本,至少要包含四个部分:建库和指定字符集、建表结构、初始化基础数据、生成若干条演示用的测试数据。分段的目的是为了让整个项目在任何一台干净的机器上都能一键复现,这个过程对应答辩时评委可能问的"你这个系统怎么在三分钟内从一个空环境跑起来",你能够清晰回答"先执行 init.sql 建库建表、再执行 data.sql 插入初始数据、最后启动 SpringBoot 和前端即可",这本身就是加分项。
有一个细节我在指导别人时反复强调:字符集必须用utf8mb4而不是utf8。原因很简单,utf8在 MySQL 里最多存 3 字节的字符,用户在评论里输入一个 Emoji 表情直接报错,报错信息长得吓人,其实就一个字符集问题。表情符号是 4 字节编码,只有utf8mb4能存下。旅游网站的评论模块又是高频使用场景,你总不能要求用户不发表情吧。这是一个小坑,但它在实际运行里很容易出现,而且一旦项目已经积累了几十张表再想改字符集,迁数据会非常痛苦,所以开局就锁死utf8mb4。
核心表结构我大致说几个重点,你可以对照着手里的 SQL 脚本看:
CREATE TABLE `t_user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT 'BCrypt加密后的密码', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `avatar` varchar(255) DEFAULT NULL COMMENT '头像URL', `role` tinyint DEFAULT '0' COMMENT '角色 0-普通用户 1-管理员', `status` tinyint DEFAULT '1' COMMENT '状态 0-禁用 1-正常', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';注意几个实战细节。第一,密码字段长度留到 100,因为 BCrypt 加密后的密码长度是固定的 60 个字符左右,留 50 的话以后想换加密算法都换不了。第二,用户名必须建唯一索引,这是用户体系的底线约束。第三,把update_time定义为ON UPDATE CURRENT_TIMESTAMP,这样每次更新记录时时间戳自动维护,不需要在 Java 代码里手动 set。
订单表的并发设计就更讲究了。旅游网站和普通商品电商不太一样的地方是,景区每天可预订的票数是有限的,比如某个景点每天只放 500 张票,用户下单时必须保证不会超卖。如果你的表设计里订单直接引用景区的总库存字段去减,那并发高点一定会出问题。更稳妥的做法是引入一个独立的库存或者版本号字段,配合乐观锁来实现扣减。参考脚本里这一张表值得你多花时间看,因为它是整个项目里最能体现业务深度的表。
CREATE TABLE `t_scenic` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '景点名称', `summary` varchar(500) DEFAULT NULL COMMENT '简介', `detail` text COMMENT '详细介绍', `address` varchar(200) DEFAULT NULL COMMENT '地址', `cover_image` varchar(255) DEFAULT NULL COMMENT '封面图', `images` varchar(1000) DEFAULT NULL COMMENT '轮播图,多个用逗号分隔', `open_time` varchar(100) DEFAULT NULL COMMENT '开放时间', `ticket_price` decimal(10,2) DEFAULT '0.00' COMMENT '门票价格', `daily_quota` int DEFAULT '500' COMMENT '每日可售票数', `version` int DEFAULT '0' COMMENT '乐观锁版本号', `status` tinyint DEFAULT '1' COMMENT '上下架状态', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='景点表';远景图片用逗号分隔存在一个字段里,听起来好像不规范,但实际开发中你真要去建一张图片子表反而会把简单问题复杂化。景点图片是只读型数据,变更频率极低,它的操作单元永远是"整个景点的图片集合",用逗号分隔存储然后在后端拆分成数组返回,完全够用且高效。数据库设计要权衡规范和实际场景,不是越规范越好。
2.2 测试数据要"仿真",别拿十条假数据糊弄评委
SQL 脚本里初始数据这部分,我见过太多项目只插个三五条记录,连图片 URL 都是随便写的xxx.jpg,启动进去一看页面都在转圈。你试想一下这个场景:答辩现场你打开前端首页,景点列表空荡荡,评委问"为什么没有数据",你说"我还没来得及录入",这种问答一次就足够毁掉整个答辩。正确做法是每个核心表至少插入 10 到 20 条有真实感的演示数据,景点名称、简介、价格、地址都要看起来像真实业务里的样子,图片 URL 要用可访问的在线图片地址,这样你演示时页面才会丰满。
还有一类数据是必须的但总被忽略,就是管理员账号和你自己的测试用户账号。你可以把管理员的用户名密码写死在初始化脚本里,同时在文档里标注"管理员账号 admin,密码 123456"。别觉得这是小事,很多人在答辩前找不到自己之前建的测试账号,当场注册又暴露了密码规则逻辑,非常尴尬。初始化脚本里预置好账号,既方便你演示,同时在回答评委"系统里有没有预置数据"这个问题时也能从容应对。模拟订单数据也建议准备几条,涵盖待支付、已支付、已取消、已完成这些不同状态,让订单列表页在演示时有内容可看。
2.3 外键、索引与连表查询的取舍:性能和安全之间的平衡
旅游网站这类项目,我的建议是:物理外键尽量少用,逻辑外键靠代码保证。也就是说,表结构里不写FOREIGN KEY约束,但是t_order里的user_id、scenic_id这类关联字段仍然要建普通索引。这样做的理由很简单:物理外键会在每次插入、更新时触发额外的约束检查,性能有损耗;更重要的是在真实开发中,很多业务需要先删主表再处理从表,物理外键会让这种操作变得很痛苦。但字段上的普通索引一定要有,因为所有订单列表查询都离不开WHERE user_id = ?这种条件,有索引和没索引在高数据量下差别巨大。
初始化脚本里还应该考虑一个点:给所有订单表按照创建时间建索引。管理端大概率要按时间范围去过滤订单,没有这个索引,数据一多查询就会慢。旅游网站的订单量虽然不像电商那么大,但是在本次项目演示中,你不需要做到毫秒级,但至少不能出现明显的卡顿。这些索引字段在数据模型设计时就要前置思考,等出现性能问题了再补索引,已经属于应急处理的范畴了。
3. SpringBoot 后端的骨架与原理解析:认证、分层与业务实现
3.1 统一返回体和全局异常处理是后端代码的第一张脸
你打开后端的common包,大概率能看到Result<T>这个类,它定义了后端返回给前端的数据包装格式。我见过多种写法,但核心结构基本一致:状态码、提示信息、数据本体。一个通用返回体长这样:
public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }不要小看这个简单的类,它解决了一个很重要的问题:前端只需要统一解析code/message/data这个三层结构,就能处理所有接口的返回,而不需要对每个接口单独写一套判断逻辑。前端 Axios 封装里一般都会有响应拦截器,只要code不是200就弹出message,否则才把data返回给调用方。这套约定一旦建立,后面写几百个接口都只需要遵循这一个模式。
与之配套的还有全局异常处理器@RestControllerAdvice,作用是把代码里抛出的各种业务异常统一转换成Result格式返回。没有它的话,后端一报错前端拿到的是乱七八糟的异常堆栈,用户看到的就是一个白屏或者"Internal Server Error"。有了全局异常处理,你可以把"库存不足""登录已过期""没有权限"这些情况都通过自定义异常抛出来,前端收到后弹一条友好的提示。这一设计在整个后端工程里看起来不起眼,但它把错误处理的复杂度降了一个量级。
3.2 JWT 登录认证:无状态会话是前后端分离项目的标配
旅游网站项目和其他类型的系统一样,登录认证的落地方式首选 JWT,而不是传统的 Session。原因简单,后端是纯 API,可能被小程序、网页、App 多个客户端共用,Session 依赖服务端存储,一旦水平扩展就会有会话同步问题;而 JWT 是无状态的,token 里直接携带用户标识和过期时间,服务端不保存任何会话信息,校验只靠签名解密。对毕设项目而言,无状态这一句话足够在答辩时解释为什么选 JWT 而不是 Session。
JWT 的实际使用流程是这样的:用户登录成功后,后端生成一个 token,里面封装用户 id、用户名、角色,用密钥签名,返回给前端;前端把 token 存放在本地存储里,每次 Axios 请求都在请求头里带上Authorization: Bearer <token>;后端加一个拦截器,对需要认证的接口校验 token 是否有效,有效就从 token 里解析出当前用户 id 放入请求上下文。
这里有一个特别容易被新手忽略的点:密码存储绝对不能用明文,也不能用简单的 MD5。明文密码一旦数据库泄露就是安全事故,MD5 又很容易通过彩虹表反查出来。正确做法是用 BCrypt 加密,Spring Security 自带的BCryptPasswordEncoder就能用,它每次加密同一个密码得到的哈希值都不同,天然抗彩虹表。注册时加密存库,登录时用matches方法比对。在答辩时如果你能说出"BCrypt 内置盐值,同一密码多次加密结果不同"这句话,评委基本不会继续追这个问题。
SpringBoot 项目里集成 JWT 的常见文件结构会是:一个JwtUtil工具类负责生成和解析 token,一个拦截器实现HandlerInterceptor负责从请求头取 token 并校验,一个WebMvcConfigurer实现类负责注册拦截器并配置哪些路径放行、哪些需要认证。注册和登录接口肯定放行,前端要的静态资源也放行,其余的接口统一走拦截器。
实现思路的关键点: - token 里只放用户 id 和用户名,不要放敏感信息 - 设置合理的过期时间,比如 24 小时,到期后前端跳回登录页 - 拦截器里对 token 解析失败的情况统一返回 401 - 前端 Axios 拦截到 401 后清除本地存储并跳转登录3.3 Service 层的事务与业务逻辑:订单扣减的乐观锁实现
后端的 Controller 层职责是接收参数、调用 Service、返回结果,不应该写具体业务逻辑。真正复杂的业务都集中在 Service 层,其中订单提交是整个项目里最适合深挖的点。一个完整的下单流程包含哪些操作?查询景点当前剩余可订票数、判断预订人数是否超过余票、计算订单总价、生成订单记录、扣减余票数。这一串操作必须处于同一个数据库事务里,任何一个步骤失败,前面已执行的数据库操作都要回滚。
SpringBoot 里实现事务非常简单,在 Service 方法上加一个@Transactional注解就完了,但扣减余票的并发控制才是真正的难点。假设两个用户同时下单,都查到剩余票数是 50,各自扣减 1,如果没有控制,最终余票只少了 1 而不是 2,这就是超卖。解决的常见方式是用乐观锁,在景点表加一个版本号字段,更新时带上版本条件:
UPDATE t_scenic SET daily_quota = daily_quota - #{count}, version = version + 1 WHERE id = #{scenicId} AND version = #{oldVersion} AND daily_quota >= #{count}这个 SQL 的含义是:只有在版本号没变而且余票充足的情况下,本次更新才会生效,update影响行数为 1 就说明扣减成功,为 0 就说明要么被别人改过了、要么票不够了,此时直接抛异常让事务回滚。这个写法实际上是做高并发的人说的乐观锁思路,它不用锁表、不用synchronized,更适合分布式场景。如果你能把这一套逻辑在答辩时讲清楚,评委基本上可以确认你不是拿了个源码跑一跑就来答辩的,你是真的理解和思考过的。
同时你需要注意一个细节:@Transactional解决数据一致性,但它管不住逻辑异常,如果你在事务方法内部用 try-catch 把异常吞掉了,事务照样不会回滚。新手经常在这里踩坑,测试的时候发现自己扣了余票但订单没生成成功,其实就是异常被捕获后吞掉,Spring 感知不到异常,自然就不会回滚。这个经验写进你的笔记里,调试时会少走很多弯路。
3.4 文件上传与静态资源映射:图片存储是前后端的同步难题
旅游网站景点信息里有大量图片,管理端上传景点图片,用户端展示这些图片,这里涉及一个经常出问题的地方。后端不能把图片存进数据库的 BLOB 字段,那不是正确做法,正确做法的落地选择通常是:图片文件保存到服务器某个磁盘目录,数据库只存图片的相对访问路径,然后通过配置把图片目录映射成 URL 路径,让前端可以直接访问。
SpringBoot 项目里的实现方式很简单:在application.yml里自定义一个上传路径,然后在配置类中把物理路径映射为静态访问 URL。例如物理存储路径/www/upload/scenic/,通过配置映射到 URL 路径/images/scenic/**,那么前端访问http://服务器地址:8080/images/scenic/xxx.jpg就能直接看到图片了。这个方案比把图片塞到项目根目录下的static文件夹要更合理——因为项目打包成 jar 之后,static目录是只读的,运行时往里写文件不可行。
还有一个很常见的坑:前后端分离部署时,后端跑在 8080,前端跑在 80 或 5173 端口,前端页面通过http://localhost:8080/images/...访问后端静态资源,这里又牵扯出跨域问题。正确的处理方式是在后端的跨域配置里开放需要的资源路径和请求方式,同时静态资源映射本身也要正确配置,否则前端页面里的图片会一片空白。图片加载不出来是整个项目演示时最直观的翻车现场,你应该提前把所有图片 URL 配置成可访问的相对路径,然后用浏览器直接打开测一遍,再启动前端集成验证一遍。
4. Vue 前端从路由到渲染:组件划分与数据协作方式
4.1 前端路由设计:页面结构与守卫逻辑
Vue 项目里,前端路由设计直接决定了系统的信息架构。旅游网站的前端路由通常分成两部分:面向游客的展示端路由和管理员的后台管理端路由。游客端的典型页面路径有/home(首页)、/scenic(景点列表)、/scenic/:id(景点详情)、/order/confirm(订单确认页)、/user/center(个人中心)、/login(登录注册)。管理端一般放在/admin下面,底部嵌套一堆二级页面,比如/admin/scenic、/admin/order、/admin/user。
路由守卫是前端最重要的安全措施之一。你要能读懂代码里router.beforeEach这一段逻辑:每次路由跳转前,先检查目标路径是否需要登录权限,需要的话再看本地有没有 token;没有 token 就跳转到登录页并且带上redirect参数,登录成功后再跳回原页面。这里有一个细节很多人理解不到位:前端路由守卫只是用户体验层面的保护,真正的权限校验永远在后端接口,前端守卫只是防止用户看到空白页或异常页。答的时候能把这一层说清楚,说明你对前后端安全边界有正确的认知。
4.2 Axios 封装与请求拦截:让每个接口都自动携带 token
你可以打开前端项目里的utils/request.js或者api/http.js这类文件,它就是一个被统一封装的 Axios 实例。这个实例的作用很明确:设置请求超时时间、设置请求拦截器、设置响应拦截器。请求拦截器在每次请求发出去之前,从本地存储里取出 token,加上请求头。响应拦截器在收到结果时统一判断 HTTP 状态码和后端业务状态码,遇到 401 就清除本地登录信息并跳转登录页,遇到其他错误就弹出错误提示。
这个封装的意义在于:你写任何页面组件时,不需要关注 token 怎么传、错误怎么处理,只需要调用封装好的函数,比如:
import request from '@/utils/request' export function getScenicList(params) { return request({ url: '/api/scenic/list', method: 'get', params }) }调用getScenicList({ page: 1, size: 10 })拿到的就是已经剥离过的后端data数据,页面里直接使用即可。这种封装在代码层面看不多,但它体现了"统一处理横切关注点"的工程思想,这个在回答"你前端项目有什么亮点"时是很好的素材。
4.3 景区详情的视频点位与 m3u8 播放:旅游场景里的一个出彩功能
现在很多旅游网站平台都会在景点详情页放一段宣传视频或者慢直播流,这个功能如果在你的项目里出现,绝对是一个超出普通毕设水准的亮点。视频播放的技术选型要看你拿到的视频格式是什么。普通的 MP4 文件用原生<video>标签就能播放,麻烦不大;但如果你接的是网络摄像机生成的流媒体地址,尤其是一些采用了 HLS 协议、地址后缀是.m3u8的视频源,那就不是原生播放器能搞定的了。
HLS 协议的原理是把一段视频切片成无数个小文件,.m3u8本身只是一个索引文件,里面记录的是每个切片的地址列表。由于浏览器原生不支持直接在<video>里播.m3u8地址,需要借助hls.js这个库把切片拉下来解析后喂给<video>。前端实现逻辑大概是这样的:先用Hls.isSupported()判断当前浏览器是否支持,支持就创建 Hls 实例绑定到 video 元素上,不支持就回退到原生播放能力。这个功能放在景点详情或景区直播里会非常有代入感,而且代码量并不大,却是实实在在的热门技术点。
不过做这个功能时必须时刻注意跨域问题,因为视频文件的响应和普通 JSON 接口一样受到 CORS 限制。如果你用的是第三方视频源,远端服务没有开放跨域头,那么就算前端代码写得再对也无法拉取切片。在实际项目里,你可以选择放在后端做一层代理转发,或者在前端开发阶段用 Vite 的 proxy 配置做代理来解决。这个坑我在调试时踩过,一开始一直怀疑是 hls.js 的参数没配对,查了半天才发现是跨域把请求拦了。现在回想起来,排查这一类问题最快的方法是直接打开浏览器 DevTools 的 Network 面板,看报错是 CORS 的还是 404 的,方向确认了再动手改代码。
4.4 环境变量、代理与打包后的布局异常:前端部署的三个老问题
前端项目在开发环境跑得挺好,一打包部署就出问题,这是毕设答辩前最常见的技术事故。问题通常出在三个地方:接口地址写死、路由模式、静态资源路径。开发时前端请求一个/api/xxx接口,由 Vite 的 dev server 代理到http://localhost:8080,这没问题;但打包后部署到 Nginx,/api的代理要重新在 Nginx 配置,否则所有请求都会 404。处理方式是用环境变量区分开发环境和生产环境的VITE_API_BASE_URL,在.env.development和.env.production里配置不同的接口前缀。
路由模式的问题也很常见。如果你的 Vue 路由用了createWebHistory(HTML5 history 模式),部署到 Nginx 后访问/scenic/1这样的路径,刷新页面会 404,因为 Nginx 找不到对应的物理文件。解决办法是在 Nginx 配置里加入location / { try_files $uri $uri/ /index.html; },把所有非静态文件请求转发到index.html,由前端路由接管。这也是为什么有些参考项目直接使用 hash 模式,但为了 URL 美观,用 history 模式再配 Nginx 回退是更专业的选择。
还有一类打包后"布局异常"的问题,表现通常是 CSS 样式丢失、字体图标不显示、图片路径多了一层目录。这类问题多半是base或publicPath配置不对,资源请求路径没有落在正确的静态文件根目录下。排查的时候看 DevTools 里的资源请求 URL,把它和线上文件实际路径对比一下,就能快速定位是多了前缀还是少了前缀。
5. 接口文档设计规范:把约定的价值发挥到极致
5.1 接口文档应该包含哪几块内容
接口文档在这套源码里的价值经常被低估,很多做毕设的人以为它是给评委看的花架子,其实它更是前后端联调的施工图。一份合格的接口文档,在每一个接口下面至少包含这些信息:接口功能说明、请求方法、请求路径、请求参数表(参数名、类型、是否必填、说明)、请求示例、成功返回示例、错误码说明。不要以为有 Swagger 自动生成的在线文档就够了,Swagger 能展示参数和返回结构,但对业务含义和错误码约定解释得不充分。好的做法是手写一份 Markdown 接口文档,同时接口代码里保留 Swagger 注解生成在线调试页,两者互为补充。
我见过很多接口文档只写"获取景点列表 GET /api/scenic/list"一行字,然后就没了。这种文档根本没法用,因为前端拿到手根本不知道要传什么参数、返回什么字段、字段名是什么类型。实际项目里前后端并行开发时,接口文档是双方唯一的沟通契约,文档不详细,前端只能反复去问后端,联调效率会直线下降。在你的毕设答辩中,完整详实的接口文档也给了评委一个明确信号:这个项目是按真实工程规范管理的。
5.2 核心接口定义示例:接口协议如何组织才能一眼看懂
下面是几个旅游平台里最有代表性的接口定义方式,你可以直接对照这套设计去检查手头源码里的接口文档是否完整:
| 功能 | 方法 | 路径 | 核心请求参数 | 返回结果 |
|---|---|---|---|---|
| 分页查询景点 | GET | /api/scenic/list | page, size, keyword, status | 分页对象 |
| 获取景点详情 | GET | /api/scenic/{id} | 路径参数 id | 景点详情对象 |
| 提交订单 | POST | /api/order/submit | scenicId, travelDate, count, contactPhone | 订单号 |
| 取消订单 | PUT | /api/order/cancel/{orderId} | 路径参数 orderId | 操作结果 |
| 用户收藏景点 | POST | /api/favorite/add | scenicId | 操作结果 |
| 管理员登录 | POST | /api/admin/login | username, password | token |
返回结果部分建议统一用这样的一段 JSON 来展示:
{ "code": 200, "message": "操作成功", "data": { "orderId": "202501120001", "amount": 360.00, "status": 1 } }这里有一个很关键的设计约定:订单号不要用数据库自增 id 直接返回给用户,而是要有一套生成规则,比如"日期+随机序列"的格式。这样做的好处是订单号对外展示更专业,同时避免暴露数据库里的真实记录数。
5.3 接口文档的维护方式:用工具把调试和数据 Mock 串起来
我个人在实际项目中强烈推荐使用 Apifox 或者 Postman 来管理接口文档。这类工具支持从 Swagger 导入接口定义,然后自动生成调试环境;你可以在工具里为每个接口造好测试数据,保存成一条条用例,之后任何一次代码改动都能快速回归测一遍接口是否正常。对毕设场景来说最大的好处是,它可以把文档、调试、测试集中在一个界面,你答辩时直接在工具里调用接口返回数据,比你切到前端页面点点点更有工程师气质。
更妙的是,好的 API 工具还支持根据接口定义自动生成前端请求代码和后端 Controller 代码骨架,这对于快速理解源码里的接口调用关系非常有帮助。我记得当时第一次用这类工具跑通"接口导出到本地 + 生成前后端代码"的时候,对"接口文档是前后端契约"这句话的理解一下子从抽象变成具象了。如果你拿到手的一套源码是接口文档和代码对不上的,那也别慌,按工具里的接口调用记录去反向梳理,往往比直接读源码走在理解的前面。
5.4 错误码约定:别让前端猜你的异常
错误码是接口文档里最容易被忽略的部分。看到很多项目自定义返回结果里 code 只有 200 和 500,这其实是偷懒的做法。实际项目里应该针对常见业务场景设计一套有语义的错误码,比如:1001表示用户名或密码错误、1002表示 token 过期或无效、2001表示景点不存在或已下架、2002表示余票不足、3001表示订单状态不允许当前操作。有了这些错误码,前端不只能弹一个提示框,还能针对不同错误码做不同逻辑处理,比如遇到1002就跳转登录页,遇到2002就置灰提交按钮并提示用户降低预订数量。
错误码表应该统一写进接口文档的开头或附录里,前后端都遵守同一份约定。你在答辩前顺手把几个高频错误码记在脑子里,评委问起"你的系统怎么处理并发扣减余票失败"这个问题时,你可以直接说"我会抛一个库存不足的业务异常,错误码是 2002,前端收到之后会做对应提示",这种回答非常有说服力。
6. 部署、联调与答辩前自查:把项目从"能在我电脑上跑"变成"在任何环境都能跑"
6.1 从开发环境到生产环境的构建部署流程
SpringBoot 后端在 IDEA 里启动只是第一步,真正的项目交付要能打包部署。后端的打包命令用 Maven 的mvn clean package -DskipTests,打出来的是一个可执行的 jar 包,放到服务器上执行java -jar xxx.jar就能启动。这里要注意,application.yml里数据库连接地址不要写成localhost,应该换成数据库服务器的实际 IP,密码也不要写明文,可以用环境变量占位符,比如${DB_PASSWORD},启动时通过--DB_PASSWORD=xxx传入。这样你的项目配置就不会在传阅过程中泄露真实数据库密码。
前端项目打包命令是npm run build,产物是一个dist目录,里面是纯静态文件。部署时的标准做法是用 Nginx 托管这个目录,然后反向代理/api前缀的请求到后端的 SpringBoot 服务。关键的 Nginx 配置示例:
server { listen 80; server_name your_domain_or_ip; root /var/www/dist; index index.html; location / { 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; } }这段配置解决了两个核心问题:前端路由在 history 模式下的刷新 404,以及后端接口的跨域代理。如果你不想在最外层用域名,直接配置成server_name _也是可以的,取 IP 访问即可。部署完成后测试时记住:只测前端页面能打开还不够,一定要实测一遍完整的用户链路,从注册到下单再到管理端处理订单,中途任何一个接口 500 你都有时间提前修。
6.2 跨域、端口、数据库连接:部署期三大高频事故
这三个问题几乎出现在每一套项目的部署阶段,提前总结一下能帮你快速定位。跨域问题的表现是浏览器控制台报CORS policy,解决办法有两个维度:后端加跨域配置类,或者 Nginx 用反向代理让前端页面和 API 请求同源。后端加跨域配置时要特别注意allowedOriginPatterns和allowedOrigins的区别,前者允许携带通配符,适合有多个前端域名的情况;后者严格要求精确匹配。SpringBoot 不同版本对这两种方法的默认行为还不太一样,如果你用的是 SpringBoot 2.7+,建议用allowedOriginPatterns("*"),否则可能遇到预检请求被拦截的诡异问题。
端口占用是最没有技术含量但最高频的报错,Port 8080 was already in use这句错误几乎每个 Java 开发者都见过。解决办法是netstat -ano | findstr 8080(Windows)或者lsof -i :8080(Linux/Mac)查端口被哪个进程占用,然后杀掉对应进程或换一个端口。数据库连接失败就按这个顺序排查:数据库服务是否启动、连接地址是否可达、用户名密码是否正确、数据库是否允许远程访问。时区问题在连接 MySQL 时也很典型,经常报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,解决办法是在 JDBC 连接串里加上serverTimezone=Asia/Shanghai。
6.3 答辩前必须自己问自己的十道题
有了能跑的代码只是起点,能撑住答辩提问才是最终目标。基于这套旅游网站项目,我列一下你拿源码后应该反复演练的问题提纲:
- 你的项目为什么选择前后端分离架构?有什么好处?
- 你的数据库表设计是怎么考虑的?核心表有哪些关系?
- 用户登录是怎么做的?token 过期了前端怎么处理?
- 下单时怎么避免超卖?乐观锁的实现细节是什么?
- 图片文件是怎么存储和访问的?为什么不用 BLOB?
- 后端接口统一返回格式是什么?异常怎么处理?
- 前端路由守卫解决了什么问题?权限控制在前端还是后端?
- 部署时跨域怎么解决?Nginx 代理的原则是什么?
- 项目里你觉得最出彩的功能是哪个?为什么?
- 如果同时有 1 万人访问你的系统,哪些地方会撑不住?怎么优化?
最后一题特别值得好好准备。你不用真的做一套高并发架构,但至少能说出来"热点数据可以加 Redis 缓存,搜索可以上 Elasticsearch,图片可以放 CDN,数据库可以做读写分离"这些方向性的解决方案。评委要听的是你有没有工程扩展意识,而不是要求一个毕设真的扛住 1 万并发。
6.4 源码二次开发时的心态建议:不是"看懂"而是"能改"
最后说我个人的真实体会。很多人拿到一套完整的 SpringBoot+Vue 项目源码,第一反应是跑起来看看,跑通了就感觉自己已经会了。但你在答辩前如果只做到这一步,评委随便改一个需求你就懵。比如评委说"如果门票价格要支持浮动,比如节假日涨价 20%,你觉得要改哪些地方",这个问题的本质是考你对数据模型、价格字段和订单价格快照的理解。如果你完整读过了订单表和景点表,你会想到在订单表里冗余一个下单时的价格快照字段,这样即使以后景点价格调整,历史订单的价格也不会变;同时景点表需要一个price与一个可选的节假日价格配置,前端提交订单时展示的是实时价格,后端算总价时用当前价格,一旦下单就冻结快照。
我把这个思路写在这里,也是想说明同一个点:源码是很好的学习材料,但学习的终点不是把代码跑起来,而是把每个设计决策背后的"为什么"都挖出来,转换成自己能表达的工程判断。当你真正做到这一步时,答辩更像是在分享自己实际做过的一个项目,而不是在背一份别人写的代码。
我坚持认为一份附带 SQL 脚本和接口文档的完整源码,价值远远超过代码本身,它是一套已经被验证过的工程决策集。你在学习它的时候,多问几个"为什么这里要这么设计",多尝试着改一改其中一个功能,这套源码带给你的成长会远超"把它跑通"这一个动作。