ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue旅游管理系统开发实战:从架构设计到功能落地

SpringBoot+Vue旅游管理系统开发实战:从架构设计到功能落地 1. 项目概述1.1 这是一套什么样的系统山清旅游公司管理系统从名字就能看出来这是给旅游公司做的一套内部业务管理平台。核心要解决的事情很清楚旅游公司的日常运营不外乎线路管理、订单处理、客户档案、财务对账、数据统计这几大块之前很多公司还在用Excel和纸质单据来回传效率低还容易出错这套系统就是把这一整套业务流程搬到线上让员工各司其职管理层能实时看到经营数据。从技术选型上看SpringBoot加Vue是当前Java后端加前端分离开发里最主流、也最适合毕设和企业级中小型项目的组合。SpringBoot负责提供稳定的后端接口服务处理业务逻辑、数据库交互、权限控制Vue负责前端页面展示和用户交互配合Element UI这类组件库能快速做出还看得过去的后台管理界面。这种前后端分离的架构既符合现在企业开发的主流模式也方便后期扩展成小程序端或者App端。这套系统适合谁参考如果你是在校学生准备做毕业设计这套系统的业务复杂度和技术难度刚好合适不会简单到没东西可写也不会复杂到做不完如果你是刚入行的Java开发想找一个完整的项目来练手理解企业级项目的标准写法这套系统同样值得过一遍。我在下面会把整个系统的设计思路、核心功能实现、踩过的坑都拆开讲尽量做到照着写就能跑起来。1.2 系统的核心价值在哪里很多人一上来就急着写代码其实先把系统的核心价值想清楚后面写起来会顺畅很多。旅游公司管理系统的核心价值我总结下来就是三个词管好线路、理清订单、看懂数据。管好线路指的是旅游产品的基础数据维护比如线路名称、行程天数、出发城市、目的地、价格、余位、上下架状态这些信息直接决定了公司有什么产品可以卖。理清订单指的是客户下单之后从订单创建、支付确认、出团通知到完成归档的全流程跟踪订单状态必须清晰可查。看懂数据指的是管理者需要知道每个月卖了多少单、营业额多少、哪条线路卖得最好、哪个月份是淡旺季这些统计报表可以辅助决策。围绕这三个核心价值系统的功能模块就很自然地拆出来了系统管理用户、角色、菜单权限、线路管理、订单管理、客户管理、统计报表。每个模块都是围绕业务需求来的不是凭空想出来的功能堆砌。2. 技术选型与整体设计思路2.1 为什么是SpringBoot加Vue这对组合后端选SpringBoot最大的原因就是它把SSHSpringMVC加Spring加Hibernate时代那一大堆繁琐的XML配置全部干掉了约定大于配置一个启动类就能把项目跑起来。对个人开发者来说这意味着可以把更多的精力放在业务代码上而不是折腾配置文件。SpringBoot的生态也足够成熟整合MyBatis操作数据库、整合Spring Security做权限控制、整合Redis做缓存都有非常成熟的方案踩坑资料也一搜一大把。前端选Vue是因为它的学习曲线相对平缓而且在国内的社区活跃度非常高。Vue的双向数据绑定机制配合Element UI的表格、表单、弹窗等现成组件做后台管理系统非常顺手。相比ReactVue在国内中小型项目和毕设里的使用率要高得多遇到问题更容易找到答案。前后端分离的开发模式下后端只需要提供JSON格式的RESTful API接口前端负责页面渲染和交互二者通过HTTP请求通信各干各的活互不干扰。这样做的好处是第一前后端可以并行开发不用互相等第二前端可以独立部署到Nginx后端部署到服务器性能上更好调配第三后期如果要开发手机端后端接口可以复用不用重写。2.2 系统架构的分层设计在正式动手写代码之前我建议先把整个项目的目录结构和代码分层想清楚。一个清晰的分层架构能让代码的可维护性大幅提升也方便后期扩展功能。这套系统的后端我采用的是经典的四层结构Controller层负责接收前端请求参数校验调用Service层处理业务最后把结果封装成统一格式返回给前端。Service层负责具体的业务逻辑处理比如下单时检查库存、计算价格、生成订单号这一层是业务规则的核心要保证业务逻辑的正确性和事务性。Mapper层DAO层负责与数据库交互通过MyBatis的XML文件或注解方式编写SQL语句完成数据的增删改查。Entity层实体层对应数据库中的表结构一张表对应一个实体类字段与表字段一一对应。除了这四层还需要有公共的工具类、配置类、异常处理类和统一的返回结果封装类。这就像盖房子要先搭好框架框架稳了往里面填业务功能就轻松了。2.3 数据库设计的关键点旅游公司管理系统的数据库设计我按照业务模块划分核心表有这些用户表sys_user存放系统登录用户的账号、密码、姓名、手机号、状态等信息。密码必须加密存储不能明文入库。角色表sys_role存放角色信息如管理员、销售员、财务等不同角色拥有不同的菜单和操作权限。菜单权限表sys_menu存放系统的菜单结构和按钮权限标识前端根据这个表动态生成侧边栏菜单后端在接口层面做权限拦截。旅游线路表tour_route存放线路的基础信息包括线路名称、线路编号、行程天数、出发城市、目的地、成人价格、儿童价格、总库存、剩余库存、线路介绍、封面图片、上下架状态等。这条表是整个业务的核心数据源。订单表tour_order存放客户的订单信息包括订单编号、线路ID、客户姓名、联系电话、出行人数、订单金额、订单状态待支付、已支付、已出团、已完成、已取消、下单时间等。客户表tour_customer存放客户的基本档案包括姓名、手机号、身份证号、会员等级、备注等方便做客户回访和二次营销。这六张表是系统的基础表与表之间的关联关系也不复杂用户和角色是多对多角色和菜单是多对多订单和线路是多对一订单和客户是多对一。设计表的时候字段类型和长度要提前规划好比如金额字段建议用DECIMAL而不是FLOAT避免浮点数精度丢失的问题时间字段统一用datetime类型每个表都建议加上create_time和update_time这两个公共字段方便排查问题。3. SpringBoot后端核心功能实现3.1 项目初始化与基础配置后端项目的创建我建议直接去Spring Initializr网站生成基础工程选择Java 8或者Java 11、SpringBoot 2.7.x版本然后在idea里导入即可。这里有个经验不要一上来就选最新的SpringBoot 3.x版本虽然新版本功能更强但是很多第三方组件的兼容性还不完善比如SpringBoot 3最低要求JDK 17MyBatis的整合方式也有变化对于学习项目来说2.7.x是最稳妥的选择。核心依赖pom.xml里面需要引入这几个关键组件dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependencymybatis用的数据库操作框架pagehelper是分页插件druid是阿里出品的数据库连接池比默认的HikariCP多了监控功能看SQL执行情况很方便java-jwt用来做登录后的令牌签发和验证lombok用来简化实体类的getter和setter。配置文件application.yml里数据源配置是关键要注意数据库连接地址、用户名、密码不要写错如果后续要部署到服务器建议把配置放到不同的profile中比如开发环境用application-dev.yml生产环境用application-prod.yml用spring.profiles.active来切换环境。3.2 登录鉴权与JWT令牌机制登录功能是整套系统的入口没有登录就别想干别的。这里我用JWT做无状态登录验证思路是这样的用户输入账号密码后端校验通过后生成一个包含用户ID、用户名、角色信息的Token返回给前端前端把Token存起来后续每次请求都放在请求头Authorization字段里后端通过一个拦截器拦截所有需要登录的请求校验Token的合法性合法就放行不合法就返回401提示重新登录。JWT的生成代码其实并不复杂private String generateToken(User user) { Algorithm algorithm Algorithm.HMAC256(secretKey); return JWT.create() .withClaim(userId, user.getId()) .withClaim(username, user.getUsername()) .withExpiresAt(new Date(System.currentTimeMillis() expireTime)) .sign(algorithm); }登录接口里先根据用户名查询用户然后用MD5或BCrypt校验密码我这里用的是BCrypt加密比MD5更安全校验通过后再查询用户拥有的角色和菜单权限一并封装到Token和登录返回结果里。前端拿到登录结果后根据角色信息动态渲染菜单。Token过期时间我设置为2小时这个时间比较合理太短了用户要频繁重新登录体验不好太长了不安全Token泄露了风险更大。3.3 我的分页查询方案列表页的数据查询一定离不开分页旅游线路可能有几百条订单可能上千条一次性全查出来既浪费网络带宽又影响页面加载速度。我采用的是PageHelper分页插件用法极其简单在Service层查询前调用PageHelper.startPage即可public PageResultRouteVO getRoutePage(RouteQuery query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); ListRoutePO routeList routeMapper.selectRouteList(query); PageInfoRoutePO pageInfo new PageInfo(routeList); return buildPageResult(pageInfo); }注意一个坑PageHelper.startPage后面必须紧跟第一条SQL查询语句中间不能有任何其他数据库操作否则分页会失效。而且startPage是线程绑定的一次请求只对这一次查询生效不用担心影响其他请求。分页的返回结构我统一封装成PageResult包含总记录数total、当前页数据list、页码pageNum、每页大小pageSize前端拿到后填充到表格组件里配合Element UI的pagination组件整个分页交互就完成了。3.4 统一返回结果封装前后端分离开发最忌讳的是Controller直接把实体类或者Map扔给前端这样接口的返回结构不统一前端处理起来非常痛苦。我定义了一个统一的返回结果类ResultData public class ResultT { private Integer code; private String message; private T data; }code为200表示成功500表示业务错误401表示未登录或Token过期。所有Controller接口的返回值都包装成Result前端拿到后先判断code再做后续处理逻辑统一、维护方便。与之配套的还有全局异常处理器用RestControllerAdvice注解定义一个类捕获Service层抛出的自定义业务异常和系统异常统一转成Result格式返回给前端。这样后端代码里不需要每一个catch都写返回值代码干净很多。3.5 RBAC权限控制的完整实现权限控制是管理系统的核心需求不能只有登录还要控制不同角色能看什么页面、能干什么操作。我的设计方案是经典的RBAC模型用户关联角色角色关联菜单权限。后端实现的思路登录成功后查询当前用户的角色和对应的菜单权限列表存到Redis缓存里并设计好键值结构。写一个拦截器或者AOP切面拦截所有的Controller请求在请求处理前检查当前用户是否有访问该接口的权限。具体做法是给每个接口定义一个权限标识符比如RequirePermission(route:add)然后在拦截器中检查当前用户的权限集合里是否包含这个标识。对于菜单级别的控制后端提供接口返回当前用户可访问的菜单树前端路由守卫配合动态路由实现不同用户登录后看到的侧边栏菜单不同。这个方案做下来系统的权限管理就比较完整了。实际操作中我发现很多初学者只做前端的菜单隐藏控制后端接口却不做检查这样只要通过接口工具直接请求就能绕过权限存在很大的安全隐患。真正的安全底线一定要放在后端。4. Vue前端核心开发要点4.1 前端项目结构与环境搭建前端使用的是Vue2加Element UI组合不要一上来就用Vue3Vue2的生态更成熟网上资料多踩坑后也有人给答案对学习项目来说足够用。搭建前端环境有几个必经步骤# 安装Node.js建议使用14.x或16.x版本 # 检查node和npm版本 node -v npm -v # 使用Vue CLI创建项目 npm install -g vue/cli vue create shanqing-admin # 安装核心依赖 npm install element-ui -S npm install axios -S npm install vue-router3 -S npm install vuex3 -S npm install js-cookie -S注意一个版本坑vue-router和vuex的版本和Vue2是严格绑定的vue-router必须用3.xvuex必须用3.x如果直接npm install使用默认配置会装成4.x版本和Vue2完全不兼容运行时报错是必然的。我在这里卡过好几次后来都是手动指定版本号安装才解决。项目的目录结构我按功能模块划分src/ api/ # 接口请求封装 assets/ # 静态资源 components/ # 公共组件 layout/ # 后台布局框架 router/ # 路由配置 store/ # Vuex状态管理 views/ # 页面视图分成system、route、order、customer、report等子目录 utils/ # 工具类request.js封装axios等4.2 Axios请求封装与拦截器前端所有请求都通过axios发送如果不做统一封装每个页面都要写一遍baseURL、请求头、错误处理代码会非常冗余。我的做法是在utils/request.js里封装一个axios实例import axios from axios import { Message } from element-ui import cookie from js-cookie const service axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带Token service.interceptors.request.use(config { const token cookie.get(token) if (token) { config.headers[Authorization] Bearer token } return config }) // 响应拦截器统一处理返回结果和错误 service.interceptors.response.use( response { const res response.data if (res.code 200) { return res } else if (res.code 401) { Message.error(登录已过期请重新登录) router.push(/login) return Promise.reject(new Error(未授权)) } else { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } }, error { Message.error(网络异常请稍后重试) return Promise.reject(error) } )在开发环境下baseURL直接用/api然后在vue.config.js里配置devServer的代理转发把/api开头的请求转发到后端服务的地址这样解决了开发阶段的跨域问题module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } }4.3 登录页与动态路由实现登录页是比较核心的页面包括表单验证、登录请求、Token存储三个环节。表单用Element UI的el-form组件在前端做一次输入校验必填项、格式校验然后调用后端登录接口成功后把token存到cookie里同时把用户信息存到Vuex里跳转到首页。动态路由是权限控制的重点。我的思路是前端定义基础路由登录页、404页登录成功后根据后端的菜单数据动态添加路由。具体实现要用到router.addRoutes方法// 从后端获取菜单格式是路由映射关系 const routeData await getMenuList() const dynamicRoutes generateRoutes(routeData) // 把后端菜单转成vue-router的路由结构 router.addRoutes(dynamicRoutes)这个方案比较灵活后端改了菜单前端不用重新打包发布登录后就能动态变化。缺点是生成路由的逻辑稍复杂需要把后端的菜单数据和前端组件的映射关系做好。我这里定义了一个组件映射表后端传菜单的路由路径前端根据路径找到对应的组件const componentMap { route/RouteList: () import(/views/route/RouteList.vue), route/RouteEdit: () import(/views/route/RouteEdit.vue), // ...其他页面对应关系 }4.4 核心业务页面设计线路管理页是全系统最重要的页面之一我用Element UI的el-table做列表展示顶部放搜索条件栏线路名称、目的地的输入框右侧放操作按钮新增、编辑、删除。表格列包括线路编号、名称、出行天数、目的地、价格、余位、状态等。状态列用el-tag展示上架显示为绿色下架显示为灰色这样一眼就能看出哪些线路在售。订单管理页稍微复杂一些因为订单状态是动态流转的。我用el-tabs把订单按状态分成几个标签页待支付、已支付、已出团、已完成、已取消。每个标签页显示对应状态的订单列表支持按时间区间和线路名称筛选。列表中的关键操作是修改状态比如点击确认收款就把状态从待支付改成已支付同时更新线路的余位数量。这里要注意事务一致性下游的数据修改必须和后端的订单状态更新保持同步我是在后端Service层用Transactional注解保证的前端只需要调用修改状态的接口即可。报表页面用可视化图表来展示数据会比纯表格直观得多这是整个系统比较亮眼的一个功能点。我使用的是ECharts这个可视化库整合进出界面并不难在Vue组件里引入echarts后设定好图表的option配置项再给图表容器初始化实例并传入配置就可以渲染出来。我实现了两个核心图表——一个是用折线图展示近半年的订单量趋势另一个是用饼图展示各线路的销售额占比管理层想看的经营情况基本一目了然。4.5 视频预览功能在旅游系统中的应用旅游公司的线路详情页往往会放一段景点的宣传视频让客户更直观地了解目的地。现在的旅游宣传视频很多都转成了m3u8格式做流媒体分发木木前端也要支持播放m3u8格式的视频流。这里要在Vue里播放m3u8格式我的方案是用video.js这个播放器库并搭配videojs-contrib-hls插件在组件里的具体写法是先在需要播放视频的页面里引入这两个库并注册然后在模板中写好video标签作为容器最后在组件的生命周期钩子里调用videojs方法来初始化播放器并在配置中指定数据源第二个参数传配置项、第三个参数传一个回调函数等播放器就绪后就可以进行播放操作了。import videojs from video.js import video.js/dist/video-js.css import videojs-contrib-hls mounted() { this.player videojs(this.$refs.videoPlayer, { fluid: true, sources: [{ src: this.videoUrl, type: application/x-mpegURL }] }) }需要注意的是videojs-contrib-hls插件只支持HLS的播放如果视频是其他格式需要额外引入flash等兼容方案但这个插件在现在的主流浏览器环境下已经不太常用了。5. 前后端联调与常见问题排查5.1 跨域问题的三种解决思路前后端分离开发跨域问题是绕不开的第一个坎。浏览器出于安全机制默认不允许跨域请求如果后端没有配置跨域处理前端请求就会报CORS错误。解决跨域的方式无非三种第一种是前端开发环境代理方案也就是我在vue.config.js里配置devServer的proxy把/api开头的请求转发到后端地址。这种方式只影响开发环境上线后前端Nginx配置一个反向代理就可以解决不需要后端改动。第二种是后端开启CORS全局配置在后端项目里写一个配置类实现WebMvcConfigurer接口重写addCorsMappings方法允许指定来源的跨域请求。这种方案对上线后的浏览器请求也生效安全性上需要注意配置允许的来源不要直接放行所有来源。第三种是后端的Nginx统一代理前端和后端都挂在同一个域名下面通过Nginx的location规则分发请求从HTTP协议层面根本就不存在跨域问题。这是生产环境最推荐的方案。我实际开发中在开发阶段用的是方案一上线后用的是方案三后端代码里根本没有跨域配置各环境各取所需。5.2 我遇到的三个高频bug和排查实录第一个bug是PageHelper分页插件失效的问题。现象是查询第一次分页正常第二次查询就变成了全表查询。排查过程发现是因为在startPage和查询SQL之间加了一段从数据库查询在线用户数的代码这样PageHelper的拦截器就应用到了错误的SQL上。解决方法是调整代码顺序确保startPage后面紧跟分页查询SQL。第二个bug是修改线路后缓存没有同步问题。现象是管理员在前台修改了某条线路的价格但用户在首页看到的还是旧价格。排查过程发现是因为线路查询接口加入了Redis缓存只有查询逻辑没有在修改线路后清除缓存。解决方法是修改线路的接口里主动删除该线路对应的缓存key或者通过Spring Cache的CacheEvict注解来声明式地缓存失效。第三个bug是Element UI表格数据更新不及时。现象是删除一条订单后前端表格数据没有刷新需要手动点浏览器刷新才能看到最新数据。排查过程发现是删除成功后没有重新调用列表接口。解决方法是在删除成功的回调函数里重新调用获取列表的action并且用async和await确保先等删除接口返回再请求列表接口避免请求时序问题。5.3 系统性能优化与上线部署功能跑通了不要急着大功告成还有两件事要做性能优化和上线部署。性能优化方面我的经验是先从数据库SQL入手用慢查询日志找出执行时间长的SQL语句然后看有没有走索引。比如订单表的查询经常按线路ID和下单时间筛选就要给这两个字段建立联合索引用户表的username字段是登录查询的必经之路也要建唯一索引。其次是对热点数据进行缓存线路列表是访问量最高的数据用Redis缓存10分钟能大幅减轻数据库压力。再次是前端打包优化使用路由懒加载的方式拆分代码块减小首屏加载体积。部署方面我采用的是一台Linux服务器比如腾讯云或阿里云的轻量服务器后端项目用maven打成jar包通过systemd配置成系统服务开机自启崩溃自动重启前端项目构建成dist目录通过Nginx做静态文件服务。Nginx还要配置接口反向代理把/api请求转发到后端的8080端口。数据库在生产环境用MySQL 8.0部署完成后只需要把数据库脚本执行一遍修改后端的application-prod.yml里的数据库连接地址就能启动运行了。6. 实操心得与扩展建议6.1 从开发到稳定运行的几个经验总结这套系统从零开始做前后花了大半个月的时间中间踩过的坑比我预想的多但回过头看每一步都不是白费的。最重要的一条经验是先想清楚业务再动手。我刚开始写的时候也是拿到需求就开码结果产后端接口的时候没想清楚字段和状态流转导致前端写一半发现接口返回的数据结构对不上来回改了好几次。如果一开始就把数据库表设计和接口文档梳理清楚后期返工的成本能省掉一大半。第二条经验是学会用日志排查问题。后端开发日志是最忠实的朋友。项目跑出奇怪的bug先去看日志文件定位是哪个接口报的错再根据异常堆栈信息一步步排查。SpringBoot默认的日志输出到控制台如果想持久化保存需要在application.yml里配置logback的日志输出路径和级别。第三条经验是前端调试要善用Vue Devtools浏览器插件。这个插件可以在浏览器里直接查看Vue组件的data数据、computed计算属性、Vuex的状态还可以查看路由跳转的参数。很多前端页面数据不对的问题用这个插件一看就明白了是我调试Vue项目最常用的工具。6.2 从毕设走向实际业务系统如果你做这个项目的目标不只是为了交作业而是想把它做成一个真正能用的系统还可以从这几个方向扩展业务功能方面可以增加导游管理、用车管理、酒店资源管理、保险对接等模块延伸旅游公司的业务覆盖范围技术层面可以引入消息队列来实现订单量突增时的削峰处理分布式缓存集群以及更细粒度的数据权限控制。系统上线后可以通过埋点统计各个页面的访问量、点击率等行为数据再结合订单转化率来评估功能的使用效果判断哪些功能投入产出比高、哪些功能模块使用率偏低就可以基于数据反馈持续优化迭代。对于一个学习项目而言做到把需求分析清楚、架构设计合理、核心功能完整、代码规范清晰这几个标准就已经超出了大多数同类项目的水准。我最初想做一个能应付毕业答辩的项目但做到后面发现按照完整项目的标准去要求自己最后收获的远远超过预期。6.3 如果你打算做类似项目我给你三个建议第一除非你已有一定技术积累否则从自己最熟悉的业务场景切入。旅游公司管理系统之所以做起来顺畅是因为我本身了解旅游行业的业务流程知道线路、订单、客户这些概念的真实含义。如果你熟悉的是餐饮、物业、健身房做这些行业的系统会更有手感也更容易在答辩或者面试的时候讲出你的思考和深度。第二代码提交要养成用Git做版本管理的习惯功能开发到关键节点就做一次提交。我在开发过程中有过一次误删代码文件的操作幸好有Git历史记录赶紧找回不然后面全乱了。版本管理看似是小事关键时刻能救命。第三多看优秀的开源项目代码不要闭门造车。我在写后端统一返回结构封装的时候参考了RuoYi等项目的最佳实践他们的代码就写得非常规范学几手直接用比自己造轮子强太多了。
返回列表