
拿到这个项目标题的第一反应是“经典中的经典”。SpringBoot2 Vue3 MyBatis-Plus MySQL8.0这套组合在当前的 Java Web 课程设计、毕业设计乃至小型企业级项目中出镜率实在太高了。标题里带了“含文档”三个字说明不是光有代码的裸项目而是连数据库脚本、部署说明、设计文档都给你备齐了这对赶工期的同学来说非常关键。这篇总结我就以“教学资源库系统”为例把这套技术栈从零搭建到上线的心得、踩坑、和实操细节一次性讲透。先给没接触过的朋友划个重点所谓教学资源库系统本质就是一个典型的管理系统——前台给普通用户浏览课程资料、下载附件、查看文章后台给管理员维护课程分类、上传教学视频和文档、管理用户和权限。这类系统的CRUD特征明显业务逻辑不复杂但正好能把你对 SSM/SpringBoot 后端、Vue 前端、MySQL 数据库的知识点全部串联起来。无论你是做毕设、课程设计还是想在公司内部快速搭一个内容管理后台这套源码的架构思路都能直接复用。1. 整体设计与技术选型思路1.1 为什么是“SpringBoot2 Vue3 MyBatis-Plus”而不是其他组合先说后端。用 SpringBoot2 而不是 SpringBoot3不是因为新的不好而是毕业设计和国内大多数教学资源、开源博客模板都还停留在 2.x 时代。SpringBoot2 的生态最成熟遇到问题时你搜到的解决方案最多这对要快速出成果的阶段来说是巨大的优势。另外很多学校机房和服务器上的 JDK 还是 8 或 11SpringBoot2 完美兼容SpringBoot3 最低要求 JDK17光是这一步就能劝退不少人。前端用 Vue3 则是顺势而为。如果你看过 Vue2 的 Options API 写多了再看 Vue3 的 Composition API会感觉代码组织更清爽。再加上 Vite 的冷启动速度比 Webpack 快了好几倍开发时改动代码只刷新对应组件体验直接拉满。更关键的是Element Plus 已经全面转向 Vue3Vue3 的ref、reactive、computed这些 API 配合script setup语法糖写列表页、表单页、弹窗的速度非常快。持久层选 MyBatis-Plus 就更不用犹豫了。它的BaseMapper直接帮你把单表的增删改查全部封装好日常开发你只需要写一个接口继承它不需要写任何 XML 映射文件。但这不代表 MyBatis-Plus 只能做简单查询——内置的Wrapper条件构造器可以链式拼接like、eq、between、orderByDesc配合分页插件PaginationInnerInterceptor复杂的多条件列表查询也应付得来。真正复杂的多表关联 SQL则保留在 XML 里手写两边互补既快又不失灵活性。1.2 系统功能模块的拆解思路拿到“教学资源库系统”这个需求第一步不是写代码而是拆功能模块。我梳理下来的核心模块如下用户模块注册、登录、个人信息修改、密码修改、头像上传。管理员可以查看用户列表、禁用/启用账号。课程分类模块课程的一级分类和二级分类支持树形结构展示。分类数据在后台维护方便以后扩展“新增课程类型”。课程资源模块这是系统的核心——展示课程列表支持按分类筛选、按关键词搜索前台可以查看课程详情、在线预览视频或下载附件。文章资讯模块发布教学公告、学习心得、新闻动态。前台展示资讯列表和详情页后台编辑器需要支持富文本。评论互动模块用户登录后可以对课程进行评论和打分管理员可以删除违规评论。后台数据概览登录后台首页展示用户总数、课程总数、今日新增、访问量等统计信息用几个 ECharts 图表呈现显得系统更完整。这六个模块定下来前后台的页面划分界限就很清晰了。前台对应的是游客能看的页面后台对应的是管理员管理的界面。很多同学一开始就急着写接口结果前台的资源查询没做分页、后台的权限控制没考虑后面返工特别痛苦。1.3 “含文档”这个标签意味着什么标题里的“含文档”不只是噱头。毕设和课程设计评审时文档往往和代码同等重要。一份完整的教学资源库系统文档通常需要包含项目概述和需求分析说明系统要解决什么问题、目标用户是谁、核心业务流程。系统设计文档功能结构图、数据库ER图、实体属性说明。数据库设计文档每张表的字段名、类型、约束、索引设计说明。接口文档每个接口的 URL、请求方式、请求参数、返回值。环境搭建和部署手册从 JDK 安装到 MySQL 建库到前后端打包部署的完整步骤。所以拿到这套源码你最先应该看的不是src目录而是文档目录里的SQL脚本和接口说明。先把数据结构读懂再去看代码逻辑效率会翻倍。2. 核心细节解析与实操要点2.1 MySQL8.0 的安装与基础配置因为这个项目指定了 MySQL8.0我就多说几句。很多人在安装 MySQL8.0 时踩坑主要集中在三件事时区问题、字符集问题、认证插件问题。安装完成之后建议在my.iniLinux 下是my.cnf中加上这样几个配置[mysqld] port3306 character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci default-time-zone08:00 [client] default-character-setutf8mb4这里有几个细节容易忽略utf8mb4才是完整的 UTF-8 编码能存 emoji 表情和特殊符号如果写成utf8在插入生僻字或表情时可能会报错。default-time-zone08:00这个必须加。不然你在代码里 JDBC 连接串上配了serverTimezoneAsia/Shanghai虽然也能用但如果数据库本身的时区不对时间字段会有 8 小时的偏差。Windows 用户如果懒得改配置文件也可以在启动后执行SET GLOBAL time_zone 8:00;但重启后失效还是改配置文件更一劳永逸。JDBC 连接串这样写jdbc:mysql://localhost:3306/resource_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue最后那个allowPublicKeyRetrievaltrue是 MySQL8.0 配合新版驱动必须要加的不然 IDE 或 Java 代码连接时可能会报Public Key Retrieval is not allowed这个问题特别经典经常有人被卡一下还以为是账号密码错了。2.2 SpringBoot2 项目的分层设计这个教学资源库系统的后端采用经典 Controller-Service-Mapper 三层结构这是前人踩过无数坑之后固化的最佳实践。Controller只负责接收参数、校验入参、调用 Service、返回统一结果对象。不要在 Controller 里写任何业务逻辑。Service / ServiceImpl承载核心业务逻辑一个方法代表一个完整业务流程比如“下架课程要同时删除课程附件和关联评论”这类事务操作就得放在这一层。Mapper直接对数据库进行操作。使用 MyBatis-Plus 后这个接口只要继承了BaseMapperT就能直接获得selectById、insert、updateById、deleteById等基础方法。分层带来的最大好处是“职责清晰出问题知道去哪找”。比如接口返回数据不对先看 Service 层方法的日志再考虑是不是 Mapper 的 SQL 写错了。另外这一套结构是面试官和答辩老师最熟悉的结构你答辩时只要能把每一层负责什么说得清清楚楚基础分基本就稳了。在项目里推荐封装一个统一返回结果Result结构大致如下public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } }任何接口都返回这个对象前端拿到code判断业务是否成功不需要每个接口单独处理异常。同时配一个全局异常处理器RestControllerAdvice这样后端没捕获的异常会统一转换成“系统异常请联系管理员”的提示而不是把 SQL 错误信息直接甩给浏览器。2.3 Vue3 前端项目的关键设计前端部分项目结构通常是这样的src/ ├── api/ # 接口定义按模块拆文件 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # Pinia 状态管理 ├── utils/ # 工具方法含 axios 封装 └── views/ ├── admin/ # 后台页面 └── front/ # 前台页面其中最重要的就是utils/request.js里的 axios 封装。我见过太多人每个页面里直接axios.get(...)代码写得很爽但后来要统一加 token 或处理登录过期时就只能在每个请求里修改。正确做法是封装一个实例import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器每次请求自动带上 token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) // 响应拦截器统一处理错误码 request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { // 登录过期跳转登录页 } return Promise.reject(error) } ) export default request这样封装之后每个页面调用接口只需要关心业务数据token 管理和错误提示全部统一处理以后维护起来非常香。前端页面上教学资源库系统比较值得一看的是课程列表页。它通常包含三个核心功能分类筛选下拉框、关键词搜索框、带分页的表格/卡片列表。在 Vue3 里这些功能用响应式变量 一个getList()函数就能玩转const queryParams reactive({ pageNum: 1, pageSize: 10, categoryId: null, keyword: }) const list ref([]) const total ref(0) async function getList() { const res await getCourseList(queryParams) list.value res.records total.value res.total }只要修改了queryParams里的任何字段再调用一次getList()就能刷新页面数据逻辑非常清晰。这也是管理系统最通用的写法学会这一个模式后台管理页面你能写出一百个。3. 实操过程与核心环节实现3.1 数据库设计——教学资源库的核心这套系统最值得仔细看的就是数据库设计。我把它拆成几张核心表来说明你照着这个思路建库基本不会出大问题sys_user用户表字段核心项id大主键username用户名唯一password存储加密后的密码nickname昵称avatar头像地址role用户角色0 普通用户 / 1 管理员status账号状态0 正常 / 1 禁用。course_category课程分类表字段核心项idname分类名称parent_id父级 id——顶级分类的parent_id为 0sort排序号create_time。course_info课程资源表这是核心表。字段核心项idcategory_id关联分类表title课程标题cover封面图intro简介content课程详细内容富文本teacher授课老师video_url视频地址file_url附件地址view_count浏览数status发布状态0 草稿 / 1 已发布create_time。course_comment课程评论表字段核心项idcourse_id关联课程user_id关联用户content评论内容rating评分1-5 之间create_time。article_info文章资讯表字段核心项idtitle标题author作者content富文本内容cover文章封面view_count浏览量publish_time发布时间。表建完之后用外键还是不用这是个经典问题。我的建议是不要在 MySQL 里建物理外键而是用逻辑外键。什么意思就是在course_info表里要有category_id这个字段但是不要用FOREIGN KEY约束去强制数据库关联。理由是物理外键在程序删除分类时会遇到很大的麻烦——你删一个分类数据库会因为这个分类下还有课程而拒绝执行或者需要设置一堆ON DELETE CASCADE的连锁规则。项目开发时会频繁调试数据物理外键反而碍手碍脚。但在设计文档里你要画出逻辑外键关系图清晰地标注“课程表通过 category_id 与分类表关联”这样既满足了规范化设计要求又保留了实际开发的灵活性。3.2 后端核心接口课程分页查询如何完成教学资源库系统最常用、也最能体现 MyBatis-Plus 实力的接口就是课程分页查询。它不是一个简单的SELECT * FROM course_info LIMIT 0,10它需要同时支持关键词模糊搜索、分类过滤、状态过滤、按浏览量或时间排序。用 MyBatis-Plus 实现这个功能非常清爽Override public PageCourseInfoVO getCoursePage(CourseQueryDTO dto) { // 创建分页对象当前页每页条数 PageCourseInfo page new Page(dto.getPageNum(), dto.getPageSize()); // 构建条件构造器 LambdaQueryWrapperCourseInfo wrapper new LambdaQueryWrapper(); wrapper.eq(CourseInfo::getStatus, 1); // 只查询已发布 if (StringUtils.hasText(dto.getKeyword())) { wrapper.and(w - w.like(CourseInfo::getTitle, dto.getKeyword()) .or().like(CourseInfo::getTeacher, dto.getKeyword())); } if (dto.getCategoryId() ! null) { wrapper.eq(CourseInfo::getCategoryId, dto.getCategoryId()); } wrapper.orderByDesc(CourseInfo::getCreateTime); // 分页查询 PageCourseInfo result courseMapper.selectPage(page, wrapper); // 联查分类名称和评论数等组装返回 VO ... return convertedPage; }这里面有几点值得说明LambdaQueryWrapper是类型安全的条件构造器字段名不是字符串而是方法引用编译阶段就能发现写错字段名的问题。StringUtils.hasText()是 Spring 自带的工具方法判断字符串是否为空且包含非空格字符比直接! null更严谨。列表查询条件里的“点击分类后只看该分类下课程”其实就是wrapper.eq(CourseInfo::getCategoryId, dto.getCategoryId())这一行非常简单。如果需要按某个字段动态排序比如点击表头按浏览量排序可以判断排序字段和排序方向后动态添加orderByAsc或orderByDesc。还有一个细节可能让你困惑如果分类有二级层级用户点击父级分类时要不要显示所有子分类下的课程更完整的做法是——先查询父分类下所有子分类的 id 集合然后用wrapper.in(CourseInfo::getCategoryId, childIds)条件查课程。这一步逻辑不复杂但属于加分项能在文档里描述清楚说明你真正理解了树形结构的业务含义。3.3 前后端联调和跨域问题处理前后端分离项目联调时绕不开跨域问题。开发环境下最常见的方式是配置 Vite 代理。在vite.config.js中server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, // 如果后端接口没有 /api 前缀这里可以重写路径 rewrite: path path.replace(/^\/api/, ) } } }这样你前端请求/api/course/listVite 开发服务器会自动把它转发到http://localhost:8080/course/list同时浏览器的同源策略不会拦截前端代码里不需要写完整的带端口号的 URL。很多同学在网上搜到跨域解决方法是在后端写一个 CORS 配置类例如Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOrigin(*); config.addAllowedMethod(*); config.addAllowedHeader(*); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }这个写法确实能解决真实环境下的跨域但注意如果前端用了Authorization这种自定义请求头那么你addAllowedOrigin(*)和后端addAllowedHeader(*)虽然方便但有些浏览器在携带凭证如withCredentialstrue时会强制要求AllowedOrigin不能是通配符*。所以更好的建议是明确指定前端域名而不是一劳永逸地用通配符。在毕设答辩或生产部署时这个细节被问到的概率非常高提前掌握能显得你比同龄人专业一截。3.4 实际部署前端打包后如何与后端共存最终交付项目时不可能一直依赖开发模式。部署方案有两种第一种是彻底分离部署后端打成 jar 包跑在 8080 端口前端npm run build生成的dist目录放到 Nginx 的 html 目录Nginx 监听 80/443 端口并配置反向代理/api到后端口。这种方案性能最好是生产环境的主流做法。Nginx 关键配置片段server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; # 解决 Vue Router 刷新 404 } location /api/ { proxy_pass http://localhost:8080/; } }注意 Nginx 配置里try_files $uri $uri/ /index.html;这一行特别容易被忽略。Vue Router 默认使用 history 模式时前端路由路径比如/course/detail如果用户直接刷新这个页面Nginx 发现没有真实文件会返回 404。加了这个配置后所有找不到的文件都会回退到index.html由前端路由接管刷新问题就直接消失了。第二种是合并部署把前端打包好的dist目录拷贝到后端的src/main/resources/static目录下重新打包 jar。启动后访问http://localhost:8080直接看到首页。这种方式适合没有独立 Nginx 资源的场景在毕设答辩演示时非常方便——一个 SpringBoot 进程所有功能跑完。我也推荐你在本地演示和打包给老师看时用这种方案省事。4. 常见问题与排查技巧实录4.1 MyBatis-Plus 分页“失效”的怪问题我第一次用 MyBatis-Plus 时照着文档写了selectPage但发现返回的数据始终是全部记录数分页完全不生效。排查了半天发现原来是忘了配置分页拦截器。很多人以为只要引入了mybatis-plus-boot-starter分页插件就自动生效了。实际上不是你必须在项目里显式配置一个MybatisPlusInterceptorBeanConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInnerInterceptor new PaginationInnerInterceptor(DbType.MYSQL); // 设置最大每页条数防止恶意查询导致数据库压力过大 paginationInnerInterceptor.setMaxLimit(100L); interceptor.addInnerInterceptor(paginationInnerInterceptor); return interceptor; } }这个配置位于后端的配置类中作用是让 MyBatis-Plus 在执行分页查询时自动改写 SQL拼上LIMIT ?。如果没有它selectPage和普通selectList效果基本一样只有一条「全表查完再内存截取」的降级逻辑数据量一大就废了。这是一道超高概率的踩坑点问题表现为“接口能调通但永远返回全部数据”遇到此症状第一优先级就查这里。4.2 MySQL8.0 驱动版本不匹配 / 驱动类找不到如果你用的 MySQL 驱动还停留在mysql-connector-java的 5.x 版本连接 MySQL8.0 时会报各种奇怪的错误比如Loading class \com.mysql.jdbc.Driver. This is deprecated或者干脆ClassNotFound。这是因为 MySQL8.0 之后驱动类的路径改成了com.mysql.cj.jdbc.Driver并且官方把驱动包重新命名了。SpringBoot2 项目中正确的依赖写法是dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency如果你用的是 SpringBoot 的版本管理直接引入mysql-connector-j不需要写版本号SpringBoot 的依赖管理会自动匹配兼容版本。在application.yml中驱动类配置如下spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/resource_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456还有一个常见的坑MySQL 8.0 的默认认证插件是caching_sha2_password而某些旧版本连接工具只支持mysql_native_password。如果你在运行 SpringBoot 项目时遇到了Unable to load authentication plugin caching_sha2_password这类提示那大概率是驱动版本不够新升级驱动即可但如果你的工具像旧版 Navicat 连不上可以在 MySQL 里执行下面的命令把指定用户的认证方式改回传统模式这只是一个兼容手段按需使用ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;4.3 富文本内容在前端显示没有渲染教学资源系统的课程详情和文章详情后台往往用富文本编辑器如 wangEditor、tinymce保存 HTML 内容存入数据库字段类型是text或longtext。前端用 Vue 渲染时如果直接用插值表达式{{ content }}你会看到一堆 HTML 标签原样显示出来——因为插值表达式默认会把内容当作纯文本转义。正确做法是使用v-html指令div classcourse-content v-htmlcourseDetail.content/div这里必须提醒一个安全点v-html会直接注入 HTML 和脚本如果内容是用户可编辑的存在 XSS跨站脚本攻击风险。对于教学资源库这种管理员可控内容的系统来说风险相对可控但依然建议在后端接口对富文本内容做一下基础过滤比如移除script标签、事件属性onclick、onerror或者引入xss过滤库。在答辩时能主动提起“我做了 XSS 过滤”是会加分的。另外还有一个小细节富文本编辑器保存的内容里可能包含style属性、class样式如果你发现内容里的图片和字体排版“很丑”大概率是前端引入的全局 CSS 把编辑器生成的 class 覆盖了。解决办法是在详情页给内容区域加一个独立的scoped样式或者引入富文本编辑器配套的 content.css 样式文件。4.4 前后端时间格式不一致列表页经常出现这种情况后端返回的时间是2024-01-15T08:30:00.00000:00前端表格直接显示一长串英文很影响观感。这个问题的根源在于后端LocalDateTime序列化时没有指定格式默认采用了 ISO 标准格式。解决方式有三种选一个你喜欢的在application.yml里全局配置统一格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai但只会对java.util.Date生效对LocalDateTime需要额外配 Jackson 的JavaTimeModule序列化器或者在字段上加注解。在实体类的日期字段上直接加注解最省事JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime;前端处理在展示处用dayjs或手写格式化函数例如import dayjs from dayjs // 页面调用 formatter formatter: (row) dayjs(row.createTime).format(YYYY-MM-DD HH:mm:ss)我个人建议在项目里统一用第二种方式后端处理好数据格式前端各页面直接展示不用到处写格式化逻辑。4.5 接口返回数据为 null 但数据库有值这是联调过程中非常tricky的一类问题。你确认数据表里有数据前端调用接口也返回了 200但返回的数据字段是null。排查思路按三个顺序走看后端实体类字段名和数据库列名是否对得上。MyBatis-Plus 默认开启驼峰转换所以createTime字段能正确映射到create_time列但如果你的字段命名不规范比如数据库列叫create_time实体属性叫createtime就映射不上了。看 SQL 查询结果。在application.yml里打开 SQL 日志mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl然后在控制台查看打印出来的 SQL 和参数是否正常。看是否因为“字段名是关键字”导致 SQL 执行错误。比如字段叫desc、order在 MySQL 里这些是保留字不加反引号会 SQL 报错。遇到这种问题的正确姿态是建表时避开关键字如果表已经用了可以在实体字段上使用TableField(desc)加反引号解决。还有一个特别隐蔽的问题如果你在 XML 里手写了 resultMap那么 MyBatis-Plus 的自动映射可能会失效。没经验的开发者一般不会轻易手写 resultMap但如果遇到“字段永远查不出来”记得回来看一眼是不是这里出了岔子。4.6 Vue3 组件中 echarts 图表不显示或报错项目后台首页很可能用了 ECharts 做数据可视化。Vue3 里这样引入import * as echarts from echarts import { onMounted, onBeforeUnmount } from vue const chartEl ref(null) let chartInstance null onMounted(() { chartInstance echarts.init(chartEl.value) chartInstance.setOption({ title: { text: 课程分类统计 }, tooltip: {}, series: [{ type: pie, data: [ { value: 10, name: Java }, { value: 8, name: 前端 } ] }] }) }) onBeforeUnmount(() { chartInstance chartInstance.dispose() })这里有两个高频坑图表容器在初始化时是隐藏的或宽度为 0比如它在el-tabs的非激活页签里ECharts 初始化后渲染出来的内容宽高是 0。你切换到该页签时才看到空白或很小的一块图。解决方法是切换后手动调用chartInstance.resize()或者用nextTick保证容器已渲染后再初始化。数据是异步获取的图表在数据返回之前就setOption了一次导致图表空白。正确流程是先init然后等待接口返回数据再setOption(option)。别在接口还没回来时就先画一个空图表。5. 实操中的经验心得5.1 提前把项目跑起来的三个黄金步骤拿到一套源码不要急着读代码最少先让它跑起来。我建议按以下顺序进行先导入数据库。找到sql目录下的建库脚本在 MySQL8.0 中执行。执行成功后用SELECT COUNT(*)验证关键表里有数据。再启动后端。确认application.yml里的账号密码改成你的本地数据库密码。启动后访问http://localhost:8080/doc.html如果项目集成了 Knife4j 或 Swagger查看接口文档是否能打开或者直接点开一个列表接口测试返回。最后启动前端。npm install时如果网络慢建议用国内镜像源在项目根目录创建.npmrc写入registryhttps://registry.npmmirror.com。运行npm run dev浏览器打开http://localhost:3000验证登录页能否打开。如果跑不起来先看启动日志的最后几行99% 的问题都能直接锁定——不是端口被占、就是数据库连不上、或者 npm 版本和 Vue3 要求的 node 版本不兼容Vue3 Vite 项目建议 Node.js 16 以上。5.2 二次开发时如何扩展“新功能”如果你要在教学资源库系统上新增一个“在线考试模块”或“作业提交模块”千万不要在原代码上大改特改。我推荐的做法是新建一张数据表比如course_homework对应实体类、Mapper、Service、Controller。前端新建views/front/homework和views/admin/homework页面。在路由表里增加对应路径在后台菜单中增加入口。登录鉴权的拦截器会自动对“需要登录后访问的页面”做校验所以你只是增加业务功能不需要重新设计权限体系。为什么强调这一点因为很多人在做毕设扩展时喜欢CtrlC / CtrlV改两个字段名然后报错改来改去浪费大量时间。实际项目开发中更标准的流程就是“加表 → 加实体 → 加 Service → 加 Controller → 加前端页面 → 注册路由”照着这个模式走扩展一个模块的最小工作时间可以控制在 30 分钟以内。5.3 答辩演示时最容易拿分的小技巧最后分享一下答辩演示时的经验。教学资源库这类管理系统最核心的价值在于“完整性”。演示时不要只点一遍列表页要有意识地按这个顺序展示第一展示登录。输入不同账号登录比如管理员账号和普通用户账号说明登录后看到的菜单不一样这就引出了权限控制的实现。第二展示新增数据。录入一门新课程填完表单提交后台列表马上出现新数据说明增删改查链路是通的。第三展示搜索和分页。输入关键词搜索切换分类筛选指出分页参数正确改变了说明 MyBatis-Plus 分页插件生效了。第四展示联动效果。比如前后台的数据统计图表因为这个图表和业务数据在同一条数据链路里所以新增一条课程后统计数字会同步变化。第五演示异常情况。比如用普通用户去访问管理员接口后端返回无权限提示说明权限配置起作用了。这一套流程走完答辩老师基本不会过多追问你是否独立完成因为系统整体观感和逻辑自洽性已经足够有说服力了。再把重点说一遍你在做这个系统时最值得投入时间的核心主题就是“基于 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0 的权限管理 CRUD 完整链路”。这一套代码结构不仅适用于教学资源库把业务换成“图书管理系统”“医院预约系统”“宠物领养平台”“电商后台”架构完全可以复用。希望这篇总结能帮你把项目跑通、看懂、会改然后带着你自己的理解去答辩顺利拿下成绩。