☰
SpringBoot+Vue旅游网站管理系统:数据库设计到前后端联调全解析
2026/9/28 8:32:22 网站建设 项目流程

做旅游网站管理系统这个选题,算是计算机专业项目里特别常见的一类了。但说实话,同样的题目,有人做出来只是把CRUD堆一遍,有人做出来能讲清业务链路、讲清每个技术决策的来龙去脉,这两者的差距,面试官和答辩老师一眼就能看出来。今天我想分享的,就是一套基于SpringBoot+Vue的喀什旅游网站管理系统,从零搭建、设计数据库、写后端接口、做前端页面,到最终部署联调的全过程。这套系统覆盖了用户注册登录、景点展示与搜索、旅游线路规划、酒店预订、评论互动、后台管理等功能,技术栈就是标题里那套经典的Java+MySQL+MyBatis,整体是一个非常典型的前后端分离项目。适合正在做毕业设计或者想系统入门全栈开发的人参考。

1. 项目整体设计与技术选型思路

1.1 为什么选SpringBoot+Vue而不是其他组合

先说后端。SpringBoot出现之前,用SpringMVC做项目,配置那是一大堆:web.xml、spring.xml、数据源、事务管理器、扫描包、视图解析器……新手光是把环境跑起来就得折腾一两天。SpringBoot把这些默认配置全部接管,内嵌Tomcat,直接一个main方法就能启动,加上起步依赖机制,想用什么功能就往pom里加什么starter,开箱即用。对于这种旅游网站管理系统来说,后端就是一堆REST接口,SpringBoot做这部分非常顺手,几乎没有多余的样板代码。

再说前端。Vue的核心优势是组件化和响应式。页面拆成组件,景点卡片、轮播图、评论区、导航栏,每个都是独立单元,改一处不会影响全局。数据双向绑定让页面状态管理变得很自然,比如用户搜索关键词、筛选景点类型、选择日期,页面视图会跟着数据实时变化,写起来比直接用jQuery操作DOM要省心太多。再说了,现在前端招聘市场Vue的占有率摆在那,学这套东西对后续找工作也有直接帮助。

前后端分离还有个实际好处:开发的时候前端和后端可以并行。定好接口文档,后端专心写接口,前端先用mock数据画页面,最后联调时再统一对接。一个人做整个项目的时候,这个优势同样明显——逻辑清晰,出错的时候能立刻判断是前端问题还是后端问题,不会一锅粥。

1.2 喀什旅游业务的核心需求拆解

喀什的旅游资源很丰富,古城、沙漠、帕米尔高原、民俗风情,游客来之前最关心的是:有哪些景点?怎么安排路线?住哪里方便?当地有什么特色活动?所以这个系统我把它拆成了两个端来设计。

游客端:景点列表和详情、按区域和主题筛选、查看旅游线路、酒店信息浏览、发表评论和评分、在线预订线路或房间。 管理端:景点信息增删改查、线路上架下架、酒店和房间管理、评论审核、订单状态管理、基础数据统计。

用户角色就两类:普通游客和管理员。游客可以注册、登录、浏览、预订、评论;管理员登录后台维护所有内容数据。系统整体走的是"内容展示+在线交易+互动评论"这个模型,本质上跟很多电商站类似,但业务细节要更贴合旅游场景,比如景点有开放时间、门票价格、建议游玩时长,线路有行程天数、成团人数,这些都是做表结构时要提前想到的。

2. 数据库设计与核心表结构

2.1 核心业务表与字段设计

先看用户表。常规字段:id、username、password、nickname、phone、avatar、role(0普通用户,1管理员)、create_time。这里有一个必须强调的点:密码绝对不要明文存储,至少用BCrypt做hash,Spring Security里的BCryptPasswordEncoder可以直接用,否则数据库一泄露,所有账号密码全暴露了。

景点表是内容核心。字段设计我建议这样:id、name、region(区域,比如喀什古城、塔什库尔干)、type(类型,自然风光/人文历史/民俗体验)、description(长文本介绍)、cover_image(封面图URL)、gallery(图片列表,存JSON多张图)、price(门票价格)、openhours(开放时间)、play_duration(建议游玩时长)、status(上下架状态)、view_count(浏览量)、create_time。price用decimal(10,2),方便处理小数点;图片存多张用JSON字符串比单独建子表省事,查询还不用join。

酒店和房间要拆两张表。酒店表hotel:id、name、address、grade(星级)、image、description、status。房间表room:id、hotel_id、room_type(大床房/双床房)、price、stock(可订数量)、area。线路表route:id、title、days(行程天数)、price、cover、description、route_detail(每天的行程安排)。

订单表是整个交易链路的核心。orders设计为:id、order_no(订单号,唯一)、user_id、product_type(0线路/1房间)、product_id、quantity、total_price、status(0待支付/1已支付/2已取消/3已完成)、create_time、pay_time。为什么要单独建订单表而不是在景点表里加个购买人数字段?因为订单是核心交易数据,跟景点是1对多的关系,游客可以同时订线路和房间,订单必须独立成表才能完整记录整个交易流程。

评论表也要好好设计:id、user_id、spot_id、content、rating(1-5分)、create_time、status(0待审核/1已通过/2已删除)。评论审核这个状态字段很多新手容易忽略,实际上旅游网站是公开展示的,评论不审核很容易被垃圾信息刷屏。

2.2 表关系与索引优化

这些表的关联关系并不复杂:用户到订单是1对多,酒店到房间是1对多,游客对景点的评论通过comment表关联user和spot。在物理设计上,我不太建议强行加数据库外键约束,原因是:MyBatis项目里我们通常用代码控制关联关系,加外键会降低插入和更新性能,而且业务调整时外键会成为迁移和修改的阻碍。逻辑外键已经足够表达关系,物理外键能不用就不用。

索引方面有几个必加:orders表的user_id、order_no(唯一索引)、status;scenic_spot表的region和type;comment表的spot_id。这些都是查询的高频入口。这里有个真实的性能坑:很多新手建表时不加索引,等数据量上来,查某个用户订单扫全表几十万行,接口直接超时。索引不是越多越好,但一定要建在真正高频筛选的列上。

字符集一定要用utf8mb4而不是utf8。原因很简单:utf8在MySQL里最多支持3字节,像emoji这种4字节字符会存不进去,直接报错。旅游网站的评论区,用户发个表情再正常不过,所以建库时就要指定utf8mb4,排序规则用utf8mb4_unicode_ci就行。

建库语句大概长这样:

CREATE DATABASE kashi_travel DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

存储引擎用InnoDB,支持事务、崩溃恢复能力强,适合订单这种对数据一致性要求高的业务。表结构确定之后,一定要写一个init.sql,把建表语句和基础数据都准备好,之后部署直接source导入,省得在服务器上一张一张建。

3. 后端核心模块实现(SpringBoot+MyBatis)

3.1 项目初始化与分层架构

创建项目用Spring Initializr就行,Java版本建议8或11,这两个版本依然是目前生产环境的主力。核心依赖不多:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.x</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>

代码分层就按经典的三层结构走:controller接收请求、参数校验、返回统一结果;service处理具体业务逻辑;mapper是MyBatis的数据访问层。再加上entity实体、dto接收参数、config配置类、common公共类。这种分层不是形式主义,最大的好处是:当业务复杂到一定程度,一次修改不会牵一发动全身。比如景点删除功能,如果你直接在controller里调mapper.deleteById,将来要加"删除前判断该景点有没有未完成订单",就得改controller,而有了service层,这个逻辑天然放在service里,controller保持不动。

application.yml配置可以参考这份:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/kashi_travel?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.kashi.entity configuration: map-underscore-to-camel-case: true

map-underscore-to-camel-case这个配置强力建议打开。数据库字段是create_time,Java实体是createTime,打开后MyBatis自动映射,不用每条SQL都写别名,能省下大量体力。

接口返回格式我统一封装成Result类,包含code、message、data三个字段。code为200表示成功,401表示未登录,500表示服务器错误。所有接口都返回这个结构,前端axios拦截器只要判断code就能统一处理错误,不用每个接口单独写异常分支。配合@RestControllerAdvice全局异常处理器,业务里抛出的异常统一捕获成Result返回,避免把Java堆栈直接暴露给前端,这点在答辩和面试时很加分。

3.2 用户登录鉴权与订单接口实现

用户登录我采用JWT方案。前端提交账号密码,后端校验通过后,生成一个包含userId和role的token返回,前端存到localStorage,之后每次请求都在header里带Authorization。后端写一个LoginInterceptor拦截需要登录的接口,从token里解析用户信息,放行或拒绝。

生成JWT用jjwt库,代码也就几十行:

String token = Jwts.builder() .setSubject(userId.toString()) .claim("role", user.getRole()) .setExpiration(new Date(System.currentTimeMillis() + 3600_000)) .signWith(secretKey) .compact();

JWT比传统Session方案好在哪?Session在集群部署时要考虑session共享问题,JWT把用户状态放在客户端token里,后端不需要存session,天然适合前后端分离和水平扩展。缺点就是token签发后无法服务端主动失效,所以一般把过期时间设短一点,比如2小时,前端拦截到401就跳回登录页重新登录。

订单接口是整个后端逻辑最密集的地方。以预订酒店房间为例,前端提交hotelId、roomId、入住日期、房间数,后端要做这几件事:

  1. 解析token拿到userId,校验用户存在;
  2. 查询room记录,校验库存和价格;
  3. 生成订单号,可以用时间戳加随机数保证唯一;
  4. 计算总价,插入orders表;
  5. 把room的库存减掉预订数量。

第2到第5步的核心在于事务控制。如果只查询价格然后直接插入订单,两个用户同时订最后一间房,两个请求都读到stock=1,都下单成功,但实际只能住一个人,这就是典型的超卖。解决方案是给service方法加@Transactional注解,而且扣库存的SQL一定要带库存判断:

update room set stock = stock - #{num} where id = #{roomId} and stock >= #{num}

执行完检查update返回的影响行数,为0说明库存不足,直接抛异常回滚事务。

3.3 MyBatis动态SQL与分页查询实战

景点列表页有各种筛选条件:区域、类型、价格区间、关键词。如果每种组合写一个固定SQL,那得写几十条。MyBatis动态SQL就是为这种场景设计的,在xml里用where和if标签灵活拼查询:

<select id="searchSpots" resultType="com.kashi.entity.ScenicSpot"> select * from scenic_spot <where> <if test="region != null and region != ''"> and region = #{region} </if> <if test="type != null and type != ''"> and type = #{type} </if> <if test="keyword != null and keyword != ''"> and (name like concat('%', #{keyword}, '%') or description like concat('%', #{keyword}, '%')) </if> </where> order by view_count desc </select>

注意like查询里用的是concat函数拼接%,不要直接在外部拼好再传进来,那样会有SQL注入风险。MyBatis的#{}是预编译占位符,自动规避注入,${}才是直接字符串拼接,除非是排序字段这种场景,否则坚决用#{}。

分页用PageHelper插件,使用方式非常简单:

PageHelper.startPage(pageNum, pageSize); List<ScenicSpot> list = spotMapper.selectAll(); PageInfo<ScenicSpot> pageInfo = new PageInfo<>(list);

PageHelper会自动拦截下一条SQL,生成带limit的查询并统计总数,PageInfo里自带total、pages、pageNum这些信息,前端直接拿来渲染分页组件。

关于MyBatis缓存,这里也说一下。一级缓存默认开启,同一个SqlSession里重复查询会命中;二级缓存需要手动开启配置。但这个项目我建议暂时不开二级缓存,原因很简单:查询结果跟数据库直接相关,景点一更新就容易出现脏读,等业务量大了以后交给Redis管理缓存会更稳妥。

4. 前端Vue页面与核心交互实现

4.1 前端工程化与axios网络层封装

前端我用Vue CLI创建项目,路由用vue-router,UI组件库用Element UI,这样管理后台和用户页面都能快速搭出规范的表单表格,不用从零写CSS。目录结构按views页面、components组件、router路由、api接口、utils工具来组织。

axios封装是整个前端能否省心的关键。我在src/utils/request.js里创建一个axios实例,设置baseURL指向后端地址,加两个拦截器:请求拦截器从localStorage取token塞到Authorization头;响应拦截器统一处理返回结构,code不等于200就弹提示或者跳登录页。这样业务代码里每个接口调用都是干净的一行:

export function getSpotDetail(id) { return request.get(`/api/spot/${id}`) }

路由设计上,游客端页面有首页、景点列表、景点详情、线路列表、酒店列表、我的订单、登录注册;管理端页面有控制台、景点管理、线路管理、酒店管理、订单管理、评论管理。两种角色之间用一个路由守卫判断:进入管理端页面前检查当前用户role是否为1,不是就重定向到登录页。但这里要特别注意:前端守卫只是用户体验层面的保护,真正权限控制必须后端接口也做同样的校验,否则有人绕过前端直接调接口就崩了。

4.2 游客端核心页面:景点列表、详情与预订流程

景点列表页是整个系统对外展示的第一张脸,我把它做成顶部分类筛选栏加左侧区域筛选、右侧景点卡片瀑布流的布局。筛选条件变化时,触发重新请求后端接口,带上参数,后端用刚才那个动态SQL拼出对应结果。景点卡片上展示封面图、名称、区域、类型、门票价格、浏览量,点击进入详情页。

详情页是游客信息密度最高的页面。顶部是图片轮播,接着是基本信息表格,包括开放时间、建议游玩时长、门票价格,然后是详细介绍文字,底部是评论区。评论加载用分页,每次10条,用户提交评论后清空输入框,重新拉第一页数据。这里容易踩一个坑:提交评论后如果还用当前页的数据刷新,因为分页偏移问题,可能显示不出刚提交的内容。我的处理是提交成功后直接调用加载第一页评论,让评论列表回到最新状态。

预订流程分线路预订和酒店房间预订两条链路。线路预订相对简单,游客选日期、输人数、确认订单、生成待支付订单;酒店房间预订需要先选房间类型和入住天数,系统自动算总价。订单确认页要把房间信息、入住日期、总价这些关键信息展示清楚,还要留一个取消按钮,对应后端把状态改成已取消、库存加回去。订单列表页和详情页也要随时能查,这些都是游客信任感的来源。

景点详情页的核心展示代码大概长这样:

<template> <div v-if="spot"> <el-carousel> <el-carousel-item v-for="item in spot.gallery" :key="item"> <img :src="item" /> </el-carousel-item> </el-carousel> <h1>{{ spot.name }}</h1> <el-rate :model-value="spot.rating" disabled></el-rate> <p>{{ spot.description }}</p> <el-button type="primary" @click="handleBook">立即预订</el-button> </div> </template>

4.3 管理后台:CRUD表格、图片上传与数据统计

管理员登录后进入管理后台,第一个页面是控制台,我接入了ECharts做两个统计图:近7日订单量的柱状图,景点访问量Top5的饼图。数据来自后端一个简单的聚合查询接口。这个功能不复杂,但对项目完整度的提升非常明显,答辩时也能体现工程量。ECharts配置项比较多,建议先去官网看基础柱状图的demo,再改数据源,踩坑最少。

景点管理页是整个后台最典型的CRUD页面。Element UI的el-table展示景点列表,工具栏放新增按钮和搜索框,操作列有编辑、删除、上下架。新增和编辑共用一个弹窗表单,用isEdit变量区分插入还是更新。图片上传用el-upload组件配合后端一个upload接口,文件传到服务器uploads目录,接口返回URL,再把URL填到表单的coverImage字段。这里提醒一下:上传接口必须做文件类型和后缀校验,只允许jpg、png、webp这些白名单格式,大小也限制一下,2MB以内比较合理,否则被传个恶意脚本就麻烦了。

订单管理页做两件事:查看所有订单、修改订单状态。表格里展示用户昵称、产品类型和名称、数量、总价、状态、下单时间,状态用el-tag显示不同颜色,订单号加复制按钮方便管理员记录。

5. 联调部署与避坑指南

5.1 前后端联调与跨域配置

本地联调时,前端开发服务器跑在8081,后端跑在8080,前端调后端接口就产生了跨域。解决办法有两种:后端加CORS配置,或者前端用devServer的proxy代理转发。我建议开发阶段用proxy方案,前端写接口时直接写相对路径/api/xxx,由devServer把请求代理到后端真实地址,以后部署上线前端代码一行都不用改。上线时前端打包成静态文件交给Nginx托管,再让Nginx把/api开头的请求反向代理到后端服务,整个架构非常清晰。

开发阶段的vue.config.js配置很简单:

devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

如果非要用后端CORS方案,那就配一个WebMvcConfigurer,注意allowedOriginPatterns要用通配符,配合allowCredentials(true)时不能直接用allowedOrigins("*"),这是很容易踩的版本兼容坑。

5.2 常见问题速查表与解决实录

我把自己开发过程中遇到的典型问题和解决方案整理成一张表,基本都是新手必踩的坑。

现象可能原因解决方案
启动时MyBatis报BindingException: Invalid bound statementMapper接口和XML的namespace不匹配,或XML没被扫描到检查mapper-locations路径是否正确,确认XML的namespace与接口全限定名一致
后端启动报数据库连接失败MySQL没启动、URL错误、密码错误先用客户端软件测试连接;确认库名存在;检查serverTimezone参数
前端请求后端报跨域前后端没配代理,或CORS没放开开发阶段用devServer proxy;上线用Nginx反代同源
接口返回401但用户已登录token过期、拦截器放行路径没配、前端没带header查token有效期、拦截器注册路径、axios请求拦截器是否正确附加Authorization
页面刷新后404vue-router用的history模式,服务端没做兜底开发时用hash模式;history模式上线要在Nginx配try_files
MyBatis查询结果字段全为null没开map-underscore-to-camel-case,下划线字段没映射配置里打开该选项,或SQL里写列别名

还有几个容易被忽略的细节:后端项目里涉及上传图片、生成临时数据的目录,上线前要给足写权限,否则部署到Linux服务器上,进程没有对应目录的写权限,上传图片会静默失败,排查半天才找到原因。MySQL导入数据时如果报Unknown collation错误,多半是客户端版本太低,升级到8.x再导就行。

写在最后。这类管理系统,本质上是一个典型的业务加技术的复合项目,代码并不高深,真正有价值的地方在于:你能否把"游客要什么、管理员要什么"翻译成表结构、接口、页面,再串联成一个能跑通的闭环。如果你正在做类似的毕业设计或者想入门全栈,我的建议是先花一两天把用户故事、业务流程、角色权限梳理清楚,画一张逻辑图,数据表设计就会顺水推舟地清晰起来。一套完整的旅游网站系统做下来,你对SpringBoot和Vue的理解一定比看十遍文档要深,这也是这类项目能成为经典练手题目的原因。

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

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

立即咨询