尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot3+Vue3学生管理系统全栈开发:从建表到部署

SpringBoot3+Vue3学生管理系统全栈开发:从建表到部署 学校学生管理系统是前后端分离练手中很典型的一个题目。用 SpringBoot3 做后端、Vue.js3 做前端、MySQL 8 做数据库做出来的东西不是单纯的“增删改查”而是能把登录鉴权、分页查询、接口规范、跨域代理、前后端联调、部署配置这一整条链路都走一遍。对正在学 Java 全栈的人、准备做课程设计或毕业设计的人来说这个项目非常适合作为第一个完整全栈项目。这篇文章按实际开发顺序拆解。先说业务边界和表结构再讲 SpringBoot3 后端怎么搭、JWT 登录怎么做然后到 Vue3 前端页面如何对接接口最后补上联调排错和几个加分功能。代码块里的内容偏向核心骨架很多重复代码没有展开但每一步的意图和判断标准会写清楚。1. 先确认系统边界再写表和接口1.1 三个角色与六张核心表做管理系统第一个动作不是打开 IDE 写代码而是把业务对象梳理清楚。学校学生管理系统虽然名字叫“学生管理”实际需要管理的对象包括学生、班级、教师、课程、成绩、系统用户。这里容易犯的错是一开始就把功能想得太大比如消息通知、排课、宿舍管理、考勤打卡。功能越多表越多改起来越痛苦。你需要的是一门课不是一套 SAP。从最小可用闭环出发角色先分三种管理员维护班级、教师、课程管理学生账号查看统计数据。教师查看自己授课班级的学生录入成绩或导出成绩单。学生查看个人信息和选课成绩。这三类角色的数据不需要建三张用户表可以统一放到一张系统用户表sys_user通过role字段区分。业务数据建议拆成六张核心表系统用户表sys_user账号、密码、姓名、角色、状态。班级表clazz班级名称、年级、班主任。学生表student学号、姓名、性别、所属班级、手机号、入学日期。教师表teacher工号、姓名、职称、所属院系。课程表course课程名称、学分、授课教师。成绩表score学生、课程、学期、分数。设计时有一个关键判断学生表要不要用物理外键关联班级表我的建议是不要用数据库外键约束只保留clazz_id作为逻辑外键。原因很简单课设和真实项目都经常要调整班级数据物理外键会限制删除和批量更新出现问题后排查也比较麻烦。业务约束放到 Service 层做比如删除班级前先检查是否还有学生引用程序自己控制比数据库硬约束更好维护。1.2 功能模块按最小闭环划分基础版建议只做五个模块登录认证。首页统计看板。学生管理包含分页、条件查询、新增、编辑、删除。班级管理维护班级下拉数据。课程与成绩管理让老师可以添加成绩学生可以查看成绩。这里最核心的是登录加学生管理。如果时间紧张先把学生管理整个闭环跑通其他模块都是同一种写法的重复。班级管理本质上是“一个下拉框的数据源”课程和成绩本质上是“多表联合分页查询”。不要每个模块都重新设计一套复杂的架构统一风格即可。还要提前定好权限粒度。基础版做到“三种角色能进入不同菜单”就够了不一定要做按钮级别权限。如果用 SpringBoot3快速方案是用拦截器解析 JWT把userId和role放进请求上下文前端根据角色控制菜单。这样后端不需要引入完整 Spring Security学习成本降低很多。等以后要做到细粒度权限再迁移到 Spring Security 也来得及。2. 技术版本和环境要提前对齐2.1 SpringBoot3 不是 SpringBoot2 的小升级搜索这个问题时你大概率会看到大量旧教程但 SpringBoot3 底层已经变了。最明显的是下面三点基于 Jakarta EE很多包名从javax.变成了jakarta.。要求 JDK17 及以上JDK8 无法直接跑 SpringBoot3。部分第三方框架必须使用适配 SpringBoot3 的新版本。所以不要直接复制网上 SpringBoot2 项目里的配置。下载项目时优先用 Spring Initializr 选好 Java 版本和依赖再手动引入 MyBatis-Plus 这类扩展。如果你机器上已经装好 JDK8建议不要为了跑项目就学网上改成混乱的环境变量而是两个 JDK 版本共存通过JAVA_HOME切换或者直接用 IDE 给项目指定 JDK17。Vue3 也一样。Vue3 项目通常搭配 Vite不再像 Vue2 时代默认走 webpack。Node 环境建议使用 18 以上太旧的 Node 版本可能无法正常创建项目或安装新依赖。前端安装依赖时容易卡住建议先把 npm 镜像切到国内源否则npm install element-plus vue-router4 pinia axios可能要等很久甚至报网络错误。2.2 项目目录结构和环境验证建议用两个单独的目录放前后端根目录里再放数据库脚本。比如这样school-project/ school-server/ SpringBoot3 后端 school-web/ Vue3 前端 sql/ 初始化表和数据后端项目里按包分层com.example.school common 统一返回、异常处理 config 跨域、拦截器配置 controller 接口层 entity 数据库实体 mapper 数据访问层 service 业务层前端项目里按功能分目录school-web/src api 每个模块的请求方法 router 路由配置 stores Pinia 状态管理 views 页面组件 layout 整体布局 utils 请求封装目录结构不是随便分的。前端接口调用方法统一放到api组件里不直接写axios.get后面接口变了只需要改一处。后端 Controller 里不直接写 SQL 业务逻辑只接收参数、调 Service、返回结果否则代码一多就会变成大杂烩。环境装好后不要急着写代码先跑三组命令确认java -version mvn -v node -v如果java -version显示的是 JDK17Maven 能打印版本后端环境就算准备好。MySQL 安装后要确认服务已经启动用客户端能连上 3306 端口。不要跳过这一步很多学生管理系统写到最后才发现连接数据库失败根本原因是最初的 MySQL 服务没有启动或者 root 密码记错。2.3 接口规范先约定返回结构前后端分离项目最容易出的问题就是接口返回格式不统一。如果登录接口返回的是{ success: true }学生列表接口返回的是{ code: 200, data: [...] }成绩接口又返回了直接数组前端每个请求都要单独处理非常痛苦。所以第一步要定义统一返回类后端所有接口都使用同一种格式public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message 操作成功; result.data data; return result; } public static T ResultT error(String message) { ResultT result new Result(); result.code 500; result.message message; return result; } }登录、列表、新增、编辑、删除全部统一走这套结构。业务异常通过全局RestControllerAdvice捕获后返回Result.error(message)前端拿到非 200 的code就弹错误提示。这里不需要每个接口都写 try-catch但全局异常类必须做否则数据库异常会直接暴露给页面既不美观也不安全。接口路径建议统一带/api前缀比如/api/auth/login、/api/student/page。这样既方便后端区分接口资源也方便前端在 Vite 代理里统一转发。3. 后端从建表到登录鉴权的落地顺序3.1 MySQL 初始化和建表数据库脚本我通常放在sql/init.sql。建表时字符集统一使用utf8mb4因为要支持中文和特殊符号不要用默认的latin1。以用户表和学生表为例核心 SQL 可以这样写CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL COMMENT BCrypt加密后的密码, real_name VARCHAR(50) COMMENT 真实姓名, role VARCHAR(20) NOT NULL COMMENT ADMIN/TEACHER/STUDENT, status TINYINT DEFAULT 1 COMMENT 1启用 0停用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表; CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL, gender TINYINT COMMENT 1男 2女, clazz_id BIGINT NOT NULL COMMENT 所属班级ID, phone VARCHAR(20), admission_date DATE COMMENT 入学日期, status TINYINT DEFAULT 1 COMMENT 1在校 0离校/删除, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生表;初始密码不建议直接写明文。你可以写一个DataInitializer在项目第一次启动时检查管理员账号是否存在不存在就用BCryptPasswordEncoder加密后写入admin123。这样比手动生成一段固定的 BCrypt 密文放在 SQL 里更清晰也不会出现因为密文换行或转义导致无法登录的问题。班级表和数据记录的细节可以先不展开但这个阶段要明白一件事表结构决定业务边界。学生表的字段如果是clazz_name那么改班级名称时就必须同步改学生表这会产生数据冗余又很难维护。正确的做法是学生表只存clazz_id查询时通过 JOIN 或 VO 转换把班级名称带出来。3.2 后端包结构和持久层选择这里以 MyBatis-Plus 为例。如果你学校要求必须用原生 MyBatis 或 Spring Data JPA思路也是一样的只是 SQL 写法和实体映射方式不同。使用 MyBatis-Plus 可以减少日常 CRUD 代码分页和条件查询也比较直接。SpringBoot3 项目里引入 MyBatis-Plus 时要注意不能直接抄 SpringBoot2 使用的mybatis-plus-boot-starter。需要找适配 SpringBoot3 的 starter具体版本以后缀为准。如果你用 Maven依赖版本建议先查官方最新稳定版不要用一个不确定的旧版本。后端核心分层做三件事Controller 负责接收参数和返回值。Service 负责业务判断。Mapper 负责数据库访问。下面是一个接口的分层示意RestController RequestMapping(/api/student) public class StudentController { Resource private StudentService studentService; GetMapping(/page) public ResultPageResultStudentVO page( RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 10) int pageSize, String keyword, Long clazzId) { return Result.success(studentService.pageStudent(pageNum, pageSize, keyword, clazzId)); } }接口层不要出现LambdaQueryWrapper或 SQL因为 Controller 如果知道太多数据访问细节后面改动边界就模糊了。前端需要的可能是“学生列表”但数据库里的实体Student没有班级名称。最稳妥的方法是新建一个StudentVO包含学生基础字段和班级名称。查询时先用分页查学生再根据clazz_id集合一次性查出班级列表最后组装。不要在循环里逐条查询班级数据量一大性能容易出问题。3.3 JWT 登录和拦截器登录流程要做到三件事校验用户名密码。校验用户状态是否启用。签发 JWT 返回给前端。密码校验推荐使用BCryptPasswordEncoder。它的特点是每次加密结果不同但可以用 matches 方法校验原始密码和密文是否一致。数据库里永远不要保存明文密码这是一个基本底线。JWT 生成代码可以用 jjwt但版本 API 变化比较大。当前比较简洁的写法是这样的String token Jwts.builder() .subject(String.valueOf(user.getId())) .claim(role, user.getRole()) .claim(realName, user.getRealName()) .expiration(new Date(System.currentTimeMillis() 24 * 60 * 60 * 1000)) .signWith(getSecretKey()) .compact();这个 token 是前端后续请求的身份凭证。前端每次请求在请求头里携带Authorization: Bearer token后端写一个拦截器拦截除了登录接口之外的/api/**请求解析 token把用户信息放入 ThreadLocal。这样每个接口都能通过一个UserContext拿到当前用户是谁。解析失败就返回 401前端收到 401 后清掉本地 token跳转回登录页。要注意一个边界JWT 适合做“身份认证”但不适合做“强制下线”。如果账户被管理员停用只要旧 token 没过期接口仍然可能被访问到。所以在查询用户状态时不能只看 token每次访问数据库前还需要判断一下用户是否仍存在或者把登录状态存到 Redis。基础版里可以在拦截器解析 token 后再去sys_user表查一次状态。这个查询很轻量但能解决停用账号仍然有效的问题。4. 学生管理模块的前后端完整走一遍4.1 后端学生管理接口要做哪些验证学生管理模块先说后端。主要接口看这张表接口方法用途/api/student/pageGET分页查询学生/api/student/{id}GET根据主键查学生详情/api/studentPOST新增学生/api/student/{id}PUT修改学生/api/student/{id}DELETE删除学生/api/clazz/listGET返回班级下拉框选项写 Service 时最容易忽略的是“业务校验要放在哪一层”。我的顺序是新增时先判断学号是否为空。再按sno查库判断学号是否已经被占用。然后判断clazzId对应的班级是否存在。最后执行插入。修改时需要额外判断当前要修改的学生是不是存在如果学号变了排除自己主键后再查重。删除前还需要看成绩表中有没有这个学生的成绩记录如果有就不能直接硬删否则成绩明细表里的学生信息会全部断裂。课设阶段可以用逻辑删除方案给student表加一个deleted字段删除操作只更新状态查询条件带上未删除标记。这样历史数据不会丢成绩表也能继续查询。不要只接收前端传来的所有字段直接执行更新。比如学生修改时如果传了createTime要不要允许修改正常情况下createTime应该由后端自己管理更新只允许更新业务字段。实际开发中常出现学生误传用户 ID 或角色字段所以新增和修改时要做字段白名单。基础系统可以不用 DTO但至少要清楚 Controller 接收的参数不应该直接等于数据库实体。分页查询的筛选条件一般有姓名、学号、班级。后端逻辑是LambdaQueryWrapperStudent wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), Student::getName, keyword) .or(StringUtils.hasText(keyword), w - w.like(Student::getSno, keyword)); wrapper.eq(clazzId ! null, Student::getClazzId, clazzId); wrapper.orderByDesc(Student::getCreateTime);这段代码要注意一个地方keyword 为空时不要拼like条件否则查出来会是空列表。StringUtils.hasText或类似工具可以先判断参数。MyBatis-Plus 的.or()如果配合前面的.eq()使用要注意 SQL 条件括号会不会错乱。建议把 name 和 sno 的模糊查询用嵌套 wrapper 包起来否则可能出现clazz_id ? AND name LIKE ? OR sno LIKE ?这个 SQL 结果是错的。这类问题看起来像是框架 bug实际是条件构造顺序没处理好。前端需要回显班级名称所以查询结果的 VO 里建议给一个clazzName字段。如果你不想写 JOIN可以先查学生分页数据再通过clazzId集合批量查班级然后用 Map 组装班级名称。4.2 Vue3 项目初始化Vue3 前端创建项目时我习惯用 Vite。命令通常是npm create vitelatest school-web -- --template vue cd school-web npm install npm install vue-router4 pinia axios element-plus安装完成后浏览器打开开发地址能看到一个默认页面就说明 Vite 和 Vue3 已经跑通了。这里需要解释一下为什么要引入 pinia。它是一个状态管理库。登录接口返回的 token、用户基本信息都需要让很多页面共享。如果每个页面都从 localStorage 读 token代码会散落得到处都是。更好的做法是统一通过 pinia 维护token 持久化保存到 localStorage页面需要时从 store 读取。不过新手最容易踩的坑是刷新页面后 pinia 数据丢失。所以 localStorage 依然有必要。一个简单的做法是登录成功后同时写入 pinia 和 localStorageaxios 请求拦截器固定从 localStorage 读 token因为请求是异步的如果 store 还没有恢复就会拿不到。这一点在网上会看到很多不同写法最稳妥的其实是 token 只从 localStorage 读用户信息再通过接口获取。Element Plus 可以全局引入也可以按需引入。课设项目直接全局引入更方便首屏体积问题可以后面再优化项目还没跑通前不用过度追求打包体积。4.3 学生管理页面实现页面整体结构可以拆成四块搜索表单区。功能按钮区。表格区。分页区。搜索表单的例子el-form :inlinetrue :modelquery el-form-item label关键字 el-input v-modelquery.keyword placeholder姓名或学号 clearable keyup.enterhandleSearch / /el-form-item el-form-item label班级 el-select v-modelquery.clazzId placeholder请选择班级 clearable el-option v-foritem in clazzList :keyitem.id :labelitem.name :valueitem.id / /el-select /el-form-item el-form-item el-button typeprimary clickhandleSearch查询/el-button el-button clickhandleReset重置/el-button /el-form-item /el-form表格要绑定列表数据和分页信息el-table :datastudentList v-loadingloading border el-table-column propsno label学号 width120 / el-table-column propname label姓名 width120 / el-table-column label性别 width80 template #default{ row } span{{ row.gender 1 ? 男 : 女 }}/span /template /el-table-column el-table-column propclazzName label班级 / el-table-column propadmissionDate label入学日期 / el-table-column label操作 width180 fixedright template #default{ row } el-button link typeprimary clickopenEdit(row)编辑/el-button el-button link typedanger clickhandleDelete(row)删除/el-button /template /el-table-column /el-table分页组件和数据加载逻辑el-pagination v-model:current-pagequery.pageNum v-model:page-sizequery.pageSize :totaltotal :page-sizes[10, 20, 50] layouttotal, sizes, prev, pager, next size-changeloadData current-changeloadData /const loadData async () { loading.value true try { const data await getStudentPage(query.value) studentList.value data.records total.value data.total } finally { loading.value false } }这里要注意“搜索和重置”的逻辑。搜索时如果不把pageNum重置为 1当前页已经翻到第 5 页时搜索的结果可能只有 1 页就会出现“当前页没有数据”。正确的做法是在handleSearch里先把query.pageNum 1再调用loadData。新增和编辑表单可以共用一个弹窗。弹窗打开时先判断是新增还是编辑如果是编辑就根据 id 调详情接口回填表单如果是新增就把表单清空。保存成功后关闭弹窗并重新加载列表。这种逻辑很像流水账但也是所有管理系统都逃不掉的模式。4.4 开发环境跨域与接口代理前端代码运行时默认地址是http://localhost:5173后端接口默认地址是http://localhost:8080。直接让前端通过浏览器向 8080 发起请求会发生跨域问题浏览器会拦截响应。最简单的方案是在 Vite 配置里做代理让前端发出的请求还是走 5173 自己的地址Vite 开发服务器再把请求转发到 8080。修改根目录的vite.config.jsimport { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })同时前端 axios 的baseURL要写成/api不要写成http://localhost:8080/api。这样请求类似/api/student/page能被 Vite 代理转发开发环境和生产环境都不会被硬编码的地址卡住。也可以在后端加一个全局跨域配置用CrossOrigin或WebMvcConfigurer处理。但要注意一旦把允许的域名设置为*带 cookie 的请求就可能出问题。开发环境优先使用 Vite 代理不要在后端把所有跨域请求全部放开。这个习惯越早养成越好后面部署到服务器就不需要来回改代码。5. 联调部署与常见问题排查5.1 数据库连不上先按这个顺序查很多学生管理系统看起来什么都写好了最后卡在 MySQL 无法连接。这类问题的排查顺序我认为应该是先确认 MySQL 服务是否启动。再确认使用的端口是不是 3306。然后确认账号密码和连接地址是否正确。接着看数据库名是否存在。最后看 JDBC URL 参数是否满足当前 MySQL 版本。MySQL 8 默认使用caching_sha2_password认证插件某些旧版本驱动会出现连接报错。如果用 JDBC 直连通常要在 URL 后面加上useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue这句配置只是开发环境的表现生产环境要重新评估 SSL 策略不能为了图省事无脑加。网络热词里搜“mysql 安装教程”“mysql 安装配置”的人很多但数据库安装好后先跑一个最简单的连接测试比后面在项目里反复排查更省时间。可以用 Navicat 或命令行先连接一次能连通再继续写项目。如果提示Access denied for user rootlocalhost先检查密码是否正确不要急着改权限。如果服务能启动但连不上再看端口有没有被其他进程占用。Linux 环境下如果是用 Docker 安装 MySQL还要确认容器端口映射和容器内部的网络地址不能用localhost直连容器内部端口。5.2 接口返回异常时的排查链路接口正常时页面一般能加载出来。一旦页面空白、数据不出现不要一上来就改代码。我通常建议按下面顺序看打开浏览器开发者工具的 Network 面板。找到对应请求看 HTTP 状态码。看接口响应体里的code和message。看浏览器控制台有没有 JavaScript 报错。最后再看后端控制台有没有异常堆栈。HTTP 404多数是路径不一致。前端请求/api/student/page后端映射必须完整匹配包括/api前缀。如果后端配置了server.servlet.context-path/school请求路径很可能变成/school/api/student/page而 Vite 代理里的 target 如果不带/school同样会 404。HTTP 405一般是请求方法不一致。前端用了POST后端接口却是GetMapping就会 405。HTTP 401通常是 token 缺失或 token 过期。先看请求头里有没有Authorization再看拦截器解析逻辑是否正常。常见错误是前端 token 存到了 pinia刷新页面后 localStorage 里没有同步导致发出请求时没带 token。我的建议是请求拦截器从 localStorage 读取 token不依赖 pinia。HTTP 200 但前端一直没数据要看后端返回的数据结构是什么。如果后端返回的是{ code: 200, data: { records: [], total: 0 } }前端就要取response.data.records如果后端把 Page 的records放在了最外层前端又要换一种写法。这里出现问题的原因不是后端或前端坏了而是两边的字段约定不一致。改造时先统一返回结构能够避免这类问题。如果后端返回 500先看后端控制台异常第一行。不要只把“请稍后重试”这五个字截图给同事第一行报错信息才有价值。常见的 500 原因包括数据库表字段不存在、MyBatis-Plus 映射失败、空指针、SQL 语法错误。5.3 加分项班级统计看板和成绩导出基础增删改查做完后建议把系统做成“看起来真的能用”的状态。第一个加分项是首页统计看板。最简单的统计是“班级人数分布”。后端写一个聚合接口SQL 类似SELECT c.id, c.name AS clazzName, COUNT(s.id) AS studentCount FROM clazz c LEFT JOIN student s ON c.id s.clazz_id GROUP BY c.id, c.name ORDER BY c.id前端拿到这个数组后用 ECharts 画一个柱状图或饼图。这里的关键是区分“统计接口”和“列表接口”。统计接口不用分页返回的是汇总数组。前端可以放三个统计卡片分别展示学生总数、班级总数、课程总数再做一张图表展示班级人数系统完整度立刻会上一个台阶。第二个加分项是学生列表导出 Excel。推荐使用 EasyExcel。后端接口可以按查询条件导出当前筛选结果不要把所有学生全部导出来。导出需求最容易出的坑是数据量很大时直接在 HTTP 请求线程里生成文件导致请求超时。基础课设里数据量通常不大可以先按同步导出写然后把导出结果的表头、列顺序和查询列表保持一致。导出时还要注意文件名的编码问题涉及中文文件名时HTTP 响应头里的Content-Disposition需要做 URL 编码否则浏览器下载下来会变成乱码。这个问题在很多管理系统里都出现过排查思路也比较固定先看后端日志有没有异常再用浏览器开发者工具看响应头。5.4 上线部署前要检查的几项配置如果项目要部署到服务器不要等到部署时才想配置问题。第一是数据库账号。生产环境不要使用 root 账号要新建一个业务账号只授予当前数据库的增删改查权限。第二是前端接口地址。开发时写了 Vite 代理部署时前端静态文件由 Nginx 托管Nginx 里要把/api反向代理到后端 Java 进程。例如location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }第三是日志。后端项目启动时如果是在 Linux 服务器上不要只把日志打到控制台最好用nohup或 systemd 把日志输出到文件。出了问题后先看日期最近的日志文件而不是重新启动一次项目试运气。第四是初始化数据。管理员账号、基础班级、课程数据要能在服务器上自动初始化否则页面打开是空的会误以为部署失败。初始化脚本可以用 Flyway 管理也可以用一个简单的启动执行器但尽量保证“脚本能重复执行”不会因为重复插入造成主键冲突。如果这套链路能完整跑通你已经具备了把一个全栈管理系统从零带到可交付状态的基本能力。下一步再继续加 Spring Security、Redis、异步任务、前端权限路由都会有更清晰的结构可以挂靠而不是边写边乱。
返回列表