ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot+Vue旅游平台实战:数据库设计到前后端联调

SpringBoot+Vue旅游平台实战:数据库设计到前后端联调 前几天还有读者私信我说毕设题目是“甘肃旅游服务平台”一看要求就是SpringBootVue那套技术栈配MyBatis和MySQL代码倒是从某处拿了一份结果跑起来各种报错。这情况太常见了我前后带过不少这类Java Web项目甘肃旅游平台本质上就是一个典型的前后端分离管理系统前台给游客看景点、酒店、线路、攻略后台给管理员做数据维护核心无非是CRUD加几张关联表。今天就把这类项目从需求到落地完整拆一遍从MySQL表怎么设计、MyBatis的XML怎么写、Vue页面怎么对接接口到源码怎么跑起来、常见的坑怎么排一次讲清楚。无论你是拿它交课程设计、应付毕业设计还是想借这类项目吃透前后端分离的完整链路这篇应该都能帮上忙。1. 甘肃旅游服务平台项目到底在做一件什么事1.1 从业务视角拆解它包含哪些核心模块旅游服务平台这个词听起来大落到代码里其实就两类角色、若干功能块。用户端要能完成“浏览—查询—预订—评论”这条完整行为链管理端要能维护这些业务数据。具体展开就是下面这些景点模块甘肃的知名景点非常多莫高窟、月牙泉、嘉峪关、张掖丹霞、麦积山石窟、拉卜楞寺这些都要有独立页面展示图片、简介、开放时间、门票价格、所在城市、评分。列表页要支持按城市筛选、按名称搜索详情页要把图片轮播和文字介绍整合在一起。酒店模块旅游平台离不开住宿酒店要有名称、地址、房型、价格、剩余房间数、设施标签。用户能按城市查酒店、看详情、提交预订。线路模块甘肃旅游最经典的玩法是“河西走廊线”和“甘南环线”线路功能本质上是一个多景点组合的产品里面需要存储行程天数、途经景点顺序、线路报价、封面图。攻略文章模块这是内容运营的部分管理员发图文攻略用户在前台浏览可以按分类查看比如“敦煌攻略”“甘南自驾”这类标签文章列表支持摘要展示详情页有正文渲染。订单模块用户在前台下单预订酒店或线路订单状态要有待支付、已支付、已取消、已完成这几类我的订单页面能按状态切换查看。用户与权限模块注册、登录、个人信息维护管理端要区分管理员和普通用户两种角色后台只有管理员能进。收藏与评论模块用户可收藏景点或酒店能在景点详情页发表评论、查看评分这是提升系统交互感的部分也是课设答辩时老师比较喜欢问的地方。很多同学拿到源码第一件事就是看代码我建议反过来先花十分钟把这几个模块在脑子里过一遍。你只有知道了系统该有什么才能理解代码里的表为什么这么建、接口为什么这么拆后面出问题也好定位。1.2 为什么这套技术选型是“标准答案”SpringBootVueMyBatisMySQL这个组合放到2025年来看依然是Java Web项目里最稳的一套搭配尤其是课程设计和毕业设计场景。原因很实际SpringBoot解决了传统SSM项目里大量XML配置的痛点。过去搞一个SSM项目web.xml、spring-mvc.xml、mybatis-config.xml一个个配光是环境调试就能劝退一半新手。SpringBoot靠自动配置把这一切压缩到几行配置文件里内嵌Tomcat启动就是一个main方法的事。Vue负责前端页面组件化开发让页面模块非常清晰。景点卡片、轮播图、分页栏、表单弹窗每个都是独立组件改一个地方不会牵连整个页面。Vue响应式数据绑定也让列表渲染、搜索联调这些操作变得很直接。MyBatis是这类管理系统的利器。旅游平台的查询往往带很多条件比如按城市价格范围评分排序查酒店这种动态SQL用MyBatis写起来很自然where标签配合if标签就能搞定。相比JPA的自动SQLMyBatis把SQL控制权完全交给你出了问题也好排查。MySQL就不用多说了开源免费、轻量、生态好大学环境里几乎默认就是它。整个组合的部署要求也很低一台普通电脑完全跑得动。这里有个常见的选型疑问现在也有不少人用MyBatis-Plus替代MyBatis确实能在单表CRUD上省不少代码。但标题既然锁定MyBatis我建议你把MyBatis的XML写法吃透再考虑Plus毕竟面试和答辩时问你SQL怎么优化、动态SQL怎么写你总不能只会说“Plus自动生成的”。项目里手动写XML反而是加分项因为你能讲清楚每条SQL的实际逻辑。1.3 甘肃地域特色如何决定表结构设计旅游平台遍地都是但甘肃这个前缀不是白加的。地域特色直接影响数据库表字段的规划这也是博主评阅时判断你是否认真做了需求分析的地方。甘肃的旅游格局可以用“一条线、一个圈”来概括。一条线是河西走廊从兰州往西经武威、张掖、酒泉到敦煌景点呈线状分布适合做“几日游”线路产品一个圈是甘南拉卜楞寺、扎尕那、郎木寺这一圈以藏地人文和自然风光为主。所以线路表里必须包含“途经城市”和“行程天数”这两个关键字段而不是简单地把几个景点ID拼在一起。还有一点甘肃地域辽阔景点之间距离动辄几百公里酒店和景点的关系很紧密。设计酒店表时城市字段要做索引因为用户查询酒店的第一条件永远是城市而不是品牌或星级。景点表里的“开放时间”字段也要考虑淡旺季差异虽然课设不需要做得太复杂但字段类型设计成字符串比时间区间更灵活能直接存储“08:00-18:00”这种文本。所以拿到需求别急着建表先把地域特点梳理清楚表结构自然就有了。2. 数据库设计与后端实现SpringBoot和MyBatis的高频写法2.1 MySQL表结构怎么设计才不会被答辩老师挑刺先强调一点所有表的主键一律用自增idBIGINT不用业务字段做主键。理由很简单业务字段会变比如景点名称改一个字如果它是主键所有关联表都要跟着改灾难。自增id作为代理主键是最稳妥的做法。用户表sys_user核心字段id、username唯一索引、password、real_name、phone、role1管理员0普通用户、create_time。密码存的是MD5或BCrypt加密后的密文千万不要明文存储这条在答辩时大概率被问到。景点表scenic核心字段id、name、city、cover_image、images存多张图用逗号分隔或JSON字符串、description长文本、open_time、ticket_priceDECIMAL(10,2)、rating浮点评分、view_count。city字段加普通索引因为前台按城市筛选很频繁。酒店表hotel核心字段id、name、city、address、cover_image、price、room_count、description、facility_tags、rating。价格和剩余房间数是下单时要校验的数据类型上价格用DECIMAL房间数用INT。线路表route核心字段id、title、cover_image、days行程天数、city_list途经城市集合、price、detail图文详情、create_time。city_list用逗号拼接的字符串即可课设没必要单独建线路景点关联表除非你想展示多对多的表关系那也可以建一个route_scenic中间表里面存route_id和scenic_id及顺序号。订单表orders核心字段id、order_no订单号唯一、user_id外键、product_type1酒店2线路、product_id、product_name、price、status0待支付1已支付2已取消3已完成、create_time。这里存product_name的快照很重要因为商品名称以后可能会改但订单里必须保留下单那一刻的名称。评论表commentid、scenic_id、user_id、content、rating、create_time。 收藏表favoriteid、user_id、scenic_id、create_time联合唯一索引(user_id, scenic_id)防止重复收藏。 文章表articleid、title、cover_image、category、summary、content长文本、author、create_time。关联关系就三类用户与订单一对多用户与收藏多对多通过favorite表实现订单与商品多对一用product_type和product_id做多态关联。这几张表建完用Navicat或者MySQL Workbench画个ER图放文档里整个系统数据层就立住了。2.2 MyBatis的XML映射、动态SQL与缓存机制MyBatis在项目里的核心文件是Mapper接口和对应的XML。Mapper接口定义方法XML里写SQL两者通过namespace绑定。我见过不少同学把Mapper接口写好了结果XML里的namespace写错或resource路径没在application.yml里配全启动直接报Invalid bound statement这个错后面排坑部分专门讲。Mapper接口示例Mapper public interface ScenicMapper { ListScenicVO selectScenicList(Param(keyword) String keyword, Param(city) String city); }对应XMLmapper namespacecom.example.travel.mapper.ScenicMapper select idselectScenicList resultTypecom.example.travel.entity.Scenic SELECT * FROM scenic where if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if if testcity ! null and city ! AND city #{city} /if /where ORDER BY id DESC /select /mapper这里有两个细节值得说。第一模糊查询建议用CONCAT(%, #{keyword}, %)而不是直接写%${keyword}%。${}是字符串拼接存在SQL注入风险#{}是预编译参数占位安全。第二where标签会自动去掉多余的AND或OR这几个条件全是空时不会拼出WHERE子句写起来很省心。MyBatis的一级缓存默认开启作用范围是同一个SqlSession。在Spring集成下每次Mapper操作都在独立SqlSession里执行所以一级缓存对业务几乎没感知。二级缓存是namespace级别默认关闭开启后能跨SqlSession缓存查询结果但缓存的是对象引用更新操作后可能出现脏读。课程设计这种并发量不高的场景我建议别开二级缓存少一个不确定性就少一个bug源答辩时还能有理有据地说“生产环境二级缓存需要考虑分布式一致性本系统并发规模暂不引入”反而是加分回答。MyBatis的分页插件是个高频需求。旅游平台的景点列表、酒店列表、评论列表都要分页手写LIMIT #{}要自己算offset还要再写一条count查询太麻烦。项目里用的是PageHelper用法极其简单PageHelper.startPage(pageNum, pageSize); ListScenic list scenicMapper.selectScenicList(keyword, city); PageInfoScenic pageInfo new PageInfo(list);PageHelper.startPage()之后的第一条查询会被拦截自动拼接LIMIT ?并生成count查询最后通过PageInfo拿到总记录数、总页数、当前页数据。原理是MyBatis的Interceptor接口PageHelper实现了一个PageInterceptor在执行Executor前拦截SQL通过ThreadLocal传递分页参数执行完再清掉。这也是MyBatis面试题里“拦截器能做什么”的经典答案。2.3 SpringBoot分层架构与项目目录划分后端代码的目录结构决定了这个项目好不好维护。我拆过的源码里有的把所有代码堆在一个包下面Controller里写SQL这种代码能跑但答辩会很煎熬。合理分层是这样com.example.travel ├── controller # 接收请求返回JSON ├── service # 业务逻辑 ├── mapper # MyBatis的Mapper接口 ├── entity # 数据库实体类 ├── vo # 视图对象承载页面需要的数据组合 ├── common # 统一返回结果、异常处理、工具类 └── config # 跨域、拦截器等配置类Controller只干三件事接收参数、调用Service、返回Result统一结果。判断登录、参数校验这些逻辑放Service里不要在Controller里堆if。Service接口加实现类的形式课设里可以省掉接口层直接写ServiceImpl代码量能少一点但如果想体现工程规范保留接口更完整。统一返回结果类是前后端对接的约定一般长这样public class ResultT { private Integer code; // 200成功500失败 private String message; // 提示信息 private T data; // 业务数据 public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } }前后端约定好这个结构后前端axios响应拦截器里就能统一判断code不用每个接口单独写成功失败逻辑。登录状态这里建议用最简单的方式登录成功后把用户信息放进Session后端写个拦截器检查需要登录的接口是否带Session。有人会用JWT写Token鉴权技术上更现代但课设里Session方案已经完全够用也更容易向老师解释清楚。跨域配置也是必写的。前端开发服务器在localhost:8080跑Vue后端在localhost:8081端口不同就构成了跨域。最省事的方式是写一个WebMvcConfigurer配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }另一种方式是在vue.config.js里配置devServer代理把/api前缀的请求转发到后端端口这样浏览器看到的请求还是同源也能解决跨域。项目部署到生产环境时前端静态文件由Nginx托管用Nginx反向代理到后端这是后话。3. Vue前端页面结构、路由与接口联调的完整链路3.1 用户端和管理端两套页面如何组织Vue前端我一般拆成两套布局一套是面向游客/用户的前台一套是面向管理员的后台。前台偏展示型包含首页、景点列表、景点详情、酒店列表、线路列表、攻略文章、个人中心这些页面后台偏管理型包含登录页、数据看板、景点管理、酒店管理、订单管理、评论管理、文章管理等页面。router里用嵌套路由实现两种布局比如{ path: /, component: LayoutUser, children: [ { path: , component: Home }, { path: scenic, component: ScenicList }, { path: scenic/:id, component: ScenicDetail } ] }, { path: /admin, component: LayoutAdmin, meta: { requiresAuth: true }, children: [ { path: scenic, component: AdminScenic }, { path: order, component: AdminOrder } ] }路由守卫用来做登录校验判断访问后台页面时本地有没有存token或用户信息没有就跳转登录页router.beforeEach((to, from, next) { if (to.meta.requiresAuth !localStorage.getItem(userInfo)) { next(/login); } else { next(); } });路由传参在景点列表跳详情页特别常用。一种方式是/scenic/12这种路径参数用this.$route.params.id取另一种是通过query传/scenic?id12用this.$route.query.id取。列表页跳详情页推荐用路径参数URL更直观刷新后参数不会丢。3.2 axios封装请求拦截器与响应拦截器前端和后台交互全靠axios但绝不能在每个页面里直接axios.get()就完事。统一封装一个request模块好处是拦截器只写一遍所有接口自动带token、自动处理错误状态。import axios from axios; const request axios.create({ baseURL: /api, timeout: 10000 }); request.interceptors.request.use(config { const user localStorage.getItem(userInfo); if (user) { config.headers.token JSON.parse(user).token; } return config; }); request.interceptors.response.use(response { const res response.data; if (res.code 200) { return res; } if (res.code 401) { router.push(/login); } return Promise.reject(new Error(res.message)); }, error { return Promise.reject(error); });这里有两个细节容易踩坑。第一baseURL设为/api后后端所有接口路径都要带/api前缀或者开发环境配置代理时把/api映射到后端根路径。我见过太多联调失败的案例就是前后端路径约定没对齐。第二响应拦截器里直接判断res.code要求后端Result类的字段名固定是code如果后端用的是status或success前端这里就得改所以上一节后端Result结构的设计也是前后端联调的约定基础。3.3 旅游站点的典型组件实战轮播、评分、富文本旅游平台前端有几个高频组件掌握了这些基本就能hold住整个项目。轮播图是首页和详情页的标配直接用Element UI的el-carousel组件配合v-for渲染图片数组即可。要注意的是图片路径。开发时后端本地上传的图片一般存在服务器的某个目录下前端需要用完整的URL才能访问如果后端只返回了相对路径前端要么拼接后端的访问前缀要么通过代理转发。课设项目里更常见的做法是图片用外链或静态资源目录简单省事。评分展示用el-rate组件的只读模式显示景点的平均评分管理端评价景点时用可交互模式用户点几个星就提交几分。这个组件的v-model绑定一个Number类型的字段即可注意后端存的rating要是数字不能是字符串否则组件显示会异常。富文本编辑器用于后台写攻略文章推荐wangEditor轻量、中文文档友好、集成成本低。文章内容以HTML字符串存到MySQL的content字段前台详情页用v-html渲染。这里有个安全提醒v-html渲染用户输入内容时有XSS风险课设里体验一下没问题但如果做正式项目后端一定要做HTML标签过滤只允许白名单标签。前端页面写完前后端联调时最常遇到的就是接口404、返回的JSON结构和前端预期不一致、字段名对不上。建议后端启动后用Postman先自测一遍所有接口确认code/data/message结构统一再让前端去对接能省掉大量无意义的沟通。4. 环境搭建与源码运行SpringBoot项目从零跑通的完整流程4.1 开发环境版本搭配这个项目跑起来的前提是环境版本匹配版本不对会出现各种莫名其妙的错。我推荐一套比较稳的搭配工具推荐版本说明JDK1.8兼容性最好SpringBoot 2.x标配Maven3.6依赖管理SpringBoot2.7.x稳定文档多MySQL8.0课程设计主流Node.js14/16Vue CLI项目常用版本Vue2.6 Element UI课程设计最常见组合JDK版本这里多说一句。SpringBoot 3.0之后要求JDK 17起步如果你的源码是基于SpringBoot 2.x的强行用JDK 17跑会有兼容问题。核心看源码里pom.xml的parent版本如果是2系列老老实实用JDK 8。同样Node版本太高时有些旧依赖会编译报错Node 14或16是比较稳妥的选择。4.2 数据库初始化与配置文件编写拿到源码后第一步不是启动而是把数据库准备好。在Navicat或命令行执行项目附带的travel.sql脚本创建数据库并导入表结构和初始数据。导入成功后重点检查三件事表是否建全、初始账号是否存在、各表是否有测试数据。没有初始数据的话前台页面全是空白看起来像没开发完记得往景点表、酒店表里插入几条带图片的样例数据。数据库连接配置在src/main/resources/application.yml里server: port: 8081 spring: datasource: url: jdbc:mysql://localhost:3306/travel?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.travel.entity configuration: map-underscore-to-camel-case: trueserverTimezoneAsia/Shanghai这个参数几乎必加。MySQL 8.0的时区设置和旧版不同不加的话连接时经常报The server time zone value is unrecognized的错误。map-underscore-to-camel-case配置很有用它让数据库的create_time字段自动映射到实体的createTime属性省去手动写resultMap的麻烦。MyBatis的mapper-locations指定XML文件位置type-aliases-package让实体类能在XML里直接写类名而非全限定名。这两个配置漏掉一个最常见的结果就是启动报错或Mapper方法无法绑定。4.3 前后端启动顺序与访问路径启动步骤有固定顺序先后端再前端。第一步用IDEA打开后端项目等待Maven把依赖下载完。注意IDEA要配置好本地的Maven仓库否则依赖下载到一半断了会报Could not resolve dependencies。确认application.yml里的数据库账号密码改成你自己的然后运行TravelApplication类的main方法。控制台出现Started TravelApplication就是启动成功用浏览器访问http://localhost:8081如果能出现后端提示页或接口返回JSON说明后端OK。第二步用VSCode或IDEA打开前端项目打开终端执行npm install装依赖。如果网络慢或者某些依赖装不上先给npm换淘宝镜像源npm config set registry https://registry.npmmirror.com依赖装完后执行npm run serve控制台提示App running at Local: http://localhost:8080/就可以访问了。默认端口是8080如果被占用会提示Port 8080 is already in use有两个办法把正在占用8080的进程关掉或者修改vue.config.js里的port配置换一个端口。这里有个前后端联调的关键配置我直接在vue.config.js里放一段最常用代理配置module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true, pathRewrite: { ^/api: } } } } };含义是前端发送的所有/api开头的请求都转发到http://localhost:8081并把/api前缀去掉。比如前端请求/api/scenic/list后端实际收到的是/scenic/list。这个配置直接决定了前后端联调通不通如果你发现浏览器里网络请求返回404优先检查代理配置和后端实际路径是否匹配。5. 常见问题与排错记录课设答辩前必看的排查手册5.1 后端启动与运行阶段的报错速查表报错现象常见原因解决办法Port 8081 was already in use端口被占用netstat -ano查占用进程结束进程或改server.portAccess denied for user rootlocalhost数据库密码错误修改application.yml中的password为自己的账号密码The server time zone value is unrecognized时区配置缺失URL后加serverTimezoneAsia/ShanghaiInvalid bound statement (not found)Mapper接口和XML没绑定检查namespace是否等于Mapper接口全限定名检查XML路径是否在mapper-locations指定范围内Field xxx doesnt have a default value数据库字段有非空约束但插入时没传值检查实体类对应字段是否有值或修改SQL语句Unknown column xxx in field list实体类和表字段名不对应确认驼峰映射是否开启或手动加resultMap前三个属于环境类错误最常见的场景是换了一台电脑跑项目数据库密码或者端口不一样导致启动失败。中间两个是代码类错误Invalid bound statement出现的频率最高原因往往是Mapper接口放在com.example.mapper包但XML放在resources/mapper下时路径不一致。最后两个属于数据类错误解决思路是反向检查字段映射。5.2 前端联调阶段的高频问题与处理思路前端的问题往往比后端更隐蔽因为报错有时只在浏览器控制台里体现。我整理几个高频的npm install执行很慢或者卡住。大概率是网络源问题执行npm config set registry https://registry.npmmirror.com换源再删除node_modules和package-lock.json重新安装。浏览器Network请求显示404 Not Found。先看请求URL是不是/api开头再看后端控制台有没有收到这个请求。没收到就是代理没配好收到了但404就是后端路径不匹配去Controller里确认RequestMapping和前端请求路径完全一致。请求能到后端但报500。看后端控制台的异常堆栈常见的是SQL语法错误、空指针、类型转换异常。这类问题用IDEA打断点调试最快在Service方法里打上断点追踪每一步的数据变化。前端页面能打开但接口返回CORS error。原因是跨域配置没生效要么在后端加上CorsConfig配置类要么在前端代理里转发请求二者选其一即可两个都配有时反而因为allowedOriginPatterns和代理的重复导致冲突。页面刷新后变成404。Vue Router如果用了history模式刷新时Nginx或开发服务器没有对应的路由回退配置。开发环境把路由改成hash模式就行地址栏会多个#不影响功能。生产环境需要Nginx配置try_files $uri $uri/ /index.html。5.3 业务逻辑里几个容易被问住的细节业务逻辑层面的坑不是报错但答辩时老师很喜欢往这里问。提前想明白能让你答得更从容。第一个是数据库的删除操作。删除景点时如果这个景点已经被收藏或已经被评论直接DELETE会因为外键约束报错或者留下悬空引用。项目里常见的处理方式有两种一是逻辑删除加一个deleted字段删除时置为1查询时默认过滤掉二是删除前先检查关联表有引用就提示“该景点下有评论或收藏无法删除”。课设里我更推荐第二种逻辑简单、能讲清楚。第二个是库存超卖问题。酒店预订时先查剩余房间数再判断是否足够然后下单扣减库存。这个流程在并发场景下存在超卖风险比如只剩1间房两个人同时下单都通过了。老师问到的话回答思路是“给房间数字段加乐观锁版本号更新时WHERE room_count 0条件同时更新受影响行数为0则提示库存不足”这就足够展示你对并发问题的思考深度。第三个是查询性能问题。景点列表按城市查询很频繁城市字段加索引后查询效率会明显提升。虽然课设数据量小感觉不出来但回答“为什么给这个字段加索引”时回答“该字段是高频查询条件加B树索引能减少扫描行数”就是很标准的加分回答。最后分享一个我个人的实操习惯这类SpringBootVue的旅游平台项目我接手过很多份源码踩过的坑多得数不过来。现在养成了一个习惯拿到源码后不急着运行先不看代码而是打开SQL文件把所有表结构通读一遍再对照前端路由把页面骨架理一遍。等脑子里有了“这张表对应哪个页面、这个接口对应哪个操作”的地图之后再启动项目。这样做最大的价值不是让你更快跑通而是当项目出问题时你能在几分钟内判断出问题出在数据层、业务层还是展示层而不是像无头苍蝇一样到处找。如果你是自己要开发这个项目而不是只跑别人代码我建议的路线是先建表再写MyBatis映射再写Service和Controller用Postman把后端接口全部测通最后才写Vue页面。这个顺序虽然看起来慢但每一步的成果都是可验证的不会出现写到最后一堆bug扎堆的情况。最后再分享一个小技巧出租车司机常说“跑得多不如认得路”开发也一样。这个项目做完真正值钱的不是那几万行代码而是你理解了“用户在前台点了一个按钮请求是怎么经过Vue路由、axios、SpringBoot Controller、Service、MyBatis、MySQL再返回数据渲染到页面上的”。把这个链路讲清楚不管答辩还是面试你都稳了。
返回列表