☰
SpringBoot+Vue+MyBatis+MySQL企业级旅游网站管理系统全栈实战
2026/10/21 7:17:30 网站建设 项目流程

做旅游网站管理系统,很多人第一反应是找个现成的CMS改一改。但真正到了企业级场景,你会发现事情远没有那么简单。白天要管景点、线路、酒店、套餐,晚上还要应付订单状态、价格策略、客户评价、后台权限……这套基于SpringBoot+Vue+MyBatis+MySQL架构的旅游网站管理系统源码,就是把旅游企业最常遇到的这些业务场景,全部用一套完整可运行的前后端分离工程串起来。它在实战项目中能帮团队快速建立业务骨架,也比较适合正在做毕业设计或想系统学习企业级Java全栈开发的人参考。下文我会按这个项目从设计到落地的完整链路,把架构决策、数据库设计、核心功能实现、部署运维和踩坑记录都展开讲清楚。

1. 项目定位与核心价值拆解

1.1 这套系统到底解决什么问题

先明确一个问题:企业级旅游网站管理系统,和普通的企业展示官网、内容管理系统有什么区别。

普通的官网只解决“让用户看到信息”的问题,而旅游业务的核心是“信息展示 + 交易转化 + 履约管理”。用户看景点介绍时,要能关联到线路、酒店、交通;用户下单后,后台要能处理订单状态、退款、评价、核销。信息不打通,运营人员就得在不同Excel里来回复制粘贴,订单出错率会直线上升。

像某旅游公司之前的实际场景,前台有景点库、线路库、酒店库,散落在三个系统里,后台人员每天手工整理套餐价格,运营要统计销量还得导出多张报表再手动合并。这套系统源码的核心价值,就是把这些零散操作收敛到同一套数据模型和业务流程中。围绕景点、线路、酒店三个基础资源,统一管理用户、订单、评价、营销位和系统权限,让前台能卖、后台能管、财务能对账。

对开发者来说,这套项目最值得参考的也不是某个单一功能,而是它的“业务闭环”。从用户注册登录,到浏览景点、选线路、下订单、支付回调刷新状态,再到后台审核、订单查询、数据统计,整条链路是完整的。

1.2 功能模块全景与边界划分

按照使用角色,我把系统划分为用户端(C端门户)和管理端(B端后台)两部分。

用户端主要包含:

  • 景点展示:景点列表、详情、图片、视频、地理位置、开放时间。
  • 线路套餐:跟团游、自由行、一日游等多类型线路,支持日历价格、库存余位。
  • 酒店住宿:酒店列表、房型、价格日历。
  • 订单中心:购物车、提交订单、订单详情、取消订单、支付状态。
  • 个人中心:注册登录、个人信息、订单记录、收藏夹、评价晒单。

管理端主要包含:

  • 内容管理:景点、线路、酒店、轮播图、公告、资讯的增删改查与上下架。
  • 订单管理:订单查询、改价、审核、取消、退款、导出。
  • 用户管理:会员列表、禁用、重置密码、等级设置。
  • 营销管理:优惠券、限时促销、推荐位配置。
  • 数据统计:销量统计、订单金额趋势、热门景点排行。
  • 系统管理:管理员账号、角色、菜单权限、操作日志。

模块边界划分有个重要原则:用户端只读展示,管理端负责写操作;涉及金额变动的操作必须走订单模块,不允许在内容模块直接改“虚拟销量”。这个边界一旦被打破,后面做数据统计时就会非常痛苦。

2. 技术选型:为什么是SpringBoot+Vue+MyBatis+MySQL这套组合

2.1 选型背后的决策逻辑

这套源码选用的技术栈,看起来是Java Web开发中的“常规套餐”,但每一项选择都有实际业务背景。

SpringBoot承担后端基础框架。它最大的价值不是某一个功能,而是把配置、依赖管理、内嵌容器、健康检查这些东西全部标准化。开发团队不用再像使用传统SSH框架那样,写一堆XML配置去管数据源、事务、扫描路径。SpringBoot的自动配置能让我们把精力集中在业务代码上,这对项目交付周期紧张的企业级系统非常关键。

Vue承担前端界面开发。旅游网站界面交互不算特别复杂,但列表筛选、日历选价、购物车联动这些场景对响应速度要求很高。Vue的组件化开发方式,让景点卡片、线路价格日历、订单状态标签这些UI元素可以被反复复用;配合Element Plus这类组件库,后台管理页面能很快搭起来。不是说JSP+FreeMarker不行,而是前后端分离之后,前端团队和后端团队可以并行开发,这才是效率提升的核心。

MyBatis承担数据访问层。很多人在选型时会纠结用MyBatis还是JPA(Spring Data JPA)。我的观点很明确:旅游业务有大量动态SQL场景,比如景点多条件筛选、线路价格区间查询、订单按时间段统计,这些用MyBatis的XML映射文件反而更直观。你可以精确控制每一条SQL,把多表关联、子查询、动态条件都写在明处;遇到慢查询,直接把日志里的SQL拿出来用工具分析,定位速度非常快。JPA在简单CRUD上确实省事,但一旦业务查询复杂,自动生成的SQL会变得难以优化,排查问题时要多绕很多路。

MySQL作为存储层,配合InnoDB事务引擎,能保证订单表和库存表的数据一致性。考虑到旅游订单频率远低于电商秒杀,MySQL配合合理索引完全能支撑中小企业级访问量。如果过早引入分布式数据库或NoSQL,反而会带来数据一致性、运维成本的问题。

2.2 前后端分离与工程组织方式

这套源码采用前后端完全分离的工程结构,前端和后端各是一个独立项目,通过RESTful JSON接口通信。前后端分离的工程组织方式,第一层好处是职责清晰:后端人员只管接口和数据,前端人员只管页面和交互。第二层好处是部署灵活,前端构建后是一堆纯静态文件,可以交给Nginx处理;后端是一个可执行Jar包,两者的扩容、发布可以独立操作。

接口规范采用统一前缀/api,后端提供JSON格式数据。前端通过Axios发起请求,统一在请求拦截器里携带Token,在响应拦截器里统一处理业务码。这样做的好处是,前后端联调时不需要反复协商异常格式,所有接口都遵循同一套返回结构。

2.3 项目目录结构参考

后端工程建议按模块分包,而不是按Controller、Service、Mapper这种技术类型堆文件。一个实际的参考结构如下:

travel-backend ├── src/main/java/com/travel │ ├── common # 通用返回、异常、工具类 │ ├── config # 拦截器、跨域、MyBatis配置 │ ├── controller # 接口层 │ ├── service # 业务层 │ ├── mapper # 数据访问层 │ ├── entity # 实体类 │ └── TravelApplication.java ├── src/main/resources │ ├── mapper # MyBatis XML文件 │ └── application.yml └── pom.xml

前端工程用Vite创建,结构上按页面维度划分视图目录。

travel-frontend ├── src │ ├── api # 接口封装 │ ├── assets # 静态资源 │ ├── components # 通用组件 │ ├── router # 路由配置 │ ├── store # 状态管理 │ ├── views # 页面 │ ├── App.vue │ └── main.js ├── vite.config.js └── package.json

强调一点:分包方式没有绝对标准,但一定要保证“你三个月后再打开这个项目,还能一眼找到某个功能对应的代码”。我见过很多项目初期图省事,所有业务类都堆在controller包里,后期维护时只能靠搜索文件名定位,非常痛苦。

3. 数据库设计与核心表结构详解

3.1 数据模型总览与表清单

数据库是这个系统的地基。表结构设计得是否合理,直接决定后续开发效率。这套旅游网站管理系统包含大约12张核心业务表。

表名作用说明
sys_user用户表,记录注册用户和管理员
sys_role角色表,定义管理员角色
sys_menu菜单权限表,用于后台动态菜单
scenic_spot景点表,存放景区基础信息
travel_route线路表,存放跟团游/自由行等线路
route_price线路价格库存表,按日期存价格和余位
hotel_info酒店表,存放酒店及房型信息
product_category分类表,景点/线路/酒店的分类体系
travel_order订单主表,一笔订单一条记录
order_item订单明细表,记录订单中的具体产品
review_info评价表,用户对线路/酒店的评论
banner_info轮播图表,管理首页营销位

这12张表基本覆盖了旅游电商的核心业务。实际项目里可能还会增加优惠券表、支付流水表、操作日志表,但最小可运行版本这12张表足够了。

3.2 核心表字段设计细节与原则

企业级项目设计字段,必须从一开始就把容易踩的坑避免掉。我挑几张关键表说明。

用户表(sys_user)核心字段:

  • id:主键,BIGINT,自增。
  • username:用户名,唯一索引。
  • password:加密后的密码,使用BCrypt存储。
  • phone:手机号,业务登录凭证。
  • nickname、avatar、gender等基础信息。
  • status:账号状态,1正常,0禁用。
  • create_time、update_time:创建和更新时间。

景点表(scenic_spot)核心字段:

  • id、name、summary、detail、cover_image、images。
  • province、city、address:用于地区筛选。
  • latitude、longitude:定位信息,便于接入地图。
  • status:上下架状态,1上架,0下架。
  • sort:排序权重,数值越小越靠前。

订单主表(travel_order)是重点。它需要区分订单类型(景点票、线路、酒店),所以设计如下关键字段:

  • order_no:业务订单号,唯一,格式如TO202506011430001。
  • user_id:下单用户ID。
  • order_type:订单类型。
  • total_amount:订单总额,用decimal(10,2)。
  • pay_amount:实际支付金额。
  • status:订单状态,用tinyint。比如0待支付、1已支付、2已完成、3已取消、4已退款。
  • pay_time:支付时间。
  • contact_name、contact_phone:出行人联系信息。

线路价格库存表(route_price)在旅游系统里比较特殊,因为线路价格是按日期变化的。核心字段包括:

  • route_id:关联线路ID。
  • travel_date:出发日期。
  • price:成人价,decimal(10,2)。
  • child_price:儿童价。
  • stock:余位,int。
  • 唯一索引(route_id, travel_date)。

订单明细表(order_item)需要冗余产品快照信息,比如产品名称、产品图片、单价、数量、出发日期。这个设计非常关键。

关于金额,必须强调一个原则:不要用float或double存金额。数据库里所有涉及金额的字段统一用decimal(10,2),Java实体中对应BigDecimal。否则一分钱误差在账目核对时会被无限放大。所有状态字段用tinyint,并且加上默认值和注释,方便维护。

时间字段统一用datetime类型,和Java的LocalDateTime配合使用。订单类表一定要有create_time、update_time,且建议让数据库侧自动维护默认值,减少代码出错的可能。

3.3 索引设计、冗余字段与查询优化

索引设计要结合真正的查询场景,而不是给每个字段都加索引。旅游系统最常见的查询有三类。

第一类是用户订单列表查询,高频SQL形如SELECT * FROM travel_order WHERE user_id = ? ORDER BY create_time DESC。这种查询必须在user_id上建索引。如果订单量大,还要考虑user_id和create_time的复合索引。

第二类是景点/线路的关键字搜索,形如SELECT * FROM scenic_spot WHERE name LIKE CONCAT('%', ?, '%')。这种前缀模糊查询在数据量几千条时问题不大,但超过几万条就会出现明显的性能下降。如果初期数据量可控,name字段加上普通索引配合like查询可以接受;一旦数据量大,建议引入全文检索或独立搜索引擎,而不是继续在MySQL里硬扛。

第三类是后台订单管理页的筛选统计,经常按订单状态、下单时间、产品名称筛选。这类查询建议在status和create_time上建立复合索引。

在设计时,可以把一些高频展示字段做冗余。比如在travel_order表中冗余product_name和product_image,这样订单列表页就不需要再去关联查询产品表。虽然违背了第三范式的严格规则,但对查询性能的提升非常明显。冗余的本质是空间换时间,适合读多写少的业务场景。

4. 核心功能实现:从后端接口到前端页面

4.1 后端基础设施:统一返回结构、异常处理与登录认证

一个企业级后端项目,最先搭的不是业务接口,而是基础设施。这套源码里的基础设施包括三块:统一返回结构、全局异常处理和登录认证。

统一返回结构,我建议定义成Result对象,包含code、message、data三个字段。所有业务接口都返回Result,前端拿到后先判断code是否为200。这样做的好处是,前端Axios响应拦截器里只需要处理一次业务码,不需要每个页面都做重复判断。

全局异常处理使用Spring的@RestControllerAdvice。自定义一个BizException,业务层判断参数不合法、数据不存在、库存不足时直接抛出异常,由全局处理器统一捕获并返回对应的业务码。这样做能避免业务层到处写try-catch,代码会干净很多。

登录认证是基于JWT实现的。用户登录成功后,后端生成一个Token返回给前端,前端存储在本地并放在请求头里。后端通过拦截器校验Token是否有效。密码加密使用BCrypt,不放明文,也不建议用MD5这种可逆碰撞风险高的方案。

一个精简的JWT工具类核心逻辑可以这样理解:生成Token时,把用户ID、用户名、角色编码放进Claims里,设置过期时间;校验Token时,通过密钥解析签名,如果过期或签名不正确直接抛异常。开发阶段可以把过期时间设长一点,比如24小时;进入生产环境后建议缩短到2小时,并配合RefreshToken刷新机制。

4.2 景点与线路管理模块实现

景点管理后端,核心是提供一个分页查询接口。查询条件包括关键字、城市、分类、状态。用MyBatis动态SQL实现,核心片段是这样的:

<select id="selectSpotPage" resultType="ScenicSpot"> SELECT * FROM scenic_spot <where> <if test="keyword != null and keyword != ''"> AND (name LIKE CONCAT('%', #{keyword}, '%') OR summary LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="city != null and city != ''"> AND city = #{city} </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY sort ASC, id DESC </select>

分页我用了PageHelper插件,前端传pageNum和pageSize,后端返回分页对象。PageHelper的坑在于,它基于ThreadLocal实现,使用时必须在查询方法内紧跟着PageHelper.startPage(),然后执行第一条查询语句,否则分页参数会残留到下一次查询中。

线路管理比景点复杂一点,因为它是一对多关系:一条线路对应多个日期的价格库存。后台保存线路时,要同时提交线路基本信息和价格日历列表。事务必须加在保存方法上,否则线路主表保存成功但价格库存保存失败,会造成数据不一致。

前端实现上,景点列表页用Element Plus的el-table展示,配合el-form做筛选区。图片上传组件需要注意,上传前要校验文件类型和大小。图片上传接口建议单独提供,返回访问URL,再把URL和表单一起提交,这样可以避免大表单超时。

4.3 订单流程与状态机设计

订单模块是整个系统里业务复杂度最高的部分。它的核心不是CRUD,而是状态流转。

我设计的订单状态机如下:

  • 待支付:创建订单后的初始状态,用户可以取消,也可以去支付。
  • 已支付:支付回调成功后进入该状态,开始占用库存。后台可以介入审核或直接确认。
  • 已完成:出行日期结束后,系统自动或后台手动标记完成。
  • 已取消:用户在待支付状态取消,系统释放库存。
  • 已退款:已支付订单发生退款,后台操作后进入该状态,同时回补库存。

订单创建接口的执行顺序必须严格控制,尤其是余位扣减。一个常见方案是,先查询线路当天的剩余库存,判断是否足够;足够则使用乐观锁更新库存,UPDATE route_price SET stock = stock - #{num} WHERE route_id = ? AND travel_date = ? AND stock >= #{num}。这样做可以防止并发超卖。执行更新后,影响行数为0说明库存已经被抢光,直接抛出“余位不足”提示。

支付回调设计要注意幂等性。支付平台回调可能会重复发送,后端必须根据订单号判断当前状态,如果已经由待支付变成已支付,则不能再次处理。否则会出现重复加库存、重复发短信之类的问题。

开发阶段没有真实支付渠道,我用了模拟支付接口。前端点“去支付”,后端直接生成一条模拟支付流水,把订单状态更新为已支付。等到真实对接支付时,只需要替换这个接口的调用实现,状态机部分不用大改。

4.4 前端工程化与页面落地

前端工程使用Vite搭建,比Webpack的启动速度快很多。路由管理使用Vue Router,并在路由配置里区分用户端页面和后台管理页面。

登录状态控制的核心在路由守卫里。每次路由跳转前,检查本地是否存在Token;如果不存在且目标页面需要登录,则跳转到登录页。后台管理页面还要额外校验角色权限,没有权限直接跳转到403页面。

Axios封装是关键的一步。我在请求拦截器里统一从本地存储取Token并添加到请求头;在响应拦截器里统一对Result的code做判断,如果是401或Token过期,则清除本地登录信息并跳转到登录页。

状态管理我推荐用Pinia,比Vuex更简洁。用户信息、菜单权限这些跨页面需要共享的数据放在store里。登录成功后,从后端拉取用户信息和可访问菜单列表,动态生成后台侧边栏。这里会涉及动态路由问题,建议后端返回菜单权限编码列表,前端通过权限指令控制按钮级可见性。

页面落地时,用户端的景点列表页需要做到自适应栅格布局,因为用户可能在不同设备上访问;后台管理页则优先保证信息密度和操作效率。首页轮播、热门推荐、线路搜索这些营销位,通过banner表和推荐位字段配置,运营人员不需要改代码就能更新。

5. 部署上线与运维:从本地打包到服务器运行

5.1 后端Jar打包与前端静态资源构建

后端打包前,先确认application.yml里的数据库连接、Redis连接是否指向正确的环境。执行命令:

mvn clean package -DskipTests

打包完成后,target目录下生成一个可执行Jar包。这里有一个常见问题:如果用SpringBoot Maven插件打包,默认是可执行Jar;但如果pom里配了多个模块,记得检查主类配置是否正确,否则会出现“找不到主清单属性”的报错。

前端构建命令:

npm run build

构建完成后,dist目录就是纯静态文件。可以直接把这个目录丢给Nginx托管,也可以打包上传到服务器后解压。

5.2 Linux环境部署与Nginx配置

服务器环境需要安装JDK、MySQL、Redis,还要确认端口没有被占用。我习惯把后端Jar包放到/opt/travel/app目录下,然后用Systemd管理进程,而不是直接java -jar挂在终端里。

一个典型的Systemd服务配置片段:

[Unit] Description=Travel Backend Service After=network.target [Service] User=travel ExecStart=/usr/bin/java -Xms512m -Xmx1024m -jar /opt/travel/app/travel-backend.jar Restart=on-failure [Install] WantedBy=multi-user.target

这样配置的好处是,服务崩溃后会自动重启,开机也会自启动,不需要人肉维护后台进程。

Nginx用来托管前端静态资源,并且反向代理后端接口。关键配置片段:

server { listen 80; server_name travel.example.com; root /opt/travel/dist; 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; } location / { try_files $uri $uri/ /index.html; } }

这里的try_files配置很重要。SPA路由模式下,如果用户直接刷新某个子页面路径,比如/route/102,Nginx会先去找真实文件,找不到就回退到index.html,由Vue Router接管路由解析。不配置这条,刷新页面就会变成404。

5.3 Docker Compose一键编排

如果不想自己装环境,推荐使用Docker Compose编排整套服务。一个最小编排文件包括MySQL、Redis、后端、前端Nginx四个服务。

version: "3.8" services: mysql: image: mysql:8.0 container_name: travel-mysql environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: travel_db ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql redis: image: redis:7 container_name: travel-redis ports: - "6379:6379" backend: image: travel-backend:1.0 container_name: travel-backend ports: - "8080:8080" depends_on: - mysql - redis frontend: image: nginx:1.24 container_name: travel-frontend ports: - "80:80" volumes: - ./dist:/usr/share/nginx/html:ro - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro volumes: mysql-data:

使用Docker Compose之后,项目迁移部署变得非常方便。但我建议生产环境还是把MySQL放在宿主机或者云数据库服务上,容器里的数据卷虽然存在,但备份恢复流程还是要自己严格管理。开发测试环境用Compose完全足够。

5.4 数据备份与日志排查

数据备份不能只靠“想想”。MySQL备份最常用的工具是mysqldump,配合crontab定时任务即可。

0 2 * * * /usr/bin/mysqldump -uroot -p密码 travel_db > /backup/travel_db_$(date +\%Y\%m\%d).sql

注意crontab里如果命令包含百分号,需要转义成\%,否则定时任务会报错。备份文件建议保留最近7天,避免磁盘被撑爆。

日志排查优先看后端日志。SpringBoot默认日志打印在控制台,如果使用Systemd管理,日志会进入journald,可以用journalctl -u travel-backend -f追踪。如果使用Docker,用docker logs -f travel-backend。上线前最好再配置logback将日志写入文件,按天滚动,这样排查问题更有据可依。

6. 实践中的坑与排查速查表

6.1 高频问题实录

我把这套系统实际跑起来过程中遇到的高频问题,整理成一个速查表,照着查能省不少时间。

现象根因解决方式
前端请求接口报跨域错误后端没有配置跨域或Nginx没有转发正确开发环境后端配置CorsFilter,生产环境用Nginx反向代理同源
查询结果返回的中文乱码数据库连接URL缺少characterEncoding参数,或表字符集不对JDBC URL加上useUnicode=true&characterEncoding=utf8,表结构统一utf8mb4
spring.datasource连接超时数据库连接池配置太小,并发一高就等不到连接根据并发量调整HikariCP的maximum-pool-size,一般设置20~50
JWT过期时间太短导致频繁重新登录生产环境配置与开发环境不一致开发环境适当延长,生产环境用2小时并加记住登录
SPA刷新后404Nginx没有配置try_files回退增加try_files $uri $uri/ /index.html;
图片上传后访问403Nginx禁止了上传目录的读取权限检查静态目录配置,给上传目录加上location和正确的alias
PageHelper分页数据混乱startPage调用位置不对,导致后续查询也带上了分页确保startPage后紧跟着要分页的查询语句
MyBatis返回字段全是null数据库字段下划线命名没有映射到Java驼峰属性application.yml开启map-underscore-to-camel-case: true
前端打包后体积过大引入了不必要的完整依赖包路由懒加载、按需引入组件库、开启Gzip压缩
订单重复提交导致库存超卖下单接口没有做幂等控制前端按钮防重复提交,后端在创建订单前校验并锁库存

6.2 排查思路与工具

遇到问题时,我建议按照“从前端到后端、从网络到代码”的顺序排查,而不是一上来就猜业务逻辑。

当前端报错时,先打开浏览器开发者工具看Network面板。如果请求状态是CORS error,先解决跨域;如果返回500,直接看后端接口日志;如果返回结果是业务code失败,再去看后端日志中对应的异常栈。后端排查时,先看日志输出,再根据时间点和接口地址定位代码位置。SQL层面的问题,可以在application.yml里开启MyBatis SQL日志,把实际执行的SQL捞出来,放到数据库工具里执行查看执行计划。

还有一个我常用的做法:在本地环境启动项目时,用一个小的测试账号把核心链路先跑一遍,下单到支付再到后台确认,确认每一步状态都正常再改其他模块。很多时候系统看起来复杂,但90%的问题是登录认证、数据库连接、跨域这三个基础环节造成的,先把这三个环节弄扎实,后面的业务开发会顺畅很多。

如果确实需要改表结构,记住不要在业务高峰期直接改生产库。先备份,再在测试环境验证SQL脚本,最后执行到生产。数据库变更脚本建议单独维护一个目录,记录每次变更的时间和内容,后续排查问题能少很多扯皮时间。

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

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

立即咨询