☰
SpringBoot+Vue3旅游管理系统源码解析:架构、数据库与避坑指南
2026/9/28 7:35:56 网站建设 项目流程

很多同学拿到一套旅游管理系统源码,SpringBoot + Vue3 + MyBatis + MySQL这套前后端分离的组合,第一反应是“东西挺全,但不知道从哪下手”。尤其做毕设或者公司内部要快速搭业务后台的时候,光是把数据库脚本跑通、前端依赖装完、接口调通,就能卡掉一批人。这篇文章我不打算贴完整代码,而是把这类系统的骨架拆开讲——业务模块怎么划分、表怎么设计、后端接口怎么组织、前端怎么对接,以及最容易出问题的那几个地方。不管你是想拿源码二次开发,还是准备手写一个旅游管理系统,照着这个思路走,都能少踩一半的坑。

1. 旅游管理系统到底要管什么:业务模块梳理与需求边界

很多人在动手写代码之前根本不画业务图,上来就建表,结果做到一半发现“景点评价不知道挂在哪张表上”“订单跟线路的关系理不清”,返工成本极高。我先用业务视角把一套完整的旅游管理系统拆成三块:用户端、管理端、订单流转中心。

1.1 用户端功能清单

用户端通常是一个独立的前端项目,对应游客和注册会员。核心功能可以归成四类:

  • 内容浏览:景点列表、景点详情、旅游线路/套餐展示、酒店民宿展示。图片、简介、价格、库存(成团人数/余位)是标配。
  • 预订操作:选择线路/酒店/门票 → 确定日期和人数 → 提交订单 → 模拟支付 → 生成订单记录。支付环节一般对接不到真实微信支付/支付宝,多数源码采用“模拟支付”或“待支付状态”。
  • 个人中心:查看订单列表、订单详情、取消订单、评价已完成的订单、收藏喜欢的景点。
  • 辅助功能:搜索、筛选(价格区间、目的地、出行天数)、分页、验证码登录/注册。

这里要注意一个容易做歪的点:用户端的核心不是界面多花哨,而是“下单链路是否顺畅”。我见过不少系统把景点介绍页做得非常复杂,但提交订单时逻辑漏洞百出——库存没减、订单号重复、取消订单不回滚库存。做管理系统的精力分配,永远要把交易链路放在第一位。

1.2 管理端功能清单

管理端是典型的后台管理系统,角色一般是管理员或运营人员。功能上跟用户端一一对应,但操作维度不同:

  • 景点管理:景点信息的增删改查、上下架、图片上传、详情富文本维护。
  • 线路/套餐管理:配置一个线路包含哪些景点、价格、出发日期、成团人数、库存。
  • 酒店/民宿管理:房型、价格、库存、基础信息维护。
  • 订单管理:查看全量订单,按状态筛选,对“待支付”“已支付”“已取消”“已完成”做人工干预(比如手动标记退款)。
  • 用户管理:用户列表、禁用/启用账号。
  • 轮播图/公告管理:做首页运营位。

管理端的技术难度不高,但有一个常见问题:权限控制往往只做了“登录拦截”,没有做“菜单级权限”。如果是商业项目,至少要区分“超级管理员”和“普通运营”,不然一个运营把自己手滑删了全量景点数据,你后悔都来不及。

1.3 需求边界的取舍:做毕设和商业项目的区别

如果是毕业设计,评审老师更看重“模块完整、技术栈合理、代码能跑通、论文有东西写”,所以不需要去碰高并发、分布式、消息队列这些重型组件。SpringBoot 提供接口,Vue3 提供页面,MyBatis 操作 MySQL,加上 JWT 做登录,这个组合已经足够。

如果是商业项目,那要多想三层:

  • 库存一致性:用户提交订单后,线路库存要锁住还是只做超卖检查?
  • 支付回调:模拟支付将来要替换成真实支付,订单状态字段和回调接口要预留。
  • 文件存储:图片是存本地磁盘、云存储还是 MinIO?

所以我的建议是:第一次做,先把普通 CRUD 和订单流程做完整,再去考虑 Redis 缓存、ElasticSearch 搜索这些加分项。顺序反了,往往连基础模块都收不了尾。

2. 前后端分离架构下的技术选型:为什么是这套组合

“为什么选 SpringBoot + Vue3 + MyBatis + MySQL”如果只回答“因为流行”,那面试和写论文都过不了关。技术选型背后是有明确理由的,搞清楚这些理由,你不仅能回答“为什么”,还能说明“什么时候不该用这套”。

2.1 后端:SpringBoot 与 MyBatis 的职责划分

SpringBoot 的核心价值是“减少配置、快速启动、生态成熟”。它内置了 Tomcat,提供了 starter 机制,依赖引入、自动装配、健康检查都做好了。Java 生态里做 Web 项目,除非你团队对性能有极端要求,或者想尝试 Quarkus、Vert.x 这类新框架,否则 SpringBoot 永远是第一选择。

MyBatis 则解决的是“数据库访问层”的问题。它的定位比 JPA 更轻:

  • SQL 由自己控制,复杂多表查询好排查、好优化;
  • 支持动态 SQL,景点列表的“目的地 + 价格区间 + 天数”联合筛选很容易写;
  • 缓存可控,一级缓存默认开启,二级缓存可以按命名空间配置。

如果换成 JPA/Hibernate,业务简单时开发速度确实快,但旅游系统里的筛选条件和统计报表会让人头疼——自动生成的 SQL 可能跟你想的完全不一样。用 MyBatis 就是选择“把 SQL 控制权握在自己手里”。

2.2 前端:Vue3 + Vite + Pinia + Element Plus 的工程化组合

Vue3 已经不是新东西了,但很多源码项目还在用 Options API 写,这并不影响跑通。真正影响开发体验的是工程化工具,现在主流组合是:

工具作用选择理由
Vite构建工具冷启动快,开发时热更新体验远好于 Webpack
Pinia状态管理替代 Vuex,API 更简洁,天然支持组合式 API
Vue Router路由前后端分离必须,控制页面跳转和路由守卫
Element PlusUI 组件库后台管理界面开发效率最高,表格、表单、弹窗都对口

有人会问:一定要用 Element Plus 吗?不一定,但旅游管理系统的后台端有大量“数据表格 + 表单弹窗 + 分页”场景,Element Plus 的表格组件、分页组件、表单校验组件能省一半工作量。用户端可以另选更灵活的样式框架,或者直接手写样式。

2.3 数据库选型与连接池配置

MySQL 在这个项目里是“数据底座”。之所以选它,一是成熟稳定、资料多,二是跟 SpringBoot、MyBatis 的适配资料最好找。要注意的是版本匹配:MySQL 5.7 和 8.0 的驱动依赖不一样,MySQL 8.0 还需要显式配置serverTimezone,不然时间字段会查错。

连接池我建议用 HikariCP,SpringBoot 2.x+ 默认就是它。配置时关注两个参数:

  • maximum-pool-size:建议 10 到 20,毕设项目 10 就够;
  • minimum-idle:建议保持和最大连接数一致,避免频繁创建连接。

还有一个容易被忽略的配置:建议在 JDBC 连接串上追加useUnicode=true&characterEncoding=utf8,不然中文存进去容易乱码。MySQL 8.0 之后要加allowPublicKeyRetrieval=true,否则本地连数据库时会报 “Public Key Retrieval is not allowed”。

3. 数据库设计:景点、线路、订单、支付状态的核心表关系

数据库是整套源码的根。我见过太多“代码写得还行但表设计一塌糊涂”的项目,后面改了十几次接口。旅游管理系统的表可以拆成几个核心域:用户域、内容域(景点/线路/酒店)、交易域(订单/支付/评价)。

3.1 核心表结构拆解

先说用户表,这是最没有悬念的:

CREATE TABLE `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL COMMENT '用户名', `password` VARCHAR(100) NOT NULL COMMENT 'BCrypt加密后的密码', `nickname` VARCHAR(50) DEFAULT NULL, `phone` VARCHAR(20) DEFAULT NULL, `avatar` VARCHAR(255) DEFAULT NULL, `status` TINYINT DEFAULT 1 COMMENT '1正常 0禁用', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

景点表字段一般是:id、名称、所属城市/目的地、景点简介、详细描述、封面图、图片列表、门票价格、开放时间、评分、浏览量、状态。这里的city字段建议单独拆成城市表或用固定枚举,否则后面做“按目的地筛选”会很别扭。

线路表是整个系统的“交易核心”。一张线路通常包含:线路名称、出发城市、目的地、行程天数、价格、儿童价、成团人数、当前报名人数、出发日期、包含景点(多对多关系)、封面图、详情介绍。

关键点在于**“线路”和“景点”是多对多关系**,需要一张中间关联表,而不是把景点 ID 塞在某个字段里:

CREATE TABLE `route_scenic` ( `route_id` BIGINT NOT NULL, `scenic_id` BIGINT NOT NULL, `day_no` INT DEFAULT 1 COMMENT '行程第几天', `sort_no` INT DEFAULT 0 COMMENT '当天游览顺序', PRIMARY KEY (`route_id`, `scenic_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

有了day_no和sort_no,前端展示“行程安排”时按天分组排序就非常方便,不用再单独维护一套行程表。

3.2 订单表为什么必须设计成状态机

订单表是交易系统里最重要的表之一,字段大致包括:订单号、用户 id、线路 id、出发日期、预订人数、订单金额、联系人姓名、联系电话、订单状态、支付时间、创建时间。

订单状态我用的是这一组:

状态值含义说明
0待支付创建订单后未支付
1已支付模拟支付或真实支付成功后
2已取消用户主动取消或超时未支付
3已完成出行日期结束后自动完成或手动完成
4退款中 / 已退款商业项目才需要,毕设可不做

很多新手写订单状态喜欢用字符串“pending”“paid”,看着语义清晰,但数据库排序、统计、索引都不方便。用整型枚举 + 注释才是正确做法。

状态机设计的核心是什么?限制非法的状态转移。比如“待支付”可以直接到“已取消”,“已支付”不能直接跳回“待支付”,只能跳到“已退款”。这块如果用 if-else 写容易漏,在代码里做一个OrderStatusEnum维护“可转移状态表”,比在每个方法里写判断更清晰。

3.3 关联查询与冗余字段的取舍

旅游系统的列表页特别多:景点列表、线路列表、订单列表。每个列表都要带出关联信息,比如订单要显示线路名称、用户昵称;线路要显示包含的景点数量。

如果每处都做多表联查,SQL 会越写越长,而且性能越查越差。我的习惯是分两类处理:

  • 列表页用“单表查询 + 关键字段冗余”:订单表直接冗余route_name、user_name,查询订单列表时不需要 Join user 表。
  • 详情页用关联查询:用户想看到完整的信息,再查关联表,保证数据的完整性。

有些程序员听到冗余字段就摇头,认为是坏味道。但在管理系统中,适当地冗余高频展示字段,能换来查询性能和解耦。比如订单表冗余线路名称,线路下架后订单依然能正常显示,这反而是优点。要注意的是冗余字段必须在写入时同步赋值,不能从别处查了再临时拼。

4. 后端核心实现:JWT鉴权、MyBatis缓存与SQL调优

后端接口的代码结构基本是固定的:controller接收参数 →service处理业务 →mapper操作数据库。但真正决定项目质量的,是几个“横切面”的设计:登录鉴权、缓存、SQL 打印和安全过滤。

4.1 JWT登录鉴权与全局异常处理

前后端分离后,Session 不再好使,因为浏览器域名和端口可能跟后端不同,传统 Session 还要处理跨域 Cookie 问题。JWT 的方案是:登录成功后返回一个带签名的 Token,前端保存起来,每次请求在 Header 里带Authorization,后端用过滤器校验。

SpringBoot 里实现 JWT,一般就是引入jjwt依赖,登录接口生成 Token,然后写一个拦截器或过滤器:

@Component public class JwtFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { try { Claims claims = JwtUtil.parseToken(token.replace("Bearer ", "")); request.setAttribute("userId", claims.get("userId")); } catch (Exception e) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"msg\":\"token无效\"}"); return; } } chain.doFilter(request, response); } }

这里有个细节很容易踩坑:前端会把 Token 放进 axios 请求拦截器,但静态资源请求和跨域预检请求(OPTIONS)不应该被拦截。放行规则要写好,否则浏览器 CORS 预检全部失败,页面根本打不开。

全局异常处理也是必写的。SpringBoot 用@RestControllerAdvice统一捕获业务异常和系统异常,输出统一格式{code, msg, data}。没有这一层,前端拿到一个莫名其妙的 HTML 错误页或者堆栈信息,根本没法做提示。

4.2 MyBatis 一级缓存和二级缓存到底要不要开

MyBatis 的缓存是很多人面试时背过、开发时装死的点。先说结论:

  • 一级缓存:默认开启,作用在同一个 SqlSession 内。
  • 二级缓存:默认关闭,需要配置cache标签开启,作用在同一个 Mapper 的命名空间。

“同一个 SqlSession”在 Spring 环境里是什么概念?如果你的 Service 方法没有开启事务,每次 Mapper 调用都可能创建新的 SqlSession,那一级缓存基本帮不上忙。如果开启了@Transactional,方法内的多条相同 SQL 才能命中一级缓存。

二级缓存的问题在旅游管理系统这种多表动态查询场景里更明显:只要某张关联表发生增删改,涉及它的多条动态查询缓存就全要失效,一旦 flush 粒度没控制好,就会出现脏数据。我的建议是:这个项目一级缓存保持默认就行,二级缓存不要开。真要提升性能,优先考虑对热点数据(景点详情)加 Redis 缓存,而不是用 MyBatis 二级缓存。

4.3 MyBatis SQL 日志打印与慢查询排查

调试联调阶段,最需要的就是“看得到 SQL”。在application.yml里这样配置:

mybatis: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

这样每执行一条 Mapper 方法,控制台都会打印完整 SQL 和参数。map-underscore-to-camel-case: true也很关键,它能把数据库的create_time自动映射成 Java 属性createTime,少写一大片 resultMap。

SQL 打印出来以后,重点看两个问题:

  • 慢 SQL:打开 MySQL 的慢查询日志开关,long_query_time=2,超过 2 秒的 SQL 全部记下来。
  • N+1 查询:打印日志里如果某接口连续出现几十条同结构 SQL,那大概率是循环查表了。典型场景是“遍历线路列表,每查一条线路再查一次景点”,这时候应该用一次查询把关联数据查出来。

4.4 全局 XSS 过滤器的必要性

旅游管理系统的后台肯定有“景点详情”“公告”这类富文本输入框,富文本天然是 XSS 攻击的温床。很多源码项目不设防,被人在内容里塞一段<script>就能偷 Cookie、改页面。

SpringBoot 项目常用的方案是写一个全局过滤器,对请求参数做清理。但要注意:不能把所有标签都滤掉,那样富文本就废了。实践中我会分两条路:

  • 普通字段:严格过滤<script>、onerror这类危险内容;
  • 富文本字段:采用白名单策略,只保留p、img、strong、em等安全标签。

如果你拿到的源码没有这个过滤器,二开时建议加一个,成本不高但安全收益很大。

5. Vue3 前端如何把页面和数据串起来

后端接口是“数据源”,前端就是把数据变成页面。很多新手卡在“接口通了但页面全是空白”“登录成功后刷新又跳回登录页”这些问题上,根源是对路由、状态管理、请求封装的理解不够。

5.1 路由设计:静态路由还是动态路由

后台管理端的页面一般分两类:公共页面(登录页、首页)和需要鉴权的页面(订单管理、景点管理、用户管理)。

最简单可靠的做法是静态路由写死 + 全局前置守卫判断登录状态:

const router = createRouter({ history: createWebHistory(), routes: [ { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/', component: () => import('@/layout/MainLayout.vue'), children: [ { path: 'scenic', component: () => import('@/views/scenic/ScenicList.vue'), meta: { requiresAuth: true } }, { path: 'order', component: () => import('@/views/order/OrderList.vue'), meta: { requiresAuth: true } }, ] } ] }) router.beforeEach((to) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { return '/login' } return true })

动态路由听起来更高级,比如按后端返回的权限动态添加菜单,但对旅游管理系统来说往往过度设计。静态路由 + 守卫拦截 + 按钮级权限控制,已经覆盖 90% 的需求,还更容易维护。

5.2 Axios 封装与 Token 刷新

前后端分离项目里,所有请求都应该走统一的 axios 实例,而不是每个页面import axios直接请求。核心封装包括三部分:

  • 请求拦截器:从 localStorage 取出 Token 塞进Authorization头;
  • 响应拦截器:判断code,非 200 统一报错;遇到 401 时清除 Token 并跳转登录页;
  • API 模块化:把同类型的接口放到一个文件里,比如api/order.ts里放订单相关的所有请求。
const service = axios.create({ baseURL: '/api', timeout: 15000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code === 200) return res.data ElMessage.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) }, error => { if (error.response?.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } )

Token 过期处理要注意:旅游管理系统一般不需要做“无感刷新”,简单粗暴地让用户重新登录就行。如果非要体验好,可以配一个refreshToken,但会显著增加前端状态管理复杂度,非必要不上。

5.3 Pinia 状态管理:用户信息与全局数据

Vuex 在 Vue3 里还活着,但新项目我更推荐 Pinia。旅游管理系统需要放进全局状态的数据不多,我一般只放两类:

  • 用户信息:登录后把用户昵称、头像、角色存进 store,刷新后重新从接口拉取;
  • 全局配置:比如系统名称、字典数据、城市列表,只在首次加载时请求一次。

要注意,Pinia 不等于浏览器本地存储。刷新页面后 store 里的数据会丢,所以关键信息要么持久化到 localStorage,要么在页面初始化的生命周期里重新请求接口。很多“刷新后白屏”的问题,都是因为 store 数据没了,而页面代码还在直接访问 store 属性。

5.4 联调时最常出现的跨域问题

我用 Vue3 + SpringBoot 联调时,跨域是最烦人但最好解决的问题。两种方案:

  • 后端开启 CORS 配置:写一个WebMvcConfigurer,配置允许跨域的路径、来源、方法;
  • 前端 Vite 配置代理:开发环境设置server.proxy,把/api代理到http://localhost:8080。

我推荐第二种,因为生产环境部署时 Nginx 本来就要做反向代理,开发环境提前用代理,习惯能保持一致。注意的是,代理只能解决“开发环境”的跨域,生产环境如果前后端不在同一域,依然需要后端配置 CORS,或者在 Nginx 里统一转发。

6. 源码部署与二次开发避坑指南

最后讲落地。源码项目毕竟不是自己一行行写的,环境差异造成的幺蛾子特别多。我总结一套“从零跑通”的顺序,按这个顺序执行,成功率最高。

6.1 本地环境搭建:JDK、Maven、MySQL版本匹配

拿到源码后别急着启动,先检查三样东西:

  • JDK:SpringBoot 2.x 用 JDK 8 或 11;SpringBoot 3.x 必须 JDK 17 以上。如果源码没写,看pom.xml里的<java.version>。
  • Maven:本地建议 3.6+,并配置阿里云镜像仓库,否则下载依赖能急死人。
  • MySQL:建议 8.0。如果数据库脚本是 5.7 写的,8.0 一般能兼容;反过来 5.7 跑 8.0 的脚本可能报错。

数据库导入也要注意字符集:建库语句必须带utf8mb4,只写utf8的话存 emoji 或特殊符号会报 “Incorrect string value”。

6.2 前端依赖安装与 Node 版本坑

Vue3 项目常见问题集中在这几条:

  • Node 版本太低,Vite 启动报错。Vite 5 需要 Node 18+,最好用 18 或 20。
  • 装依赖时用npm install报权限或引擎错误,可以试试用pnpm install,或者检查.npmrc里的 registry 是否被改过。
  • 启动命令通常是npm run dev,但有些源码默认端口不是 5173,要多留意控制台输出的实际地址。

如果源码里带了package-lock.json,用npm ci安装比npm install更稳妥,可以锁定版本,避免依赖升级带来的不兼容。

6.3 前端和后端对接的核心配置文件

前后端分离项目必然有一份“连接配置”,常见位置:

  • 前端:.env.development或src/utils/request.ts里的baseURL;
  • 后端:application.yml里的端口、数据库账号密码。

最容易忽略的是前端请求路径里带了/api前缀,后端接口没有/api前缀,或者反过来,两边对不上,请求直接 404。排查思路很简单:打开浏览器开发者工具的 Network,看请求 URL 是什么,再对照后端 Controller 的@RequestMapping,哪里对不上改哪里。

6.4 常见报错与解决方案

列几个我经常遇到的报错,几乎每个跑源码的人都会碰上一次:

报错信息原因解决办法
Access denied for user 'root'@'localhost'数据库密码不对或权限不足检查 application.yml 里的用户名密码
Table doesn't exist数据库脚本没执行,或连错了库检查库名是否跟配置文件一致
Property ‘sqlSessionFactory’ or ‘SqlSessionTemplate’ not foundMyBatis 和 SpringBoot 集成依赖缺失检查是否有mybatis-spring-boot-starter
Failed to bind properties under 'spring.datasource'配置项写法错误检查缩进和字段名,YAML 别用 Tab
Uncaught TypeError: Cannot read properties of null前端拿到空数据后直接访问嵌套属性用可选链?.或在模板中判空

6.5 基于源码二开的架构约束

最后聊一下二开。很多人拿到源码后第一件事想“重构”,我建议不要一上来就大动。先跑通,再小改,最后再考虑优化。二开时有两个原则要守住:

一是保持分层结构。不要在 Controller 里直接写 SQL 或业务逻辑,沿用Controller → Service → Mapper的分层。很多源码写得乱,你可以在原基础上新增模块,但别破坏原有分层,否则后续合并别人的代码会痛不欲生。

二是改动前先建表结构的备份。旅游管理系统这类项目,数据库脚本是命脉。改表字段前一定要先mysqldump备份,哪怕只是加一个字段。我见过太多人改完表结构发现数据丢了,回滚脚本又没写,最后只能对着屏幕发呆。基本的mysqldump -u root -p dbname > backup.sql用起来就一分钟,别偷懒。

另外提一句,市面上流传的源码质量参差不齐,有些号称“SpringBoot + Vue3 旅游管理系统”的压缩包,里面可能就是几个 demo 级别页面配上并不完整的接口文档。如果发现某个模块怎么也跑不通,不要死磕,先看它的 SQL 脚本是否完整、Maven 依赖是否有缺、前端路由对应的页面文件是否存在。你花半小时排查环境问题,比花两小时怀疑人生值。

这套系统做完之后,想进阶的同学可以考虑把 Redis 加进来,缓存热门景点和线路列表;或者把图片存储从本地目录迁移到 MinIO,再配一个简单的 CDN 访问路径。每一次改动都不会白费,因为旅游管理系统覆盖了一个业务系统的完整链路,后面无论你做商城、做预约平台还是做后台管理系统,很多设计和坑都是相通的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询