最近带了几位准备毕业设计的同学,发现"SpringBoot学生管理系统"这个题目几乎是每年高校毕设的爆款。选这个题的人很多,但真正能把它做得像样、答辩时不心虚的其实不多。原因不在于这个题目太难——恰恰相反,它太容易上手了,容易到很多人做完一个CRUD就以为完事了,结果评审一问"你的权限控制怎么设计的""不同用户怎么区分的",当场卡壳。
这篇文章把我在实际开发中做这类系统的完整思路拆开来讲。我会从需求边界、技术选型、数据库设计、后端权限体系、前端联调、测试部署一路说到答辩时怎么讲清楚你的设计决策。你不需要按顺序全部照做,但每一步我都给出了当时踩过坑之后沉淀下来的做法,适合作为你独立完成毕设的参考底稿。
1. 毕设选这个题,先想清楚这三件事
很多人一开始就说"我要做学生管理系统",但真坐下来画页面时又发现不知道从哪里下手。问题往往出在——你连这个系统到底"管什么"都没想清楚。
1.1 这个系统到底在解决什么问题
高校里跟学生相关的信息是散的:学籍信息在辅导员手上有一份,成绩在教务处系统里,选课在另一套平台。学生要请个假、查个成绩、看个课表,得分别跑不同地方。
学生管理系统的核心价值,就是把散落在不同人手里的学生事务合并到一个统一入口。教师端负责录入和维护,学生端负责查看和申请,管理员负责统筹。听起来简单,但这是整个系统设计的第一性原理——你的所有页面、接口、权限设计,都应该围绕"不同角色通过同一平台完成自己的事务"这条主线来展开,而不是做成一个简单的增删改查Demo。
1.2 系统边界:哪些功能必须做,哪些可以不做
毕设最忌讳的是功能膨胀。我见过有同学非要在学生管理系统里加上"在线考试""宿舍分配""图书借阅",结果每个模块都做得很浅,评审一问细节就露馅。
一个稳妥的功能边界是这样划分的:
| 角色 | 核心功能 | 说明 |
|---|---|---|
| 管理员 | 账号管理、班级管理、教师管理、数据统计 | 系统配置层,通常不直接参与日常业务 |
| 教师 | 学生信息维护、成绩录入与修改、请假审批 | 业务操作层,频率最高 |
| 学生 | 个人信息查看、成绩查询、请假申请、课表查看 | 只读为主,少量操作型功能 |
围绕这三类角色的核心需求,系统才能形成"管理员管基础数据 → 教师录入业务数据 → 学生消费数据"的完整闭环。至于消息通知、邮件发送、数据导入导出这些,属于加分项,放在核心功能做完之后再加也不迟。
1.3 非功能性需求:答辩时"为什么这样设计"的素材
除了功能,评审老师更爱问的是系统能不能撑住真实场景。你要提前准备好这几个问题的答案:
- 系统最多能支撑多少并发用户?这决定了你需不需要加缓存(比如Redis)。
- 密码是明文存储还是加密存储?这决定了安全设计深不深。
- 如果老师同时多个班级一起录入成绩,会不会出现数据不一致?
即使你做得不深,能讲清楚"当前设计在哪些场景下够用、哪些场景下是瓶颈",也远胜过什么都说不上来。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型:SpringBoot版本怎么定,前后端要不要分离
技术选型是毕设的另一个大坑。很多同学一上来就追求最新最全,Spring Boot 3.x、Spring Cloud、微服务全套往上堆。实际上对毕设来说,稳定、熟悉、自己能讲明白比什么都重要。
2.1 Spring Boot版本:2.7.x是稳妥选择
我在生产项目里长期用的是Spring Boot 2.7.18,这也是目前国内大部分教学和中小型项目的默认选择。为什么不用最新的3.x或4.0?
- Spring Boot 3.x 基于JDK 17,而很多学校实验室、机房环境还是JDK 8。你在自己电脑上跑得飞起,换到演示环境直接起不来,这在答辩现场非常尴尬。
- 目前网上的教程、博客、问答,绝大多数基于Spring Boot 2.x。遇到问题你能搜到大量现成答案,毕设周期那么短,没必要自己踩别人早就踩过的坑。
- 最近的Spring Boot 4.0消息我关注了一下,它引入了AOT优化等新特性,但相关生态(尤其是MyBatis、shiro等国内常用组件)的兼容都还在路上。做毕设,不是追逐前沿的时候,是确保平稳交付的时候。
如果你确实已经装了JDK 17,那选Spring Boot 3.2.x也没问题,但前提是你对新的Jakarta命名空间(javax改成jakarta)有概念,别到时候引入旧依赖报一堆错。
2.2 前后端分离:建议做,但要看你的基础
前后端分离(Vue + Spring Boot)在毕设中非常常见,因为架构清晰、界面漂亮、写完可以直接部署到服务器上。但这有一个隐藏前提:你前端也得能搞定。
如果你对Vue不熟,只有两周时间又要交论文,我的建议是:后端接口加Thymeleaf服务端渲染模板,一样能完成功能,而且不用处理跨域、Token存储这些额外问题。别觉得服务端渲染"不高级",能把系统完整跑通、逻辑讲清楚,比做出一个自己都不懂怎么运行的分离项目要强得多。
如果你决定做前后端分离,那技术栈可以这样定:
- 后端:Spring Boot 2.7.18 + MyBatis-Plus + MySQL 8.0 + JWT
- 前端:Vue 3 + Vite + Element Plus + Axios + Pinia
- 部署:后端打成jar包,前端npm run build后由Nginx托管
2.3 为什么用MyBatis-Plus而不是纯MyBatis或JPA
这是我在实际开发中反复比较过的选择。MyBatis-Plus的核心优势是单表CRUD不用写SQL,对于学生管理系统这种大量单表操作的场景,能省下不少体力。比如分页查询,JPA要搞Pageable,纯MyBatis要手写PageHelper或者SQL,MyBatis-Plus一行selectPage就解决了。
JPA虽然开发效率也很高,但它的关联映射和懒加载机制对新手不太友好,经常出现N+1查询或者懒加载报错,排查起来很费时间。MyBatis-Plus的文档又是中文的,遇到问题直接翻文档就好。
当然,关联查询(比如查学生时要带出班级名)还是需要手写SQL的,这部分通过MyBatis的XML文件或者注解SQL来补充。
3. 数据库建模:学生管理系统的核心表设计
数据库设计是整个系统的地基。我复审过很多毕设代码,最常见的低级错误就是表结构设计不合理——有的把学生班级信息直接冗余进学生表,有的在课程表里存了所有选课学生ID的逗号分隔串,看起来省事,后面写统计SQL时痛苦万分。
3.1 基础数据表:学生、教师、班级之间的关系
学生管理系统至少需要这几张基础表:学生表、教师表、班级表、用户表。先看核心字段:
sql复制-- 班级表
CREATE TABLE tb_class (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
class_name VARCHAR(50) NOT NULL COMMENT '班级名称',
grade VARCHAR(20) COMMENT '年级,如2022级',
major_name VARCHAR(50) COMMENT '专业名称',
counselor VARCHAR(30) COMMENT '辅导员姓名',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 学生表
CREATE TABLE tb_student (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号',
student_name VARCHAR(30) NOT NULL COMMENT '姓名',
gender TINYINT COMMENT '性别:1男 2女',
birthday DATE COMMENT '出生日期',
phone VARCHAR(20) COMMENT '手机号',
email VARCHAR(50) COMMENT '邮箱',
class_id BIGINT NOT NULL COMMENT '所属班级ID',
enrollment_date DATE COMMENT '入学日期',
status TINYINT DEFAULT 1 COMMENT '状态:1在读 0休学 2毕业',
user_id BIGINT COMMENT '关联用户表ID,用于登录',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_class_id (class_id),
INDEX idx_student_no (student_no)
);
学生表通过class_id关联班级表,一个班级有多个学生,这是多对一关系。注意不要在设计上把班级信息直接冗余到学生表里——比如直接在学生表里存一个"班级名称"字段,那当班级改名时,你要更新所有学生记录。正确做法是只存class_id,需要班级名时通过关联查询获取。
这里关于用户登录的字段设置,有一个设计要点值得注意:user_id 关联到独立的用户表。因为系统还有教师和管理员,大家共用一套登录逻辑。后面详细讲用户权限设计。
3.2 业务数据表:课程、成绩、请假
业务层的核心表包括课程表、选课表、成绩表、请假表。以成绩表为例,它的设计最能体现"三范式"的权衡:
sql复制CREATE TABLE tb_course (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
course_name VARCHAR(50) NOT NULL,
course_code VARCHAR(20) UNIQUE COMMENT '课程编号',
teacher_id BIGINT COMMENT '授课教师ID',
credit DECIMAL(3,1) COMMENT '学分',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE tb_score (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
student_id BIGINT NOT NULL,
course_id BIGINT NOT NULL,
score DECIMAL(5,2) COMMENT '成绩,如89.50',
semester VARCHAR(20) COMMENT '学期,如2023-2024-1',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_student_course (student_id, course_id, semester)
);
这里关键是唯一键uk_student_course——同一学生同一门课同一学期只能有一条成绩记录。有了这个约束,你就不会出现一个学生下学期重复录入成绩的脏数据。这个细节在答辩时可以主动讲,评审老师会对你数据设计严谨度的印象明显提升。
请假表则相对简单:学生发起请假申请时记录请假类型(事假/病假)和起止时间,教师审批时更新状态(待审批/通过/驳回),同时记录审批意见。
3.3 用户与权限:RBAC模型简化版
很多毕设项目把用户权限做成硬编码——用user_type字段区分学生、教师、管理员,然后在代码里写if (user.getType() == 1)来判断。这在小系统里没问题,但扩展性很差。
我更推荐在毕设中使用简化的RBAC(基于角色的访问控制)模型,核心就三张表:
sys_user:用户表,存账号、密码(BCrypt加密)、状态。sys_role:角色表,预置学生、教师、管理员三个角色。sys_user_role:用户角色关联表。
sql复制CREATE TABLE sys_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL COMMENT 'BCrypt加密后的密码',
status TINYINT DEFAULT 1,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
role_code VARCHAR(30) NOT NULL UNIQUE COMMENT '角色编码,如STUDENT/TEACHER/ADMIN',
role_name VARCHAR(30) NOT NULL COMMENT '角色名称'
);
CREATE TABLE sys_user_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
UNIQUE KEY uk_user_role (user_id, role_id)
);
这样做的意义在于:权限判断不再散落在业务代码里,而是统一走拦截器/切面。以后要加一个"班长"角色,你只需要插入一条角色记录,再为用户关联即可,不用改任何业务代码。这段设计思路在论文里也值得大书一笔,因为RBAC是很多公司真实在用的权限模型,不是教学玩具。
如果你还有精力,可以把sys_role升级为sys_menu + sys_role_menu,实现菜单级权限。这部分在5.3节展开讲。
4. 后端核心设计:从项目骨架到登录鉴权
数据库设计好了,接下来是后端的工程实现。这一部分我会按照实际开发顺序来讲,从搭骨架到实现登录,再到权限控制,都是干货。
4.1 项目分包:别把代码全部扔在Controller里
我见过太多毕设代码,Controller里几百行,Service层空荡荡,MyBatis的Mapper直接写在Controller里调用。这种代码在答辩时被老师看到基本就凉了,因为你的分层设计是乱的。
一个标准的Spring Boot项目结构应该是这样的:
code复制com.example.studentms
├── controller # 接口层,只做参数接收与结果封装
├── service # 业务逻辑层
│ └── impl
├── mapper # MyBatis-Plus的Mapper接口
├── entity # 实体类,对应数据库表
├── dto # 请求/响应参数对象(与实体分离)
├── config # 配置类,如跨域、拦截器注册
├── common # 通用工具类、全局异常处理、统一返回结果
├── security # JWT拦截器、权限校验相关
└── StudentMsApplication.java # 启动类
实体类与DTO分离这一点特别想说一下。很多新手直接用实体类接收前端请求参数,结果用户传入的id覆盖了已有的主键,或者传入的password直接被更新到数据库,酿成安全漏洞。正确做法是:接收前端的参数用DTO(Data Transfer Object),只在必要字段上进行赋值,返回给前端的用VO(View Object),隐藏掉密码等敏感字段。
4.2 Maven依赖与配置文件
接下来是Maven的pom.xml核心依赖,我按实际项目经验给出一份可直接使用的配置:
xml复制<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<dependencies>
<!-- Web框架 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 参数校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- MyBatis-Plus,注意2.7.18对应mybatis-plus-boot-starter 3.5.x -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- 密码加密 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
</dependencies>
这里有一个容易踩的坑:MyBatis-Plus的版本要和Spring Boot版本匹配。mybatis-plus-boot-starter 3.5.x通常适配Spring Boot 2.x,如果你用的是Spring Boot 3.x,则要引入mybatis-plus-spring-boot3-starter,两者包名和依赖关系有差别。我在项目里看到过有人用Spring Boot 3加MyBatis-Plus 3.4的旧版本,启动时直接报ClassNotFoundException。
application.yml里还要配好数据源和MyBatis-Plus的配置:
yaml复制spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/student_ms?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: yourpassword
mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
map-underscore-to-camel-case这个配置要打开,否则数据库字段student_no映射不到实体类的studentNo属性上,所有查询结果都是null。另外开发阶段建议打开StdOutImpl日志,可以看到SQL执行过程,排查问题非常有帮助。
4.3 登录认证:JWT的落地方式
登录认证是系统最核心的公共逻辑。我推荐的方案是:JWT + 拦截器。Shiro和Spring Security虽然功能更强,但学习曲线陡峭,配置复杂,对毕设来说性价比不高。
JWT的原理用一句话解释:服务端在用户登录成功后,生成一个经过签名的Token返回给前端,前端后续每次请求都带上这个Token,服务端验证签名即可识别用户身份,无需在服务端保存Session。这和传统Session方案的核心区别在于——Session数据在服务端内存里,JWT数据在客户端Token里。
具体实现分三步:
第一步,登录接口。接收用户名密码,校验通过后生成Token:
java复制@PostMapping("/login")
public Result login(@RequestBody @Valid LoginDTO loginDTO) {
// 1. 根据用户名查询用户
User user = userService.getByUsername(loginDTO.getUsername());
if (user == null || !BCryptPasswordEncoder.matches(loginDTO.getPassword(), user.getPassword())) {
return Result.error("用户名或密码错误");
}
if (user.getStatus() != 1) {
return Result.error("账号已被禁用");
}
// 2. 查询用户角色
List<String> roles = userService.getRolesByUserId(user.getId());
// 3. 生成JWT,把用户ID和角色放进去
String token = JwtUtil.generateToken(user.getId(), user.getUsername(), roles);
return Result.success(new LoginVO(token, user.getUsername(), user.getNickName(), roles));
}
第二步,实现JWT工具类:
java复制public class JwtUtil {
private static final SecretKey KEY = Keys.hmacShaKeyFor(
"your-secret-key-at-least-256-bits".getBytes(StandardCharsets.UTF_8));
private static final long EXPIRE = 24 * 60 * 60 * 1000L; // 24小时
public static String generateToken(Long userId, String username, List<String> roles) {
long now = System.currentTimeMillis();
return Jwts.builder()
.setSubject(username)
.claim("userId", userId)
.claim("roles", roles)
.setIssuedAt(new Date(now))
.setExpiration(new Date(now + EXPIRE))
.signWith(KEY, SignatureAlgorithm.HS256)
.compact();
}
public static Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(KEY)
.build()
.parseClaimsJws(token)
.getBody();
}
}
注意your-secret-key-at-least-256-bits要至少256位,否则运行时会报错——这是JJWT库在0.10以上版本的安全要求。
第三步,实现拦截器,在请求进入Controller之前校验Token:
java复制@Component
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 放行登录接口
if (request.getRequestURI().contains("/auth/login")) {
return true;
}
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
throw new BusinessException(401, "未登录或Token无效");
}
try {
Claims claims = JwtUtil.parseToken(token.substring(7));
request.setAttribute("userId", claims.get("userId"));
request.setAttribute("roles", claims.get("roles"));
return true;
} catch (Exception e) {
throw new BusinessException(401, "Token已过期或无效");
}
}
}
在WebMvcConfig里注册拦截器,同时配置放行Swagger等资源:
java复制@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/auth/login",
"/swagger-ui.html",
"/swagger-resources/**",
"/webjars/**",
"/v2/api-docs",
"/v3/api-docs"
);
}
}
关于Swagger放行,这是热门搜索词里出现的原因就是很多同学遇到接口文档被拦截器挡掉的情况。Swagger的接口路径如果不放行,你在浏览器访问/swagger-ui.html时就会一直打不开。上面的配置已经把常见路径都放行了,可以直接用。
4.4 权限控制:接口级别的角色校验
JWT只是认证——验证"你是谁"。接下来还需要授权——验证"你能干什么"。我的做法是在拦截器里读取Token中的角色信息,再通过自定义注解做角色校验:
java复制@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRole {
String[] value();
}
然后在拦截器的preHandle里,找到处理方法上的注解做判断。一种更简洁的方式是定义一个RoleInterceptor,专门处理带@RequiresRole注解的接口。
例如:
java复制@RestController
@RequestMapping("/score")
public class ScoreController {
@PostMapping("/add")
@RequiresRole({"TEACHER", "ADMIN"})
public Result addScore(@RequestBody ScoreDTO dto) {
// 只有教师和管理员可以录入成绩
}
@GetMapping("/student/{studentId}")
@RequiresRole({"STUDENT", "TEACHER", "ADMIN"})
public Result getScores(@PathVariable Long studentId) {
// 学生只能查自己的成绩,这个逻辑在Service里做
}
}
这里有一个关键的安全细节:光靠前端按钮隐藏是不够的,后端必须校验权限。因为任何人都可以直接发HTTP请求到你的接口。我在很多毕设项目里看到前端菜单确实按角色隐藏了,但后端接口完全裸奔——学生只要知道接口地址,就能把成绩改了。这一点在论文里务必写清楚,设计评审时是加分项。
4.5 业务层常见坑:事务失效与循环依赖
在热搜词里看到有"springboot 事务失效场景"和"springboot 循环依赖"这两个高频问题,我实际在开发中也确实遇到过,这里一起说了。
事务失效最常见的原因有三个:
- 在同一个类里调用加了
@Transactional的方法。Spring事务是基于AOP代理的,同类内部调用不会经过代理,事务自然不生效。java复制解决办法:把需要事务的方法放到另一个Service类中,或者自己注入自己(// 错误示范:同类内部调用,事务不生效 public void saveStudentWithClass(StudentDTO dto) { saveStudent(dto); // 内部调用,事务失效 saveStudentClass(dto); } @Transactional public void saveStudent(StudentDTO dto) { ... }@Autowired当前类作为代理调用)。 - 异常被捕获了但没有抛出。
@Transactional默认只在RuntimeException抛出时回滚,如果你在方法里catch住了异常,事务框架感知不到,自然不会回滚。 - 方法不是public的。Spring事务只对public方法生效,private方法上的注解会被忽略。
循环依赖的表现是启动时报The dependencies of some of the beans in the application context form a cycle。我遇到过学生管理系统中,StudentService和ClassService互相调用导致的循环依赖。解决办法有三种:
- 重新设计依赖方向,避免两个Service相互依赖。比如
StudentService不再依赖ClassService,而是直接通过ClassMapper查询班级信息。 - 使用
@Lazy注解延迟注入,打破启动时的循环。 - 在Spring Boot 2.6以上版本默认禁止循环依赖,如果你坚持要用,可以在配置文件中加
spring.main.allow-circular-references=true。但我不建议在毕设里这么做,这是治标不治本。
5. 前端联调与权限控制:从页面到按钮
如果你选了前后端分离,那必然要跟Vue打交道。这一部分讲前端如何配合后端的权限设计。
5.1 Vue3 + Element Plus 的项目组织
前端项目用Vite搭建,npm create vue@latest即可。目录结构建议按模块组织:
code复制src
├── api # 接口请求封装,按模块拆分
├── assets
├── components # 公共组件
├── router # 路由配置文件
├── stores # Pinia状态管理
├── utils # axios封装、工具函数
├── views
│ ├── login
│ ├── student # 学生端页面
│ ├── teacher # 教师端页面
│ └── admin # 管理端页面
└── App.vue
axios封装时需要注意统一处理Token和错误响应:
javascript复制// utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '../router'
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 => {
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?.status === 401) {
localStorage.removeItem('token')
router.push('/login')
}
ElMessage.error(error.response?.data?.message || '网络异常')
return Promise.reject(error)
}
)
export default request
5.2 登录后动态生成菜单与路由守卫
路由守卫是前端权限控制的第一道关卡。基本逻辑是:未登录用户访问任何页面都跳转到登录页,已登录用户根据角色动态加载可访问的菜单。
javascript复制// router/index.js
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.path === '/login') {
next()
return
}
if (!token) {
next('/login')
return
}
const roles = JSON.parse(localStorage.getItem('roles') || '[]')
// 判断路由meta中需要的角色是否匹配
if (to.meta.roles && to.meta.roles.length > 0) {
const hasPermission = to.meta.roles.some(role => roles.includes(role))
if (!hasPermission) {
next('/403')
return
}
}
next()
})
比如教师端的路由配置:
javascript复制{
path: '/teacher/score',
component: () => import('@/views/teacher/ScoreManage.vue'),
meta: { roles: ['TEACHER', 'ADMIN'] }
}
这样学生登录后,即便手动在地址栏输入/teacher/score,也会被守卫拦下来。前端路由守卫的意义是优化用户体验,真正的安全靠后端接口权限来实现,这句话你写论文时也可以用上。
5.3 菜单级权限到按钮级权限的扩展
如果答辩时你能讲出"菜单权限 + 按钮权限"两层控制,那系统设计的完整度会明显高于普通CRUD项目。
菜单级权限,就是在用户登录后根据角色动态生成侧边栏菜单。这需要后端提供一个/auth/menus接口,根据用户角色返回不同的菜单项,前端拿到后动态渲染。
按钮级权限,则是页面上某些操作按钮(比如"删除学生")只有管理员能看到,教师和学生看不到。这个可以用自定义指令实现:
javascript复制// directive/permission.js
export default {
mounted(el, binding) {
const roles = JSON.parse(localStorage.getItem('roles') || '[]')
const requiredRoles = binding.value
if (!requiredRoles.some(role => roles.includes(role))) {
el.parentNode?.removeChild(el)
}
}
}
在组件中使用:
html复制<el-button v-permission="['ADMIN']" type="danger" @click="deleteStudent(row)">
删除学生
</el-button>
这样"删除"按钮只有管理员能看到。但我再次强调:这只是前端体验优化,不承担安全职责。有人通过浏览器开发者工具把按钮恢复出来,直接调用接口,后端不做校验一样会出事。所以按钮权限一定要跟后端的@RequiresRole配套使用。
6. 测试、打包与部署:让系统能跑在别人电脑上
功能写完只是第一步,毕设要能顺利答辩,测试、打包、部署这一条链路必须提前演练。
6.1 单元测试:不只是为了凑代码量
很多同学觉得写测试是浪费时间,我在实际项目中吃过亏后才知道,在答辩演示时,测试代码是证明你工程质量的最好证据。试想一下,你现场打开IDEA,mvn test跑一遍,绿色通过,再用Postman调用接口演示,评审老师对系统稳定性的信任度会大幅增加。
一个简单实用的Service层测试示例:
java复制@SpringBootTest
@Transactional // 测试结束后自动回滚,不污染数据库
class StudentServiceTest {
@Resource
private StudentService studentService;
@Test
void testQueryStudentWithPage() {
Page<StudentVO> page = studentService.queryStudentPage(1, 10, null);
assertNotNull(page.getRecords());
assertTrue(page.getTotal() >= 0);
}
@Test
void testAddStudentDuplicateNo() {
StudentDTO dto = new StudentDTO();
dto.setStudentNo("20230001"); // 假设已存在
dto.setStudentName("张三");
assertThrows(BusinessException.class, () -> studentService.addStudent(dto));
}
}
@Transactional注解在测试方法上特别有用,跑完自动回滚,不会污染你辛苦造的数据。这是我强烈推荐的一个小技巧。
6.2 后端打包与本地启动
后端打包使用Maven,命令很简单:
bash复制mvn clean package -DskipTests
在target目录下会生成一个student-ms-0.0.1-SNAPSHOT.jar,运行:
bash复制java -jar student-ms-0.0.1-SNAPSHOT.jar
这里有几个注意点:
- 配置文件里不要写死密码。如果你用
application.yml,里面数据库密码至少应该用环境变量引用:password: ${DB_PASSWORD:localpassword},这样部署到服务器时可以灵活设置,不至于把真实密码提交到Git里泄露出去。 - 打包后测试接口,使用
spring-boot-maven-plugin打包的jar是可直接运行的。如果你用了MyBatis的XML文件,确认它们被打进了target/classes目录,否则启动时会报Invalid bound statement错误。
6.3 前端构建与Docker部署
前端构建:
bash复制npm run build
生成dist目录,交给Nginx托管。下面是一份最简Nginx配置,解决前端路由history模式刷新404的问题:
nginx复制server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# 反向代理后端接口
location /api {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
如果你的学校提供了云服务器,推荐把项目部署到上面,答辩时直接用域名访问。不要等到答辩前一天才部署,提前一周就把环境搭好,因为总会有你意想不到的问题,比如服务器上没有装JDK、防火墙没放行端口、npm build在服务器上跑不动。
如果你用的是Docker Desktop,需要注意一个细节:容器内访问宿主机MySQL时,数据库地址不能写localhost,要写host.docker.internal。这是很多同学部署时连不上数据库的最常见原因。
Dockerfile我给出一个可用版本:
dockerfile复制FROM openjdk:8-jre-alpine
COPY target/student-ms-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]
6.4 答辩现场:怎么讲清楚你的系统
答辩时老师不太可能把你所有页面都看一遍,他们更关注的是你的设计思路和系统边界。我的建议是准备一个7分钟的演示脚本:
- 用一句话说明系统定位:这是一个面向高校管理人员的平台,覆盖学生信息、成绩、请假的管理闭环。
- 画一下系统架构图:前端Vue3、后端Spring Boot、数据库MySQL,讲清楚请求从浏览器到后端的完整链路。
- 讲清楚你的设计亮点:比如RBAC权限模型、JWT无状态认证、唯一键约束防重复数据。
- 挑一个最有代表性的功能演示:比如教师录入成绩、学生请假发起与审批。
- 准备两三个自问自答:为什么用JWT而不用Session?为什么用MyBatis-Plus而不用JPA?系统能支撑多少人并发?
如果你想往深了讲,可以提前研究一下Spring Boot自动装配原理,这是面试和答辩特别喜欢问的点。简单来说,Spring Boot通过@SpringBootApplication组合注解中的@EnableAutoConfiguration,配合META-INF/spring.factories文件,在启动时自动加载一大堆AutoConfiguration类,再通过@ConditionalOnClass、@ConditionalOnMissingBean等条件注解按需装配Bean。你能把这个逻辑讲清楚,基本就能撑起一个高水平的答辩。
写在最后
学生管理系统作为毕设题目,表面上看起来人人都能做,但拉开差距的恰恰是在那些非表面功夫上:数据建模的规范程度、权限设计的完整度、边界条件的处理、工程化习惯的养成。我见过太多"跑得起来但经不起问"的项目,也见过一些基础技术栈不新但逻辑非常完整的作品,后者在答辩中反而能获得更高的评价。
最后再分享一个小技巧:在开发过程中每隔两三天就看一下热门搜索词里"springboot"相关的问题,很多高频问题正是大家在真实开发中容易卡住的地方,提前了解这些坑,既能帮你少走弯路,也能在你排查代码问题的过程中提供思路。如果你的时间充裕,可以把系统再扩展一个"数据导入导出"功能,用EasyExcel把学生信息批量导入导出,这个功能在评委看来非常实用,做起来也不复杂,值得作为加分项考虑进去。
祝你的毕设顺利过关,有问题随时来交流。
