ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue旅游平台实战:前后端分离部署与避坑指南

SpringBoot+Vue旅游平台实战:前后端分离部署与避坑指南 收到一个挺典型的项目案子——基于SpringBootVue的甘肃旅游服务平台管理系统。这套东西这几年在课程设计、毕业设计、Java培训项目里出现频率非常高网上也流传着各种版本的源码。但说实话大部分拿到手的人要么跑不起来要么跑起来之后不知道每个配置是干嘛的改个需求就得翻半天代码。这篇文章我打算把这类前后端分离旅游平台的完整实现思路拆开讲清楚从技术选型、数据库设计到前后端关键代码、部署运行再到我实际调试中踩过的坑一次性说明白。不管你是拿源码做课设还是想自己从零撸一个旅游平台练手这篇文章都能让你少走不少弯路。1. 项目到底在做什么技术栈为什么这么搭先说清楚这个项目的本质。它不是一个简单的静态网页展示而是一套完整的旅游业务管理系统核心服务对象是游客和平台管理员。游客端能看景点、查攻略、订酒店、规划路线管理员端则负责维护景点信息、管理订单、审核评论、统计分析旅游数据。这套系统我拆解下来业务面覆盖得比较全用户注册登录、景点分类检索、旅游攻略发布、酒店在线预订、订单状态管理、后台数据统计这些都是旅游平台的标配模块。你要是做课设答辩光这些功能就足够撑起一个像样的项目了。技术栈选型上SpringBootVueMyBatisMySQL这个组合在前端分离项目里算是黄金搭档原因也很实在SpringBoot负责后端业务逻辑内置Tomcat配置简化到极致一个main方法就能跑起整个服务省掉一堆XML配置的繁琐事。Vue做前端SPA单页应用组件化开发维护方便配合Element UI能很快搭出后台管理界面路由控制和状态管理都有成熟方案。MyBatis负责数据库操作SQL写得灵活可控尤其适合景点查询、订单统计这类带复杂条件拼接的场景相比JPA那种全自动ORM更直观、更容易调优。MySQL存储业务数据开源免费、性能稳定学校机房和云服务器都能轻松部署是这类系统事实上的默认选择。这套组合还有一个隐形好处招聘市场上这类岗位需求多相关教程和问题解决方案沉淀丰富遇到任何问题都能搜到答案。2. 数据库设计与建模思路跑通源码之前先把数据模型看懂。旅游服务平台的核心表设计直接决定了业务逻辑能不能顺畅跑起来。2.1 数据表整体规划一套完整的旅游平台至少需要这几类表用户表user存储游客和管理员账号区分角色权限。核心字段包括用户名、密码加密存储、昵称、手机号、头像、角色标识、注册时间。景点表scenic_spot展示旅游资源的根本也是整个系统信息量最大的表。包括景点名称、所在城市、景点简介、封面图、详细信息、门票价格、开放时间、经度纬度、评分、状态。酒店表hotel支持在线预订就需要维护酒店数据。字段要覆盖酒店名称、星级、地址、房型价格、图片、联系电话、剩余房量、是否可预订。攻略表strategy用户和运营者发布的游玩攻略是提升平台内容价值的关键。包括标题、封面、正文内容、作者ID、浏览量、点赞数、发布时间。订单表orders记录用户的预订行为包括订单编号、下单用户、关联酒店或景点、入住日期、数量、总金额、支付状态、订单状态、创建时间。评论表comment游客对景点或酒店的评价包括关联目标类型和ID、用户ID、评分、内容、回复内容、状态。收藏表favorite用户收藏的景点或攻略记录用户ID、目标类型、目标ID、收藏时间。2.2 景点与城市关联设计甘肃旅游有个很明显的业务特点景点极度分散在十四个地州市敦煌、兰州、天水、张掖、嘉峪关各有特色。这种地域性强的数据在设计上建议单独做一个城市表city景点表用外键关联城市ID。这样前端就能实现按城市筛选景点的效果后台统计也能按地区维度出报表。城市表字段很简单城市ID、城市名称、所在省份、城市简介、经度纬度、排序值。2.3 表设计必须注意的几个细节密码字段不要明文存储使用BCrypt加密后写入哪怕数据库泄露也不至于直接暴露账号密码。金额字段用decimal(10,2)别用double或float否则订单金额计算会出现精度偏差这在支付类场景里是大忌。状态字段建议用tinyint配合注释说明含义比如景点状态0下架1上架订单状态0待支付1已支付2已取消3已完成。比起直接用字符串数字类型存储占用更小、查询更快注释能让维护者一眼看懂。所有表都建议加上create_time和update_time字段排查问题时能判断数据是什么时候写入或变更的。3. 后端核心实现SpringBootMyBatisMySQL后端是整个系统的心脏我按功能模块把关键代码逻辑拆开讲。3.1 项目初始化与分层结构创建一个SpringBoot项目时推荐用Spring Initializr快速生成Maven坐标选择spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、lombok这几个核心依赖。工程结构建议按controller → service → mapper三层划分严格分层com.gs.travel ├── controller # 接收前端请求返回JSON ├── service # 业务逻辑处理 ├── mapper # MyBatis数据访问接口 ├── entity # 数据库实体类 ├── dto # 前端交互数据传输对象 ├── config # 配置类跨域、拦截器、分页插件等 ├── common # 统一返回结果、异常处理、工具类按这种分包方式写一是逻辑清晰容易维护二是课设答辩时老师问你架构是怎么设计的你能讲出三层架构的职责边界和调用关系印象分直接拉满。3.2 配置文件要点application.yml是后端命脉数据源配置直接决定项目能不能跑起来spring: datasource: url: jdbc:mysql://localhost:3306/gansu_travel?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.gs.travel.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这里有几个细节值得注意serverTimezoneAsia/Shanghai必须加上否则MySQL 8.x连接时区报错直接启动失败。map-underscore-to-camel-case开启下划线转驼峰数据库字段create_time才能自动映射成Java属性createTime少写大量ResultMap。log-impl配置成StdOutImpl方便开发阶段在控制台查看SQL日志排查问题神器。3.3 MyBatis分页插件配置旅游平台的景点列表、攻略列表、订单列表全都需要分页MyBatis分页插件PageHelper是标配方案。记得选对版本PageHelper 5.x对应MyBatis 3.xSpringBoot 2.x项目用pagehelper-spring-boot-starter版本号用1.4.x。引入依赖后配置一个Configuration类Configuration public class MybatisPlusConfig { Bean public PageInterceptor pageInterceptor() { PageInterceptor interceptor new PageInterceptor(); Properties properties new Properties(); properties.setProperty(helperDialect, mysql); properties.setProperty(reasonable, true); interceptor.setProperties(properties); return interceptor; } }使用方法极其简单在Mapper方法执行前调用PageHelper.startPage(pageNum, pageSize)紧随其后的第一条查询就会自动拼接LIMIT语句public PageResultScenicSpotVO getScenicList(int pageNum, int pageSize, String cityId) { PageHelper.startPage(pageNum, pageSize); ListScenicSpotVO list scenicSpotMapper.selectScenicList(cityId); PageInfoScenicSpotVO pageInfo new PageInfo(list); return PageResult.success(pageInfo.getList(), pageInfo.getTotal()); }注意一个高频坑startPage()后面必须紧跟Mapper查询方法中间不能有任何其他SQL操作否则分页会串到别的查询上导致数据错乱。这个我调试时踩过不止一次。3.4 统一返回结果与全局异常处理前后端分离项目最忌讳返回格式五花八门约定统一JSON结构能让前端封装一次到处使用。定义一个Result类public class ResultT { private Integer code; // 200成功500失败401未登录 private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }光有统一返回还不够后端难免抛出各种异常用RestControllerAdvice做全局异常兜底避免把堆栈信息直接暴露给前端RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(BusinessException.class) public Result? handleBusinessException(BusinessException e) { return Result.error(e.getMessage()); } ExceptionHandler(Exception.class) public Result? handleException(Exception e) { log.error(系统异常, e); return Result.error(系统繁忙请稍后重试); } }3.5 登录鉴权与Token方案旅游平台需要区分游客和管理员权限后端必须做登录鉴权。最简实现方案是使用JWT生成Token登录成功后把Token返回前端前端在请求头中携带后端用拦截器校验。JWT工具类核心方法就三个生成Token、解析Token、校验Token。Token中载荷claims存用户ID、用户名、角色过期时间设置为24小时。public class JwtUtil { private static final String SECRET gansu-travel-secret-key; private static final long EXPIRE_TIME 24 * 60 * 60 * 1000; public static String generateToken(Long userId, String username, String role) { return Jwts.builder() .claim(userId, userId) .claim(username, username) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } }拦截器里校验请求头Authorization字段解析失败就返回401前端检测到401自动跳转登录页。景区接口放行后台管理接口必须带Token访问。这里特别提醒JWT的密钥千万不要硬编码在代码里正式环境放到配置中心或环境变量中。课设无所谓但养成好习惯以后工作不吃亏。3.6 跨域配置方案前端Vue跑在localhost:5173后端SpringBoot跑在localhost:8080端口不一致直接导致浏览器跨域报错控制台一片红。后端有三种处理方式我习惯用WebMvcConfigurer配置全局限流Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意allowedOriginPatterns(*)配合allowCredentials(true)是SpringBoot 2.4以后的标准写法老版本用allowedOrigins(*)虽然也行但新版会直接报错。跨域问题在前后端分离开发中属于最容易卡新手的第一道坎配好后一劳永逸。3.7 文件上传与M3U8视频支持旅游平台经常要上传景点图片、酒店照片、攻略封面后端需要一个统一的文件上传接口。本地存储方案最简单接收到MultipartFile后生成UUID文件名按日期分目录存储返回访问URLPostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(请选择文件); } String originalFilename file.getOriginalFilename(); String extension originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID().toString().replace(-, ) extension; String datePath new SimpleDateFormat(yyyyMMdd).format(new Date()); String filePath uploadDir / datePath / fileName; file.transferTo(new File(filePath)); return Result.success(/upload/ datePath / fileName); }这里两个坑提醒一下上传目录要提前创建好否则transferTo报FileNotFoundException。还有大文件视频要设置spring.servlet.multipart.max-file-size和max-request-size默认1MB根本不够景点宣传视频用。设置了application.yml里的图片访问映射把本地磁盘路径映射成URL虚拟路径否则临时文件上传后访问不到图片。M3U8视频播放部分如果景点宣传用到m3u8格式的HLS流媒体前端用video.js的videojs-contrib-hls插件就能直接播放后端只需保证.m3u8文件和视频切片能被访问到即可。4. 前端核心实现Vue3Element Plus前端部分我推荐用Vue3 Vite Element Plus这套组合比Vue2的Webpack方案启动更快组件生态也更现代。4.1 Vue环境配置与项目初始化开发Vue项目先确认Node.js环境建议用16.x以上版本。然后Vite一行命令创建项目npm create vitelatest gansu-travel-web -- --template vue cd gansu-travel-web npm install npm run dev装依赖的过程如果网络慢可以把npm镜像切换成国内源速度立竿见影。项目跑起来后安装Element Plus和Vue Router、Axiosnpm install element-plus vue-router4 axiosElement Plus在入口文件全量引入即可课程设计项目追求效率优先不用太纠结按需加载的性能优化import { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue import router from ./router const app createApp(App) app.use(ElementPlus) app.use(router) app.mount(#app)4.2 路由设计与参数传递前端路由是整个SPA的地图按页面角色划分模块const routes [ { path: /, component: Home, meta: { title: 首页 } }, { path: /scenic, component: ScenicList, meta: { title: 景点列表 } }, { path: /scenic/:id, component: ScenicDetail, meta: { title: 景点详情 } }, { path: /hotel, component: HotelList, meta: { title: 酒店预订 } }, { path: /strategy, component: StrategyList, meta: { title: 旅游攻略 } }, { path: /login, component: Login, meta: { title: 登录 } }, { path: /admin, component: AdminLayout, meta: { title: 管理后台, requiresAuth: true } }, ]路由参数传递最常见的场景是点击景点列表跳转到详情页。声明式跳转用to/scenic/ id查询参数方式用to{ path: /scenic, query: { id: row.id } }。详情页通过route.params.id或route.query.id读取参数再请求后端接口获取详情数据。需要提醒的是Vue Router 4和Vue2的Vue Router 3写法差别不小new Router()变成了createRouter()mode: history变成了createWebHistory()照着老教程写会报错。4.3 Axios封装与请求拦截Axios直接import axios from axios然后到处写axios.get虽然能用但一改接口地址就得全项目替换太痛苦。建议在utils/request.js里统一封装import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: http://localhost:8080/api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) request.interceptors.response.use( response { const res response.data if (res.code 401) { localStorage.removeItem(token) router.push(/login) return Promise.reject(new Error(未登录)) } return res }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } ) export default request这样每个页面调用时关注业务逻辑就好登录状态判断、错误提示全部收敛到一处。页面里请求后端数据就变得非常干净const res await request.get(/scenic/list, { params: { cityId, pageNum, pageSize } })4.4 核心页面实现详解景点列表页是用户最先看到的模块之一通常分为左侧筛选区城市、门票价格区间、搜索关键字和右侧景点卡片列表。筛选条件绑定到ref对象中修改后重新请求接口const queryParams reactive({ cityId: , keyword: , pageNum: 1, pageSize: 12 }) const fetchData async () { const res await request.get(/scenic/list, { params: queryParams }) if (res.code 200) { scenicList.value res.data.list total.value res.data.total } } watch(queryParams, fetchData)后台管理页布局采用经典左侧菜单右侧内容区结构菜单项包括景点管理、酒店管理、订单管理、用户管理、攻略审核、数据统计。每个子页面是独立的Vue组件通过子路由挂载到AdminLayout下面。表格用Element Plus的el-table分页用el-pagination表单弹窗用el-dialog配合表单校验规则后台CRUD界面一天就能撸完。数据统计模块可以集成ECharts做可视化大屏比如柱状图展示甘肃各城市景点数量排行折线图展示平台近7日订单趋势饼图展示评论评分比例分布。这块做好了答辩时极具视觉冲击力老师一眼就能看出项目完整度。import * as echarts from echarts const initChart (data) { const chart echarts.init(document.getElementById(statisticsChart)) chart.setOption({ xAxis: { data: data.cities }, yAxis: {}, series: [{ type: bar, data: data.counts, itemStyle: { color: #409EFF } }] }) }4.5 前端M3U8视频播放的坑景点详情页经常要放风光宣传视频很多素材是m3u8格式。Vue里播放m3u8最省事的方式还是video.js配合hls插件npm install video.js videojs-contrib-hlstemplate video idmy-video classvideo-js vjs-default-skin controls preloadauto width100% height400 /video /template script setup import videojs from video.js import video.js/dist/video-js.css import videojs-contrib-hls onMounted(() { videojs(my-video, { sources: [{ src: videoUrl.value, type: application/x-mpegURL }] }) }) /script这里有个典型坑如果m3u8文件涉及跨域访问后端必须配置对应的CORS头否则视频一直卡在黑屏转圈。另外Safari浏览器原生支持HLS但Chrome必须代码方式处理所以统一走video.js最稳。5. 源码本地运行与部署实操拿到了源码第一步就是让它在你本地跑起来。我按前后端分别梳理启动步骤。5.1 后端环境准备后端需要JDK 8或11SpringBoot 2.x系列对应JDK 8如果你的源码是SpringBoot 3.x则需要JDK 17注意区分、Maven 3.6、IDEA开发工具2021.1以上版本这三个基础工具。JDK安装注意配置JAVA_HOME环境变量Maven安装后修改settings.xml里的本地仓库路径和阿里云镜像否则依赖下载慢到怀疑人生。IDEA打开源码工程后等待Maven自动下载依赖。这一步网络不好可能耗时很久如果发现依赖下载失败检查Maven镜像配置把中央仓库地址换成阿里云mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror5.2 数据库初始化MySQL安装完成后创建一个名为gansu_travel的数据库然后导入源码中的SQL文件。推荐用Navicat或DataGrip导入直接执行SQL脚本即可CREATE DATABASE IF NOT EXISTS gansu_travel DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE gansu_travel; source /path/to/gansu_travel.sql;SQL文件通常包含建表语句和基础数据导入后注意检查几个关键点管理员账号是否已初始化、景点测试数据是否完整、城市表是否有数据。如果发现管理后台登录不了大概率是admin账号没初始化手动执行一条INSERT即可。5.3 后端启动流程改好application.yml里的数据库账号密码确认端口8080没被占用直接运行TravelApplication主类。看到类似Started TravelApplication in 4.32 seconds日志时后端就启动成功了。建议先验证一个接口通不通浏览器访问http://localhost:8080/api/scenic/list能返回JSON数据说明整个后端链路通畅。5.4 前端启动与打包部署前端项目根目录执行npm install装依赖如果源码用的是Vue2注意Node版本不要太高建议14~16新版Node跑老Webpack项目经常报OpenSSL错误。出现这个报错时在package.json的scripts里加一行dev: SET NODE_OPTIONS--openssl-legacy-provider vue-cli-service serve能临时解决。装完依赖npm run dev启动开发服务器浏览器访问http://localhost:5173配合后端接口联调。开发测试完毕需要部署上线时执行npm run build打包生成dist静态目录交给Nginx托管server { listen 80; server_name your-domain.com; root /usr/share/nginx/html/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080; } }try_files指令是Vue路由history模式部署的精髓不配置的话刷新页面直接404。/api/反向代理解决前后端接口跨域问题上线后就不需要后端CORS配置了。6. 常见问题与排查技巧这部分是我实际调试源码过程中积累的经验也是这套系统最容易翻车的地方整理成速查表供你对照排查。6.1 数据库连接失败MySQL连接报错一般分三种数据库账号密码错误检查application.yml、MySQL服务没启动Windows服务管理器里启动mysql服务、时区错误URL里加serverTimezoneAsia/Shanghai。还有一种隐蔽情况——MySQL 8.x使用com.mysql.cj.jdbc.Driver驱动5.x用com.mysql.jdbc.Driver写反了同样启动报错。6.2 MyBatis分页不生效PageHelper.startPage()调用后紧接着的那条Mapper方法没有分页效果多半是分页拦截器没有被SpringBoot加载。检查PageInterceptor是否被Bean注册或者配置类是否被主类扫描到。另一种情况是调用startPage()后没有马上执行SQL中间插入了其他逻辑把分页线程变量消耗掉了。6.3 MyBatis缓存导致数据不一致MyBatis一级缓存默认开启同一个SqlSession内重复执行相同SQL直接返回缓存结果这在单次请求内往往没问题。但二级缓存如果配置不当且没有及时清理修改数据后查询结果还是旧数据就是典型的缓存脏读问题。解决思路分两层开发阶段把MyBatis的缓存日志打开观察SQL是否真的发送到数据库对频繁修改的表在Mapper XML里设置useCachefalse或者干脆不开启二级缓存。旅游平台这类查询多、修改频繁的业务二级缓存的收益不大别过度设计。6.4 查询速度慢接口响应慢先看SQL日志分析执行计划。排查思路是接口查的字段是否走了索引多表关联是否产生全表扫描LIKE %关键词%前缀模糊查询是否造成索引失效。旅游平台最常用的查询场景是景点列表按城市筛选、按名称模糊搜索city_id和name字段一定要建索引ALTER TABLE scenic_spot ADD INDEX idx_city_id (city_id); ALTER TABLE scenic_spot ADD INDEX idx_name (name);点赞量、浏览量统计类的COUNT查询如果数据量大且实时性要求不高可以先聚合到一张统计表定时任务更新而不是每次都扫全表。6.5 SpringBoot版本过高导致的兼容问题拿到一个老源码直接在官网初始化最新版SpringBoot结果各种报错。典型的比如SpringBoot 3.x改用jakarta.*命名空间老代码里javax.*的包全部编译失败。还有spring-boot-starter-web版本升级到3.x后部分第三方starter没有适配。处理方式很直接查看源码pom.xml里锁定的SpringBoot父版本原样依赖不要轻易升级。需要升版本时检查三件事——JDK版本、javax换jakarta、第三方starter的兼容性。6.6 前端刷新404与本地接口不通Vue开发模式下刷新404把createWebHistory改成createWebHashHistory即可上线环境用Nginxtry_files解决。本地开发接口报跨域优先检查后端CORS配置是不是被拦截器拦截了预检请求OPTIONS请求需要无条件放行否则前端真实请求根本发不出去。7. 安全细节与上线前清单源码能跑起来只是第一步真正拿得出手的项目还得过安全这道关。7.1 密码加密存储用户密码绝不能明文落库。Spring Security自带的BCryptPasswordEncoder是业界标准方案同一密码每次加密结果不同数据库泄露也不容易撞库。后端校验时用matches()方法比对原文和密文。7.2 防SQL注入与XSSMyBatis的#{}预编译机制天然防SQL注入但要注意有人图省事用了${}拼接这是注入口子。前端富文本编辑区提交的内容要过滤脚本标签最简单的方式是后端统一过滤器转义script标签。我之前处理过一个案例后台表单提交PDF上传时文件名做XSS校验时漏掉了重定向情况结果被插入了恶意脚本。虽然旅游平台攻击价值不高但养成好习惯很重要。7.3 上线前检查清单正式上线前过一遍这个清单数据库弱口令是否修改、管理员弱口令是否更换、application.yml敏感信息是否外置、后端临时上传目录是否清理、前端打包资源是否走CDN或压缩、HTTPS证书是否配置。这些做完整个项目才算真正达到可公开展示和部署的水平。整套系统从技术架构上看并不复杂难点全在地基工程数据库设计是否贴合业务、各个模块的边界是否清晰、细节配置是否有人踩过坑告诉你。希望这篇文章能让你在拿到源码时不再一头雾水而是能站在一个相对高的视角去理解整个项目让它真正变成你的东西。
返回列表