SpringBoot3+Vue3+AntDesign:企业级后台管理系统快速搭建实战

发布时间:2026/11/2 21:47:25
SpringBoot3+Vue3+AntDesign:企业级后台管理系统快速搭建实战 后台管理系统几乎是每个开发团队绕不开的基建活儿。我做过不下十个中后台项目从早期手写jQuery表单到后来用模板引擎拼页面再到前后端分离刚兴起时自己搭脚手架踩过的坑能写满一个笔记本。这几年技术栈基本收敛了SpringBoot3 Vue3 AntDesign这套组合成了我做企业级后台的默认起手式——后端用SpringBoot3提供RESTful接口和权限控制前端用Vue3的组合式API组织页面逻辑UI层直接吃AntDesign的组件红利三件套配合下来一个带登录鉴权、菜单路由、增删改查的后台骨架熟练的话半天就能跑起来。这套东西解决的核心问题就一个把重复的基建工作标准化。登录、鉴权、动态菜单、请求封装、异常处理、分页表格、表单弹窗这些每个后台都要有的东西不该每次从零写。但网上的教程要么版本太老还在用SpringBoot2和Vue2的选项式API要么只讲一半前端搭完了后端没影或者后端接口写完了前端不知道怎么对接要么把简单事情复杂化上来就搞微服务、搞工作流引擎。这篇东西我打算按自己实际搭项目的顺序来写从环境准备到前后端联调把每个环节的关键决策和容易翻车的地方都摊开讲适合有一定Java和前端基础、想快速搞出一套能用的后台骨架的开发者参考新手跟着走也能跑通老手可以重点看避坑那几节。1. 技术选型背后的取舍逻辑1.1 为什么是这三个凑一起先说后端。SpringBoot3不是随便选的它最低要求JDK17这个门槛直接把一堆老项目挡在外面但也正因为如此它带来了虚拟线程支持JDK21下更明显、更好的GraalVM原生镜像兼容性以及Spring Security 6的全新配置模型。我选它的实际理由很朴素生态成熟、招人好招、出问题搜得到答案。你可能会问为啥不用Go或者Node写后端答案是中后台项目里大量的ORM操作、事务管理、权限拦截Java这套东西的轮子最全SpringBoot3把自动配置做得足够傻瓜一个spring-boot-starter-web依赖加几行配置就能跑起来一个带内嵌Tomcat的服务。前端选Vue3核心是组合式API对中后台这种逻辑复用需求极高的场景太友好了。以前Vue2写后台一个列表页的查询、分页、弹窗、表单校验逻辑散落在data、methods、computed里稍微复杂点就成一锅粥。Vue3的setup语法配合ref、reactive可以把列表查询封装成一个可复用的组合函数哪个页面要用直接引入逻辑内聚得很干净。再加上Vite的冷启动速度改一行代码浏览器几乎秒刷新开发体验比Webpack时代强太多。AntDesign Vue这个选择稍微有点讲究。国内中后台UI库主流就三家Element Plus、AntDesign Vue、Naive UI。我最终选AntDesign Vue主要是它的表格和表单组件做得最厚实——中后台80%的页面就是表格加表单AntDesign的a-table自带分页、排序、筛选、行选择a-form的校验规则配置化程度高配合a-modal做弹窗表单几乎不用写额外样式。Element Plus也不错但它的设计语言偏轻做复杂数据密集型页面时信息密度不够。Naive UI更新潮但社区案例相对少遇到怪问题不好搜。1.2 版本搭配的坑别让依赖打架这里必须单独拎出来说因为版本问题是新手翻车重灾区。SpringBoot3要求JDK17这个没得商量你机器上如果还是JDK8先把环境切过来。Vue3要求Node 16建议直接上Node 18 LTS或20 LTSNode 16已经停止维护了。AntDesign Vue对应Vue3的版本是4.x别装成1.x或2.x那是给Vue2用的。我整理了一张实际项目里验证过的版本对照表照着配基本不会出幺蛾子组件推荐版本关键约束JDK17 或 21 LTSSpringBoot3最低17SpringBoot3.2.x3.0/3.1也可3.2更稳Maven3.8低版本可能拉不到依赖Node.js18 LTS 或 20 LTS16已停维护Vue3.4.x组合式API稳定版Vite5.x配合Vue3.4AntDesign Vue4.1.x对应Vue3Pinia2.1.x状态管理替代Vuex注意SpringBoot3.2开始对Spring Security的配置写法有微调网上很多老教程的WebSecurityConfigurerAdapter已经彻底废弃别照抄会编译不过。下面权限那节我会给新写法。1.3 整体架构长什么样在动手之前脑子里得有个清晰的图。这套后台是标准的前后端分离结构前端Vue3打包成静态资源开发时跑在Vite的dev server上默认5173端口通过代理把/api开头的请求转发到后端后端SpringBoot跑在8080端口对外提供JSON接口用JWT做无状态鉴权。生产环境前端打包后的静态文件可以扔给Nginx托管也可以直接塞进SpringBoot的static目录打成一个大jar小项目我倾向后者部署省事。分层上后端走经典的Controller-Service-Mapper三层Controller只管收参和返回业务逻辑在Service数据访问用MyBatis-Plus比原生MyBatis省掉大量单表CRUD的XML。前端按views页面、components通用组件、api接口封装、store状态、router路由分目录权限控制靠路由守卫加动态路由注入。这套结构不新鲜但胜在清晰新人接手看目录就知道东西在哪。2. 后端骨架搭建与核心配置2.1 项目初始化与依赖清单用IDEA的Spring Initializr或者直接上官网生成都行选Maven项目JDK17依赖勾这几个Spring Web、Spring Security、MySQL Driver、Lombok。生成后打开pom.xml我习惯再手动补几个常用依赖下面是我实际项目里的核心依赖片段dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.12.3/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这里有个细节值得说MyBatis-Plus的starter在SpringBoot3下必须用mybatis-plus-spring-boot3-starter用老的mybatis-plus-boot-starter会报自动配置类找不到的错因为SpringBoot3的自动配置注册机制从spring.factories换成了AutoConfiguration.imports。这个坑我踩过一次排查了半天才发现是starter名字不对。2.2 配置文件与数据源application.yml我一般这么写把数据源、MyBatis-Plus、JWT密钥分开配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/admin_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 jwt: secret: your-256-bit-secret-key-please-change-in-production expiration: 86400000map-underscore-to-camel-case这个必须开数据库的create_time字段才能自动映射到实体的createTime。逻辑删除配置也建议一开始就加上中后台数据基本不做物理删除加个deleted字段标记查询时MyBatis-Plus自动过滤省得每个SQL都手写where deleted 0。提示JWT的secret长度有要求HS256算法要求密钥至少256位32字节太短会抛WeakKeyException。生产环境这个值一定要换掉别用示例里的。2.3 统一返回结构与全局异常处理中后台接口返回格式必须统一前端才好统一拦截处理。我定义一个Result泛型类包含code、message、data三个字段Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(操作成功); r.setData(data); return r; } public static T ResultT error(Integer code, String message) { ResultT r new Result(); r.setCode(code); r.setMessage(message); return r; } }光有统一返回还不够异常得统一兜住不然前端收到一堆500堆栈信息没法处理。用RestControllerAdvice加ExceptionHandler做全局异常处理业务异常、参数校验异常、权限异常分别处理返回对应的code和友好提示。这块代码不长但极其重要我见过不少项目每个Controller里try-catch重复且容易漏全局处理一次到位。2.4 JWT鉴权与Spring Security 6配置这是后端最绕的一块Spring Security 6的配置模型和5.x差别很大老教程基本作废。核心思路是自定义一个JwtAuthenticationFilter继承OncePerRequestFilter从请求头拿token、解析、塞进SecurityContext然后在配置类里用SecurityFilterChainBean声明放行规则。Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf - csrf.disable()) .sessionManagement(sm - sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/login).permitAll() .anyRequest().authenticated()) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }注意csrf.disable()和sessionCreationPolicy(STATELESS)这两句前后端分离加JWT必须这么配否则跨域请求会被CSRF拦截或者session机制干扰。requestMatchers是Spring Security 6的新API老版本的antMatchers已经废弃。登录接口放行其余全部要求认证过滤器在用户名密码过滤器之前执行把JWT解析出来的用户信息放进上下文。3. 前端工程化与页面骨架3.1 Vite初始化与目录规划前端用npm create vitelatest选Vue模板生成然后装依赖vue-router、pinia、ant-design-vue、axios。目录我按这个结构组织src/ ├── api/ # 接口封装按模块分文件 ├── components/ # 通用组件 ├── layout/ # 布局组件侧边栏、顶栏 ├── router/ # 路由配置与守卫 ├── store/ # Pinia状态 ├── utils/ # 请求封装、工具函数 └── views/ # 页面这个结构的好处是职责边界清楚。api目录下每个模块一个文件比如user.js里导出getUserList、addUser这些函数页面里import进来直接用接口地址变了只改一处。utils/request.js里封装axios实例统一加token、统一处理响应码、统一弹错误提示页面里就不用每个请求都写一遍loading和错误处理。3.2 axios请求封装的关键细节请求封装这块我踩过坑值得细说。核心是创建axios实例、请求拦截器加token、响应拦截器处理业务码import axios from axios import { message } from ant-design-vue import { useUserStore } from /store/user const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { message.error(res.message || 请求失败) if (res.code 401) { useUserStore().logout() } return Promise.reject(new Error(res.message)) } return res.data }, error { message.error(error.message || 网络异常) return Promise.reject(error) } ) export default request这里有个容易忽略的点响应拦截器里return res.data意味着页面里await getUserList()拿到的直接是业务数据不用再.data.data套两层。这个约定要在团队里统一不然有人按axios原始响应写、有人按封装后写代码就乱了。另外401的处理要跳登录页并清token这个逻辑放在拦截器里最省事。3.3 动态路由与菜单联动中后台的菜单通常是根据用户权限动态生成的不是写死的。我的做法是登录后调后端接口拿到该用户的菜单树前端把菜单数据转成路由配置用router.addRoute动态注入同时Pinia里存一份菜单数据给侧边栏渲染。菜单表设计上我一般用一张sys_menu表字段包括id、parent_id、name、path、component、icon、sort、type目录/菜单/按钮。后端根据用户角色关联的菜单返回树形结构前端递归处理成路由。component字段存的是前端组件路径字符串比如views/user/index前端用import.meta.glob动态导入匹配。这个方案的好处是加菜单不用改前端代码数据库里配一条就行。注意import.meta.glob是Vite的特性路径匹配要写对import.meta.glob(../views/**/*.vue)这样把views下所有vue文件映射成动态导入函数然后按component字段的路径去取。用错了会报组件找不到。4. 前后端联调与典型页面实现4.1 跨域代理配置开发阶段前端5173、后端8080跨域是必然的。最省事的方案是在vite.config.js里配代理把/api转发到后端export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端请求/api/user/listVite dev server转发到http://localhost:8080/api/user/list浏览器看来是同源请求跨域问题绕过去了。生产环境用Nginx配同样的转发规则或者前后端同域部署。别在后端配CrossOrigin到处撒虽然能解决但不够干净代理方案更符合实际部署形态。4.2 登录流程串起来登录是第一个要跑通的闭环涉及前端表单、后端认证、token存储、路由跳转。前端登录页用a-form做账号密码校验提交调/api/auth/login后端校验密码数据库存BCrypt加密后的值用BCryptPasswordEncoder.matches比对通过后生成JWT返回。前端拿到token存Pinia同时持久化到localStorage刷新不丢然后调菜单接口、注入动态路由、跳首页。这里有个时序问题要注意动态路由注入是异步的如果直接在登录成功后router.push(/)可能路由还没加完导致404。我的处理是在路由守卫里判断如果已登录但路由未初始化先拉菜单、加路由再next({ ...to, replace: true })重新进入。这个模式能避免刷新页面时动态路由丢失的问题。4.3 增删改查页面的标准套路中后台最多的就是列表加表单的CRUD页。以用户管理为例页面结构是顶部搜索区a-forminline布局、中间表格a-table带分页、右上角新增按钮、表格行内编辑删除操作、弹窗表单a-modal包a-form。表格数据加载用onMounted调接口分页参数current、pageSize绑定到a-table的pagination翻页触发change重新请求。新增和编辑共用一个弹窗组件用editingId区分是新增还是编辑编辑时先回填表单数据。删除加a-popconfirm二次确认。这套模板我封装成了一个组合函数useTable把列表数据、分页、加载状态、查询方法都收进去新页面引入后只需传接口函数和查询参数代码量能砍掉一半。export function useTable(apiFn, initParams {}) { const list ref([]) const loading ref(false) const pagination reactive({ current: 1, pageSize: 10, total: 0 }) const params reactive({ ...initParams }) const load async () { loading.value true try { const res await apiFn({ ...params, ...pagination }) list.value res.records pagination.total res.total } finally { loading.value false } } return { list, loading, pagination, params, load } }这个组合函数是我实际项目里复用率最高的东西几乎每个列表页都在用。它的价值在于把查询-分页-加载态这套固定逻辑抽出来页面组件只关心自己的业务字段。5. 常见问题排查与避坑实录5.1 后端启动与依赖问题速查现象大概率原因解决方向启动报自动配置类找不到MyBatis-Plus用了老starter换mybatis-plus-spring-boot3-starter编译报WebSecurityConfigurerAdapter不存在照抄了Spring Security 5教程改用SecurityFilterChainBean写法JWT解析抛WeakKeyExceptionsecret太短换32字节以上密钥数据库字段映射不上没开驼峰映射配map-underscore-to-camel-case: true接口返回401但token明明带了过滤器顺序或放行规则错检查addFilterBefore和permitAll路径这张表里的每一条我都真实遇到过尤其是第一条和第二条属于版本升级带来的教程时效性陷阱。判断方法很简单看报错里的类名如果是spring.factories相关或者Adapter相关基本就是版本不匹配。5.2 前端联调的典型故障跨域报错是最常见的但配了代理还报跨域通常是代理没生效——检查vite.config.js改完有没有重启dev serverVite的配置改动不会热更新。另一个高频问题是token带了但后端说没带多半是请求拦截器里config.headers.Authorization的写法在axios新版本里有变化或者Pinia的store在拦截器里拿不到在模块顶层调用useUserStore()可能报未初始化解决办法是把store调用放到拦截器函数内部而不是模块顶层。动态路由刷新后404也很典型原因是刷新时Pinia状态重置、动态路由丢失但浏览器直接访问了那个路径。前面说的路由守卫里重新拉菜单加路由的方案就是治这个的。还有一种情况是菜单配了但页面白屏检查component字段路径和import.meta.glob的匹配规则是否一致大小写、斜杠方向都可能是元凶。5.3 我踩过的三个印象深刻的坑第一个是逻辑删除和唯一索引打架。用户表给username加了唯一索引逻辑删除后想重新注册同名用户插入时报唯一键冲突因为旧记录还在只是deleted1。解决办法是把唯一索引改成(username, deleted)联合索引或者删除时把username改写成带时间戳的形式。这个坑不遇到想不到遇到了查半天。第二个是AntDesign表格的rowKey没设导致选中行错乱。a-table默认用索引当key翻页或数据更新后选中状态会串。必须显式指定rowKeyid这是硬性要求别偷懒。第三个是SpringBoot3的虚拟线程。JDK21下开了spring.threads.virtual.enabledtrue理论上吞吐能提升但如果代码里有synchronized块包着阻塞IO虚拟线程的载体线程会被钉住反而可能更慢。中后台项目IO不算密集我实测下来开不开差别不大建议先别开等有明确压测需求再说。6. 部署与后续扩展的一点经验打包部署这块前端npm run build出dist目录后端mvn clean package出jar。小项目我直接把dist内容拷进后端src/main/resources/static打成一个大jarjava -jar一跑全有了Nginx都不用配。大点的项目还是前后端分开部署Nginx托管静态资源并反代/api到后端这样前端发版不用重启后端。后续扩展方向权限粒度可以从菜单级细化到按钮级菜单表加个perms字段存权限标识前端用自定义指令v-perm控制按钮显隐后端用PreAuthorize注解校验。操作日志可以加个AOP切面拦截标注了自定义注解的方法记录操作人、时间、参数。数据权限比如只能看自己部门的数据稍微复杂点得在MyBatis-Plus的拦截器里动态拼SQL条件这个按需再做别一上来就全铺开。我个人在实际操作中的体会是这套骨架的价值不在于技术多新而在于把决策成本前置了。版本怎么选、目录怎么分、鉴权怎么做、异常怎么兜这些事在项目初期定好后面加业务就是往里填页面和接口节奏会顺很多。真正拖慢中后台项目的从来不是技术难点而是每个页面都在重复解决同样的问题。把重复的部分抽干净剩下的就是纯粹的业务表达这才是这套组合最实在的地方。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询