
这段时间我把手上一套维护了挺久的 SpringBoot3 Vue3 TypeScript 企业级后台框架整理了一版准备限时开源出来。这套东西不是临时拼的 demo而是我这两年在实际项目里反复磨出来的基础架构里面涉及权限认证、动态菜单、代码生成、多环境打包这些真正能落地的能力。如果你正在选型后台管理系统或者想自己搭一套脚手架但又不想从零开始踩坑这篇文章正好可以给你一个相对完整的参考。1. 项目整体定位与技术选型思路1.1 为什么是 SpringBoot3 而不是 SpringBoot2先说后端。SpringBoot3 是 2022 年底发布的最大的变化是底层从 Java EE 规范切换到了 Jakarta EE 9包名从javax.*换成了jakarta.*。很多人觉得这只是换个包名的事实际上它代表了 Spring 官方对 Java 17 和云原生时代的全面拥抱。我这套框架直接用 SpringBoot3 还有一个原因Spring Security 6 的配置方式和 5.x 差别非常大如果现在还从 SpringBoot2 起步等于学了一套即将过时的写法后面升级成本很高。另一个现实因素是 SpringBoot3 天生带了 GraalVM 原生镜像的支持能力。虽然我日常开发还是用 JAR 包方式部署但遇到需要秒级启动、低内存占用的交付场景时靠 SpringBoot3 的 AOT 处理机制可以比较平滑地把应用转成原生镜像。这点在 SpringBoot2 时代想做需要额外配置很多东西现在方便多了。技术选型不能只看当下还得看未来一年两年的维护成本。Java 版本我选的是 17不是 21。原因很实际Java 17 是 LTS 版本市面上主流云厂商的构建镜像、运维脚本、监控组件对它支持最成熟。21 虽然也 LTS但部分第三方依赖还没有完全跟上没必要为了追版本给自己找麻烦。1.2 前端为什么必须用 Vue3 TypeScript前端这边Vue3 的组合式 API 解决了我最头疼的问题——大型后台系统里组件越来越臃肿Options API 把逻辑分散在 data、methods、watch 里维护起来非常吃力。用组合式函数Composable可以把表格查询、表单校验、弹窗控制这些高频逻辑全部收拢成可复用的函数一个复杂的业务页面拆下来也就两三百行可读性和可维护性都好了很多。TypeScript 在这个项目里属于“不用不行”的角色。后台管理系统最怕什么最怕接口返回的数据结构变了前端还蒙在鼓里直到上线了才发现某个字段是 undefined。有了 TypeScript我可以给每个 API 接口定义完整的请求参数和响应类型后端改字段时前端编译阶段就能发现错误。举个具体例子用户列表接口返回createTime我把它定义成string类型后端如果某天改成时间戳数字前端 TS 检查立刻报错根本等不到运行时才暴露问题。很多小型项目觉得 TypeScript 会增加工作量这是误区。对于后台管理系统这种强数据结构的场景类型系统带来的不是负担而是保险。特别是团队协作的时候成员看代码不需要去翻接口文档就能知道数据结构长什么样这省下来的沟通成本相当可观。1.3 “企业级”到底指什么“企业级后台框架”这几个字听起来有点虚实际上它是有一套硬性标准的。首先权限模型不能只是登录后区分管理员和普通用户至少得是 RBAC基于角色的访问控制模型用户关联角色角色关联菜单和按钮权限。我这套框架里菜单维度做了三级目录操作权限精确到按钮级别比如“新增用户”和“删除用户”是两个独立权限项这样分权才够细。其次是企业级系统最常见的多环境部署问题。开发环境、测试环境、生产环境的接口地址、日志级别、加密配置全都不一样。我单独抽出一个application-{profile}.yml配置体系不同环境只要指定不同的 Spring Profile 就能切换配置加上 Nacos 做配置中心之后连重启都不用。还有代码的可复用性。企业级系统的特点是业务复杂但模式重复无非是增删改查、导入导出、审批流。所以我把通用 CRUD、分页查询、批量删除、Excel 导入导出全部下沉到基础代码里业务层只需要写自己的差异化逻辑开发效率能提高不少。2. 后端核心架构与关键模块实现2.1 整体工程结构与模块划分后端我采用的是 Maven 多模块结构而不是 SpringBoot 单模块塞到底。模块拆分如下ruoyi-cloud-style-parent ├── ruoyi-common │ ├── ruoyi-common-core # 通用工具、常量、基础实体 │ ├── ruoyi-common-security # Spring Security 安全配置 │ ├── ruoyi-common-redis # Redis 配置与工具封装 │ ├── ruoyi-common-log # 操作日志注解与切面 │ └── ruoyi-common-datasource # 多数据源动态切换 ├── ruoyi-modules │ ├── ruoyi-system # 用户、角色、菜单、部门等系统管理模块 │ ├── ruoyi-job # 定时任务模块 │ └── ruoyi-generator # 代码生成模块 └── ruoyi-admin # 启动模块聚合所有模块的依赖拆模块的价值在团队开发时非常明显不同模块的负责人只需要关注自己的代码目录公共模块变动时 Maven 会自动触发依赖模块的重新构建模块边界清晰不会出现所有人都在一个大源码包里乱改的情况。虽然单体应用拆这么多模块看着有点重但对于预期会长期迭代的项目来说前期多花半天拆分的成本远小于后期模块耦合严重时的重构成本。2.2 认证授权怎么做Spring Security 6 OAuth2认证授权这一块是后台框架的重中之重。我直接选择了 Spring Security 6 OAuth2 的组合没有自己去手写拦截器和 Token 工具类。为什么因为安全领域的轮子太难造了自己写的登录校验大概率会漏掉 CSRF 防护、Session 固定攻击防护、密码加密策略这些细节。Spring Security 6 的配置方式改成了 Lambda DSL逻辑上更简洁。核心配置大概是这样的Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeHttpRequests(auth - auth .requestMatchers(/auth/login, /auth/captcha, /doc.html).permitAll() .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 - oauth2 .jwt(jwt - jwt .jwtAuthenticationConverter(customJwtAuthenticationConverter()) ) ); return http.build(); }Token 这块我用了 JWT但不是简单地把用户 ID 塞进去就完事。我的 JWT 里会携带用户的 userId、角色编码集合、权限标识集合并且设置合理的过期时间短令牌 2 小时 刷新令牌 7 天。每次请求进来后端通过 JWT 解析出用户身份和权限不需要查数据库就能完成鉴权性能上比 Session 方案好很多。这里要提醒一个坑JWT 是无状态的一旦签发出来在过期之前无法主动让它失效。所以我在 Redis 里维护了一个“在线用户”列表用户修改密码或管理员踢人时把对应的 token 标记为失效接口每次请求都会去检查这个黑名单。这样做虽然多了一次 Redis 读取但换来了安全性的显著提升。2.3 通用返回体、异常体系与操作日志企业级系统前后端协作最重要的一件事就是约定返回格式。我封装了一个统一的ResultT结构public class ResultT { private Integer code; // 200 成功500 业务失败401 未认证403 无权限 private String message; private T data; private Long timestamp; }前端 Axios 拦截器会根据code做统一处理业务层只需要返回数据不需要关心响应格式。这样有两个好处一是前端处理逻辑简单只需要在拦截器里写一次错误提示和登录过期跳转各业务页面就不用重复处理错误分支二是一旦接口风格统一后续要做接口文档自动生成、接口测试甚至前后端 Mock 都很方便。异常体系我也是单独设计的。系统里定义了几种业务异常类型ServiceException业务逻辑错误、AccessDeniedException权限不足、NotFoundException资源不存在。通过RestControllerAdvice全局异常处理器统一捕获业务代码里只需要throw new ServiceException(xxx)前端就会收到格式正确的错误信息然后弹出提示框。这样避免了满屏幕的 try-catch 嵌套代码读起来清爽很多。操作日志的维度我也做了细化。通过自定义注解Log标记需要记录的接口AOP 切面会自动记录操作人、操作类型新增/修改/删除/查询、请求参数、执行耗时和操作结果。这些日志异步写到 MySQL 的sys_oper_log表不影响主流程性能。我特别加上了一个值得注意的细节参数序列化时排除密码、Token 等敏感字段避免日志里泄露用户隐私。3. 前端工程化与核心功能落地3.1 Vite TypeScript 工程配置前端工程我选的 Vite而不是 Webpack。原因很直接Vite 基于 ESM 的开发服务器在冷启动和热更新速度上比 Webpack 快一个数量级。我自己实测下来同样一个百余个路由的后台项目Vite 冷启动大概在 1 秒左右Webpack 要跑到 10 秒以上开发体验差距非常明显。构建方面 Vite 用的是 Rollup对于后台管理系统的打包场景完全够用。TypeScript 配置上我开了strict: true不给自己留偷懒的余地。很多项目为了省事把 strict 关掉结果类型检查形同虚设。在严格模式下any类型会被大量规避配合 ESLint 规则里禁止显式any代码质量会有一个明显的提升。Vite 配置文件里的路径别名和代理也值得说一下// vite.config.ts import { defineConfig } from vite import vue from vitejs/plugin-vue import path from path export default defineConfig({ plugins: [vue()], resolve: { alias: { : path.resolve(__dirname, src), }, }, server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ), }, }, }, })开发环境的代理配置特别重要直接解决了前后端联调时的跨域问题。所有带/api前缀的请求Vite 会自动转发到后端的 8080 端口前端代码里只需要写相对路径不用去区分环境。生产环境则由 Nginx 做同样的反向代理前后端代码不需要为环境差异做任何特判。3.2 Axios 封装与多环境请求拦截Axios 封装我做了三层设计底层是基础的请求实例中间层是拦截器处理 Token 和错误最上层是按业务模块拆分的 API 函数集合。底层实例配置了基础 URL、超时时间和是否携带凭证的选项请求拦截器负责从 Pinia 里读取 Token 并添加 Authorization 请求头响应拦截器根据返回的 code 做不同处理。响应拦截器里的代码大概是这样service.interceptors.response.use( (response) { const res response.data if (res.code 200) { return res.data } if (res.code 401) { // Token 过期清除本地信息并跳转登录页 userStore.reset() router.push(/login) return Promise.reject(new Error(登录状态已过期)) } ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) }, (error) { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } )这个封装的巧妙之处在于业务代码里永远不会出现response.data.data这种二重取数据操作接口函数直接返回的就是干净的res.data配合 TypeScript 泛型可以做到调用时自动推断返回类型。如果不做这层封装每个页面都要自己处理错误提示和登录跳转代码会非常冗余。3.3 动态路由与菜单权限的实现后台系统的权限控制光靠前端隐藏菜单按钮是不安全的但后端已经做了接口权限校验后前端动态路由的价值就在体验层面。用户登录后后端根据其角色返回可访问的路由表和按钮权限标识前端再通过router.addRoute动态注册。这样每个用户看到的菜单都是根据自己的权限动态生成的不会出现“菜单能看到点进去却是 403”的尴尬情况。动态路由的数据结构我定义成了这样export interface RouteVO { path: string component: string // 组件路径如 system/user/index name: string meta: { title: string icon: string hidden?: boolean keepAlive?: boolean } children?: RouteVO[] }前端拿到这个结构后会通过路由映射表把组件路径转换成实际的组件引用。这里有个经典的问题——如果要支持动态加载组件就不能用静态 import得用import.meta.glob来批量导入所有views目录下的页面组件const modules import.meta.glob(/views/**/*.vue) const component modules[/src/views/${route.component}.vue]这样写才能懒加载用户实际访问到哪个页面才去加载对应的组件 JS 文件首屏加载速度会快不少。很多同学从 Vue2 转 Vue3 都会在动态路由 动态组件这里踩坑最典型的就是路由跳转后页面空白但地址栏变了往往是addRoute后没有调用router.replace重新进入一次路由导致的。3.4 Pinia 状态管理与组件类型安全状态管理我用的 Pinia它相比 Vuex 更轻量而且对 TypeScript 的支持是天然的。用户信息、Token、权限标识、标签页缓存这些都放在 Pinia 里项目刷新时再从本地存储恢复。Pinia 的定义方式非常简单不需要像 Vuex 那样写一堆 mutation 来更新状态直接修改 store 的属性就行——当然在严格模式项目里我还是习惯通过 action 统一修改。Pinia 配合 Composition API 使用时的类型体验很好。比如// stores/user.ts import { defineStore } from pinia export interface UserState { token: string userInfo: UserInfo | null roles: string[] permissions: string[] } export const useUserStore defineStore(user, { state: (): UserState ({ token: , userInfo: null, roles: [], permissions: [], }), actions: { setToken(token: string) { ... }, fetchUserInfo() { ... }, hasPermission(code: string) { ... }, }, })组件里调用useUserStore()后store 上的所有属性和方法都会有完整的类型提示写错拼写、传错类型的概率直接降到最低。对于团队新成员来说这比任何文档都直观。4. 实操过程与核心环节实现4.1 从创建项目到前后端联调的全流程如果你拿到这套源码想跑起来大致流程是这样的后端拉取代码后用 IDEA 打开先修改application-dev.yml里的数据库连接和 Redis 连接然后创建数据库并导入sql目录下的初始化脚本最后启动RuoYiApplication主类。前端进入ui目录执行npm install安装完依赖后执行npm run dev浏览器访问localhost:3000就会跳到登录页。我第一次跑这套流程大概花了十分钟其中大部分时间是在等 npm 依赖下载。可能遇到的第一个坑是 JDK 版本不对。SpringBoot3 强制要求 Java 17如果你机器上还是 Java 8启动时会直接报UnsupportedClassVersionError。所以第一步先检查java -version确认是 17 或以上再继续。前后端联调的关键点是接口路径统一。我后端接口统一带/api前缀前端开发环境通过 Vite 代理转发到后端请求生产环境通过 Nginx 转发。如果你发现前端请求 404十有八九是代理配置或 Nginx 转发规则的问题先确认前端页面 Network 面板里实际发出的请求地址是什么再回过去检查代理配置。4.2 代码生成器与低代码配置的意义这套框架里我最得意的模块是代码生成器。在系统管理的“代码生成”菜单里填写数据库表名和业务名称它就能生成从后端 Entity、Mapper、Service、Controller 到前端 Vue 页面、API 函数的全套代码。生成的代码直接下载下来解压到项目里就能跑。为什么要做这个因为后台系统的业务 70% 都是标准 CRUD这些代码毫无技术含量手动写就是浪费生命。代码生成器把开发者的精力从简单重复劳动中解放出来让人集中精力去写那 30% 的真正有业务逻辑的代码。我自己测试过生成一个完整的单表管理模块包含分页查询、新增、修改、删除、批量删除大约需要 3 秒如果手写大概是 30 分钟效率提升非常明显。生成器的实现原理也不复杂核心是读取数据库表的元数据字段名、字段类型、字段注释、是否主键等再套用预先写好的 Velocity 模板渲染代码。底层的模板文件都会放在项目里如果你想自定义生成代码的样式直接改模板文件就行。4.3 部署与发布要点部署这部分我踩过不少坑尤其是前端路由模式和后端打包之间的问题。前端我默认用的是 history 模式路由生产环境 Nginx 必须配置一个兜底规则把所有前端路由请求都重定向到index.html否则刷新二级页面就会 404。Nginx 配置大概是这样的location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }少了try_files那行用户只要在用户管理页面按下 F5就会看到 Nginx 的 404 页面。这个问题在本地开发时根本发现不了因为 Vite 开发服务器自动处理了 history 路由的 fallback所以很多项目上线后才暴露。后端部署我用 Docker 镜像方式。写一个多阶段构建的 Dockerfile第一阶段用 Maven 镜像编译打包第二阶段用 JRE 17 基础镜像运行。这样做出来的镜像体积不大部署时只需要docker run一下环境差异全部被镜像隔离掉了。如果你有 K8s 环境同一套镜像可以直接上容器编排很方便。5. 常见问题与排查技巧实录5.1 高频报错排查速查表我在社区和实际维护中收集了不少使用者反馈的问题整理出一个高频问题排查表基本上覆盖了从零搭建这套框架的绝大部分场景问题现象可能原因解决方案启动报UnsupportedClassVersionErrorJDK 版本低于 17安装 JDK 17 并切换 IDE 项目 SDK前端启动后接口全部 404Vite 代理配置错误或后端未启动检查vite.config.tsproxy 和后端 8080 端口登录成功后菜单不显示后端返回的路由组件路径与 views 目录不一致逐个检查动态路由组件路径是否有效上传文件后预览失败本地存储路径配置错误检查文件上传目录配置确保有读写权限定时任务不执行未开启任务调度开关确认配置xxl.job.admin.addresses等参数正确刷新页面后 404前端路由 history 模式缺少 Nginx fallback添加try_files $uri $uri/ /index.html;这张表的含金量在于这些都是真实出现过的不是凭空想象的。尤其路由组件路径不一致这个坑新旧版本后端接口返回的路径可能不一样前端views目录结构调整后忘了同步后端菜单表的数据就会触发。5.2 几个容易误判的隐蔽问题有几个问题我要单独拿出来说一说因为它们不太容易排查但遇到的人很多。第一个是vue3 项目在 Edge 浏览器中有时无法关闭右上角的最小化按钮这种听起来很“玄学”的问题。我排查过多次后发现这类问题往往不是前端代码的问题而是浏览器扩展程序或者系统级别的优化软件导致的少数情况下和后端实时推送的浏览器通知有关。处理方式就是让项目尽量少依赖浏览器特定特性同时建议用户清理缓存或检查扩展程序不要浪费大量时间去查自己的代码。第二个是打包时 TypeScript 报选项“baseurl”已弃用并将停止在 TypeScript 7.0 中运行的警告。这是 Vite 项目里的 tsconfig 配置还是老写法。新版 TypeScript 推荐在paths中直接使用相对路径映射替代旧的baseUrlpaths组合我在新版框架里已经改成这样{ compilerOptions: { paths: { /*: [./src/*] } } }第三个是vue3 uncaught syntaxerror: invalid or unexpected token这类报错。十有八九是代码里的中文字符或者是引号问题比如在模板字符串里不小心用了中文引号或者在标签属性里混入了不可见字符。遇到这种报错先看报错指向的代码行再往前看几行重点找引号和特殊符号。5.3 一个我重复强调的细节命名规范在团队协作场景下命名规范的重要性经常被低估。这套框架的前端 API 文件、后端 Controller、数据库表名我都做了统一约定数据库表用下划线命名如sys_user后端实体用驼峰命名如SysUser前端 API 函数用驼峰命名并与后端方法一一对应如getUserList。这套规范写进了项目的README里新成员加入后第一件事就是读规范。命名规范的价值在排查问题时体现得最明显。当你需要把一个用户列表功能的代码从数据库一直追踪到前端页面时遵循规范的命名可以让整个链路非常清晰sys_user→SysUser→SysUserController→sysUser.ts→UserManagement.vue不用想都能猜到下一个文件在哪里。如果每个人命名习惯都不同排查问题时光找文件就要花半天。最后再分享一个实用经验这套框架我从最早的单体版本改到现在中间最大的感受是技术选型可以激进但工程化设施必须保守。SpringBoot3、Vue3、TypeScript 这些新技术要用就用全套不要在项目里混着写 Options API 和 Composition API也不要在 TS 项目里到处留any。但像权限模型、日志体系、代码生成器这些基础设施尽量用经过验证的方案不要自己去发明奇奇怪怪的架构。如果你拿到源码后想改造建议优先从这几处入手一个是短信登录和第三方登录的接入框架里预留了扩展点另一个是数据权限的细分目前做到了部门级别的数据隔离如果想做到用户级或者自定义规则可以在现有的DataScope注解上做文章。这些改造不会破坏整体结构适合拿来练手熟悉代码。最后说一句开源不是为了秀代码而是让这套东西在更多场景下被验证。如果你在使用的过程中发现 bug 或者有更好的实现思路欢迎提 Issue 或者直接改代码提交 PR。这种相互促进的方式才是开源项目持续变好的根本。