基于SpringBoot+Vue的大学生班级管理系统——从零到可跑通源码的全流程拆解
大学生班级管理系统,这个选题放在系统设计类毕设和练手项目里,可以说是经典中的经典。但很多人拿到“SpringBoot+Vue+MySQL+MyBatis”这套关键词组合后,第一反应是茫然:前端到底怎么搭,后端表结构该建几张表,连调时跨域报错怎么处理,最后怎么把源码跑起来交差——每一步都可能卡住。我自己前后带过几个实习生和辅导过的同学做类似项目,拿到标题第一眼就知道这是个能落地、需要讲清楚的东西。
这篇博文不搞花架子,直接把市面上最主流的方案拆开讲:后端用SpringBoot统一处理业务逻辑,MyBatis负责数据库操作,MySQL存储数据,前端用Vue单页应用提供交互页面。目标是让你哪怕只写过Java基础语法,也能顺着这篇文章把整套系统跑起来,并且明白每一层代码为什么这样写。
需要这套源码和设计方案的读者,通常有三类:一是做毕业设计的学生,需要完整的选题说明、系统结构图和数据表设计;二是想趁假期提升全栈能力的开发者,想借一个具体业务把SpringBoot和Vue体系完整串一遍;三是团队里需要快速做内部管理工具的人,班级管理系统完全可以改造适配成社团管理、题库管理、报名系统等场景。我写这篇文章时,三条线索都会覆盖到。
1. 整体设计与技术选型:为什么这套组合能“扛住”毕设和练手项目
先解决认知问题:“SpringBoot+Vue+MySQL+MyBatis”这几个词放在一起,为什么是绝大多数系统类项目的事实标准?原因很简单——分工明确,各自解决自己最擅长的问题,组合起来就是一个可维护的前后端分离架构。
1.1 各技术组件在系统里的角色分工
用一句话概括这套架构:Vue负责“脸面”,SpringBoot负责“头脑”,MyBatis负责“手脚”,MySQL负责“记忆”。
- Vue(前端):负责页面渲染、用户交互、路由跳转、状态管理。用户看到的登录界面、学生信息表格、选课操作按钮,全部由Vue组件生成。Vue的响应式数据绑定让页面更新变得非常直接——数据一变,界面跟着变,不需要手动操作DOM。
- SpringBoot(后端):提供RESTful接口,接收前端发来的请求,处理业务逻辑,然后返回JSON数据。学生信息的增删改查、选课学分的计算、登录校验都在这一层完成。SpringBoot内置Tomcat,不用额外配置服务器,这一点对于新手来说非常友好。
- MyBatis(持久层):把Java方法和SQL语句做一个映射。你不写一堆JDBC样板代码,只定义mapper接口写SQL就行,比JPA那种自动生成SQL的方案更可控,尤其适合需要写多表连接查询、条件统计的场景。
- MySQL(数据库):负责把数据真正存下来。班级、学生、课程、选课关系、公告通知,每一类数据都是独立的表,通过主外键关联。
这套分工逻辑有什么好处?最直观的一点是前后端可以并行开发。前端开发者只需要知道后端接口长什么样(路径是什么、传什么参数、返回什么结构),甚至可以直接用模拟数据先画页面,等接口联调时再替换。后端开发者也不必关心页面长什么样,专心把接口字段定义好、状态码约定好。
1.2 为什么是MySQL而不是其他数据库
很多同学问过这个问题:“我能不能用SQLite?”、“是不是应该换MongoDB?”。对于班级管理系统这个业务场景,MySQL是更稳的选择。原因有三点:
- 数据是强结构化关系型数据。学生属于班级,选课记录关联学生和课程,这天然适合关系型数据库的外键约束和关联查询。用文档型数据库反而要处理一堆冗余字段和关联逻辑。
- MySQL的资料和工具链极其丰富。Navicat可视化操作、mysqldump备份、binlog日志排查,遇到问题搜索引擎一搜一大把答案。教学和答疑效率完全不一样。
- 与MyBatis搭配最成熟。市面上95%以上MyBatis教程默认连的都是MySQL,驱动的兼容性、SQL语法的坑基本都被踩平了。
如果你非要用PostgreSQL也行,但项目的学习成本会往上走一截,没必要在核心业务系统上给自己增加变量。
1.3 前后端分离架构,MVP模式先行
做系统设计时我还想多提一个概念:不要一上来就想实现完美版的“角色权限+操作日志+消息通知+数据大屏”,而是先把最小可行产品(MVP)跑通。班级管理系统最常见的角色有三种:
- 管理员:维护系统基础数据,管理班级信息、教师账号、课程安排
- 教师:查看所教班级的学生名单,录入成绩,发布课程通知
- 学生:查看个人信息,浏览可选课程,提交选课,查看成绩
我见过太多的项目卡在“想得太大”:昨天要加考勤打卡功能,今天要加宿舍评分功能,明天又想要移动端适配,最后代码写了一堆,数据库表建了十几张,没有一个模块是完整可用的。系统设计的核心能力是“砍需求”,先保证三种核心角色在“班级管理、课程管理、选课管理、成绩管理、信息查看”这五个核心场景全流程跑通,后续再谈扩展。
用一个图表帮助理解整体结构(这里用文字描述,不画图):浏览器页面 → Vue路由与组件 → axios HTTP请求 → SpringBoot Controller接收参数 → Service层处理业务 → Mapper接口 → MySQL数据库,数据返回时按原路一层层封装回JSON。搞清楚这条链路,后面写的每一行代码都知道自己在链路中的位置。
2. 核心功能拆解与数据库设计:先画好表,代码才能少返工
班级管理系统的数据库设计是整个项目的地基。我见过太多同学在建表这事上吃亏:要么字段命名随意导致后续接口对不上,要么缺外键关联导致查询时只能写复杂的JOIN逻辑,要么没有唯一约束导致重复数据。数据库设计阶段多花一小时,能帮你后面节省五小时。
2.1 角色与权限的需求分析
先梳理用户故事,把需求转化成功能点:
- 学生登录后可以看到自己的个人信息、所属班级、已选课程和成绩
- 学生可以浏览可选课程列表,点击选课按钮,系统自动校验是否已选过、是否冲突、学分上限
- 教师登录后可以看到自己负责课程的选课学生列表,可以录入平时成绩和期末成绩
- 管理员可以维护班级、用户(学生/教师)、课程的基础信息,也可以重置密码
从这个需求清单出发,系统最初的核心表就能确定了。我的建议是至少包含六张表:用户表(user)、班级表(class)、教师表(teacher)、学生表(student)、课程表(course)、选课表(course_selection)。这里的用户表我特意使用user而不是单独建admin表,是因为管理员、教师、学生统一走一份账号密码表,用字段区分角色类型即可。这比三张用户表更简洁,也方便后续扩展其他身份时不用改表结构。
2.2 数据表设计清单
这里给出一份我在项目中实际使用过的核心表设计,字段不算多,但覆盖面足够广,可以直接复用到自己的系统里。
用户表(user)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键,自增 |
| username | varchar | 登录账号,唯一索引 |
| password | varchar | 密码,BCrypt加密存储 |
| role | tinyint | 角色:0管理员,1教师,2学生 |
| status | tinyint | 状态:0禁用,1正常 |
班级表(class)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| class_no | varchar | 班级编号,如CS2001 |
| class_name | varchar | 班级名称 |
| grade | varchar | 年级 |
| major | varchar | 专业名称 |
| counselor | varchar | 辅导员姓名 |
教师表(teacher)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| user_id | int | 关联用户表id |
| teacher_no | varchar | 工号 |
| teacher_name | varchar | 姓名 |
| title | varchar | 职称 |
| department | varchar | 所属院系 |
学生表(student)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| user_id | int | 关联用户表id |
| student_no | varchar | 学号,唯一 |
| student_name | varchar | 姓名 |
| gender | varchar | 性别 |
| phone | varchar | 手机号 |
| class_id | int | 关联班级表id |
课程表(course)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| course_no | varchar | 课程编号 |
| course_name | varchar | 课程名称 |
| credit | int | 学分,整数值最方便计算 |
| teacher_id | int | 关联教师表的授课教师 |
| course_time | varchar | 上课时间描述 |
| course_place | varchar | 上课地点 |
| capacity | int | 选课容量上限 |
| selected_count | int | 当前已选人数 |
选课表(course_selection)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| student_id | int | 关联学生表 |
| course_id | int | 关联课程表 |
| select_time | datetime | 选课时间 |
| score | int | 总评成绩 |
| score_detail | varchar | 成绩明细JSON,存平时成绩和期末成绩 |
看到这里,有人可能会问:为什么课程表和选课表要单独拆开?直接给学生表加一个课程字段不好吗?答案是选课关系是多对多——一个学生可以选多门课,一门课可以被多个学生选。这种关系必须要一张中间表来记录,否则学生表里存课程列表会变成逗号分隔的字符串,检索和统计都极其痛苦。这就是基础的关系型数据库设计常识,也是面试官很喜欢追问的点。
2.3 选课逻辑的设计细节
选课模块是班级管理系统里业务逻辑最集中的地方,涉及到几个常用且重要的校验规则:
- 学生不能重复选择已经选过的课程。通过course_selection表查student_id和course_id组合是否存在即可。
- 选课人数不能超过课程容量。在course表记录capacity和selected_count两个字段,选课时判断selected_count是否小于capacity。注意,这个操作在并发场景下存在超卖风险,但在毕设级别的单用户测试场景下,先查后改基本够用。如果要考虑并发安全,可以在SQL中使用UPDATE course SET selected_count = selected_count + 1 WHERE id = ? AND selected_count < capacity这样的原子操作。
- 总学分不能超过上限。查询当前学生所有选课记录的课程学分求和,判断是否超过上限,比如24学分。
- 上课时间冲突检测。这一步计算量比较大,可以先把已选课程的course_time取出来,再与新选课程的时间字符串做交叉比对。对于班级管理系统,用字符串匹配判断星期几和第几节的方式,完全可以对付过去,不需要引入复杂的算法库。
这些校验逻辑放在Service层写,前端只负责把选课请求发送过来,后端做真正决策。这样做的好处是安全,即使有人绕过前端接口直接调API,也不能提交非法选课请求。
3. 后端实现:SpringBoot+MyBatis的分层设计与核心代码讲解
后端是整套系统真正“碰数据”的部分,也是很多同学第一次直面SpringBoot和MyBatis的地方。本节讲的不是一份代码的全部源码逐行注释——那太长,挑最核心的几个环节把关键模式和常见坑一次讲透。
3.1 项目结构怎么铺
我的习惯是把后端项目按照“Controller → Service → Mapper”三层结构铺开,再配合一个config包和common包:
src/main/java/com/example/classmanager/ ├── controller/ // 接收前端请求,返回结果 │ ├── AuthController.java │ ├── StudentController.java │ └── CourseController.java ├── service/ // 业务逻辑层,选课校验、成绩管理都在这里 │ ├── StudentService.java │ └── CourseService.java ├── mapper/ // MyBatis的Mapper接口 │ ├── StudentMapper.java │ ├── CourseMapper.java │ └── CourseSelectionMapper.java ├── entity/ // 实体类,对应数据库表 │ ├── Student.java │ └── Course.java ├── config/ // 配置类,比如跨域CORS配置、拦截器配置 └── common/ // 通用返回结构、异常处理 └── Result.java为什么Controller层要尽量薄?这个问题的答案其实就是后端开发的核心习惯:Controller只做三件事——接收参数、调用Service、返回Result。如果把业务逻辑塞进Controller里,代码很快就会变成一坨大杂烩,后面哪块逻辑出问题都要在一大段里翻来找去。把校验、计算放在Service层,事务边界也好控制。
3.2 返回结构的统一约定
前后端联调最大的坑,就是返回数据格式不统一。一会儿返回成功直接给对象,一会儿失败返回一个字符串,前端前端调用的同学会疯掉。因此我强烈建议在项目最先定义一个统一的Result类:
public class Result { private Integer code; // 200成功,500失败 private String message; // 提示信息 private Object data; // 返回数据 }整个系统的所有接口都返回这个结构,前端axios拦截器里对code做统一判断:code为200时正常处理数据,否则弹出message错误信息。这个约定早定好,能避免后面至少三次无意义的联调沟通。
3.3 Student增强CRUD的完整示例
这里写一个最简单但也最典型的例子:分页查询学生信息列表。这段代码的模式可以复制到所有模块。
Controller层:
@RestController @RequestMapping("/api/student") public class StudentController { @Resource private StudentService studentService; @GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword) { return Result.success(studentService.getPage(pageNum, pageSize, keyword)); } }Service层(配合PageHelper分页插件):
@Service public class StudentService { @Resource private StudentMapper studentMapper; public PageResult<Student> getPage(Integer pageNum, Integer pageSize, String keyword) { PageHelper.startPage(pageNum, pageSize); List<Student> list = studentMapper.selectByCondition(keyword); PageInfo<Student> pageInfo = new PageInfo<>(list); return PageResult.build(pageInfo.getTotal(), pageInfo.getList()); } }Mapper层(XML中写SQL):
<select id="selectByCondition" resultType="com.example.classmanager.entity.Student"> SELECT s.*, c.class_name AS className FROM student s LEFT JOIN class c ON s.class_id = c.id <where> <if test="keyword != null and keyword != ''"> AND (s.student_name LIKE CONCAT('%', #{keyword}, '%') OR s.student_no LIKE CONCAT('%', #{keyword}, '%')) </if> </where> ORDER BY s.id DESC </select>这段代码把三层关系展示得很明白:Controller接收分页参数和搜索关键字,Service开启分页并处理分页结果封装,Mapper负责动态SQL查询——keyword为空时列出全部,不为空时同时匹配姓名和学号。这个模式在班级管理系统的所有列表查询功能上可以无限复用,替换表名和字段名即可。
3.4 登录认证与拦截器
没有登录认证的系统是不完整的,但毕设项目又不需要引入Spring Security全家桶那种复杂度。我的建议是介于两者之间的做法:使用JWT生成token,写一个HandlerInterceptor做登录校验。
大概逻辑如下:
- 登录接口验证账号密码,成功后在服务端生成一个JWT token(可以把userId和role信息放进claims),返回给前端
- 前端把token存储到localStorage,之后每个请求都在请求头里加上Authorization字段
- 后端写一个拦截器,在所有需要登录的接口前校验token是否合法,校验失败返回401状态码
注册拦截器时注意:登录接口本身不需要校验,否则用户还没登录就被拦住了。还要放行一些静态资源和预检请求OPTIONS,否则前端浏览器发送跨域预检请求时会直接失败。
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !JwtUtil.validate(token)) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } return true; } }我没有引入Spring Security,不是说它不好,而是对班级管理系统这个量级,写Spring Security安全配置的学习成本超过业务本身。JWT+拦截器是业内非常常见的轻量级认证方案,既能说明你懂认证原理,代码量又可控,更符合这个项目的规模。
4. Vue前端从搭建到对接:路由、组件、封装请求一个不缺
前端部分很多做后端出身的同学容易犯怵,其实Vue的上手成本比想象中低。关键在于你愿不愿意抽一天时间把Vue的基础概念过一遍:组件、数据绑定、生命周期、路由、发请求。过了这道坎,后面全是重复劳动。
4.1 项目初始化与依赖整理
前端我推荐用Vue CLI或者Vite来创建项目骨架。命令行两步就能完成:
npm install -g @vue/cli vue create class-manager-frontend创建时把Router勾上,如果不太了解Vuex也可以先不用,系统不大时各组件之间传参、配合路由参数完全够用,不需要引入状态管理库增加认知负担。
依赖方面项目需要的东西不多:
- axios:发HTTP请求
- element-ui / element-plus:UI组件库,表格、表单、弹窗、菜单直接用现成组件,省去写CSS的痛苦
- vue-router:路由管理
同样的功能如果用纯HTML+CSS+JavaScript手写,可能要多写几千行样式代码。UI组件库存在的意义就是把你从“调margin、调padding”的深渊里解放出来,专心写业务逻辑。
4.2 前端请求封装的前置工作:跨域与axios拦截器
前端最头疼的问题之一就是跨域。开发环境下跨域最简单的解决方案,是用Vue脚手架自带的proxy配置。在vue.config.js文件中配置代理,让前端请求的/api路径在开发服务器级别直接被转发到后端的8080端口:
// vue.config.js module.exports = { devServer: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }浏览器里看到的请求地址是localhost:3000/api/student/list,但实际Java后端收到的是localhost:8080/api/student/list。从浏览器视角看,整个请求过程是同源的,不存在跨域问题。这个配置文件是新手特别容易漏掉的一环——很多人后端配置了CORS还是有问题,其实在前端开发服务器这一层就拦住了。
axios拦截器的配置也很重要。我们希望在每次发送请求时自动附带token,并且在返回code非200时自动弹出错误信息,省去每个页面重复写。下面是一份比较精简的封装:
import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截:附带token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = token } return config }) // 响应拦截:统一处理业务码 request.interceptors.response.use(response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { ElMessage.error('登录已过期,请重新登录') localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) })这封装在整个项目里只写一次,后面的页面都是这样调用接口:
const res = await request.get('/student/list', { params: { pageNum: 1, pageSize: 10 } })需要关注的点在于:这里的很多同学有一个常见的误区——把baseURL写成Vue项目的IP或完整后端地址,然后直接在后端用CORS允许跨域,两条路叠加反而容易出问题。开发环境优先用proxy,这是最简单、最不容易暴雷的方式。
4.3 路由与页面骨架搭建
页面的结构建议采用经典的后台管理布局:左侧是用于导航菜单的侧边栏,右侧是内容区。Vue Router是处理页面跳转的关键。在路由配置中依据角色做权限控制,而不是简单地在页面里塞一个判断逻辑。
const routes = [ { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/home', component: () => import('@/layout/Layout.vue'), children: [ { path: 'dashboard', component: () => import('@/views/Dashboard.vue') }, { path: 'student', component: () => import('@/views/StudentList.vue') }, { path: 'course', component: () => import('@/views/CourseList.vue') }, { path: 'selection', component: () => import('@/views/MySelection.vue') }, { path: 'score', component: () => import('@/views/ScoreList.vue') } ] } ]路由守卫用来做未登录跳转和权限校验。每次跳转前判断localStorage里是否有token,没有就直接踢回登录页:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { next('/login') } else { next() } })4.4 用一张表格和表单拼出一个完整的页面
拿学生管理页面举例,核心就是一个el-table加一个搜索框加一个新增/编辑对话框。把交互流程串起来看:
- 页面加载时调用分页接口,把第一页数据显示在表格里
- 搜索框输入关键字后,重新调用接口并携带keyword参数
- 点击新增按钮弹出对话框,填写学生基本信息后调用保存接口
- 保存成功后刷新表格,最后给用户弹一个成功提示
这里我要提醒一个经验之谈:表格的每一列显示内容要跟后端接口返回的字段严格对应。如果后端的SQL查询结果里Student实体类只有student_name而没有className,表格里头像的班级列就会一片空白。很多人调试半天Java代码没发现问题,最后发现是SQL少关联了一张班级表。这种问题在联调阶段最常见,排查思路是先看接口实际返回的JSON结构,再对齐表格绑定字段。
前端代码参考(简化版):
<template> <el-table :data="studentList"> <el-table-column prop="studentNo" label="学号"></el-table-column> <el-table-column prop="studentName" label="姓名"></el-table-column> <el-table-column prop="className" label="班级"></el-table-column> </el-table> </template> <script setup> import { ref, onMounted } from 'vue' import request from '@/utils/request' const studentList = ref([]) async function loadStudents() { const res = await request.get('/student/list', { params: { pageNum: 1, pageSize: 10 } }) studentList.value = res.data.list } onMounted(loadStudents) </script>这段代码对于掌握了Vue基础语法的人来说,读起来应该不费力:通过script setup语法组织逻辑,通过request封装好的axios发起请求,通过ref定义响应式数组,在生命周期onMounted里加载数据。这就是前端页面的“最小值”,其他页面往这个模板里套就行。
5. 环境准备与项目启动:完整跑起源码的每一步
代码写得再漂亮,如果跑不起来,一切都等于零。这一章我明确按步骤来,把从环境到后端到前端的完整启动流程梳理清楚,并指出最容易踩坑的地方。
5.1 环境清单与版本推荐
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 8或11 | Java开发运行环境 |
| Maven | 3.6+ | 后端依赖管理 |
| MySQL | 5.7或8.0 | 数据库 |
| Node.js | 16+ | 前端运行环境 |
| IDE | IDEA或VSCode | 分别开发前后端 |
JDK8在遇到旧项目时能少很多麻烦,但它停止更新维护了,如果你不想折腾,JDK11或JDK17也完全可以跑SpringBoot 2.x项目。MySQL建议装8.0,虽然5.7也够用,但8.0的字符集默认是utf8mb4,存中文不会出现乱码问题。Node.js版本尽量选16或以上,Vue CLI对新版本Node的兼容性更好。
5.2 数据库初始化:脚本到底怎么执行
通常拿到源码会附带一份数据库初始化脚本(.sql文件)。执行流程如下:
mysql -u root -p source /path/to/class_manager.sql;如果执行过程中出现乱码或者字符集报错,多半是SQL文件的编码不是UTF-8,用文本编辑器打开另存一下编码即可。如果嫌命令行操作麻烦,使用Navicat远程工具可视化执行SQL脚本会更直观。
初始化完成后检查一下表结构是否齐全。别急着启动,先确定数据库用户名密码和项目配置保持一致。后端的application.yml或者application.properties文件里需要配置数据源:
spring: datasource: url: jdbc:mysql://localhost:3306/class_manager?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password提示:serverTimezone这个参数非常容易踩坑。不配置时IDEA运行后端经常会报日期时区相关异常,配置成Asia/Shanghai即可解决。密码如果含有特殊字符,记得在YAML里加引号。
5.3 后端启动流程
我平时在IDEA里操作的步骤是:
- 打开IDEA,选择File → Open,选中后端项目根目录
- 等待Maven把依赖拉完。第一次启动时Maven会下载很多jar包,这一步比较吃网速,如果等了太久没反应,在settings.xml里把镜像换成国内源
- 在src/main/resources文件夹下找到application.yml,把数据库账号密码改好
- 找到主类(类名上带@SpringBootApplication注解),右键运行
看到类似“Tomcat started on port(s): 8080”的日志,后端就起来了。出现红色ERROR不要慌,先看最前面的几行,一般是数据库连接问题、端口被占用、或者MyBatis映射文件找不到。这三种问题基本占掉了90%的启动失败情况。
端口被占用的解决方式有两种:关掉占用8080端口的进程,或者在application.yml中修改server.port。后者更直接,但要注意前端proxy配置里target端口也要跟着改。
5.4 前端启动流程
前端启动相对简单:
npm install npm run servenpm install这一步有几个必踩的坑。第一,依赖非常多,下载速度很慢,可以用npm换成国内镜像源或者使用yarn app。第二,出现node-sass报错,多半是Node版本与项目依赖的node-sass不兼容,这时候最省事的选择是升级项目到sass新版本,或者把Node版本切换到项目要求的大版本。第三,出现ERESOLVE依赖树冲突,可以在install命令后加--legacy-peer-deps参数强制安装。
启动成功后浏览器输入http://localhost:3000(具体端口以终端显示为准),就能看到登录页面。接下来就是联调测试环节,逐项验证管理员、教师、学生三种角色的功能流程。
6. 真实排坑记录:这些报错我见的太多了
运行项目的过程,本质上就是一个“报错-定位-修复”的循环。这里把我见过的高频问题整理出来,按出现频率从高到低排,每一条都给排查思路和解决方案。
6.1 数据库连接失败:Access denied for user
这个报错几乎每个第一次运行项目的新手都会遇到。看到三行字就明白:
- “Communications link failure”表示你的数据库地址或端口不对
- “Access denied for user”表示账号密码不对
- “Unknown database”表示数据库名拼错了或者没有导入
逐个排查网卡,通常不超过五分钟定位。我建议不要在生产环境用root账号连接数据库,但在自己的开发机上用root是最省事的,不必新建授权账号。
6.2 Invalid bound statement (not found)
这个报错出现时,你的Controller和Service方法看起来都对,但运行到调用Mapper方法时抛异常。核心原因通常是MyBatis扫描不到XML映射文件。
SpringBoot项目里Mapper接口和XML文件的默认约定是放在同一个包路径下,但Maven打包时默认不打包src/main/java下的XML文件。解决方法是在pom.xml里加一段build配置,把xml文件也打包进classes目标目录:
<resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources>还有一个设置容易被忽略:在application.yml里配置mapper-locations路径,告诉MyBatis去哪里找XML文件:
mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.classmanager.entity检查顺序建议是:路径配置是否正确 → XML文件是否被编译进target目录 → Mapper接口方法名有没有写错(比如selectAll写成了selectAllList)。
6.3 前端调接口报跨域错误
跨域在第五章里用Vue的开发服务器proxy解决了大半,但有些同学没有走proxy方案,仍然配置了CORS。如果你的后端是8080,前端是3000,并且前端直接用axios发送完整URL,浏览器就会拦截这个请求。
CORS配置需要在SpringBoot里加一个配置类:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }这里的一个经验是:如果配置了CORS,却仍然出现跨域错误,查看后端是否配置了拦截器拦截了OPTIONS请求。JWT拦截器会拦掉前端发来的预检请求,CORS配置就会失效。解决办法是在拦截器里放行所有OPTIONS请求,或者把CORS和拦截器的执行顺序处理好。开发环境下优先用proxy方案,真到部署时再把CORS配好,这是最稳妥的路径。
6.4 数据存进去中文乱码
中文乱码在MySQL中几乎都是字符集问题。检查三个地方:
- 建表时是否指定了utf8mb4字符集
- JDBC连接URL里是否带了characterEncoding=utf8
- 前端传参时Content-Type是否是application/json;charset=UTF-8
还有一个隐蔽的排查点:MySQL服务器的默认字符集可能是latin1,即使建表时指定了utf8mb4,某些操作也会自动转回默认。稳妥的做法是在my.cnf配置文件里统一设置:
[mysqld] character-set-server=utf8mb46.5 接口请求报401,但明明登录成功了
这个问题的典型case是:登录成功后,前端把token保存到了localStorage,但后续请求总是报401。排查方向也很明确:
- token是否真的成功保存?用浏览器开发者工具查看Application标签栏确认
- axios拦截器里有没有把localStorage的token塞进请求头?打印出来看一下
- JWT校验逻辑是否对token做了过期时间判断?如果生成时设了过期时间过短,也可能出现刚登录就失效
我在实际排查中还遇到过更隐蔽的:后端Interceptor每次请求都从请求头拿Authorization,但前端请求头字段大写或小写与后端取的名称不一致。HTTP Header本身是不区分大小写的,但代码里写的时候最好统一,避免给自己挖坑。
7. 常见问题速查表与操作心得
把前面提到的内容浓缩成一张速查表,方便遇到问题时快速对照。
| 问题现象 | 优先排查方向 | 解决方案要点 |
|---|---|---|
| 后端启动报数据库连接错误 | datasource url、用户名密码 | 修改application.yml配置,检查端口3306 |
| 后端启动报端口占用 | 8080端口被其他程序占用 | 换端口或杀掉占用进程 |
| Mapper方法报Invalid bound statement | mapper-locations路径、XML文件未打包 | 修改pom.xml资源打包配置 |
| MyBatis查询结果字段为null | 实体类属性与SQL列名不对应 | 使用别名或开启驼峰映射 |
| 前端npm install报node-sass错误 | Node版本与依赖不兼容 | 切换Node版本或升级sass |
| 前端发送请求跨域 | 开发服务器代理或CORS配置 | 优先配置vue.config.js中的devServer.proxy |
| 登录后请求始终401 | 拦截器配置、header传参 | 确认被拦截路径是否放行登录接口 |
| 乱码问题 | 数据库连接字符集、表字符集 | 统一使用utf8mb4 |
| 分页总数不对 | 分页插件使用方式 | PageHelper.startPage必须在查询前调用 |
最后分享几个操作上的心得:
- 第一,单独写接口时先看返回的JSON。前端联调遇到问题,第一件事不应该是改Vue代码,而是打开浏览器开发者工具的Network面板,看接口实际返回什么。分清是前端没渲染数据、还是后端根本没返回数据、还是压根没发请求。这三者排查方向完全不同。
- 第二,不要让前端页面在本地进行业务判断。比如课表冲突检测、学分上限校验,这类逻辑放在后端才是合理的。前端的校验只是为了让用户体验更好,方便你填写表单时不让用户提交无效数据,真正决定能不能选课的一定是后端。这样说也符合前后端分离的安全设计。
- 第三,把SQL日志打开。在application.yml里配置MyBatis的SQL日志输出,遇到问题能直接看到每条查询语句实际执行的SQL内容。排查JOIN关联错误、条件拼接问题时,这个信息比任何IDE调试都管用。
logging: level: com.example.classmanager.mapper: debug8. 项目扩展方向:同一套骨架能演变成多少种系统
如果你把这个项目完整做下来,并且想进一步提升,有几个非常自然的扩展方向。这些方向不是凭空想出来的,而是班级管理系统在真实校园环境里常见的升级需求。
第一个方向是把教务相关的规则做深。比如现在选课只是简单的增删记录,可以升级成培养方案校验——按年级和专业自动过滤可选课程,毕业学分要求自动计算等。这类逻辑考验的是业务建模能力,代码量增加不多,但会让系统看起来更专业。对面试来说,你能把这些业务规则讲清楚,比写一堆重复的CRUD接口更能看出设计能力。
第二个方向是数据可视化。用ECharts做一个班级管理系统的大屏页面,展示各专业人数分布、课程选课率、成绩区间统计等图表。前端ECharts库很容易上手,而这类图表在答辩演示时视觉冲击力很强,很容易给评审留下好印象。
第三个方向是引入Redis做缓存。比如把登录token存到Redis里,实现简单的会话管理;把学生个人信息这类热数据放到Redis缓存里,减轻MySQL查询压力。引入了Redis,整个项目的技术栈就多了一个重要组件,复杂度只提升一点点,但面试时可以说出一些像样的亮点。
第四个方向是导入导出功能。很多真实管理场景都需要Excel批量导入学生名单、导出课程成绩表。后端用框架处理Excel文件、前端用工具库下载模板,这套功能不难但非常实用,能让系统从“玩具”往上迈一个台阶。
我的建议是:优先把业务规则做深,而不是盲目增加新模块。哪怕只是把选课的时间冲突校验做得很严谨,也比加一个不完整的考勤功能要好。经验之谈是,答辩或面试时,三五个讲得清楚、逻辑完善的功能点,比十二个半成品模块有价值得多。
9. 上线部署的一点交代
很多同学做完系统只停留在本地跑通,这一步距离真正能对外访问还差“最后一公里”。虽然班级管理系统不一定要做完整的生产部署,但知道怎么把它部署到服务器上,对理解项目全貌大有帮助。
后端打包命令:
mvn clean package -DskipTests打出来的jar包会放在target目录下。最简单的方式是把这个jar包扔到一台装了JDK的服务器上,通过nohup命令后台运行:
nohup java -jar class-manager.jar > app.log 2>&1 &前端打包命令:
npm run build打完后dist目录下是一堆静态文件。最常见的部署方式是把dist目录里的文件放到Nginx的html目录下,然后配置Nginx把/api前缀的请求反向代理到后端8080端口:
server { listen 80; server_name your_domain; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; } }这里有两个点需要注意。第一,前端路由是history模式,nginx需要配置try_files防止刷新页面时出现404;第二,前端打包请求的域名就已经是部署域名了,不要再把后端地址写成本地地址,否则用户访问时会请求到他自己电脑上。只要保证了这两点,部署环节基本不会有太大问题。
如果你只是在本地玩,想给朋友看效果,也可以用ngrok或者内网穿透工具把本地服务映射到公网,不过这种方式不适合生产使用,仅临时演示。上线前把这套流程跑一遍,比单纯会启动项目写代码又多了一层理解。