Spring Boot+Vue+MyBatisPlus 学生成绩管理系统实战解析
2026/9/23 1:28:28 网站建设 项目流程

简介:一份面向Java开发者的前后端分离项目实战教程,以学生成绩管理系统为例,完整演示Spring Boot、Vue.js与MyBatisPlus三者的整合开发。资源只有一个PDF文件,整体大小为2.8MB,内容从数据库设计讲起,逐步推进到后端服务、接口开发与前端页面构建。教程详细讲解了用户表、学生表、成绩表三张核心表的建表语句,以及用于软删除的删除标记字段设计逻辑;后端部分基于Spring Boot框架搭建项目骨架,使用MyBatisPlus操作数据库,前端部分采用Vue.js框架实现登录、学生列表、成绩录入等页面,并通过axios库完成前后端数据交互。读者可以从中掌握数据表关系设计、后端接口编写、前端组件化开发以及前后端分离项目的整体架构。目前已有448人学习,适合具备一定Java基础、希望快速上手前后端分离开发的初学者参考。

1. 前后端分离的学生成绩管理系统,为什么值得拆开看

学生成绩管理系统算是 Web 开发里最经典的练习项目了,但这次这份实战教程有意思的地方在于:它把 Spring Boot 2.7.17 + Vue + MyBatisPlus 3.4.1 的完整链路走了一遍,而且保留了真实项目里才会出现的坑——比如成绩表 XML 里score_math被拼成了socre_math,比如学号在软删除机制下故意不加唯一索引。这些细节恰恰是教学项目里最能让人长经验的部分。整套系统只用了三张表(用户表、学生表、成绩表),却覆盖了 JWT 登录校验、Swagger 接口文档、逻辑删除、参数校验、前后端 RESTful 联调这些日常开发高频考点。适合两类人:一是准备用前后端分离做毕设或面试项目的同学,可以直接对照源码一步步复现;二是工作中用的是 SSM 或 JSP 那套、想了解 MyBatisPlus 到底怎么省 SQL 的工程师。这篇就按「数据库设计 → 后端配置 → 实体与 Mapper → 联调与排错」的顺序,把整个项目的关键决策和坑都讲透。

2. 数据库设计先行:三张表、软删除与学号唯一索引的取舍

前后端分离项目里,后端最核心的产出是 API,而 API 的质量有一半取决于数据库设计。这份教程一上来就把三张表的建表语句给了出来,看似简单,其实里面藏着两个值得展开的设计点:软删除字段的约定,以及学号为什么不加唯一索引。

2.1 三张表的职责划分与字段设计

am_user管登录账号,am_student管学生基本信息,am_score管每次考试的各科成绩。成绩表通过student_id关联学生表,这属于典型的「一学生多成绩」一对多关系。

drop table if exists am_user; create table am_user( id bigint not null auto_increment primary key comment '主键id', user_name varchar(50) not null comment '用户名', pwd varchar(50) not null comment '密码', is_deleted tinyint default 0 comment '是否删除,1删除,0正常' ) comment '用户表'; insert into am_user values (1,'admin','1234',0); drop table if exists am_student; create table am_student( id bigint not null auto_increment primary key comment '主键id', number varchar(50) not null comment '学号,因为使用软删除,所以未设置唯一键索引', real_name varchar(50) not null comment '姓名', enroll_time datetime not null default CURRENT_TIMESTAMP comment '入学日期', is_deleted tinyint default 0 comment '是否删除,1删除,0正常' ) comment '学生表'; drop table if exists am_score; create table am_score( id bigint not null auto_increment primary key comment '主键id', student_id bigint not null comment '学生id', exam_name varchar(100) not null comment '考试名称', score_cn decimal(5,2) default 0 comment '语文成绩', score_en decimal(5,2) default 0 comment '英语成绩', score_math decimal(5,2) default 0 comment '数学成绩', is_deleted tinyint default 0 comment '是否删除,1删除,0正常' ) comment '成绩表';

需要说明几个设计决策:id全部用bigint auto_increment,在单机项目里自增主键的索引写入效率最高,完全够用;成绩字段用decimal(5,2)而不是double,因为考试成绩涉及小数比较和排名,decimal能避免浮点误差;每条insert语句都显式给出is_deleted默认 0,这保证了即使代码层漏传该字段,数据也不会被误判为已删除。

2.2 逻辑删除字段 is_deleted 的约定与代价

三张表都带了is_deleted tinyint default 0。逻辑删除的意思是:删除操作不执行delete,而是执行update is_deleted = 1。这样做的直接好处是数据可恢复、保留审计痕迹,在学生成绩这种场景里,老师误删一条成绩后可以直接改回标记值找回。

代价也很明显:所有查询都要带上is_deleted = 0条件,否则会把已删除数据查出来。MyBatisPlus 解决这个问题的方式是全局逻辑删除配置,后面第 3 章会细说。这里要提醒的是,tinyint类型的默认值一定要在表结构里写死default 0,否则历史数据可能出现null,而null在逻辑删除判断时既不等于 0 也不等于 1,查询会漏数据。

2.3 学号不加唯一索引:软删除机制下的典型取舍

建表语句里number字段的注释写得很直白:「因为使用软删除,所以未设置唯一键索引」。这在面试里是一个很好的讨论点。如果加了唯一索引,插入一条number = '001'is_deleted = 1的记录后,下次再录入一个同样学号的新学生就会索引冲突,因为唯一索引约束的是整行数据而非「未删除」的行。

不加唯一索引的结果就是同一学号可以被插入多条记录,包括多条未删除记录。如果项目要防重,常见做法有几种:在 Service 层先查is_deleted = 0的记录是否已存在该学号;或者用 MySQL 8.0 的生成列配合唯一索引做「部分唯一约束」;再或者干脆用deleted_at时间戳字段替代tinyint,让每次删除的时间戳都不一样,从而允许重复学号存在。这个项目选择的是最轻量的方案:不加索引,靠业务层控制。跟着教程复现时,可以直接照做,但上线前建议至少给number加一个普通索引,因为学生表的查询基本都走学号条件。

3. Spring Boot 2.7.17 启动层配置:pom 依赖选型与数据源参数

后端项目的基础是 Spring Boot 2.7.17 + Java 1.8。这个版本组合现在已经很成熟,网上资料多,遇到问题也好搜。pom 里值得注意的不是 spring-boot-starter-web 这种标配,而是 MyBatisPlus、JWT、Swagger 这三组依赖的具体版本和用途。

3.1 pom.xml 里这些依赖分别解决什么问题

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.17</version> <relativePath/> </parent> <dependencies> <!-- ORM 框架:MyBatisPlus 核心依赖 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.1</version> </dependency> <!-- 代码生成器:根据数据表反向生成实体、Mapper、Service --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.4.1</version> </dependency> <!-- 模板引擎:代码生成器依赖它渲染模板 --> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.30</version> </dependency> <!-- MySQL 驱动,注意是 5.1.47 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <!-- JWT:无状态登录令牌 --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.11.0</version> </dependency> <!-- Swagger:自动生成接口文档 --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> </dependencies>

版本选择上注意三点。第一,mybatis-plus-boot-starter3.4.1 与 Spring Boot 2.7.x 兼容良好,但如果你把 Spring Boot 升到 3.x,就得换用mybatis-plus-spring-boot3-starter,因为 Spring Boot 3 的 Jakarta 命名空间变了。第二,MySQL 驱动用的 5.1.47 是 5.x 系列的最后一个稳定版,如果你的数据库是 MySQL 8.0+,建议换成mysql-connector-j8.0.33,否则连接时可能报Public Key Retrieval is not allowed。第三,Swagger 2.9.2 在 Spring Boot 2.6+ 会遇到路径匹配策略冲突,必须加配置spring.mvc.pathmatch.matching-strategy=ant_path_matcher,教程的 application.properties 里已经处理了。

3.2 application.properties 逐行拆解:数据源、日志、逻辑删除、白名单

配置文件是整个后端能不能跑起来的关节。把重点几行拆开看:

# 服务端口,注意不是 8080 server.port=8089 # MySQL 连接配置 spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimeZone=UTC spring.datasource.username=root spring.datasource.password=mysql123 # JSON 序列化时的时间格式 spring.jackson.date-format=yyyy-MM-dd spring.jackson.time-zone=GMT+8 # 打印 MyBatisPlus 执行的 SQL,调试用 mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl # 实体类别名包,XML 里可以用短类名 mybatis-plus.type-aliases-package=com.shrimpking.pojo # 解决 Spring Boot 2.6 之后 Swagger 的路径匹配报错 spring.mvc.pathmatch.matching-strategy=ant_path_matcher # 全局逻辑删除配置 mybatis-plus.global-config.db-config.logic-delete-field=is_deleted mybatis-plus.global-config.db-config.logic-delete-value=1 mybatis-plus.global-config.db-config.logic-not-delete-value=0 # 无需 JWT 登录即可访问的 URL 白名单 auth.ignoreUrls=/user/login,/swagger-ui.html,/swagger-resources/**,/csrf,/error,/favicon.ico,/user/login2,/webjars/springfox-swagger-ui/**,/student/add,

几个关键参数的调整建议:

server.port=8089是因为本机可能同时跑着其他项目,8080 和 8081 容易冲突。如果你本地 8089 也被占了,改成 8090 就行,但前端 Vue 项目里的 axios 请求地址也要同步改,否则跨域请求直接失败。

spring.jackson.date-format=yyyy-MM-dd会把后端返回的Date类型序列化成yyyy-MM-dd格式。如果前端 Vue 的 el-date-picker 需要精确到时分秒,这里要改成yyyy-MM-dd HH:mm:ss,同时数据库字段类型对应datetime才能存得下。

mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl会在控制台打印每一条 SQL 以及参数占位符的替换结果。这个日志在开发时强烈建议打开,排查逻辑删除条件是否自动拼接、参数是否传错、分页 SQL 是否正常,都得靠它。上线前记得关掉,避免日志刷屏和敏感 SQL 泄露。

auth.ignoreUrls最后一项/student/add需要注意:它把「新增学生」接口直接放进了白名单,意味着未登录用户也能调用这个接口新增数据。如果照抄配置部署到公网,这是典型的安全隐患。建议上线前从这个列表里移除/student/add,只保留登录接口和 Swagger 相关路径。

3.3 MyBatisPlus 逻辑删除的完整链路

配置里写了三行全局逻辑删除参数后,实体类的isDeleted字段上再标@TableLogic,MyBatisPlus 就会自动处理所有 SQL:

  • select会自动追加WHERE is_deleted = 0
  • deleteById会变成UPDATE ... SET is_deleted = 1
  • 自定义 SQL 如果不走 MyBatisPlus 的 wrapper,而是写在 XML 里,那就得自己拼is_deleted = 0条件

这里有一个容易踩的坑:如果你的某个查询场景确实需要查已删除的数据(比如管理员的回收站列表),全局逻辑删除会拦住所有查询,此时只能用自定义 SQL 绕过。我会在最后一章给出恢复数据的实际做法。

4. 实体类、Mapper 与 Service:MyBatisPlus 把 SQL 省到什么程度

MyBatisPlus 的核心价值在于:单表 CRUD 完全不需要手写 SQL。这一章看三个 POJO、三个 Mapper 接口和三个 Service 接口的写法,感受一下代码量到底少了多少。

4.1 POJO 注解:@TableName、@TableId、@TableLogic、@TableField

先看用户实体类:

package com.shrimpking.pojo; import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data; import lombok.EqualsAndHashCode; @Data @EqualsAndHashCode(callSuper = false) @TableName("am_user") public class User implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Long id; private String userName; private String pwd; @TableLogic private Integer isDeleted; }

@Data来自 Lombok,自动生成 getter/setter,代码量直接砍掉一半。@TableName("am_user")把实体类和表名绑定,因为类名User默认映射到user表,如果表名是am_user,必须显式声明。@TableId(type = IdType.AUTO)表示主键由数据库自增生成,插入时不传 id,由 MySQL 填值。@TableLogic标注了逻辑删除字段,与全局配置联动。

Student 实体类里用了@NotBlank@NotNull,这是 Jakarta Validation 的参数校验注解:

@NotBlank private String number; @NotNull private Date enrollTime;

@NotBlank校验字符串不为 null 且去掉空格后非空,@NotNull校验对象不为 null。这些注解要生效,Controller 层接收参数时必须加@Validated@Valid。教程的代码示例没有贴出 Controller 部分,但你跟着写的时候,一定记得在方法参数前加@Validated @RequestBody Student student,否则注解不生效,非法参数会带着空学号直接入库。

4.2 Mapper 接口:继承 BaseMapper 后免费获得哪些能力

package com.shrimpking.mapper; import com.shrimpking.pojo.Score; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface ScoreMapper extends BaseMapper<Score> { }

继承BaseMapper<Score>后,自动拥有了insertdeleteByIdupdateByIdselectByIdselectListselectPage等 17 个方法。对于成绩管理这个场景,基础 CRUD 已经全覆盖了。

BaseMapper提供的selectPage是分页查询的关键,注意 MyBatisPlus 3.4.1 的分页插件需要自己配置一个MybatisPlusInterceptor

package com.shrimpking.config; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(); // 设置最大单页限制为 500,防止一次查太多拖垮数据库 pagination.setMaxLimit(500L); interceptor.addInnerInterceptor(pagination); return interceptor; } }

这个配置类教程里没有贴出来,但实际跑分页功能时必须加,否则selectPage会查出全表数据而不是分页结果。网上反映的 mybatisplus 分页失效问题,八成就是漏了这个拦截器配置。setMaxLimit(500L)这个参数很实用,它能兜底拦截前端传过来的超大分页参数,避免一次查询加载过多数据。

4.3 Mapper XML 里的 typo:score_math 变成 socre_math 的连锁反应

教程里 ScoreMapper.xml 的 resultMap 和 Base_Column_List 都有同一个拼写错误:socre_math而不是score_math。这在 MySQL 里不会直接报错,因为 MyBatis 的 resultMap 只在做 ORM 映射时使用,如果查询 SQL 是额外手写的,并且 SQL 里写了socre_math,MySQL 会报Unknown column;如果查询走 MyBatisPlus 自动生成的 SQL(列名来自实体类字段),那么 resultMap 里的错误列名可能根本不会被触发。坏情况是:等你某天手写了一条自定义 SQL,用了SELECT socre_math FROM am_score,错误就暴露了。

score_math这列本身也存在命名规范问题。MySQL 在 Linux 下默认表名列名大小写敏感,而下划线命名法与 Java 实体类的驼峰命名法scoreMath之间的转换,需要 MyBatisPlus 的驼峰映射开关。MyBatisPlus 默认开启map-underscore-to-camel-case,所以score_cn能自动映射到scoreCn。如果你在 XML 里手写 resultMap,也要记得保持列名和实体字段一一对应,用数据库实际列名,不要凭记忆写错。

4.4 Service 层:继承 IService 后多出批量操作能力

package com.shrimpking.service; import com.shrimpking.pojo.Score; import com.baomidou.mybatisplus.extension.service.IService; public interface ScoreService extends IService<Score> { }

IService<Score>在 BaseMapper 之上又封装了一层,多出了saveBatch(批量插入)、listByIds(按 ID 集合查询)、lambdaQuery链式查询等方法。典型的链式查询写法:

// 查某学生的所有成绩,按考试名称倒序 List<Score> scoreList = scoreService.lambdaQuery() .eq(Score::getStudentId, studentId) .orderByDesc(Score::getExamName) .list();

lambdaQuery用方法引用代替字符串列名,编译期就能发现拼写错误,比QueryWrapper的字符串写法安全得多。开发中建议优先用lambdaQuerylambdaUpdate,只有需要动态拼接复杂条件时才退回QueryWrapper

5. 前后端联调时的登录白名单、时区与数据恢复技巧

前端 Vue 部分的核心工作是与后端 API 对接。联调阶段最容易出问题的三块:登录接口与 JWT 白名单、时间字段的时区偏差、逻辑删除之后的数据恢复。这一章把这三个问题的验证方法和处理技巧说透。

5.1 登录白名单:JWT 拦截器放行了哪些路径

教程配置中的auth.ignoreUrls是一个自定义配置项,通常由一个拦截器读取:

// 伪代码,展示拦截器如何读取白名单 String ignoreUrls = environment.getProperty("auth.ignoreUrls"); List<String> urlList = Arrays.asList(ignoreUrls.split(",")); // 拦截器里判断:请求路径属于白名单则直接放行 if (urlList.contains(request.getRequestURI())) { return true; } // 否则校验请求头中的 Authorization String token = request.getHeader("Authorization");

联调时验证白名单是否生效的方法很简单:先不带 token 访问/user/login(应返回 200),再不带 token 访问学生列表接口(应返回 401 或重定向到登录页)。如果发现某个受保护接口能匿名访问,检查两处:一是配置项里的路径是否和 Controller 的@RequestMapping路径完全一致,二是拦截器的放行判断是否用了startsWith而不是equals/user/login2这类相似路径很容易被误判放行。

Swagger 页面本身也在白名单里,所以如果你按教程配置了 Swagger,浏览器直接访问http://localhost:8089/swagger-ui.html就能看到所有接口文档。这是一个非常好用的联调工具,不用再靠 postman 手动维护接口参数。上线前建议把 swagger 相关路径从白名单移除,或者直接用 profile 控制只在 dev 环境开启。

5.2 前端联调时的时间字段与时区大坑

serverTimeZone=UTCspring.jackson.time-zone=GMT+8的组合,是前后端分离项目里最容易出现时间错乱的源头。

场景是这样的:数据库连接串里写了serverTimeZone=UTC,但 JVM 时区是GMT+8,Jackson 序列化Date类型时用GMT+8,而 MySQL 驱动按 UTC 存取。最终效果是:后端存入数据库的时间比实际时间早 8 小时,前端拿到的 JSON 时间字符串按北京时间解析后又对不上。特别是成绩表里如果想记录考试日期,这个偏差直接导致考试日期显示的年份都错。

验证方法是在 Spring Boot 启动类或配置类里打印当前时间:

System.out.println(new Date());

处理建议有两种。第一种:连接串改成serverTimeZone=Asia/Shanghai,同时删掉spring.jackson.time-zone=GMT+8(或保留也行,保持一致)。第二种:实体类的时间字段改用LocalDateTime,配合 Jackson 的JavaTimeModule,彻底绕开java.util.Date的时区语义。后一种方案更推荐,LocalDateTime本身就是无时区概念的时间描述,适合成绩录入这种场景。

顺带提一个常见问题:前端 el-date-picker 传的日期格式如果是2023-01-01T00:00:00.000Z,而后端@RequestBody接收Date类型时没配格式,解析会直接报JSON parse error。解决方式是加一个 Jackson 全局配置:

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss

这样前端传2023-01-01 00:00:00就基本不会出问题。如果你的前端组件就是习惯传 ISO 格式,也可以在后端加一个自定义反序列化器,但那是后话,先用统一格式最省事。

5.3 逻辑删除后如何恢复数据:update 语句与 MyBatisPlus 的边界

逻辑删除最大的优势是数据可恢复。实际场景里常见两种恢复需求:恢复单条成绩记录,以及恢复某个学生的全部成绩。

-- 恢复单条成绩:把 is_deleted 改回 0 UPDATE am_score SET is_deleted = 0 WHERE id = 1 AND is_deleted = 1; -- 批量恢复某个学生的全部成绩 UPDATE am_score SET is_deleted = 0 WHERE student_id = 1 AND is_deleted = 1;

但这里有个陷阱:MyBatisPlus 的逻辑删除配置会让所有自动生成的update语句都拼接WHERE is_deleted = 0,意味着你没法通过updateById把一条已删除记录改回来。这个场景必须用自定义 SQL,在 Mapper XML 里手写一条绕过逻辑删除的 update:

<update id="recoverById"> UPDATE am_score SET is_deleted = 0 WHERE id = #{id} </update>

注意这段 SQL 并不会被全局逻辑删除拦截,因为它是你自己写在 XML 里的,MyBatisPlus 不会为手写 SQL 注入逻辑删除条件——除非你启用了@InterceptorIgnore等高级功能。所以你要自己保证这里的is_deleted条件是对的,不要误恢复了「本来就不该出现」的脏数据。

同理,如果你想在管理后台查看包含已删除学生的列表,也需要手写 SQL 或用@Select注解:

@Select("SELECT * FROM am_student WHERE number = #{number}") List<Student> selectIncludingDeleted(@Param("number") String number);

这段 SQL 完全绕过了逻辑删除,能查出已删除记录,供开发者确认数据是否被误删。日常调试时建议先跑这条确认数据还在,再执行恢复 SQL,两步验证都通过,才敢跟业务方说「数据找回来了」。

最后一提:成绩录入场景里,如果前端传的scoreCn只有一位小数,后端实体类用的是Double接收,而数据库是decimal(5,2),MyBatis 会自动补齐小数位,但如果你在前端做了平均值或排名的二次计算,Double的精度误差会在数据量变大之后慢慢浮现。正式项目里成绩字段建议统一用BigDecimal接收和运算,decimal(5,2)的最大值是 999.99,单科成绩不会超,够用。Decimal的 equals 运算和比较要基于compareTo而不是equals,这也是BigDecimal的另一个隐藏坑,写单元测试时容易踩到。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询