如果你还在用 JSP 拼旅游网站页面,或者前端还在用 jQuery 手动操作 DOM,我建议你认真看一眼 SpringBoot + Vue3 + MyBatis 这套组合。这些年我接手过不少旅游管理系统源码,问题通常不在功能不够,而在技术栈太老、前后端耦合严重、数据库设计没经过业务推敲。一套前后端分离、基于 MySQL 存储、由 SpringBoot 提供接口、Vue3 负责页面渲染的旅游管理系统,恰恰是目前最适合用来讲清楚业务闭环、也最容易二次扩展的项目模板。
这篇文章不写空洞的介绍,直接拆解这套源码里真正值得关注的设计和实现。包括数据库表到底怎么建、后端接口怎么定义、前端如何对接、部署时会踩哪些坑,以及你拿到源码后怎么改才能把它变成自己真正能讲清楚的项目。无论你是准备课程设计、毕业设计,还是在简历里放一个能展示完整能力的案例,这篇文章都适用。
1. 项目整体设计与技术选型:为什么是这套组合
1.1 前后端分离到底解决了什么问题
很多老项目是服务端渲染,Java 代码里嵌套 HTML,后端开发改一行页面样式,可能整条业务链路都要重新构建。前后端分离之后,后端只输出 JSON 数据,前端独立负责页面交互和渲染,两边只需要约定好接口格式就能并行开发。
旅游管理系统非常适合这种模式。用户端要展示线路、景点、酒店,管理端要管理订单、评论、轮播图、公告,这两类前端页面共用同一套后端接口。如果不分离,用户前台和管理后台会互相纠缠,后期想调整其中一端的布局、功能,很容易把另一端也带崩。
分离之后,两套前端可以分别维护、分别部署,甚至可以把用户端逐步替换成移动端适配版本,后端完全不用动。这也是为什么我在评估一个项目源码时,第一件事就看它是不是前后端分离:如果是传统单体 JSP 项目,扩展和维护成本会明显更高。
1.2 SpringBoot、Vue3 与 MyBatis 的边界
这三个技术各管一段,各司其职。
SpringBoot 负责接口、业务逻辑和依赖管理。它内置 Tomcat,可以让项目打包成可执行的 jar,本地启动、服务器部署都很方便。自动配置机制也让项目不用再堆一堆繁琐的 XML 配置,这对学习型项目尤其友好,可以把精力花在业务实现上。
Vue3 负责前端体验。Composition API 让组件代码可以按功能逻辑组织,而不是被动地按选项类型堆一起。相比 Vue2 的 Options API,Vue3 在代码复用和逻辑抽取上明显更顺手。配合路由和状态管理,用户从首页搜索线路,到进入详情页下单,再到查看订单列表,整个页面流转非常自然。
MyBatis 负责数据库访问。旅游管理系统里有大量动态查询场景,例如按出发地、目的地、价格区间、出行天数来筛选线路。这类条件不固定的查询,用 MyBatis 的<if>动态 SQL 拼条件非常方便。相比 JPA 的自动派生查询,MyBatis 能让你精确控制 SQL,遇到联表和多条件组合时不会出现意料之外的查询。相比纯 JDBC,MyBatis 又免去了手动封装结果集的重复工作。选型逻辑很简单:要 SQL 可控性,MyBatis 比 JPA 合适;要开发效率,MyBatis 比 JDBC 合适。
1.3 源码目录结构的正确打开方式
拿到源码第一件事不是急着运行,而是先看顶层目录。前后端分离项目一般长这样:
backend或server:SpringBoot 后端工程frontend或web:Vue3 前端工程database或sql:建表语句和初始化数据README或 部署文档:环境版本和启动说明
先按这个路径读一遍,你就能快速定位“用户提交订单时后端到底做了什么”。前端看src/router能找到路由配置,看src/api能找到所有请求接口,看src/views能找到页面;后端看controller能知道接口入口,看service能知道业务逻辑,看mapper和entity能知道数据访问方式。
很多源码跑不起来,不是代码有问题,而是没有按目录约定找到对应配置文件,或者把前后端路径搞混。我建议拿到源码后,先打开数据库脚本文件跑一遍,再打开后端的application.yml修改连接信息,最后再去启动前端。这个顺序能省掉一半的报错排查时间。
2. 数据库设计:旅游业务如何落到 MySQL 表结构
2.1 核心业务表与字段设计
旅游管理系统的核心表大概包括:用户表、线路表、订单表、评论表、公告表、收藏表。如果你拿到手的源码连这些基础表都没有,那这个“旅游管理系统”大概率只是个小 demo。
用户表要区分身份,普通用户和管理员最好通过一个role字段控制,不要在系统里硬拆两张用户表,否则登录逻辑会很别扭。线路表除了名称、价格、天数之外,还要有封面图、出发地、目的地、线路介绍、上下架状态。注意价格字段不要用int,更不要用float,应该用BigDecimal,否则打折、儿童价、门市价这些业务出现时,浮点误差会让你很头疼。
订单表要包含线路 ID、出发日期、出行人数、联系人姓名、手机号、订单状态、下单时间,还要有一个业务订单号。很多人写订单表时只存用户 ID 和线路 ID,等用户回来看历史订单,线路名称早就被后台改掉了,那才是真正的数据灾难。
评论表要关联用户和线路,同时要有评论内容、评分、评论时间。公告表比较简单,标题、内容、发布时间就够用。收藏表其实是用户和线路的多对多关系,用中间表实现,一张表里存用户 ID 和线路 ID 即可。
2.2 表关系与冗余字段经验
用户和订单是一对多,订单和线路是多对一,收藏是用户和线路的多对多。这些关系在设计表时就应该想清楚,外键可以建,也可以不建物理外键,但逻辑关系必须明确。
关于冗余字段,我的建议是克制但要有策略。订单表里冗余线路名称和封面图,这是非常合理的快照设计,因为线路后续可能改名、下架,但用户已经下的单应该保留当时看到的信息。订单表里不要冗余线路价格,因为订单金额应该以下单时刻的线路价格为准,你需要把这个价格单独存在订单表里,而不是查询时动态去线路表取。如果你在订单列表接口里每次都 join 线路表去拿名称和价格,数据量稍大后响应速度会肉眼可见地变慢,而且一旦线路被删除,订单历史就只剩下一堆 ID 了。
类似地,线路表里可以冗余一个评论数或销量字段,用来在列表页排序展示,避免每次都要 group by 统计评论数。这种冗余属于高频查询场景下的合理优化。但冗余字段要保证更新逻辑一致,比如新增评论后要同步更新线路表的评论数。
2.3 初始化数据与表设计坑
源码通常会在database目录下放一个建表 SQL 和初始数据 SQL。导入 MySQL 时要注意字符集,统一使用utf8mb4,否则中文数据容易乱码。建表语句里的字段注释能帮助理解设计意图,所以拿到 SQL 文件后别急着直接执行,先扫一眼每个表的注释。
表设计里常见坑有三个。
第一个是物理外键过度使用。学习项目里建外键没什么问题,但在实际部署中,物理外键会让删除、更新操作受到很多约束,数据量大时甚至可能带来锁表风险。我通常建议用逻辑外键,也就是代码层面维护关联关系,数据库里只建普通索引。
第二个是日期字段类型。日期字段建议使用datetime而不是timestamp,timestamp有 2038 年问题,虽然普通项目十几年内不会遇到,但养成好习惯没有坏处。后端接收入参时,日期最好统一用字符串格式,由前端传yyyy-MM-dd,再到数据库转成 datetime。
第三个是初始化数据里经常出现重复执行报错。如果先手工建了表和部分数据,再一键导入完整 SQL,会出现主键冲突或表已存在错误。正确操作是先删除旧库再重建,或者执行前确认当前库的状态。
3. 后端 SpringBoot + MyBatis 实现要点
3.1 分层结构与接口设计规范
后端代码一般分成三层:controller负责接收请求和返回数据,service负责业务逻辑,mapper负责数据库操作。很多源码的问题在于 controller 里写了一大堆业务代码,service 层形同虚设,这会让你想复用逻辑时非常痛苦。
接口路径要符合 REST 风格。登录用/api/user/login,注册用/api/user/register,线路列表用/api/line/list,线路详情用/api/line/detail/{id},订单提交用/api/order/add。一个清晰统一的路径前缀,能让前端和外部调用方一眼就明白是哪个模块。
返回值格式也要统一。我最常用的返回结构是:
{ "code": 200, "message": "操作成功", "data": {} }前端所有请求都按这个结构解析,错误弹提示也统一处理。如果后端直接返回裸对象,或者有的接口返回{ code: 200 }、有的接口直接返回数据,前端每个页面都要单独处理异常,联调效率会低很多。
3.2 用户登录鉴权与密码处理
登录是旅游管理系统的核心入口。常见方案是 JWT,用户登录成功后,后端生成一个带过期时间的 token,前端把它存在本地,每次请求放在请求头里,后端通过拦截器校验。
使用拦截器时,要注意排除公开接口。登录、注册、线路列表、线路详情这些接口应该允许匿名访问,其余接口需要 token。很多源码出问题就出在拦截器把登录接口也拦了,前端一调登录就 401,排查半天才发现是拦截器路径没写对。
密码处理是安全底线。密码绝对不能明文存储,至少要做 MD5 加盐。更推荐 BCrypt,它的哈希结果每次都不一样,验证时要用自带的 matches 方法,而不是简单地比较密文字符串。很多初学者第一次接触 BCrypt 时会觉得“为什么每次结果不同,是不是代码有问题”,这就是没有理解 BCrypt 的设计原理。
除了登录态,权限控制也要考虑。普通用户只能操作自己的订单和评论,管理员才能进入后台管理页面。简单的做法是在 token 里塞一个角色字段,后端接口做角色校验,前端根据角色控制菜单显示。如果项目不大,没必要引入完整的权限框架,一个注解或一个拦截器判断足够。
3.3 MyBatis 动态 SQL 与联表查询
旅游管理系统最核心的功能是线路搜索。搜索条件通常有出发地、目的地、价格区间、出行天数、出发日期。如果这些条件在 Java 代码里用字符串拼接,你很快就会陷入 SQL 注入和引号配对的地狱。MyBatis 的<if>标签就是为这种场景准备的。
<select id="searchLines" resultType="com.example.entity.Line"> SELECT * FROM line <where> <if test="departure != null and departure != ''"> AND departure = #{departure} </if> <if test="destination != null and destination != ''"> AND destination = #{destination} </if> <if test="minPrice != null"> AND price >= #{minPrice} </if> <if test="maxPrice != null"> AND price <= #{maxPrice} </if> <if test="days != null"> AND days = #{days} </if> </where> ORDER BY create_time DESC </select>这段 SQL 里,<where>标签会自动处理第一个条件前面的AND,这是 MyBatis 很实用的功能。注意价格区间要用>=和<=转义,直接写>=在 XML 里会报错。
联表查询要分清LEFT JOIN和INNER JOIN。用户查看线路列表时,即使某条线路还没有评论,也应该正常展示,所以线路和评论关联应该用LEFT JOIN。但是统计哪些线路有有效订单时,只关心存在订单的线路,这时候用INNER JOIN更合适。乱用 join 类型会导致数据缺失或者重复,这是排查列表数据异常时要重点怀疑的点。
另外,列表查询一定要分页。如果一次性SELECT *返回全表数据,前端页面会卡顿,后端内存也会被拖垮。用 PageHelper 插件,或者手动在 SQL 末尾加LIMIT #{offset}, #{pageSize}都可以。分页参数不要写死在 SQL 里,要从接口入参读取。
3.4 接口返回格式与全局异常处理
后端接口设计不能只处理正常情况,还要处理异常情况。如果代码里直接throw new RuntimeException,前端拿到的往往是一句笼统的“系统错误”,用户根本不知道是自己操作问题,还是服务端真的崩了。
正确做法是自定义一个业务异常,配合 Spring 的@RestControllerAdvice做全局异常处理。这样业务异常可以返回友好的提示,例如“库存不足”“订单已取消”“登录状态已过期”;系统异常才返回“系统繁忙,请稍后重试”,同时在后端日志里记录完整堆栈。
日志也要分级。业务操作记info,异常堆栈记error,不要把敏感信息打到日志里。很多源码的日志配置是缺失,运行时报错只在控制台闪现,根本看不出问题在哪个环节。加上一个简单日志文件配置,能让你上线后排查问题的效率提升一个档次。
4. 前端 Vue3 对接:从页面到接口的真实细节
4.1 项目结构与请求封装
前端拿到源码后,先看src/api目录。正常情况下,这里会有一个request.js或http.js,封装了 axios。封装的核心意义就三件事:设置基准地址、统一注入 token、统一处理返回状态码。
axios 的请求拦截器负责把 token 加进请求头,响应拦截器负责判断返回码,如果code不是 200,就弹出提示或者跳回登录页。没有做响应拦截的源码,每个页面都写了一遍错误判断,代码重复到让人头疼。
关于接口地址,我建议把baseURL放到环境变量文件里。开发环境指向本地后端地址,例如http://localhost:8080,生产环境指向服务器地址。这样打包上线时,只要改环境变量配置,不用动代码。很多源码把接口地址写死在页面里,线上环境一换,前端所有请求全部失效。
4.2 核心页面与业务交互流程
旅游系统前端页面一般包括首页、线路列表、线路详情、订单填写、订单列表、个人中心、登录注册,以及管理员后台页面。你要真正读懂的是这条完整链路:用户在首页搜索线路,搜索参数通过路由 query 或状态管理传给列表页;列表页调用/api/line/list请求数据;点击线路进入详情页,再通过/api/line/detail/{id}获取详情数据;用户提交订单时,调用/api/order/add,把线路 ID、出发日期、人数、联系人信息传给后端;订单提交成功后在订单列表页可以看到记录。
这条链路走通了,整个系统的骨架就掌握了。Vue3 里大概率会看到setup语法,理解ref和reactive的区别很关键。基本类型用ref,对象类型用reactive,或者干脆统一用ref也能保证响应式。不要在ref包裹的变量上直接解构赋值,否则响应式会丢失,这是个高频坑。
4.3 前后端联调时最容易翻车的几个点
第一是跨域。开发环境下,前端地址是localhost:5173,后端是localhost:8080,端口不同,浏览器会拦截跨域请求。解决方式有两种:后端配置 CORS,或者前端 Vite 配置 proxy。个人推荐用 Vite proxy,因为配置在本地即可生效,逻辑也更直观。
// vite.config.js 示意 export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })第二是日期格式。后端返回的日期字段通常是 ISO 字符串,例如2025-06-01T12:00:00,前端直接显示在页面上很难看。建议在公共工具方法里统一格式化日期,或者让后端在返回 VO 时直接输出yyyy-MM-dd。
第三是字段名大小写。MyBatis 如果开启了驼峰映射,返回对象里的createTime能对应数据库的create_time;如果没有开启,前端拿到的字段名可能是create_time或 null,导致页面渲染不出来。联调时前端一定要先打开浏览器开发者工具看响应原始数据,确认字段名对不对,再怀疑自己的页面代码。
5. 部署上线与常见问题排查
5.1 本地环境准备与数据库导入
本地跑这套项目,先统一环境。Java 版本看后端pom.xml里的配置,不要凭感觉装最新版。Spring Boot 2.x 通常配 JDK 8 或 11,Spring Boot 3.x 才需要 JDK 17 以上。MySQL 建议 5.7 或 8.0,导入 SQL 文件时先建库再导入,记得确认字符集是utf8mb4。
后端连接配置集中在application.yml或application.properties:
spring: datasource: url: jdbc:mysql://localhost:3306/travel_db?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC username: root password: ${DB_PASSWORD:root}很多源码连接失败,都是因为数据库账号密码和本地不一致。把密码放到环境变量里读取,不同机器上不用改代码,只要导出环境变量即可。这个习惯越早养成,后面部署越省心。
5.2 跨域与接口访问问题
开发环境用 Vite proxy 通常能搞定跨域,但部署到服务器后,前端和后端如果是两个独立服务,仍然会有跨域问题。最简单的方案是后端加一个全局 CORS 配置,允许指定来源访问。开发阶段可以把允许来源放宽,生产环境要收紧,避免任意网站都能调用接口。
另一个经典问题是:前端页面能访问到,但请求接口 404。先看后端接口路径和前端请求路径是否一致,再看接口是否有/api前缀。如果后端在上下文路径里加了/api,前端baseURL也要对应修改。
登录接口经常被拦截,确认拦截器配置是否正确,公开接口白名单是否包含登录和注册。很多人辛辛苦苦配好了前端,一调登录就报“未登录”或 401,大概率是拦截器问题。
5.3 打包部署的两种方式和资源路径
前端执行npm run build,生成dist目录,里面是纯静态文件。部署方式有两种:一种是放在 Nginx 的站点目录下,由 Nginx 提供静态资源和反向代理;另一种是把dist目录直接复制到后端项目的src/main/resources/static,重新打包后端 springboot jar,用一个进程同时提供接口和静态资源。
第二种方式适合小项目,部署简单、管理成本低。但要注意,如果后端拦截器拦截了所有请求,静态资源也会被拦截,导致页面打不开,所以拦截器需要放行静态资源路径。
前端如果使用了 history 路由模式,Nginx 需要配置try_files $uri $uri/ /index.html;,否则刷新二级页面时会出现 404。如果你用的是 hash 模式,通常不需要额外配置,但 URL 里会多一个#,看起来不够干净。
5.4 常见错误速查表
| 错误现象 | 可能原因 | 处理建议 |
|---|---|---|
| 前端请求 404 | 接口路径不一致或 baseURL 错误 | 检查后端接口和后端 context-path |
| 登录成功后刷新失效 | token 没存本地或路由守卫逻辑有问题 | 确认 localStorage 存储和路由白名单 |
| 页面中文乱码 | 数据库字符集不是 utf8mb4 | 重建库或修改连接字符集参数 |
| 数据库连接失败 | 密码错误、服务没启动、库名不对 | 用客户端工具测试连接 |
| 提交订单失败 | 后端服务事务未生效 | 检查 service 层是否加事务注解 |
| 图片不显示 | 静态资源映射没配置 | SpringBoot 配置资源映射路径 |
这张表是我梳理这套源码时最常见的问题清单。实际上大部分问题都不是复杂技术导致的,而是环境配置和路径细节。
6. 复盘:这套源码还能怎么改、怎么用
6.1 围绕现有功能的扩展方向
如果拿这套源码做课设或面试项目,我建议做几个低成本高收益的扩展。
第一,给线路表增加“热门推荐”标记,前端首页展示热门线路。后端可以在线路详情接口里用切面记录浏览量,这样项目里就有 AOP 的使用场景,面试时能展开讲。
第二,把订单状态流转做成状态机。已支付、已出票、已完成、已取消,用枚举管理状态,在订单操作接口里做前置校验。这比单纯在字段里存一个数字状态要好得多,也能体现你的设计能力。
第三,给系统加一个简单的后台权限管理,区分普通用户和管理员。后端基于角色做接口校验,前端根据角色控制页面菜单。这个扩展不引入复杂权限框架也能完成,但已经足够说明你对前后端权限控制有理解。
扩展的原则是:不要为了炫技引入微服务、消息队列、大数据组件,而是围绕现有业务把设计做扎实。面试官更看重你能讲清楚一个功能的完整链路,而不是堆了一大堆技术名词。
6.2 长期维护时值得优化的地方
代码层面,Controller 里尽量不要出现业务代码,所有业务处理放到 Service 层并加上事务注解。事务注解不能用错位置,一般加在 public 方法上,且要通过 Spring 代理调用,否则不生效。
数据库层面,订单表的用户 ID、线路 ID,线路表的出发地、目的地,这些高频查询条件要建索引。没有索引时,数据量一上来,列表查询会越来越慢。
前端层面,不要把所有请求都放在一个文件里,按模块拆分成user.js、line.js、order.js,这样后续维护和定位问题会轻松很多。环境变量里的接口地址要保持规范,不同环境用不同配置文件。
日志方面,建议按天滚动输出,避免单个日志文件越来越大。上线前把控制台打印的 SQL 日志级别调低,生产环境不要打印完整 SQL,避免信息泄露和性能损耗。
这套源码我最喜欢的部分,其实是数据库设计里的下单快照意识。很多初学者写订单表只会存用户 ID 和线路 ID,等用户回来看历史订单,发现线路名称早就被后台改掉了,那才是真正的数据灾难。旅游管理系统看起来是个课程设计级别的业务,但把订单快照、动态搜索、权限控制、前后端分离这些点讲清楚之后,它在面试里能发挥的作用,远超过“一个 CRUD 项目”这个标签。
最后送大家一个建议:拿到任何源码,先改数据库连接跑起来,再把登录、搜索、下单这条链路走通,最后再谈优化和扩展。跑不通的代码对你没有任何价值。把一条真实的业务链路完整走一遍,比只看源码十遍都有用。