ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue前后端分离旅游管理系统实战与部署详解

SpringBoot+Vue前后端分离旅游管理系统实战与部署详解 最近在整理一套前后端分离的旅游管理系统刚好有不少朋友问我要完整的源码和部署教程索性把整个项目从架构设计、核心代码实现到服务器部署流程全部梳理出来。这套系统基于SpringBootVueMyBatisMySQL用户端和管理端分开覆盖了景点展示、线路预订、订单管理、评论回复等常见业务场景。对正在做毕业设计、课程设计或者想系统学习前后端分离项目实战的开发者来说代码量不大不小刚好能把登录鉴权、分页查询、文件上传、跨域处理、打包部署这些高频技术点串起来。我写的所有操作步骤都基于这套系统实际跑通的版本不是纸面教程。从本地开发环境搭建到最终用Nginx部署上线中间踩过的坑和排查过程都会写清楚你照着做基本能复现。如果你已经有一定Java和Vue基础可以直接跳到第三章看部署如果是刚接触前后端分离的新手我建议按顺序读每一步对后面都有影响。1. 项目整体设计与技术选型思路1.1 设计思路前后端分离到底在分离什么先聊一个容易被忽略的问题前后端分离不只是把代码分成两个目录本质上是把“数据接口”和“页面渲染”拆成两个独立交付物。后端只负责输出JSON数据前端负责拿到数据后渲染页面。这样做的好处很明显最直接的一点就是后端不用再关心浏览器兼容、页面跳转这些事专心把接口做好前端也不用被后端模板语法绑死Vue组件化的写法比JSP和Thymeleaf灵活太多。这套旅游管理系统在设计时就定了几条规矩后端统一返回Result结构里面包含code、message、data三个字段前端根据code判断请求是否成功。前端所有请求走axios通过拦截器统一带token。后端除了登录接口和获取验证码接口其余接口都校验token。管理员接口和用户接口通过角色字段区分权限而不是分别做两套登录逻辑。规矩定好之后前后端并行开发就非常顺畅。我在实际开发中先定义接口文档把URL、请求参数、返回结果都列清楚然后后端按接口开发前端用mock数据先做页面。这样两边不互相等等到了联调阶段问题基本就只剩跨域和字段名对不齐这种小事情。1.2 技术栈选型的几点考量选择SpringBoot而不是继续用SSM是因为SpringBoot解决了大量配置问题。以前搭SSM项目要写一堆XML配置SpringBoot直接starter引入依赖内嵌Tomcat一键启动这对团队协作和后续维护都很友好。Vue这边选择的是Vue 2版本不是因为我不会Vue 3而是这套系统里很多现成的管理端组件库在Vue 2生态下更稳而且大部分学校的毕业设计和教学资源还是Vue 2为主遇到问题搜起来方便。MyBatis和MyBatis Plus之间我最终选了MyBatis理由有两点第一旅游管理系统的查询场景比较多样比如按景区名称模糊查询、按价格区间筛选、按热门程度排序这些SQL用MyBatis的XML文件写起来更直观第二MyBatis对于学习SQL本身更有帮助如果你以后要面对复杂的报表统计场景直接写SQL的能力是躲不掉的。MySQL作为数据库没有悬念开源免费性能足够个人开发和小型景区完全够用。1.3 数据表设计与模块划分整个系统的业务不算复杂但表与表之间的关系值得理清楚。核心表有六张表名说明关键字段t_user用户表id, username, password, role, avatart_scenic景点表id, name, address, ticket_price, description, cover_imgt_route线路表id, scenic_id, days, price, schedule, statust_order订单表id, order_no, user_id, route_id, people_num, total_price, statust_comment评论表id, user_id, scenic_id, content, score, create_timet_favorite收藏表id, user_id, scenic_id, create_time数据库设计上有个小经验订单号不要在数据库里用自增id直接展示给用户而是用时间戳加随机数生成一个业务订单号这样既避免暴露真实数据量也方便后续对接支付平台时做唯一标识。另外价格字段全部用decimal不要用float否则金额计算会出现精度丢失。2. 核心代码实现与细节拆解2.1 登录鉴权JWT让前后端各自独立登录模块是每个系统都躲不开的部分。这套系统用的是JWT方案用户登录成功后后端生成一个token返回给前端前端把token存在localStorage里每次请求时通过axios拦截器放到请求头中。后端通过拦截器统一校验token解析出用户信息后放到ThreadLocal里供业务代码使用。JWT的主要代码结构如下// JwtUtil.java 核心方法 public class JwtUtil { private static final String SECRET your-secret-key; public static String generateToken(User user) { // 设置过期时间 7天 return Jwts.builder() .setSubject(user.getUsername()) .claim(userId, user.getId()) .claim(role, user.getRole()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }拦截器这边的核心逻辑是从请求头获取token解析失败直接返回401状态码解析成功就把userId和role放进去放行接口。需要注意一点JWT加解密本身不复杂但密钥一定要放到配置文件里不要硬编码在代码里。我之前见过有人把密钥写在类里面就提交到Github这种低级错误很容易被扫描工具抓出来。实际开发中有个很容易忽略的问题token过期时间怎么定。设置太短用户频繁重新登录体验很差设置太长被截获后风险越大。我通常的做法是7天过期然后前端在axios响应拦截器里判断code为401时跳转到登录页。如果要做“记住我”功能再单独生成一个refresh_token来续期这套系统为了控制复杂度就没有做。2.2 MyBatis分页查询PageHelper的正确用法景区列表、订单列表、评论列表都是典型分页场景这里重点说PageHelper的用法因为这个插件虽然简单但用错的人非常多。先看正确写法// 分页查询景点列表 public PageResultScenicVO getScenicList(int pageNum, int pageSize, String keyword) { PageHelper.startPage(pageNum, pageSize); ListScenicVO list scenicMapper.selectScenicList(keyword); PageInfoScenicVO pageInfo new PageInfo(list); // 返回总记录数和列表数据 return PageResult.of(pageInfo.getTotal(), pageInfo.getList()); }对应的Mapper XML如下select idselectScenicList resultTypecom.example.vo.ScenicVO SELECT s.id, s.name, s.address, s.ticket_price, s.cover_img, r.id AS route_id, r.price AS route_price FROM t_scenic s LEFT JOIN t_route r ON s.id r.scenic_id where if testkeyword ! null and keyword ! AND (s.name LIKE CONCAT(%, #{keyword}, %) OR s.address LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY s.create_time DESC /selectPageHelper的核心原理是在你执行第一条SQL之前通过拦截器自动生成一条COUNT查询和分页查询再把结果封装到Page对象里。所以有一个铁律要注意PageHelper.startPage()必须紧接着你的Mapper查询方法中间不能插入其他SQL查询。我见过有人在startPage后面先执行了一次别的查询结果分页参数全部飘到那条无关SQL上查回来的数据完全不对。另一个容易踩的坑是分页和关联查询的性能问题。比如查询订单列表时如果你在XML里写了嵌套的collection查询PageHelper会自动对主SQL做count但子查询不会被拦截最终统计的总记录数可能是正确的但每页数据里多条记录重复。解决方法是先把主表分页查出来再通过IN查询或者第二步查询补充关联信息也就是平时说的“分页后组装”。除了分页查询MyBatis的SQL打印也是一个高频需求。开发阶段你需要看到实际执行的SQL和参数配置很简单在application.yml里加mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: truemap-underscore-to-camel-case这个配置特别关键数据库字段create_time可以自动映射到实体类的createTime省去大量写resultMap的时间。不过提醒一句如果业务表里存在userName和user_name这种容易误伤的情况记得先把自动映射关掉用resultMap手动指认。2.3 视频播放与m3u8景区宣传视频怎么接旅游类项目基本都要放宣传视频这里说一个网上讨论很多的问题Vue怎么播放m3u8格式的视频流。m3u8是HLS协议下的视频索引文件很多景区监控、直播流、云端视频都采用这个格式。用Vue播放m3u8的办法不止一种最稳的组合是video.js加上videojs-contrib-hls插件。先说引入方式。如果你的项目里直接用script引包那就引这几个文件link hrefhttps://cdn.jsdelivr.net/npm/video.js7/dist/video-js.min.css relstylesheet script srchttps://cdn.jsdelivr.net/npm/video.js7/dist/video.min.js/script script srchttps://cdn.jsdelivr.net/npm/videojs-contrib-hls5/dist/videojs-contrib-hls.min.js/script如果是用npm管理依赖执行安装npm install video.js videojs-contrib-hls --save在Vue组件里的用法如下template video idmyVideo classvideo-js vjs-default-skin controls preloadauto width800 height400/video /template script export default { mounted() { let player videojs(myVideo, { autoplay: true, sources: [{ src: https://your-server.com/video/demo.m3u8, type: application/x-mpegURL }] }); player.on(error, function () { console.error(视频播放出错); }); } } /script实际使用中你会发现有些m3u8链接在浏览器里能直接打开播放有些却不行。这大概率不是前端的问题而是服务器没返回正确的Content-Type或者视频源存在跨域限制。排查方法很简单用浏览器F12看network请求如果视频分片请求出现跨域报错就需要在Nginx层配置跨域响应头。另外本地开发时会发现明文http的m3u8地址在Chrome下可能被拦截因为Chrome对混合内容Mixed Content有安全限制你公司内网和景区提供的视频源如果没上HTTPS就只能在部署阶段绑定域名证书来解决。2.4 安全处理上传场景下的XSS防御旅游管理系统里有一个常见功能管理员上传PDF介绍文件、用户上传头像。上传本身不复杂重点是不要忽略安全过滤。之前的项目就在上传PDF时遇到过XSS攻击问题因为PDF本身可以被嵌入恶意脚本在上传时如果不对文件内容和文件名做过滤恶意文件会在用户端触发脚本执行。SpringBoot里做一个全局过滤器去处理上传请求的安全问题思路是拦截所有multipart请求检查文件类型、文件大小和文件内容中的可疑脚本特征。下面的代码是一个简化版过滤器Component public class XssAndFileFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest (HttpServletRequest) request; if (httpRequest.getContentType() ! null httpRequest.getContentType().toLowerCase().contains(multipart/form-data)) { // 包装请求对文件名和文件内容做校验 XssRequestWrapper wrapper new XssRequestWrapper(httpRequest); chain.doFilter(wrapper, response); return; } chain.doFilter(request, response); } }XssRequestWrapper的核心逻辑是重写getParameter方法对参数值里的script标签、事件属性等做过滤转义。需要注意这种全局过滤会带来一个副作用接口返回的JSON里的富文本内容也被转义了导致前端无法正常显示html。我当时的处理方案是配置一个白名单评论内容因为允许用户写一些简单表情符号所以只过滤危险关键字不整体替换后台富文本编辑的内容则直接跳过过滤。还有一个非常关键的实践点文件上传目录一定不能放在项目classpath里。我见过有人把上传的文件直接保存在resource/static/upload下一旦重新打包部署上传的文件全部丢失而且classpath下的文件对外访问容易暴露路径。正确做法是在服务器上建独立的upload目录通过Nginx映射成静态资源路径数据库里只存相对路径。3. 前后端联调与部署实战3.1 Vue环境配置与代理调试先把Vue开发环境配好。安装Node.js之后我用npm安装依赖但国内网络情况下npm install失败率很高建议直接配置淘宝镜像源。命令行执行npm config set registry https://registry.npmmirror.com然后进入前端项目目录安装依赖并启动开发服务npm install npm run serve前端开发服务默认跑在8080端口后端SpringBoot跑在8081端口。前后端分离开发时最大的障碍就是跨域但这里我建议开发阶段不要习惯性用后端注解CrossOrigin解决问题因为一旦到生产环境用Nginx反代CrossOrigin配置反而容易引发更复杂的CORS冲突。更优雅的方式是在vue.config.js里配置代理module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true, pathRewrite: { ^/api: } } } } };这样配置之后前端页面请求/api/scenic/list开发服务器会自动转发到http://localhost:8081/scenic/list。浏览器里看不到跨域请求因为浏览器只认识同源的8080端口。这个方案的好处是前端代码里可以统一写相对路径等到了生产环境只需要通过Nginx做相同规则的转发前端代码一行都不用改。3.2 SpringBoot打包与jar部署后端部署的第一步是打包。如果用的是Maven执行mvn clean package -DskipTests打包完成后target目录下会生成一个jar文件。SpringBoot内嵌Tomcat所以只要服务器上有JDK环境就能直接运行。我推荐在服务器上用以下命令启动nohup java -jar tourism-system.jar --spring.profiles.activeprod server.log 21 这里用--spring.profiles.activeprod指定生产环境配置文件。生产环境至少要改三处数据库连接信息、日志级别、文件上传路径。数据库这部分尤其注意别把本地开发库的账号密码直接带到生产环境这不是什么大道理但是我在实际交接项目中真的见过有人把password写死成123456。如果你非得部署到外部Tomcat而不是用内嵌Tomcat需要改一下打包方式packagingwar/packaging再加上SpringBootServletInitializer子类。不过我的建议很明确能用jar就用jar。war包部署到Tomcat后你不能随意指定端口而且外部Tomcat版本和SpringBoot版本之间的兼容性偶尔会出幺蛾子。jar包部署在进程管理上更干净日志输出也容易用shell重定向统一管理。3.3 Nginx反向代理与前端路由刷新404前端打包同样是常规操作npm run buildbuild完成后dist目录就是纯静态文件把整个dist目录上传到服务器然后配置Nginx反向代理。这里放一个我验证过的Nginx配置同时解决两个大问题接口转发和页面刷新404。server { listen 80; server_name your-domain.com; # 前端静态资源 root /usr/share/nginx/html/tourism; index index.html; # 接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8081/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 前端history路由刷新支持 location / { try_files $uri $uri/ /index.html; } # 上传文件静态访问 location /upload/ { alias /data/tourism/upload/; } }try_files $uri $uri/ /index.html;这行是Vue路由history模式必须配置的。不配置的话你访问/scenic/1这个页面刷新一下服务器找不到对应的物理文件直接返回404很多第一次部署的人都会在这里卡住。如果你用的是hash路由URL里会带#号不需要这行配置也能刷新但URL不美观而且部分分享场景带#号有转义问题所以项目里统一用了history模式。Nginx配置完成后记得执行nginx -t检查配置语法然后nginx -s reload生效。我遇到过一种情况前端build之后页面空白打开控制台发现js文件404原因是dist里的js文件名带了hash但是本地缓存了旧的index.html。这种问题一般不是配置问题而是浏览器缓存先强制清缓存试一次别再反复重定向了。3.4 用Jenkins在Windows上做一键部署如果项目迭代频率高手动打包上传发布实在是浪费时间。我在Windows服务器上搭过Jenkins来做自动部署。最简单的流程是代码推送到Git仓库后Jenkins拉取代码执行前端build和后端package最后把产物复制到指定目录并重启服务。Windows下Jenkins部署前后端分离项目的几个关键点Jenkins安装时选择作为Windows服务运行开机自启。执行shell的地方换成“Execute Windows batch command”。后端构建用mvn clean package -DskipTests注意在系统环境变量里配置JAVA_HOME和MAVEN_HOME。前端构建用npm install npm run build注意Jenkins服务默认账号可能没有npm的全局路径需要在系统环境变量里手动加上Node.js的安装目录。发布这一步可以用批处理脚本完成echo off set APP_PATHD:\deploy\tourism echo 停止旧服务 wmic process where commandline like %%tourism-system.jar%% call terminate timeout /t 5 /nobreak nul echo 复制jar包 copy /Y D:\workspace\tourism\backend\target\tourism-system.jar %APP_PATH%\tourism-system.jar echo 复制前端静态文件 xcopy /E /Y /I D:\workspace\tourism\frontend\dist %APP_PATH%\static\dist echo 启动服务 start javaw -jar %APP_PATH%\tourism-system.jar echo 部署完成这个流程比较粗糙但胜在简单有效。如果再加一步把上传目录同步也写进脚本基本可以做到全自动发布。Jenkins最容易被忽视的问题是时区设置默认Jenkins时区不是东八区构建日志时间会差8个小时排查问题时会看懵可以在系统设置里把时区改成Asia/Shanghai。4. 常见问题与排查技巧实录4.1 MySQL安装和配置的那些坑MySQL的安装教程网上非常多这里不再重复下一步点哪里只说我实际遇到过且高频的几个问题。第一个是安装到最后一步卡在“starting server”无法启动。这个问题在大都是因为3306端口被占用或者是my.ini里datadir指定的目录权限不对。处理办法是先用命令检查端口占用情况然后把my.ini里的datadir指到一个完全空白的文件夹重新初始化数据目录。绝对不要直接删除C:\ProgramData\MySQL目录来重置那样容易把注册表信息弄乱。第二个是root密码没记住。装完MySQL想登录发现密码不对如果是在Windows上可以通过跳过权限表的方式重置密码在my.ini里加一行skip-grant-tables重启MySQL然后用命令更新root密码。重置成功之后务必注释掉这行再重启服务。这个方法我自己用了不下五次每次都是因为安装时设置的密码忘了。第三个是MySQL 8.0的默认认证插件问题。MySQL 8.0默认用caching_sha2_password而一些旧版本的驱动和图形化工具不支持这种认证方式连接时会报Authentication plugin错误。解决方式有两种一种是把连接驱动和工具升级到兼容版本另一种是执行SQL把该用户改回mysql_native_password模式。我在实际项目中为了让团队同事直接用Navicat连数据库一般直接改掉。4.2 MyBatis日志与缓存问题排查MyBatis在开发阶段有一套典型问题。首先是动态SQL写错了却不报错只是查出来的数据不对。最常见的是SET子句写成了WHERE或者if标签里漏掉AND导致SQL拼接失败。排查这类问题时把SQL打印出来是最直接的。如果没有配置日志输出你甚至不知道MyBatis到底执行了什么语句。我个人的习惯是在开发初期就配置打印SQL然后还有一个加分项利用MyBatis的慢SQL日志功能。mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl还有Mapper接口和XML绑定失败的报错比如Invalid bound statement (not found)。这种问题九成是因为XML文件的namespace写错了或者Mapper接口的方法名和XML里的id没对上也可能是因为Idea没有把xml文件作为资源文件复制到target目录。记得在pom.xml里配置build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource /resources /buildMyBatis的一级缓存和二级缓存也是要留意的地方。默认情况下一级缓存作用域是SqlSession同一个SqlSession里执行两次相同查询只会查一次数据库。但在Spring整合下如果每次请求都创建新的SqlSession其实一级缓存意义不大。二级缓存默认是关闭的如果你手动开启一定要考虑到数据变更后缓存失效问题尤其是景区价格、线路状态这些频繁变化的字段一不小心就会读到脏数据。我的经验是时刻表和价格表这种高频率更新数据完全不需要开二级缓存。4.3 Vue和SpringBoot联调常见问题先说跨域请求报错。开发环境配了代理就不应该报跨域如果你配了代理还报错八成是前端请求的URL开头没带/api请求根本没走代理规则。用F12的Network面板看一眼请求URL是http://localhost:8080/api/...还是http://localhost:8081/api/...马上就能定位。第二个是会话失效问题。系统用户登录后过一段时间再操作就跳回登录页。之前遇到过明明JWT过期时间设置的是7天但每次半小时就失效了。后来排查发现是前端每次刷新页面后重新从localStorage里拿token而localStorage的key在代码里写错了导致拿不到token。这种事在开发中挺常见的最好在封装request工具类时统一处理token的存取。第三个是文件上传的大小限制。SpringBoot默认单个文件最大1MB景点图片稍大一点就上传失败。需要在配置里调大spring: servlet: multipart: max-file-size: 20MB max-request-size: 100MB注意联调时前端上传组件那边也有自己的限制ElementUI的Upload组件有个limit属性两个限制要一起改只改后端前端照样报错。4.4 一些实用的小技巧这套系统开发下来有几个小工具和习惯让我省了不少时间。后端接口调试我一直用Postman但有个更好用的细节通过导入OpenAPI格式的接口文档Postman可以自动生成所有接口的请求样例省去手工填参数的麻烦。SpringBoot里配合springdoc-openapi能够自动生成接口文档。前端调试我推荐Vue Devtools确实可以很直观地看到组件数据变化、路由跳转记录和Vuex状态更新的时间线。但很多人忽略一点Devtools在生产环境中一定要禁用可以在main.js判断一下环境变量再决定是否引入。数据库设计阶段有个习惯很重要每张表必须有create_time和update_time两个字段。这套系统里所有订单和评论都按时间排序如果没有时间字段后面加功能就得改表加列非常被动。还有一个细节时间字段不要用varchar存储直接用datetime排序比较都不要格式化转换省事且准确。最后分享一个小经验我在本地开发时后端接口如果是从Github上拉下来的项目跑不起来不要先怀疑代码有问题先用mvn clean compile看一下依赖是否完整下载然后看数据库连接是否通再看端口是否有冲突。我排查过无数次项目跑不起来的问题80%以上都是数据库密码错或者Redis没启动代码本身反而很少有问题。这套系统做完之后我自己最大的收获不是学会多少新技术而是把以前零散的前后端分离知识点串成了一条完整的链路。从写接口、调试接口、打包部署到上线每个环节都踩过坑也都在不断复盘。如果你正在用自己的项目练手建议一定完整走一遍部署流程不要只满足于本地能跑通。部署过程中暴露出来的问题才是真正能拉开差距的经验。
返回列表