做旅游类平台的项目源码,最怕遇到那种登录页做得像模像样,一进后台全是空表格的半吊子货。但拿到这套基于 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0 的甘肃旅游服务平台源码时,我第一感觉是技术选型很正,前后端分离,数据层该有的处理都有,还带文档,作为学习参考或者毕业设计底子都挺合适。这篇文章我不会让你干看一遍代码,而是把项目里真正值得留意的点、我接手跑通之后踩过的坑,以及从数据库设计到部署上线的完整思路,一次性拆开讲清楚。
这套系统的场景其实很典型:面向区域旅游服务,包含景点、线路、酒店、攻略、特产展示,前台给游客浏览和下单,后台给管理员维护内容。说白了就是一套带业务闭环的内容管理加交易流程的平台。我预估它适合三类人:一是想做 Java Web 毕设的同学,二是准备从单体项目转向前后端分离的初学者,三是想快速搭一套旅游业务系统做二次开发的工程师。如果你属于其中任何一类,下面这些内容值得看完。
1. 项目定位与整体架构
1.1 一套完整的旅游业务闭环长什么样
旅游平台和普通博客系统最大的区别在于业务对象多、关联关系复杂。甘肃旅游服务平台这套系统里,我数了一下业务模块,至少覆盖了用户端、管理员端、景点模块、线路模块、酒店模块、攻略模块、订单模块、收藏与评论模块这八块。用户端不是单纯展示,而是能真实操作:浏览景点详情、筛选旅游线路、预订酒店、收藏喜欢的内容、评论互动、然后生成订单记录;管理员端则是针对这些内容做增删改查,比如景点上下架、线路价格维护、轮播图管理、攻略审核等。
从真实开发角度看,这套系统的核心价值不是“功能多”,而是把最常用的业务场景串成了一条线。比如用户从首页进入景点详情,看到线路推荐,顺手把酒店加入行程,最后下一笔订单,整个过程的数据会落到相互关联的表中。很多刚做项目的同学把精力放在单个 CRUD 上,忽略了表之间的关联设计,结果做到订单模块就卡住。这套源码在这一点上处理得比较合理,表结构有主外键关系设计,查询路径也能从用户一直跟踪到订单明细。
1.2 技术选型为什么是这套组合
关于选型,SpringBoot2 在目前的生产环境中依然占据很大比例,稳定压倒一切。很多学校课程还在讲 SSM,但真正到公司项目或者毕设答辩场景,SpringBoot 已经是默认选项。搭配 MyBatis-Plus,主要是图它不开 SQL 也能把单表 CRUD 写得飞快,同时保留 XML 写复杂 SQL 的能力,这点特别适合旅游这种多表查询频繁的场景。
前端选 Vue3 而不是 Vue2,核心原因是生态已经迁移得差不多了。Vue3 配合 Vite 的启动速度,开发阶段比 Webpack 那套舒服太多。组合式 API 写业务逻辑也更清晰,比如把“景点列表的加载逻辑”封装成一个函数,页面里直接调用,维护成本比 options API 低。MySQL8.0 就更不用说了,窗口函数、JSON 字段、更好用的索引机制,在统计景点热度、处理富文本数据时优势明显。
这套组合放在今天看,虽然不算最新最炫,但绝对是最不容易出问题的方案。我把这种选型叫做“安全牌”,对于需要快速交付、稳定运行、后续好扩展的项目,比盲目上微服务或者分布式中间件靠谱得多。
1.3 前后端分离后的目录结构规划
源码拿到手,第一步先看目录。工程一般拆成 backend 和 frontend 两块,后端是标准的 Maven 结构,controller、service、mapper、entity、config、utils 一层层分好;前端则是 Vite 生成的 Vue3 工程,下面有 views、components、api、router、store 这些目录。
有个经验值得分享:好项目从目录结构就能看出来。这套源码里,后端的 controller 只负责参数接收和结果返回,业务逻辑全部下沉到 service;mapper 层只写数据库操作,严禁把 SQL 逻辑混进 controller。前端这边,api 目录里每一个文件对着一类接口,比如 scenic.js、hotel.js、order.js,页面只用 import 对应方法,接口地址改动时只需要找 api 文件,不用满页面去翻请求地址。
这种结构的好处不用多说:前后端可以并行开发,后端出接口文档,前端 mock 数据先行;出现问题也能快速定位是接口层还是页面层。如果你拿到源码后准备二次开发,我建议你也保持这个结构,别为了少写几个目录就把代码堆在一起,后面你会感谢当初的克制。
2. 数据库建模:核心表设计与MyBatis-Plus实践
2.1 表结构设计里我看到的合理之处
打开数据库脚本,我第一眼关注的是表设计是否考虑了真实业务场景。这套系统的主表我大致列一下:
| 表名 | 核心用途 | 关键关联 |
|---|---|---|
| user | 用户及管理员账号 | 与订单、评论、收藏关联 |
| scenic_spot | 景区信息 | 含地区、分类、图片、热度 |
| travel_route | 旅游线路 | 关联景区、价格、天数 |
| hotel_info | 酒店信息 | 含房型、价格、位置区域 |
| guide_article | 游玩攻略 | 富文本内容、封面图 |
| order_info | 用户订单 | 关联用户、线路或酒店 |
| comment_info | 评论管理 | 关联用户与景点/攻略 |
| collect_record | 收藏记录 | 用户与内容多态关联 |
比较让我满意的是:景区和线路之间不是简单一个外键字段,而是设计了一套关联关系。有些景点可以出现在多条线路里,一条线路也可能包含多个景点,这种场景如果硬塞一个 scenic_id 到线路表,后续查询会特别痛苦。我看到的设计是采用了中间表或者冗余字段的方式,具体实现要看源码里的 SQL 脚本,但思路是对的。
另一个细节是逻辑删除。几乎所有业务表都带 del_flag 字段,删除操作走 update 而不直接 delete。旅游平台的数据有公示性和历史追溯需求,比如下架的景区只是不展示,订单里关联的信息不能直接没了。这个设计很贴近真实业务逻辑,建议你在二次开发时也坚持。
2.2 MyBatis-Plus 容易踩坑的三个配置项
MyBatis-Plus 确实省事,但不代表没有坑。第一坑是分页插件,很多人写完 selectPage 发现 SQL 没带 limit,数据全出来了,原因就是没注册 PaginationInnerInterceptor。我看了这套源码的 config 包,里面应该有 MybatisPlusConfig 配置类,下面这段是核心,建议保留不动:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }第二坑是自动填充,create_time、update_time 这类字段,如果每张表手动 set,代码会冗余到怀疑人生。MP 提供 MetaObjectHandler,可以在 insert 和 update 时统一填充。这套源码里应该有对应实现,重点看 FieldFill 注解怎么配合的。
第三坑是主键策略。MP 默认的 ASSIGN_ID 会生成雪花 ID,如果数据库主键设置了自增,实体类里的 @TableId 又没配 type,容易出现主键冲突。反过来,雪花 ID 是 Long 类型,传到前端会因为 JavaScript 最大安全整数问题丢失精度。解决办法通常是在后端统一转 String 返回,或者用 Jackson 配置 Long 转 String。这个坑非常隐蔽,前端从列表页跳到详情页时发现 ID 变了,十有八九就是这个原因。
2.3 复杂查询还是要写 XML,别硬刚 Wrapper
MP 的 LambdaQueryWrapper 确实方便,但旅游业务里多表关联、条件动态拼接的查询特别多,比如“筛选某地区 3 天内、价格在 1000 到 2000 之间、包含某景区的线路”,这种需求用 Wrapper 硬拼,代码可读性会迅速下降。在这套源码中,我看到的关键做法是:单表查询用 MP 的 CRUD 方法,多表关联、报表统计、复杂条件查询统一写在自定义的 XML 里。
XML 里写 SQL 要注意几个细节:一是 resultMap 要配好,数据库下划线字段自动映射驼峰属性能靠 map-underscore-to-camel-case 帮我省不少事;二是动态 SQL 用 、 包裹,避免拼接出带 1=1 的尴尬语句;三是尽量用 LEFT JOIN 代替子查询,MySQL 在多表 JOIN 时执行计划更好优化。如果你准备在这套源码基础上改,建议先翻开 resources/mapper 目录下的 XML 文件,把已有查询风格看一遍再动手。
3. SpringBoot2后端实现:从接口规范到JWT鉴权
3.1 统一返回结果和全局异常是后端第一课
很多新手项目有个通病:接口返回格式不统一,有的返回 JSON 对象,有的返回字符串,前端处理起来全靠猜。这套源码里应该有一个公共的 Result 类,正常返回 data,出错返回 errorCode 和 message。
我强烈建议用统一响应而不是直接返回 Map 或者裸对象,原因很简单:前端 Axios 拦截器只要判断 code 就能决定是跳登录还是弹提示,不用每一项接口单独处理。下面这段是我习惯的返回结构,和这套项目里常见的写法基本一致:
@Data 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("success"); result.setData(data); return result; } }全局异常处理也别省,用 @RestControllerAdvice + @ExceptionHandler 把业务异常、参数校验异常、系统异常分开捕获。尤其参数校验,实体类字段上加 @NotBlank、@Email 这些注解,在 Controller 参数前加 @Validated,省掉一堆手动 if 判断。这套源码里如果异常处理做得完备,二次开发时只需要往枚举里加错误码就行,非常舒服。
3.2 JWT鉴权怎么做到轻量又不失安全
后端还有一块核心是登录鉴权。旅游平台必然有普通用户和管理员两种角色,接口权限要分开。这套源码用的方案我估计是 JWT + 拦截器,没有上 Spring Security,因为轻量级项目引入 Security 的学习成本和配置成本都偏高。
实现思路大概是:登录成功后生成 JWT 字符串并在 token 里塞入 userId 和 role,前端每次请求在 Authorization 头带上 token,后端自定义拦截器解析 token 并在 HandlerMethod 里判断角色权限。需要注意几个要点:
- 密码存储用 BCrypt 加密,不允许明文存库;
- token 要设置过期时间,建议 2 小时或一天,刷新机制按业务需求来;
- 拦截器里遇到 token 过期,返回 401,前端统一跳登录页;
- 放行路径要明确,登录、注册、首页展示数据、景点详情这些公开接口不需要 token。
如果你拿这套源码学习,建议把 HmacSHA256 或者 HS256 签名的密钥放到 application.yml 里,不要写在代码中。这个习惯看起来不起眼,但代码一旦开源或者交接,带密钥的项目就是安全隐患。
3.3 文件上传和静态资源映射的两个细节
旅游平台离不开图片处理,景点封面、酒店实拍、攻略插图都要上传。后端上传接口一般接收 MultipartFile,然后保存到本地目录或者云存储。这套源码大概率是本地存储方案,那么有两个细节你必须检查:
第一个是 application.yml 里的上传大小限制,Spring Boot 默认单文件 1MB,用户传个高清景点图就直接报错。配置要手动放开:
spring: servlet: multipart: max-file-size: 20MB max-request-size: 50MB第二个是静态资源映射。图片保存到本地磁盘后,前端要用 URL 访问,必须在配置里把虚拟路径映射到真实目录。比如磁盘目录是 /data/upload,那访问地址就是 http://ip:8080/upload/xxx.jpg,配置大约长这样:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceMapPath("file:D:/data/upload/"); } }注意实际项目中路径要写到服务器上的绝对路径,别写相对路径,否则工作目录变了图片就找不到了。另外,上传文件名一定要用 UUID 或者时间戳重命名,不处理中文和空格,容易出现乱码,更要注意防止上传一些可执行文件。
4. Vue3前端搭建:从Vite初始化到接口联调
4.1 Vue3工程结构和组合式API的使用边界
看前端源码时,先别急着跑 npm install,先把 Vite 配置和 main.js 看一遍。这套系统用的是 Vue3 + Vite,入口文件通过 createApp 创建实例,然后注册 Element Plus、Pinia、Router。我比较喜欢看到的一个习惯是:通用组件在 main.js 里按需引入,业务组件在页面里局部引入。为什么?虽然 Element Plus 可以全量引入省事,但打包体积会大到让首屏加载变慢,按需引入配合 unplugin-vue-components 自动注册,是更专业的选择。
组合式 API 的代码组织,建议按照“变量 -> 计算属性 -> 生命周期 -> 方法”的顺序写。不要把所有逻辑都塞在 setup 里变成一个巨型函数,而是要拆成复合函数。比如旅游列表页,把加载列表的 fetchList、搜索条件的 searchParams、分页的 pageInfo 组合成一个 useTravelList 函数,页面里只调这个函数拿到数据和操作函数,这是 Vue3 相对 Vue2 很核心的使用思路。
4.2 Axios封装必须做这四件事情
前端和后端联调,最烦的就是每个页面重复写请求代码,还要处理 token、错误提示、loading 状态。所以 Axios 封装非常关键,这套源码里大概率有 utils/request.js 这类文件。封装至少要做四件事:
第一,创建 Axios 实例时设置 baseURL,建议写成 /api,后面交由 Vite 或 Nginx 做代理转发;第二,请求拦截器从 Pinia 的 store 里拿 token 放到请求头;第三,响应拦截器统一判断返回码,code 为 200 直接返回 data,401 清 token 并跳转登录页,其他错误走 ElMessage 提示;第四,导出 get、post 的通用方法,页面直接引入调用。
封装之后,所有接口调用的代码量会减少一半以上。比如:
export const getScenicList = (params) => { return request({ url: '/scenic/list', method: 'get', params }); };页面里就是一个 await 搞定。如果二次开发要加“请求 loading”或者“取消重复请求”,也只需要在封装里加,不用动每个页面。
4.3 路由懒加载、Pinia状态和跨域代理
前端另外三个关键点值得说。第一个是路由懒加载,旅游平台页面多,首页、景点列表、详情、订单、后台都挤在一个 bundle 里会让首屏很慢。Vue Router 使用 import() 动态引入组件,打包后自动拆包,首屏只加载需要的部分,这是体验优化的基础配置。
第二个是状态管理,这套源码用 Pinia。比 Vuex 好用的点在于:store 里可以直接写逻辑和调用接口,比如用户登录后把用户信息存到 store,全局都能访问,刷新页面时再从本地持久化恢复。注意 token 和用户信息要分开存储逻辑,token 失效时清理用户信息也要一起做,不然会出现“已登录”但用户信息为空的半状态。
第三个是跨域配置。本地开发阶段,前端跑在 5173,后端跑在 8080,直接请求必然跨域。Vite 配置里加个 server.proxy 就能解决:
server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } }这套源码如果是用这种方式做代理,那生产环境的 Nginx 也要同样配置好 /api 的转发,否则就会出现“本地能跑、上线就挂”的尴尬情况。
5. 部署上的那些坑:从本地到服务器
5.1 环境准备和Jar包构建
部署这套系统之前,先确认环境:JDK 8 或 11,Maven 3.6+,Node 16+,MySQL 8.0。很多人以为 JDK 版本无所谓,实际上 SpringBoot 2.7 配合 JDK 11 比 JDK 8 更顺滑,但 JDK 8 也能跑。前端部分建议 Node 16 以上,太低会直接报 Vite 版本不支持。
后端构建就是 Maven 打包,执行 mvn clean package -DskipTests,打完的 jar 包在 target 目录下。运行用 java -jar 命令,但别直接在前台跑,用 nohup 或者 systemd 托管更稳。我习惯写成:
nohup java -jar travel-server.jar --spring.profiles.active=prod > app.log 2>&1 &日志输出到文件,出问题直接 tail -f app.log 查。如果一次部署不稳定,怀疑内存配置,可以在启动参数里加 -Xms256m -Xmx512m,旅游平台这种体量,512M 堆内存基本够用。
前端构建是 npm run build,产物在 dist 目录。构建前注意检查 .env.production 文件里的接口地址,如果是相对路径 /api,就交给 Nginx 转发;如果是写死的 http://localhost:8080,部署到服务器必然挂。
5.2 MySQL8.0连接配置的几个细节
MySQL8.0 和 5.7 的区别,新手最容易踩的就是驱动和认证。数据库连接串要注意驱动类名是 com.mysql.cj.jdbc.Driver,加上 useSSL=false 和 serverTimezone=Asia/Shanghai 这两个参数。少了时区参数,时间字段会整体差 8 小时;不关 SSL,控制台会刷一堆警告。
一个更容易被忽略的问题是 MySQL8.0 默认的认证插件 caching_sha2_password,有些旧版本驱动连不上。解决方案有两个:换新版本驱动,或者创建用户时指定 mysql_native_password。我建议直接换驱动,毕竟 5.1.x 版本的驱动早就过时了,用 8.x 的 mysql-connector-j 最省心。
另外,数据库初始化时如果遇到“Unknown database”,先手动执行 CREATE DATABASE travel_db DEFAULT CHARACTER SET utf8mb4;,千万别把建库语句写进导入脚本,否则服务器上没有这个库名会直接卡住。导入完成后,敲几条 SQL 验证数据是否落位,尤其是所有表的数据量和关键表的 id 自增起点,确认没问题再启动后端。
5.3 Nginx配置和前后端联动的正确姿势
后端 jar 跑起来后,前端 dist 丢到 Nginx 的 html 目录,然后配置一个 server 块。最核心的是把 /api 开头的请求转发到后端接口:
server { listen 80; server_name your-domain.com; root /www/travel-front; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } location /upload/ { proxy_pass http://127.0.0.1:8080/upload/; } }为什么 location / 要写 try_files?因为 Vue Router 用的是 history 模式,用户刷新 /scenic/12 这个路径时,Nginx 找不到对应物理文件,如果配置不正确就会白屏 404。加上 try_files 把所有未命中路径都引到 index.html,前端路由自己去处理页面跳转,这个问题就解决了。
还有,图片上传后的访问路径,如果后端是本地存储,也要在 Nginx 里单独代理到后端端口,或者在 WebConfig 里已经映射了 /upload/ 的静态路径,Nginx 就不要强行指磁盘路径,直接代理后端最稳。
6. 常见问题和排错速查
6.1 后端和数据库的典型报错
我整理了一些跑这类项目时常见的报错,按出现频率排个序:
| 报错场景 | 可能原因 | 解决办法 |
|---|---|---|
| Access denied for user | 数据库密码错误或认证插件问题 | 确认账号密码,或换新驱动 |
| Unknown database | 库没有创建 | 先建库再导入脚本 |
| Table doesn't exist | 表名大小写混乱 | Linux 下 MySQL 表名区分大小写,统一用下划线小写 |
| Invalid bound statement | Mapper 接口和 XML 路径不对 | 检查 mapper-locations 配置 |
| Page 参数失效全表返回 | 分页插件没注册 | 配置 MybatisPlusInterceptor |
| Connection reset | 数据库连接池耗尽 | 调整连接池参数,检查慢 SQL |
| Port already in use | 8080 被占用 | 换端口或 kill 旧进程 |
SQL 层面的问题,建议把 MyBatis-Plus 的 SQL 日志打开,执行时在控制台看到完整 SQL,排错效率会高很多。配置是:
mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl正式上线的时候记得关掉,不然每一条 SQL 都打日志,文件增长非常快。
6.2 前端白屏、请求失败的高频原因
前端问题我遇到最多的是白屏和请求跨域。白屏最常见的原因就是上面说的 history 路由没有 try_files 支持,刷新页面直接 404;第二常见是构建后路由 base 配置不对,导致资源加载路径变成 /assets/xxx 却找不到文件,检查 Vite 配置文件里 base 属性是否设置为相对路径或 /。
请求失败的情况,先用浏览器 F12 看网络面板。如果请求 URL 是 http://localhost:8080 且跨域报错,说明 Vite 代理没生效,确认请求路径是不是以 /api 开头。如果没有报跨域但一直 404,多半是代理重写路径出了问题,把 /api 前缀匹配到后端接口后,后端没来得及处理多出来的前缀,这时要检查 rewrite 规则。
还有一个小问题,前后端联调时接口返回的数据格式不符合前端预期,容易报 “Cannot read properties of undefined”。这种问题最好的解决方式是后端 Result 结构统一,前端响应拦截器只要判断 code 直接返回 data,页面拿到的永远是实体对象或列表,就不会到处判空。
6.3 文档和源码使用建议
最后说下文档和源码配合使用的思路。拿到源码后,我建议按“文档 -> 数据库 -> 后端 -> 前端 -> 部署”的顺序过一遍:先看 README 里的环境要求,再导入数据库,接着后端改配置启动,前端启动联调,最后做打包部署。不要跳过文档直接跑代码,这套系统的设计思路、接口说明都写在文档里,跳过的结果就是你连登录账号都不知道。
运行项目时如果发现某个功能没生效,先查数据库数据是否存在,再查接口是否报错,最后查前端是否正确处理返回结果。大部分时候问题出在配置而不是代码本身。项目里的账号体系、管理员后台账号,文档里如果没有,可以从 user 表里直接查询,密码 BCrypt 加密后无法反解,重置的逻辑通常要写一个临时接口或者用已知明文 BCrypt 生成后 update 数据库。
我自己的习惯是把文档中提到的接口列表导出成一份接口清单,通过 Swagger 或手动整理的 Markdown 文件,调用的时候直接对照,能省出大量 debug 时间。
写在最后的个人体会
把这套系统完整跑通之后,我最大的感受是:源码本身并不难,难的是理解它为什么这样做。SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0 这套组合,放在今天的 Java Web 生态里不算新潮,但恰恰因为每层都有人用、有成熟的解决方案,才更适合作为学习和二次开发的底子。旅游平台不是单纯的管理系统,它要同时处理内容展示和用户行为,表结构的关联、接口的粒度、前端状态的管理,每个环节都是真实项目经验的浓缩。在改代码之前,先花半天时间把它的表结构关系理清,再顺着一条完整业务路径从前端页面跟到后端 SQL,你会比直接动手加功能收获多得多。