ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Vue管理系统开发实战:从架构搭建到部署避坑

SpringBoot+Vue管理系统开发实战:从架构搭建到部署避坑 1. 项目全貌与设计思路1.1 这套系统到底在做什么直接说结论SpringBoot Vue 管理系统是目前中小团队做企业内部系统、毕业设计、外包交付时用得最多的一套组合没有之一。它解决的核心问题就一句话——用一套相对标准的前后端分离架构快速把“登录、权限、增删改查、文件上传、数据图表”这些管理后台的常规需求落地。我接手过不少这类项目从校园教职员工考勤管理系统到商品管理系统骨架基本一致后端 SpringBoot 负责业务逻辑、数据存储、接口鉴权前端 Vue 负责页面渲染、交互反馈、状态管理。两者通过 RESTful API 或 JSON 进行通信开发时并行推进部署时再合并或分置。这套方案之所以受追捧主要因为 SpringBoot 把 Spring 家族繁琐的 XML 配置几乎清零了内置 Tomcat打好 Jar 包就能跑Vue 的组件化开发让页面复用变得顺手加上 Element UI 之类的组件库表格、表单、弹窗这几件套拖出来就能用。如果你只是要做一个内部管理系统而不是高并发、高可用的互联网产品这套架构是最省心的选择。从技术栈的完整度来看一个合格的管理系统往往不只有 SpringBoot 和 Vue 本身还会涉及 MySQL 存储业务数据、Redis 缓存 Token 和热点数据、MinIO 做文件对象存储、Maven 做后端构建、Npm/Yarn 做前端依赖管理、Nginx 或 SpringBoot 静态资源映射做最终部署。这些组件配合起来才是一个真正能上线、能交接给运维的完整项目。1.2 为什么非要用前后端分离很多刚入行的同学会问我直接用 Thymeleaf 模板把页面套在后端里不好吗为什么非要拆成两个工程我得说如果你的需求就是十几个页面、不打算做移动端、也没多端复用的需求Thymeleaf 确实更简单。但一旦系统规模上来前后端分离的优势就会特别明显。第一职责边界清晰。后端只出接口前端只管展示。后端开发不用管页面跳转、按钮交互前端开发不用管 SQL、事务、表结构。两边只要约定好接口文档就能并行开工把开发周期压缩得很短。第二多端复用的空间大。同一套后端接口可以同时支撑 Vue 管理后台、小程序、App 客户端。正常情况下你只需要在接口层做好权限校验和数据裁剪前端可以无缝切换。第三部署和容错更灵活。前端打包成静态文件可以扔到 Nginx 上做动静分离也可以直接拷贝进 SpringBoot 的resources/static目录里由后端统一托管。静态资源由前端单独发版不影响后端服务的重启后端更新时也不会拖累静态资源。这种松耦合关系在实际交付和故障排查时都省心不少。当然分离也带来了跨域、鉴权、联调成本上升的问题但这都是可以通过配置和规范解决的完全不构成“放弃分离”的理由。1.3 技术选型SpringBoot Vue 之外还要什么这里分享一套我实测下来比较稳的选型组合适用于绝大多数管理后台项目模块推荐方案说明后端框架SpringBoot 2.7.x 或 3.x2.7 兼容性最稳3.x 适合新项目且需要 JDK17前端框架Vue 2.7 或 Vue 3 Vite老项目尽量升级到 Vue3新项目直接用 Vue3UI 组件库Element PlusVue3 最佳搭档表格表单组件成熟数据库MySQL 5.7 / 8.0管理系统首选生态好、招人容易缓存Redis存 Token、验证码、热点数据文件存储MinIO开源对象存储支持断点上传、预览、分享链接鉴权方案Sa-Token 或 Spring Security JWT小型系统推荐 Sa-Token简单直接构建工具Maven Npm/Vite前者是后端标配后者是前端打包首选这套组合的好处是每个环节都有成熟的文档和社区案例卡住时搜一下基本都有答案。而且后续无论是加消息队列ActiveMQ/RabbitMQ、接流媒体服务还是引入分词组件比如 HanLP都有现成的 starter 或示例可参考不会让你从零开始造轮子。2. 核心细节解析与实现要点2.1 项目结构一条代码该往哪里放我一直强调一个观点代码结构不是形式主义它决定了后期维护时的心情。拿一个典型的 SpringBoot Vue 管理系统来说后端建议按照模块分包而不是堆一堆 Controller 完事。springboot-admin/ ├── src/main/java/com/example/admin/ │ ├── controller/ # 控制层只做参数接收与结果包装 │ ├── service/ # 业务逻辑层事务、业务规则都在这里 │ ├── mapper/ # MyBatis-Plus 的 Mapper 接口 │ ├── entity/ # 数据库实体类 │ ├── dto/ # 接口出入参对象 │ ├── config/ # 配置类跨域、拦截器、Redis、MinIO │ ├── common/ # 统一返回结果、异常处理、工具类 │ └── AdminApplication.java ├── src/main/resources/ │ ├── mapper/ # XML 文件复杂 SQL 放这里 │ ├── application.yml # 主配置 │ └── static/ # 前端打包后可直接放入 └── pom.xml前端部分用 Vue CLI 或 Vite 创建后推荐的目录模式是vue-admin/ ├── src/ │ ├── api/ # 所有接口请求模块按业务划分 │ ├── assets/ # 静态资源 │ ├── components/ # 公共组件 │ ├── router/ # 路由配置文件动态路由在这里生成 │ ├── store/ # Pinia 或 Vuex 状态管理 │ ├── views/ # 页面文件一个业务模块一个文件夹 │ ├── utils/ # 请求封装、工具函数 │ └── App.vue ├── vite.config.js # 开发代理配置 └── package.json这样拆分之后前后端人员看到目录就能猜到代码的位置。新同学接手时也不需要靠“搜索大法”找代码。前端还有一个容易忽视的点api层必须统一封装请求。我见过不少项目里fetch或者axios直接写进页面遇到 Token 过期、统一错误处理时就只能到处补丁。建议在utils/request.js里统一创建 axios 实例配置好baseURL、请求拦截器自动携带 Token、响应拦截器统一处理业务码、HTTP 错误、Token 失效跳登录。2.2 关键配置让前后端按时“对上暗号”前后端分离之后“对暗号”主要靠三个配置跨域、接口前缀、Token 传递。这三个地方出问题前端报的错往往千奇百怪。跨域配置在后端做一次就够了。我用 SpringBoot 处理跨域最推荐的是实现WebMvcConfigurer把CorsRegistry注册成全局配置而不是在 Controller 上一个个加CrossOrigin。全局配置的好处是所有接口统一生效以后新增接口不需要再记着加注解。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); } }注意allowCredentials(true)和allowedOriginPatterns(*)搭配是可行但生产环境里建议把allowedOriginPatterns收窄到具体的域名列表避免所有站点都能调用你的接口。接口前缀要前后端约定好。我个人习惯是用/api开头比如/api/user/list然后前端 axios 的baseURL也写/api。这样做的唯一目的就是方便 Nginx 做反向代理时统一转发不用区分接口路径的规则。Token 传递则依赖请求拦截器前端在request.js里从localStorage或 Cookie 中取 Token加到Authorization头里。后端用一个拦截器统一解析遇到缺失或过期就直接返回 401避免业务层到处都是“判断当前用户是否登录”的重复代码。2.3 权限与路由Vue 动态路由 后端鉴权管理系统离不开权限控制这也是面试中经常被问到的点。完整的权限方案要前后端配合后端负责接口级权限前端负责菜单和按钮级权限。后端鉴权我建议用 Sa-Token 或 Spring Security两者都有成熟的 Starter。以 Sa-Token 为例登录成功后StpUtil.login(userId)即可下发 Token接口上直接加注解就能控制权限SaCheckPermission(system:user:add) PostMapping(/add) public ResultVoid add(RequestBody UserDTO dto) { userService.add(dto); return Result.ok(); }这样没有权限的用户即使知道接口地址也无法越权操作安全性就不依赖前端了。前端要做的是动态路由。用户登录后后端返回该用户的角色和菜单权限列表前端根据权限表动态生成路由并addRoute注入而不是把所有页面都写死在router里。这样不同角色登录后看到的侧边栏、可访问的页面都不一样。我踩过的一个坑是刷新浏览器后动态路由丢失页面直接白屏。原因很简单路由数据只存在内存里Pinia/Vuex刷新就没了。解决办法是在路由的全局前置守卫里加一层判断如果 Store 里没有动态路由数据就重新请求后端拉取菜单并注册然后再放行进入页面。router.beforeEach(async (to, from, next) { const store useUserStore(); if (!store.menuLoaded) { const menus await getMenus(); const dynamicRoutes generateRoutes(menus); dynamicRoutes.forEach(route router.addRoute(route)); store.setMenus(menus); store.menuLoaded true; next({ ...to, replace: true }); } else { next(); } });这个方案比直接把路由都注册好再根据权限隐藏要靠谱得多也更容易和后端返回的菜单树对应起来。3. 实操过程从零搭建完整管理系统3.1 后端 SpringBoot 工程搭建与常见细节后端我习惯直接用 IDEA 的 Spring Initializr 生成工程。如果你手动建 Maven 项目需要记得在pom.xml里补充关键依赖我用得最多的几个是dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdcn.dev33/groupId artifactIdsa-token-spring-boot-starter/artifactId version1.37.0/version /dependency申明安全注意事项是多模块父工程里版本锁定这块新手很容易踩坑。建议把 SpringBoot 的父依赖写在parent节点里其他第三方组件版本用dependencyManagement统一管理。不然今天 Jar 冲突明天缺传递依赖光是解决依赖就能耗掉大半天。application.yml里最基础但重要的几项配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/admin_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case建议开启数据库字段user_name自动映射到实体的userName少写很多TableField注解。工程的启动入口也需要留意。很多项目里会放一个 Banner 在线生成的个性化启动图案这个无伤大雅不影响业务但至少不要用SpringApplication.run返回ConfigurableApplicationContext后什么都不做然后还在代码里留一堆警告。正常写SpringBootApplication MapperScan(com.example.admin.mapper) public class AdminApplication { public static void main(String[] args) { SpringApplication.run(AdminApplication.class, args); } }这里最容易被忽略的是MapperScan。如果 Mapper 接口没扫描到启动时会报“找不到 Bean”或者运行时才报 SQL 错误。3.2 前端 Vue 工程搭建与组件设计前端我用 Vite 创建 Vue 3 项目是目前的主流选择。安装依赖时注意Vite 需要的 Node 版本建议 16 以上如果本机是旧版本先升级 Node 再装依赖。这个细节卡住过很多人。创建 Vue 3 工程的命令很简单npm create vitelatest vue-admin -- --template vue cd vue-admin npm install npm install vue-router4 pinia element-plus axios装 Element Plus 时可以顺便引入按需自动导入这样打包体积会小不少。package.json里不要用npm install的默认版本直接盲装要将已知适配的版本锁住避免哪天依赖大版本升级把项目搞挂。页面设计上管理后台最常见的布局是左侧菜单栏、顶部导航栏、主体内容区。组件拆分的时候我建议把侧边栏菜单、面包屑导航、标签页缓存做成公共组件具体页面则严格遵循“列表页 表单弹窗 详情页”的套路。列表页用 Element Plus 的el-table加el-pagination表单用el-form加校验规则。按钮操作统一调用ElMessageBox.confirm做二次确认。这样做出来的系统虽说不惊艳但非常稳而且代码风格统一后面接手的人也好维护。Vue 里还有一个容易忽略但很实用的功能是插槽Slot。列表页的表格列经常出现“操作列”不同页面操作按钮数量不同你不可能写一堆v-if判断。把操作列封装成组件用插槽暴露出来页面只需传入按钮数组和点击事件即可代码舒服很多template el-table-column label操作 width220 template #defaultscope slot nameactions :rowscope.row/slot /template /el-table-column /template3.3 文件上传、预览与 MinIO 整合管理系统里文件上传几乎必备头像、附件、商品图片、导出文件哪哪都要用。小项目可以直接存在服务器本地目录但我在多个项目里实践下来MinIO 更省心尤其文件类型多、体积大的场景比如视频预览、PDF 在线预览。MinIO 在后端的使用流程大致分三步引入依赖、配置参数、写工具类。minio: endpoint: http://localhost:9000 access-key: admin secret-key: admin123 bucket: admin-files工具类里需要完成的三个核心方法上传文件、生成预览链接、删除文件。上传文件的代码大致如下public String upload(MultipartFile file) throws Exception { String fileName UUID.randomUUID().toString().replace(-, ) _ file.getOriginalFilename(); minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(fileName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build() ); return endpoint / bucketName / fileName; }这里需要注意文件名建议用 UUID 重命名避免中文文件名乱码、重名覆盖、非法字符问题。我见过太多系统因为直接拿用户上传的原名当对象名结果换台电脑就出现访问 404。上传完成后返回的 URL 要区分内网和外网。开发时用localhost:9000能访问部署到服务器后就必须换成服务器的公网地址。所以建议在配置里把 endpoint 拆成“服务端访问地址”和“对外预览地址”两个字段否则每次换环境都得改代码。PDF 在线预览在 Vue 里可以直接用浏览器原生能力iframe :srcpdfUrl就能打开。至于图片直接放img标签即可。前端不需要额外引 PDF 插件除非要加水印、做批注那样再考虑专业渲染库。3.4 Vue 播放 m3u8 视频流的管理后台场景管理系统里还有一类需求越来越常见视频点播、监控回放、课程录播管理后台需要直接预览视频。场景里给的是 m3u8 格式这种格式本质是 HLS 流媒体协议下的索引文件浏览器原生video标签不支持直接播放需要借助 hls.js 库。前端集成 hls.js 的步骤并不算复杂。先安装依赖npm install hls.js在 Vue 组件里通过video标签挂载后判断浏览器是否支持 HLS 原生播放比如 Safari不支持的用 hls.js 加载template video refvideoRef controls stylewidth: 100%/video /template script setup import Hls from hls.js; import { ref, onMounted } from vue; const videoRef ref(null); onMounted(() { const video videoRef.value; const videoUrl http://your-server/live/stream.m3u8; if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(videoUrl); hls.attachMedia(video); } else if (video.canPlayType(application/vnd.apple.mpegurl)) { video.src videoUrl; } }); /script这套方案的优点是“免安装”不用用户装任何播放器插件浏览器打开就能看。和“vue 播放 m3u8 免安装”这个热搜词对应的正是这个点。实际项目里还有个细节m3u8 的地址如果是动态变化的比如从后端接口拿到的需要在拿到地址后再初始化播放器不能直接写死在组件里。视频源跨域时还需要后端在响应头里配置Access-Control-Allow-Origin否则 hls.js 拉取分片会被浏览器拦截。如果做的是监控类系统前端还经常需要同时播放多个视频画面这时候要注意浏览器的并发连接限制和内存占用。建议做懒加载策略只有视频区域进入视口时才初始化播放离开视口时销毁实例释放资源。3.5 后端消息队列与分词组件的整合思路管理系统里消息队列的应用也很常见比如考勤系统里批量通知、订单系统里短信提醒、商品系统里的库存异步入库日志。这些需求没必要自己写多线程引入消息中间件是标准做法。以 ActiveMQ 为例SpringBoot 整合的步骤还比较简单。引入依赖后配置 broker 地址然后在 Service 里注入JmsMessagingTemplate发送消息消费者类上用JmsListener(destination queueName)接收。Component public class MessageConsumer { JmsListener(destination sms.queue) public void onMessage(String content) { System.out.println(收到短信任务 content); // 调用短信接口或记录日志 } }选择 ActiveMQ 还是 RabbitMQ看团队熟悉度。ActiveMQ 更轻一些RabbitMQ 功能更强、社区更活跃。管理系统的业务量没那么大两者都够用。如果系统里还有文本分析需求比如考勤系统的请假理由审核、商品系统的评价关键词提取可以接入 HanLP 分词。HanLP 在 SpringBoot 中的使用也比较直接引入依赖持有HanLP的实例在 Service 里调用分词接口处理文本。重点提醒是HanLP 首次加载模型文件较慢生产环境建议在启动时预热一次避免第一次请求等好几秒。4. 常见问题与排查技巧实录4.1 SpringBoot 版本太高引发的连锁问题很多新手会直接生成最新版本 SpringBoot结果发现一堆旧依赖不兼容。SpringBoot 3.x 有本质变化Java 版本要求 17 以上、javax.*包迁移到了jakarta.*、部分 Starter 改版。如果你用的是 SpringBoot 3.x老项目里那些基于javax.servlet的拦截器、过滤器一律要改成jakarta.servlet。MyBatis-Plus 必须用适配 SpringBoot3 的版本Sa-Token 也要选择对应版本。最稳妥的做法新项目可以用 3.x但从零做管理系统2.7.x 反而更顺手教程最多、踩坑最少。版本兼容问题的排查思路很简单看启动日志的异常堆栈第一行如果是ClassNotFoundException或NoSuchMethodError基本就是依赖版本不匹配。然后去 Maven 仓库找到对应组件的 Starter看看它支持的 SpringBoot 版本范围。4.2 前后端联调跨域与代理的解决套路开发阶段前端跑在 5173 端口后端跑在 8080 端口跨域必然存在。两个方案都行后端配置全局 CORS或者前端开代理。我在开发环境更推荐前端代理因为不用后端代码参与联调。Vite 的代理配置很容易server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } }这里有个容易踩的坑如果后端接口本身就有/api前缀代理时就不要 rewrite如果后端没有前缀前端代理加 rewrite 把它剥掉。两边的约定不一致就会出现“前端请求成功了后端却 404”的情况。跨域配置最容易出问题的场景前后端都部署在 Nginx 后面但接口路径转发不对。这时候后端再开跨域也没用因为浏览器看的是 Nginx 的域名。排查顺序应该是先看浏览器 Network 里请求地址是否正确再看 Nginx 转发是否命中最后才怀疑后端跨域配置。4.3 Vue 打包放进 SpringBoot 的两种部署方式管理系统交付的时候用户未必会去配置 Nginx这个时候把前端打包产物直接塞到 SpringBoot 里是最省事的方案。做法是前端执行npm run build把生成的dist目录下的文件全部拷贝到后端src/main/resources/static目录然后重新打包 SpringBoot Jar一个文件跑天下。这种方式的优点是不需要单独部署前端but注意两个坑第一前端访问接口时的地址。打包进 SpringBoot 后前端页面从这个 Jar 服务的端口访问接口请求得是同源的/api或者相对路径不能写死http://localhost:5173。否则部署上去白屏或者接口全挂。第二前端路由模式。Vue Router 默认是createWebHistory刷新页面时后端没有对应的路由映射容易出现 404。如果采用“前端塞进后端”的部署方式建议路由模式改成createWebHashHistoryURL 上会带#刷新不会丢失。如果系统并发要求不高这种单体部署完全够用。但要注意这种模式下前端更新需要重新打包后端 Jar不适合频繁发版的团队。如果团队有运维支持我更推荐前后端分置前端静态文件交给 Nginx后端 Jar 直接跑系统服务Nginx 里把/api反向代理到后端端口。这种方式改动后端时不用动前端上线频率完全解耦。4.4 高频问题速查与避坑指南我把实际工作中最常被问到、也最常踩到的问题整理成一个速查表你直接照着排查就行现象可能原因排查与解法前端请求接口报 404代理路径 rewrite 规则错误后端 Controller 路径不一致先看浏览器 Network 实际请求 URL登录成功后刷新页面白屏动态路由未重新加载在路由守卫中拉取菜单并 addRoute上传文件时后端报空指针MultipartFile参数未加RequestParam或名称不一致检查前端 FormData 的 key 与后端参数名MySQL 存中文乱码数据库连接 URL 缺字符编码配置加characterEncodingutf8m3u8 视频播放黑屏跨域问题或视频源地址未过期检查 CORS 头与鉴权签名是否失效启动报Failed to configure a DataSource缺少数据库依赖或配置错误检查pom.xml和application.ymlSa-Token 接口拦不住未注册拦截器或排除路径错误检查拦截器注册顺序与路径匹配规则再补充两条我自己的心得。第一条SpringBoot 项目里的“Banner 在线生成”是个花活追求个性化没问题但别为了这个往项目里塞不必要的依赖。同理一些“SpringBoot 支持的漂亮配置”不要盲目照抄跑通后再说美化。第二条Vue 的插槽Slot功能一定要掌握。管理系统里大量重复的列表、表单、弹窗结构用插槽抽公共组件是最优雅的解法。#default、#header、#footer这几个插槽位用熟练了写页面的速度能提升一个档次。我在实际项目里还有一个体会这套系统真正难的不是技术而是需求的边界要清晰。很多管理系统做着做着就膨胀今天加在线预览PDF明天加视频直播后天又要求做数据大屏。项目架构保持住“前端只管渲染、后端只管数据”的底线加上接口文档及时同步后面的扩展都还算可控。最后分享一个实践小技巧前后端接口联调时Eolinker、Apifox 这类接口管理工具一定要用起来。把接口的入参、出参、异常码维护好前后端各自 mock 数据可以少很多口头沟通和返工。这类工具看起来很轻但用起来是真省时间。如果你正准备做一个 SpringBoot Vue 的管理系统别被搜索引擎里那些面试题库和参数配置吓到。按我上面梳理的路径先把登录、权限、一个带文件上传的完整 CRUD 模块跑通剩下的业务都是横向复制和微调。项目做多了你会发现这套技术栈的核心价值就在于稳定和可维护而不是炫技。
返回列表