
2. 登录认证与安全设计2.1 为什么选 Sa-Token 而不选 JWT登录认证这块我直接选了 Sa-Token 而不是 JWT这是很多教程默认方案但实际对比过会发现差异很明显。JWT 的问题在于 token 签发之后服务端无法主动让它失效。管理员的账号被踢下线、学生修改密码后强制重新登录这类场景 JWT 做起来非常别扭。Sa-Token 是服务端会话模式登录状态存在 Redis 里随时可以踢人、封禁、续期对校园管理系统这种权限敏感、角色分明的场景更贴合。2.2 密码加密不能只做 MD5密码存储这块网上很多老教程还在用 MD5甚至有的直接把密码明文放数据库。这个必须纠正——MD5 加盐虽然比纯 MD5 好一点但仍然属于快速哈希暴力破解成本极低。正确做法是用 BCrypt它内置随机盐相同密码每次加密结果都不同破解成本高几个数量级。我这里配置了强度 10也就是 2 的 10 次方轮哈希单次校验大约 100ms 左右对登录体验完全无感。2.3 权限控制的粒度设计角色只有三种管理员、教师、学生但权限不能简单按角色写死。举例来说教师可以查看自己课程的学生列表但看不到其他教师的课程。学生可以提交作业但只能提交自己课程下的作业。这类场景单靠角色判断做不了必须引入数据权限——在查询 SQL 里拼接当前登录人的 ID 条件。角色权限范围实现方式管理员全部模块角色标识直接放行教师自己的课程、班级、学生SQL 拼接 teacher_id学生自己的选课、作业、成绩SQL 拼接 student_id提示数据权限是这类系统最容易忽略的点。很多毕业设计只做接口鉴权不做数据隔离两个学生互相能看到对方的作业和成绩答辩时很容易被问到。2.4 完整登录流程实现登录接口的核心代码并不复杂真正的复杂度在流程设计上PostMapping(/login) public Result login(RequestBody LoginDTO dto) { // 1. 校验图形验证码生产环境建议升级为滑块验证 boolean valid captchaService.verify(dto.getCaptchaKey(), dto.getCaptchaCode()); if (!valid) return Result.error(验证码错误); // 2. 查询用户这里使用 LambdaQueryWrapper User user userMapper.selectOne(new LambdaQueryWrapperUser() .eq(User::getUsername, dto.getUsername())); // 3. 校验密码BCrypt 内置盐直接 matches if (user null || !BCrypt.matches(dto.getPassword(), user.getPassword())) { return Result.error(用户名或密码错误); } // 4. 检查账号状态被禁用时无法登录 if (user.getStatus() ! 1) return Result.error(账号已被禁用); // 5. 签发登录态token 存 Redis支持后续踢人下线 StpUtil.login(user.getId(), new SaLoginModel().setTimeout(7200)); // 6. 返回登录用户信息不含密码字段 return Result.ok(UserVO.from(user)); }这段代码每个步骤都有明确的意图验证码防暴力破解、BCrypt 防拖库泄露、状态位控制账号生命周期、Sa-Token 管理会话。教程里我会把每一步的原理放到注释中方便零基础读者理解为什么要这样写。3. 核心业务模块实现3.1 课程管理模块一对多关联是怎么设计的课程、教师、班级三张表的关系是典型的一对多课程表里存teacher_id指向教师表的主键。班级表里存course_id指向课程表的主键。查询课程详情时需要把教师姓名、班级列表一起返回。数据库层面的设计是-- 课程表 CREATE TABLE course ( id bigint NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 课程名称, code varchar(20) NOT NULL COMMENT 课程编号, teacher_id bigint NOT NULL COMMENT 授课教师ID, credit tinyint DEFAULT 2 COMMENT 学分, max_student int DEFAULT 50 COMMENT 人数上限, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_teacher (teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意这里建了idx_teacher索引。很多新手建表时不考虑索引数据量小的时候感受不到问题但学生选课列表、教师课程列表这类高频查询一旦数据过万全表扫描会明显变慢。3.2 学生选课事务与并发控制选课是这类系统里最容易出并发问题的业务。我在做模拟数据时用 JMeter 并发测过——100 个线程同时选同一门只剩 5 个名额的课程如果不加控制会出现超出人数上限的情况。原因是 MySQL 默认隔离级别下两个请求同时读到剩余名额相同的值然后各自扣减覆盖写入。解决办法是乐观锁或者数据库行锁。我选了SELECT ... FOR UPDATE这种悲观锁方案Transactional public boolean selectCourse(Long studentId, Long courseId) { // 锁行查询课程时锁定该行防止并发超选 Course course courseMapper.selectByIdForUpdate(courseId); // 检查已选人数用统计 SQL 直接查选课表 Long selected selectCourseMapper.selectCount( new LambdaQueryWrapperSelectCourse() .eq(SelectCourse::getCourseId, courseId)); if (selected course.getMaxStudent()) { throw new BizException(课程人数已满); } // 检查是否重复选课 Long exist selectCourseMapper.selectCount(...); if (exist 0) throw new BizException(请勿重复选课); // 插入选课记录事务提交后自动释放锁 selectCourseMapper.insert(...); return true; }核心点是selectByIdForUpdate这个方法。MyBatis-Plus 本身没有内置我用自定义 SQL 实现的FOR UPDATE行锁。两个并发请求同时进来时第二个会阻塞等待第一个事务提交等锁释放后再重新读取最新数据就不会出现超选了。需要注意的是行锁必须在事务内才有效所以方法上必须标注Transactional。还有一个坑FOR UPDATE锁的是索引行如果查询条件没走索引MySQL 会把整张表锁住性能直接雪崩。所以我给course_id和student_id都建了联合索引。3.3 成绩录入 Excel 导入导出成绩录入有两种场景需要处理单个实时录入和批量导入导出。批量这块我选了 EasyExcel 而不是 POI——POI 操作 Excel 需要自己写大量样板代码EasyExcel 封装了读写注解几行代码就能完成。核心做法是定义成绩导入的 DTO 类加字段注解public class ScoreImportDTO { ExcelProperty(value 学号) private String studentNo; ExcelProperty(value 课程编号) private String courseCode; ExcelProperty(value 平时成绩) private BigDecimal usualScore; ExcelProperty(value 期末成绩) private BigDecimal finalScore; }导入时直接用EasyExcel.read(inputStream, ScoreImportDTO.class, listener).sheet().doRead()。监听器里做逐行校验学号是否存在、课程是否匹配、分数是否在 0-100 区间最后攒够 100 条批量插入一次避免逐条插入导致频繁数据库交互。导出反向操作把查询结果填入相同结构的 VO 类设置响应头Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet就能在浏览器里直接触发下载。兼容一排不需要额外引入其他依赖。提示导出大文件时注意内存问题。EasyExcel 默认按行扫描写入不会把整个工作簿加载到内存里3 万行数据实测内存占用不到 200MB。如果自己用 POI 硬写10 万行就可能触发堆溢出。3.4 数据可视化ECharts 仪表盘首页仪表盘我放了四类统计图各学院学生人数柱状图、课程分类占比饼图、近七日登录趋势折线图、班级人数排行横向柱状图。前端用 ECharts后端提供对应的统计接口汇总数据由 SQL 直接完成。SQL 统计一个典型场景——不同学院的人数分布Select(SELECT college_name AS name, COUNT(*) AS value FROM student GROUP BY college_name) ListMapString, Object countStudentByCollege();MyBatis-Plus 里可以混合使用注解 SQL统计类查询不需要建专门的实体类用MapString, Object接收即可。ECharts 端直接把这个数组塞进series.data不需要任何转换。3.5 公告通知与站内信公告发布走管理员后台前端门户展示最新公告。这个模块本身不复杂但有个细节容易被忽略——公告详情阅读量。阅读量字段每次访问详情都UPDATE一次会带来额外的行锁竞争。我的做法是详情页查询时直接用UPDATE ... SET view_count view_count 1单条语句完成避免先查后改两步操作。站内信队列我偷了个懒没有引入 MQ。教师给全班发通知时直接组装student_id列表批量插入站内信表。如果同一个班级有 50 人就是 50 条记录一次batch insert。说到底这是一个管理系统的内部通知量级非常小不值得为它引入一套消息队列组件。4. 前端页面逐个拆解4.1 登录页与侧边栏布局Vue3 这边路由守卫逻辑是核心。没有登录 token 时跳转/login已登录但访问无权页面时显示 403 提示。页面加载后先调一次/api/user/info获取角色信息然后根据角色动态生成侧边栏菜单。这一步用 Vue Router 的动态路由管理来注册权限路由代码结构比较清晰也很容易第八轮改造点。布局我用的是经典方案顶部导航栏 左侧菜单 主内容区。UI 组件库选了 Element Plus它的布局组件el-container配合el-menu几乎零成本搭出后台管理框架。侧边菜单的选中态和路由联动用route.path逐步匹配el-menu的index属性。4.2 表格页与弹窗表单的通用套路代码生成器把整个套路固定下来之后所有页面长得都差不多组件作用常见属性el-table数据展示border、stripe、v-loadingel-pagination分页current-page、total、page-sizeel-dialog新增/编辑弹窗v-model、width、close-on-click-modalel-form表单校验ref、rules、label-widthel-input / el-select输入控件v-model、clearable、placeholder这个套路我用很多个项目验证过稳定且好维护。新增和编辑共用一个弹窗通过dialogType区分标题和提交接口表单重置用resetFields。分页查询这层前端封装了pageQuery通用函数把queryParams对象和pageNum、pageSize一起提交到后端。后端统一接收PageDTO返回PageResultT前端直接在表格里绑定rows。4.3 上传作业与预览学生端选课成功后课程详情页会显示该课程的作业列表。作业要求允许教师上传附件通常是 PDF 或 Word学生提交时上传自己的文件。文件存储我选的是本地磁盘目录配置里设置upload.path上传成功后把文件相对路径存到数据库前端用拼接后的完整 URL 展示预览链接。文件类型白名单校验在前端和后端都做了前端限制accept.pdf,.doc,.docx,.zip后端再校验文件后缀。这个双重校验很重要因为前端限制只能防君子绕过页面直接 Post 请求是可以传任意文件的。后端的校验逻辑String ext FilenameUtils.getExtension(file.getOriginalFilename()); if (!ALLOWED_TYPES.contains(ext.toLowerCase())) { throw new BizException(不支持的文件类型); }上传文件大小限制我在application.yml配置了 50MB单文件超过会直接报错不会占用过多服务器空间。上传路径用了日期分目录避免单目录文件过多导致文件系统性能下降。5. 常见问题与排查技巧实录5.1 跨域请求报错开发环境下前端跑 5173 端口后端跑 8080前后端地址不同必然触发跨域。解决方式有三种后端加 CORS 配置、前端用 Vite 代理、部署后用 Nginx 反向代理。教程选的是 Vite 代理方案开发时最省心// vite.config.ts server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }上线部署时再用 Nginx 配置/api代理到后端。这套组合拳在生产环境也是标准做法。注意如果后端写死了允许某个地址跨域部署地址变更后会出现能访问页面但调接口全部失败的情况。排查时打开浏览器开发者工具看 Network 面板里请求的状态码和 CORS 错误信息能快速定位是前端路由问题还是后端接口问题。5.2 数据库时间差 8 小时这个问题遇到过几次。JDBC 连接配置里如果没有设置serverTimezoneAsia/Shanghai插入的时间字段会跟本地时间差 8 小时。MySQL 驱动 8.x 默认使用数据库服务器的时区数据库和服务器的时区不一致就是各种时间错乱。解决办法是 JDBC 连接串统一加上时区参数jdbc:mysql://localhost:3306/campus?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8另一个常见场景是 Jackson 序列化时LocalDateTime默认输出为数组格式前端拿到不是想要的字符串。需要配置Bean public Jackson2ObjectMapperBuilderCustomizer customizer() { return builder - builder.serializers(new LocalDateTimeSerializer( DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); }5.3 MyBatis-Plus 分页查询失效很典型的低级错误分页插件没有注册。如果项目里只引入了mybatis-plus-boot-starter而没有配置分页拦截器调用selectPage方法时不会真正分页而是查出全表再在内存里截取。正确配置是在配置类里加Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }这个坑很容易被忽略因为数据量小的时候内存分页的结果看起来也是对的只有翻到第二页时发现数据不对或者控制台打印的 SQL 没有LIMIT语句才会暴露。5.4 前端页面刷新后 404Vue Router 用createWebHistory模式时页面刷新会向服务器请求当前路径而静态服务器没有对应的文件返回 404。解决方式是改用createWebHashHistory模式或者在后端/Nginx 配置try_files $uri $uri/ /index.html。教程里两种方式都讲了本地开发推荐 hash 模式部署到正式环境再做 history 模式的优雅回退。5.5 文件上传成功但访问 404本地存储路径和访问映射路径要严格区分。上传文件保存到项目外的目录比如/var/campus/upload/但前端访问的是/api/file/2025/xx.pdf此时必须给静态资源做映射Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/api/file/**) .addResourceLocations(file:D:/campus/upload/); } }这里路径末尾的斜杠不能漏漏掉会导致匹配失败。6. 数据库设计文档详解6.1 表结构全景整个系统一共 9 张表覆盖用户、角色、课程、选课、成绩、作业、公告、站内信等全部业务表名作用核心字段sys_user系统用户username、password、role、statussys_role角色表role_code、role_namestudent学生信息student_no、name、college、majorteacher教师信息teacher_no、name、titlecourse课程管理course_name、teacher_id、creditselect_course选课记录student_id、course_id、select_timescore成绩表student_id、course_id、scoreassignment作业管理course_id、title、attachmentnotice公告信息title、content、create_time数据库脚本里包含全部建表语句和必填注释。这类系统的关键点在于表字段的注释要完整我自己在做的时候就把每个字段的注释都写上后面写代码会省很多事——MyBatis-Plus 自动生成的时候不用反复去查每个字段的语义。6.2 逻辑删除字段的设计与坑所有表都加了逻辑删除位。学生误操作删除选课记录、管理员误删公告这类问题在管理系统中非常多见。逻辑删除的做法是加一个deleted字段删除时执行UPDATE SET deleted 1而不是DELETE。MyBatis-Plus 对逻辑删除的标记配置极其简单mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0配置后所有select会自动追加AND deleted 0delete自动变成update不需要改业务代码。要注意的是逻辑删除字段必须加上TableLogic注解且普通selectById也会自动带上条件。这个坑在于有些人的逻辑删除字段名和配置不一致结果就是删除完数据还在查不到原因。7. 环境搭建常见问题7.1 Node.js 版本与 Vite 兼容性Vue3 Vite 对 Node 版本有要求。Vite 4 及以上版本需要 Node.js 14.18 或 16。某些旧版本 Node 环境启动 Vite 会直接报Failed to resolve module之类的问题。建议直接用 Node.js 18 或 20 LTS 版本这两个版本目前最稳不存在任何兼容踩坑。安装完 Node 后记得检查 npm 镜像。国内直接 npm install 慢到怀疑人生换成淘宝镜像源可以提速几倍npm config set registry https://registry.npmmirror.com7.2 Maven 依赖下载缓慢Maven 默认中央仓库的速度对国内开发者不太友好。修改settings.xml里阿里云镜像地址下载速度会明显提升。这是零基础学生最容易卡壳的环节。教程里我会把settings.xml完整贴出直接替换就能用。7.3 后端启动失败常见报错汇总报错信息原因解决方案Application run failed端口被占用改端口或杀掉占用进程Access denied for user数据库账号密码错误检查 application.ymlUnknown database数据库不存在先执行创建库的 SQLTable doesnt exist没有执行初始化脚本导入数据库文件Class not found缺依赖刷新 Maven这些报错我全在教程的 QA 板块做了详细演示每个都附了截图和排查步骤。零基础用户按照表格判断自己命中了哪个问题直接看后面的修复方案就行。8. 代码生成器与二次开发8.1 低代码批处理自动生成 Controller / Service / Mapper这类系统在课程设计中往往有课表管理教室管理等模块要求。前期我把模块化套路固定下来之后给每个新模块用代码生成器生成基础 CRUD——Controller、Service、Mapper、实体类、前端列表页和表单弹窗全部自动生成然后去改动新增特殊业务逻辑。这算是工程化开发的一点经验不要每张表都手写一套基础增删改查那是纯纯的体力活把这个时间花在业务设计上更有产出比。8.2 对接支付宝模拟支付学生缴纳学杂费功能我做了对接第三方沙箱环境的模拟支付流程。核心是下单接口生成预支付订单、回调接口验签更新订单状态、前端轮询订单状态切换页面展示。沙箱环境允许自定义金额和测试账号前端流程走完之后手动置为支付成功避免真实扣款风险。这个模块的代码量不小但逻辑上的训练价值非常丰富对后续对接真实支付或其他第三方接口都有作用。8.3 集成 WebSocket 实现在线通知站内信只是单向的为了把实时性做起来我又加了 WebSocket 推送。前端建立连接后登录后端在公告发布、成绩录入、新作业发布时推送一条 JSON 通知浏览器右上角直接弹提示。本质是管理系统的浏览器提醒功能实现没有想象中复杂——SpringBoot 自带 WebSocket 支持前端用原生 WebSocket API。它的意义在于WebSocket 是面试常问的高频考点。前端建立连接、后端推送、心跳保活、断线重连这些点写进毕业设计正文里都是实实在在的内容比泛泛地写前后端交互要有分量得多。8.4 定时任务实现课表提醒课程表推送到学生端以后还有一个隐藏入口——每天早上 8 点定时任务扫描当天有课的学生发送一条站内信提醒今日课程。Spring 的Scheduled注解直接搞定表达式0 0 8 * * *表示每天早上 8 点执行。需要注意的坑是定时任务默认单线程执行如果中间某个任务阻塞会拖累后续任务。需要多个独立任务并行时配置ThreadPoolTaskScheduler设置线程池大小。9. 部署上线与运维9.1 本机部署方案开发环境 - 生产环境部署我推荐用宝塔面板 Nginx JDK17 MySQL8 的组合Windows 开发和 Linux 服务器镜像迁移各自顺畅。主要步骤后端打包mvn clean package生成 jar 包放在服务器/www/wwwroot/campus目录前端构建npm run build生成dist文件夹Nginx 指向这个目录Nginx 配置/api反向代理到 SpringBoot 的 8080 端口MySQL 导入初始化 SQL修改 JDBC 连接配置systemd 配置后台运行 Java 进程日志输出到文件9.2 前端dist部署到 NginxNginx 配置需要注意两个关键点server { listen 80; server_name xxx.com; client_max_body_size 100m; location / { root /www/wwwroot/campus/dist; try_files $uri $uri/ /index.html; } 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; } }解释一下为什么try_files那行很重要Vue3 history 路由模式刷新页面时Nginx 会尝试找对应的静态文件找不到就返回 index.html由前端路由接管。没有这一行的话刷新深层次页面就是 404。另外client_max_body_size要调大否则超过默认 1MB 的文件上传会被 Nginx 直接拒绝后端日志里看不到任何记录排查会迷惑很久。9.3 数据库备份与恢复毕业设计项目数据量不大但备份习惯要从一开始养成。我提供了一份 Linux 定时任务脚本每天凌晨 2 点通过 mysqldump 全库备份保留最近 7 天# backup.sh #!/bin/bash BACKUP_DIR/www/backup/mysql DATE$(date %Y%m%d) mysqldump -uroot -p你的密码 campus $BACKUP_DIR/campus_$DATE.sql find $BACKUP_DIR -type f -mtime 7 -exec rm -rf {} \;定时配置用crontab -e添加0 2 * * * /www/backup/mysql/backup.sh这条服务能防住删库跑路级别的失误。恢复只需要一条命令mysql -uroot -p你的密码 campus campus_20250101.sql10. 答疑与服务支持10.1 答疑包含的内容整个教程附带全过程答疑。覆盖的时间段从环境搭建到最终部署常见问题在前面章节都做了统一整理但每个人机器环境不一样遇到的报错也不完全相同。遇到教程里没有覆盖到的问题直接发截图过来我会先分析控制台完整报错信息再给解决方案。10.2 二次开发和深度改造方向这个系统做完之后可以扩展的方向非常多换成若依框架做多租户版本支持多学校同时接入对接消息中间件做实时广播通知增加支付宝小程序选课入口引入 Redis 缓存热门公告和课程列表增加教师互评和学生评教模块对接教务系统做自动排课10.3 版本兼容说明这套项目各组件版本如下按这个版本组合测试最稳定组件版本说明JDK17SpringBoot 3 强制要求SpringBoot3.2.x全程以 3.2 为准Vue3.4.x组合式 APIMyBatis-Plus3.5.7适配 SpringBoot 3Element Plus2.x当前最新MySQL8.0兼容 5.7 但推荐 8.0Sa-Token1.37轻量权限框架EasyExcel3.3.x导入导出ECharts5.x数据可视化强调一下版本匹配的重要性。网上教程很多但版本差异会导致同样的代码跑出完全不同的效果。严格按照这套组合搭建能少踩很多版本兼容的坑。11. 最后补充的一些实操心得我做完这个项目最大的感触是能跑通和能设计清楚是两回事。教程里代码可以直接抄但更建议你每个模块先自己思考这个功能如果交给你你会怎么建表怎么设计接口怎么写查询逻辑想完再看我的实现印象会深得多。这个项目我实际敲代码用了三周大部分时间花在反复调整权限设计和并发处理上。如果你用这套系统做毕业设计答辩时老师最可能问的三个问题提前准备好答案为什么用 SpringBoot 3 而不用 SpringBoot 2——回答点在于 SpringBoot 3 基于 JDK17性能更好且刚需容器化部署场景。为什么权限模型用 Sa-Token 不用 Spring Security——答辩时讲明白服务端会话管理和踢人下线的需求就够了它是一个足够轻量、易上手的工具。选课超员怎么避免——把乐观锁、行锁、事务这几个概念讲清楚答辩基本就站稳了。另外系统虽然叫校园管理系统但业务拆分很清晰改改表结构或者页面文案换成班级管理系统、企业培训管理系统、社区活动管理系统都是现成的底子。整个项目的核心价值在于完整走了一遍从零到一的全栈开发流程这部分经验是最有价值的。