简介:基于 Java 与 MySQL 实现的学生学籍管理系统,是一套面向高校课程设计、毕业设计及项目开发场景的完整资料包,能够帮助学习者深入理解学籍管理系统的功能模块划分、编码思路与项目组织方式。压缩包共包含五十一个文件,总大小约为二点四二兆字节,以 Java 源码、class 编译文件、jar 依赖库和 SQL 数据库脚本为主体,同时备有工程配置文件与 Markdown 项目文档,zip 格式解压后即可查看整体目录结构,快速定位核心代码与数据库初始化脚本。目前该资源已有约一百一十七人参与学习浏览,说明它契合常见的程序设计类选题需求,可作为课程设计或毕业设计选型时的有效参考。借助这份材料,能够获取学生信息新增、修改、删除、查询等核心模块的完整源码,将 SQL 脚本导入 MySQL 后即可完成建库与初始数据操作,再配合文档逐步部署项目,显著缩短从零搭建的周期。配套文档中对数据表结构、关键配置和部署要点均有说明,便于在此基础上继续扩展成绩管理、班级管理等模块,也适合在课程报告或答辩中快速提炼技术思路,整体内容务实精简,便于上手实践。
1. 学生学籍管理系统:最被低估的毕业设计坑,多数人倒在“跑不起来”
在毕业设计和课程设计里,“基于Java+MySQL实现的学生学籍管理系统”几乎是出镜率最高的题目——Java写后端、MySQL存数据,交付物是源码、项目文档、数据库脚本。听着老套,但真正做完的人都知道,难点从来不在增删改查,而在“答辩现场系统起不来”或“演示到一半数据库崩了”。本文按我做这类项目的一贯路子拆开讲:数据模型怎么立、技术线怎么选、核心代码怎么落,以及交作业前最值得提前规避的五个坑。适合正在做毕设课设的学生,也适合刚转Java方向、想拿一个完整CRUD项目练手的从业者。
2. 先把数据模型立住:学籍管理系统的六张核心表与建表 SQL
写业务系统最忌讳一上来就写代码。学籍管理系统的本质是“人、班级、课程、成绩”四类数据的关系维护,先把表设计定下来,后面所有代码只是对这批关系的读写。我一般会先花一个晚上画ER图,比后面返工改表省一个周末。这个阶段想清楚的,是“谁在操作、操作什么数据、数据之间什么关系”,而不是急着建Spring Boot工程。
2.1 角色与业务线:管理员、教师、学生三条操作链怎么走
系统里至少有三类角色:管理员(对应学工办老师)、普通教师、学生本人。三条业务线分别是:管理员维护专业、班级和学生基础信息,处理休学、复学、退学这类学籍异动;教师负责按课程录成绩、看某个班的成绩分布;学生只查询自己的学籍状态和成绩单。
这个划分直接决定权限模块怎么做。管理员和教师都进后台,但教师不能删学生、不能改班级信息;学生只有查询权限。常见做法是建一张独立的系统用户表,字段里带角色标识(admin / teacher / student),登录成功后把角色写进Session,拦截器里按角色放行接口。很多课程设计把角色写死在学生表里,后面加教师登录时就得重构,得不偿失。
2.2 六张核心表的设计理由
我习惯拆成六张表,数量不多不少,既覆盖学籍管理的主流程,又能让文档里的E-R图画得清楚:
| 表名 | 作用 | 核心字段 |
|---|---|---|
| sys_user | 登录账号与角色 | id, username, password, role |
| major_info | 专业目录 | id, major_name |
| class_info | 班级(归属专业) | id, major_id, class_name, grade_year |
| student | 学生学籍档案 | id, student_no, name, gender, birth_date, class_id |
| course_info | 课程目录 | id, course_no, course_name, credit |
| score_info | 学生成绩(多对多中间表) | id, student_id, course_id, score, exam_date |
sys_user 和 student 分开,是因为登录账号的声明周期和学籍记录不同。学生休学或毕业后,账号可能还要保留一段时间,混在一张表里会让“删除学生”变成“删除账号”,业务上容易出事故。class_info 和 major_info 拆开,是因为一个专业下面会有多届多个班,如果不拆,班级表里会出现大量重复的专业名。
score_info 是整张ER图的枢纽。学生和课程是多对多关系,必须通过成绩表关联;同时成绩表里还要存考试日期、学分绩点等过程数据,独立成表后,后续做“课程平均分”“补考名单”这类统计都更方便。
2.3 建表 SQL 与初始化数据脚本:字段约束和索引的取舍
建库脚本最重要的一条原则是“能反复跑”。课程设计老师经常会拿你的脚本在自己电脑上重建,如果脚本只能成功执行一次,当场翻车。做法是:开头关掉外键检查,然后按先子表后父表的顺序 DROP,再按先父表后子表的顺序 CREATE。
-- 可重复执行的建库脚本:学籍管理系统 SET FOREIGN_KEY_CHECKS = 0; DROP TABLE IF EXISTS score_info; DROP TABLE IF EXISTS student; DROP TABLE IF EXISTS course_info; DROP TABLE IF EXISTS class_info; DROP TABLE IF EXISTS major_info; DROP TABLE IF EXISTS sys_user; SET FOREIGN_KEY_CHECKS = 1; CREATE DATABASE IF NOT EXISTS stu_manage DEFAULT CHARACTER SET utf8mb4; USE stu_manage; CREATE TABLE major_info ( id INT PRIMARY KEY AUTO_INCREMENT, major_name VARCHAR(64) NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='专业表'; CREATE TABLE class_info ( id INT PRIMARY KEY AUTO_INCREMENT, major_id INT NOT NULL, class_name VARCHAR(64) NOT NULL, grade_year CHAR(4) NOT NULL, CONSTRAINT fk_class_major FOREIGN KEY (major_id) REFERENCES major_info (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班级表'; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(32) NOT NULL, name VARCHAR(64) NOT NULL, gender CHAR(1) NOT NULL, birth_date DATE, phone VARCHAR(20), class_id INT NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT '1在读 2休学 3毕业', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_no (student_no), KEY idx_student_class (class_id), CONSTRAINT fk_student_class FOREIGN KEY (class_id) REFERENCES class_info (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生学籍表'; CREATE TABLE course_info ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(32) NOT NULL, course_name VARCHAR(128) NOT NULL, credit DECIMAL(3,1) NOT NULL, UNIQUE KEY uk_course_no (course_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表'; CREATE TABLE score_info ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2) NOT NULL, exam_date DATE NOT NULL, UNIQUE KEY uk_stu_course (student_id, course_id), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student (id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course_info (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成绩表';脚本里几个细节值得解释。引擎统一指定 InnoDB,是为了让外键约束和事务真正生效;MyISAM 不支持外键。字符集统一 utf8mb4,否则后面做中文查询和导出大概率出现乱码,这一条可以和连接串、页面编码放在一起自查。外键列的类型必须和主表主键完全一致,比如主表是 INT,子表就不能用 BIGINT,否则建约束会报“Cannot add foreign key constraint”。
索引方面,我在 student.student_no 和 course_info.course_no 上建了唯一索引,这是业务兜底:学号和课程编号不允许重复,比在代码里先查后插更可靠。score_info 上建了 (student_id, course_id) 联合唯一索引,防止同一学生对同一门课录两次成绩。注意外键列本身也要建普通索引,InnoDB 不会自动给外键列补索引,不建的话,按班级查学生、按课程查成绩都会变慢。
初始化数据脚本我一般只放三样:一个管理员账号、一个专业一个班、两三门课程。管理员密码用 MD5 或 BCrypt 加密后写入 sys_user,明文密码只写在项目文档的说明里,方便答辩老师验证。如果老师要求演示“系统能跑”,初始化数据越少越不容易出错,等答辩时现录一个学生进去,演示效果比预置一堆假数据更真实。
3. 选型与工程骨架:Servlet/JSP 还是 Spring Boot + MyBatis
很多学校的课程设计还停留在 JSP + Servlet,但自选毕设已经普遍允许 Spring Boot。这里先给结论:学校不限定技术栈的,优先 Spring Boot + MyBatis;学校硬性要求 JSP 的,保留 JSP 页面,但工程分层和数据库交互方式仍然照下面的路子走。技术选型直接决定后面五天的工作量,值得单独用一章把理由说清。
3.1 两条技术线的适用场景对比
| 维度 | JavaWeb 经典线(Servlet + JSP + JDBC) | 工程化线(Spring Boot + MyBatis) |
|---|---|---|
| 学习门槛 | 低,贴近 JavaWeb 基础课 | 中等,需要理解依赖注入和ORM |
| 演示稳定性 | 容易受 Tomcat 配置影响 | 内嵌 Tomcat,一键启停 |
| 代码量 | DAO 和连接管理代码多 | 自动配置,样板代码少 |
| 资料可查性 | 老资料多但过时也多 | 热门搜索引擎一抓一大把 |
| 答辩观感 | 看着“像课堂作业” | 更接近企业开发习惯 |
我见过太多用 Servlet 写的课程设计,代码逻辑没问题,但打开 Eclipse 配 Tomcat、部署 war 包、再处理静态资源路径,半小时起不来。Spring Boot 内嵌 Tomcat,一条 java -jar 就能跑,答辩现场的出错面小很多。如果学校没有硬性禁用框架,Spring Boot 是更稳妥的选择。
需要说明的是,选 Spring Boot 不意味着抛弃 JavaWeb 基础。登录拦截、Session 管理、事务边界这些核心思路在两个技术栈里完全一致,只是实现方式不同。写文档时把两者对比写进去,反而能体现你理解框架背后的机制。
3.2 最小可运行工程:Maven 依赖与目录结构
工程骨架我习惯这样组织,按 controller / service / mapper / entity / common 分包,和课程设计文档里的三层架构能对应上:
<!-- pom.xml 核心依赖 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <!-- 使用你自己环境能拉到的稳定版本,下面只是示例 --> <version>2.7.18</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <!-- 版本号由 spring-boot-starter-parent 管理 --> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.20</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>版本号我用的是自己长期在用的组合,你拿到的版本可能更新,只要保证 spring-boot 2.7.x 和 mybatis-spring-boot-starter 2.3.x 配得上即可。依赖不是越多越好,多一个 starter 就多一层出错可能;Druid 连接池是加分项,如果嫌麻烦可以先去掉,直接用 HikariCP,Spring Boot 默认就是它。
目录结构对应关系:
src/main/java/com/example/stuadmin/ ├── StudentAdminApplication.java # 启动类 ├── controller/ # 接收请求,返回 JSON 或页面 ├── service/ # 业务逻辑与事务边界 ├── mapper/ # MyBatis 接口 ├── entity/ # 对应表的实体类 └── common/ # Result 封装、拦截器、全局异常3.3 数据库连接配置:JDBC 到连接池的参数清单
application.yml 里的数据源配置是这门课最容易出问题的地方,尤其是 MySQL 8。下面这份配置把字符集、时区、公钥检索都显式写出来了:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/stu_manage?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 validation-query: SELECT 1 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.stuadmin.entity configuration: map-underscore-to-camel-case: true几个参数逐个说。serverTimezone=Asia/Shanghai 解决 MySQL 8 和 Java 8 时区不匹配的问题,不加这个,连接时会报 CST 时区异常。allowPublicKeyRetrieval=true 是 MySQL 8 使用 caching_sha2_password 插件时的连接要求,很多新手卡在这一步。rewriteBatchedStatements=true 对批量插入很重要,后面第四章批量导入会用到,它让 JDBC 把多条 insert 重写成一条多值 insert,性能差别很大。
Druid 参数里,initial-size 设 5 表示启动时预建 5 个连接;max-active 设 20 是峰值上限;max-wait 设 60000 毫秒,意思是拿不到连接时最多等 60 秒就抛异常,避免线程无限阻塞。validation-query 用 SELECT 1 做连接存活检测,这是 Druid 对“数据库探活”的标配写法。
4. 核心功能实现:登录态、学籍 CRUD、批量导入的落地代码
数据模型和工程骨架就位后,这一章把三个最核心的功能写透:登录与拦截、学生学籍增删改查、成绩批量导入。这三块覆盖了“一个完整系统”的演示主线,也是答辩老师最可能现场点的功能。
4.1 登录与 Session 校验:拦截器放行规则怎么写
登录接口的核心是把用户查出来、比对密码、把用户信息写进 Session。密码比对用 BCrypt 或 MD5 取决于初始化数据怎么加密的,关键是不要在代码里拼 SQL 字符串。
@RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private SysUserMapper userMapper; @PostMapping("/login") public Result login(@RequestBody LoginReq req, HttpSession session) { // 1. 按用户名查账号,MyBatis 自动参数化,避免 SQL 注入 SysUser user = userMapper.findByUsername(req.getUsername()); if (user == null) { return Result.error("用户名不存在"); } // 2. 比对密码;生产环境用 BCrypt,课程设计用 MD5 请保持一致 if (!user.getPassword().equals(DigestUtils.md5DigestAsHex(req.getPassword().getBytes()))) { return Result.error("密码错误"); } // 3. 写入 Session,后续拦截器从这里判断登录态 session.setAttribute("loginUser", user); session.setAttribute("role", user.getRole()); return Result.success(user); } }参数说明:LoginReq 是前端传过来的 JSON 结构,包含 username 和 password 两个字段。Session 超时时间可以在 application.yml 里配置 server.servlet.session.timeout=30m,演示时要提前提醒自己“超过半小时不去点页面,Session 会过期”,这是一个很容易在答辩现场翻车的细节。
拦截器的作用是保护后台接口。放行规则要格外小心,只放行登录和登出接口:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); if (session.getAttribute("loginUser") == null) { // 未登录:重定向到登录页,或返回 401 给前端跳转 response.setStatus(401); return false; } return true; } }注册拦截器时,excludePathPatterns 只放行登录相关的/api/auth/login,其余/api/**全部拦截。很多课程设计把放行写成 “放行所有请求,只在页面里判断”,那拦截器就形同虚设,演示时直接输个接口地址就能绕过登录,这是答辩老师最常挑的问题。
4.2 学籍 CRUD:Service 层的事务边界与 Mapper 写法
学生学籍的新增和修改要一起讲,是因为它们共享“学号唯一 + 班级人数统计更新”这两个业务约束。这里用 @Transactional 保证要么全部成功、要么全部回滚:
@Service public class StudentService { @Autowired private StudentMapper studentMapper; @Autowired private ClassInfoMapper classInfoMapper; @Transactional(rollbackFor = Exception.class) public void saveStudent(Student student) { // 1. 学号唯一校验:数据库有唯一索引兜底,这里做友好提示 Student exist = studentMapper.findByStudentNo(student.getStudentNo()); if (exist != null) { throw new BizException("学号已存在"); } // 2. 插入学生主表 studentMapper.insert(student); // 3. 更新班级人数统计字段(假设 class_info 表有 student_count 列) classInfoMapper.increaseStudentCount(student.getClassId()); } }逻辑说明:@Transactional 是 Spring 声明式事务的核心写法。默认只回滚 RuntimeException,这里显式指定 rollbackFor = Exception.class,是为了让业务异常(BizException)也触发回滚——否则可能出现“学生插入成功、班级人数没加”的脏数据。increaseStudentCount 这一步在真实系统里也可以不做,但考虑到毕业设计要展示“数据一致性意识”,加上这一个动作,答辩时能讲出东西。
Mapper 接口和 XML 的对应关系按 MyBatis 常规写法即可。需要强调的是,所有查询条件字段尽量走索引。比如 findByStudentNo 要保证 student_no 列有唯一索引,本章前面的建表 SQL 已经加了 uk_student_no,这就是为什么建表阶段设计索引比代码阶段硬编码更重要。
4.3 批量导入导出:POI 处理 Excel 的幂等写法
成绩批量导入是课程设计里比较亮眼的功能,也是坑最多的功能。核心需求是:老师上传一张 Excel 成绩表,系统批量写入 score_info;如果文件被重复上传,不能产生重复记录。
public void importScores(MultipartFile file) { List<ScoreRow> rows = parseExcel(file); // 用 Apache POI 逐行读取 if (rows.isEmpty()) { throw new BizException("Excel 中没有有效数据"); } // 分批插入,每 100 条 flush 一次,避免大事务堆积 int batchSize = 100; for (int i = 0; i < rows.size(); i += batchSize) { List<ScoreRow> subList = rows.subList(i, Math.min(i + batchSize, rows.size())); scoreMapper.batchInsertOrUpdate(subList); } }逻辑说明:batchInsertOrUpdate 对应的 SQL 用INSERT INTO score_info (student_id, course_id, score, exam_date) VALUES ... ON DUPLICATE KEY UPDATE score = VALUES(score)。配合 score_info 表的联合唯一索引 (student_id, course_id),重复上传同一份成绩单时,唯一索引冲突会触发 UPDATE,而不是新增一行。这就是“幂等导入”的落地方式。
参数说明:batchSize 设为 100 是事务和内存的折中。每次 insert 都是一个隐式事务,如果一次插 5000 条,占用的 undo log 很大,出错了回滚也慢;拆成 100 条一批,单批失败时能定位到具体批次。前面 yml 配置里的 rewriteBatchedStatements=true 在这时才生效,没有这个参数,JDBC 的批量插入仍然是逐条执行,性能上毫无提升。
Excel 解析时还有一个常见坑:POI 读取日期单元格拿到的是数字,需要对 CellType 做分支判断。日期格式在 Excel 内部是序列号,直接 toString 会得到一串数字。解析时统一用 cell.getDateCellValue(),再转成 java.time.LocalDate,避免数据变成 45123 这种莫名其妙的值。
5. 常见问题与避坑:课程设计交作业前最容易翻车的五个位置
到这一步,功能基本能跑了。但课程设计最惨的不是写不出代码,而是答辩前一晚环境崩了。下面按我见过的高频故障整理成五条踩坑记录,每条都是“现象 → 原因 → 解决”的结构,建议对照排查一遍再交。
5.1 MySQL 8 装好连接不上:时区、公钥检索与初始密码策略
- 现象:Spring Boot 启动时控制台报
Public Key Retrieval is not allowed,或The server time zone value 'CST' is unrecognized,数据库连不上。 - 原因:MySQL 8 默认认证插件是 caching_sha2_password,JDBC 首次连接需要做公钥检索,但连接串没授权;同时 8.x 的时区默认值在部分系统里解析不了。
- 解决:连接串补上
allowPublicKeyRetrieval=true和serverTimezone=Asia/Shanghai。另外,刚装完 MySQL 8 的 root 初始密码通常不在安装提示里,而在日志文件里,如果提示 Access denied for user 'root',先去服务端日志找临时密码,再ALTER USER 'root'@'localhost' IDENTIFIED BY '新密码';。这套配合 mysql 安装教程一起看,基本能覆盖九成连接问题。
5.2 建库脚本跑一半报错:外键顺序与字符集不一致
- 现象:执行建表 SQL 时,到 score_info 或 student 表报
Cannot add foreign key constraint。 - 原因:最常见的是父表还没创建就建子表;其次是父表和子表的字符集或字段类型不一致,utf8mb4 和 utf8 混用也可能触发;还有一种少见情况是外键字段的 unsigned 属性不一致。
- 解决:脚本开头加
SET FOREIGN_KEY_CHECKS = 0;,先全部 DROP,再按父表 → 子表顺序 CREATE;所有表统一DEFAULT CHARSET=utf8mb4;外键列的类型、长度、无符号属性必须和主表主键逐字一致。养成“脚本能反复跑”的习惯,老师在自己电脑上重建时就不会卡住。
5.3 批量导入同一份成绩单导致数据翻倍
- 现象:同一个 Excel 成绩单导入两次,score_info 表出现完全相同的两行记录,统计平均分时被算了两遍。
- 原因:没有唯一约束,代码里也没有先查后插。数据库层面没有兜底,纯靠代码判断很容易漏。
- 解决:score_info 表加 (student_id, course_id) 联合唯一索引,SQL 用
INSERT ... ON DUPLICATE KEY UPDATE,让数据库自己处理冲突。这里涉及的 MySQL 锁分类中,行锁、间隙锁都在 InnoDB 处理重复键时起作用;理解到这个层面写论文的“原理”部分会更扎实。
5.4 查询结果页面中文全部变成问号
- 现象:数据库里存的是中文,页面或接口返回却是
???或乱码。 - 原因:一是连接串没加 characterEncoding=utf8,JDBC 用默认编码读数据;二是表字符集建成了 latin1;三是后端返回 JSON 时没设置 UTF-8。
- 解决:连接串加
characterEncoding=utf8,建库用 utf8mb4,Spring Boot 的配置里再加server.servlet.encoding.force=true。三处统一后基本能根治。排查时从数据库往下游:先SELECT看到底是库里坏了还是展示层坏了,别一上来就动前端。
5.5 LocalDateTime 序列化导致前端时间变成数组
- 现象:实体里用了 java.time.LocalDateTime,前端拿到的时间是一串
[2024, 5, 20, 14, 30]这样的 JSON 数组。 - 原因:Jackson 默认不支持 LocalDateTime 的格式化输出,会把它序列化成对象内部结构。
- 解决:在 application.yml 里加全局时间格式化:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai如果实体里还有其他日期类型,统一用 java.time.LocalDateTime 或 LocalDate,避免 Date、Timestamp、LocalDateTime 混用。这个问题的隐蔽性在于页面单独看某个字段可能没注意,等打印整套学生信息时才发现时间错乱。
6. 答辩前的三件小事:用事务、EXPLAIN 和统计页把系统讲出“工程感”
代码能跑只是及格线,“工程感”才是课程设计拿高分的关键。我建议答辩前集中做三件事,每一件都是几十行代码以内的改动,但能让评委看到你不是只会调接口。
第一件,演示一次事务回滚。在批量导入接口里故意传一条学号超长的数据,让 Service 层抛异常,然后查数据库确认整批数据没有半插入。这个演示比嘴巴解释“我加了事务”有说服力得多。记得把回滚日志截图放进项目文档。
第二件,给成绩统计写一条聚合 SQL,并用 EXPLAIN 验证索引。比如统计每个班的课程平均分:
SELECT c.class_name, AVG(s.score) AS avg_score FROM student st JOIN class_info c ON st.class_id = c.id JOIN score_info s ON st.id = s.student_id GROUP BY c.id;跑之前先 EXPLAIN,确认 type 不是 ALL,看到 ref 或 eq_ref 就说明 join 走了索引。课程设计的数据量可能只有几百行,全表扫描也能秒出结果,但你要在答辩时主动说一句“这个统计查询走了 student_id 和 class_id 上的索引”,评委的观感会完全不同。
第三件,做一张首页统计看板。最少放三个数:在校学生总数、班级总数、本月已录成绩条数。用 MyBatis 写三条 COUNT 查询,一个 Controller 接口返回,前端随便摆几张卡片就行。别小看这几行代码,它把系统从“功能型”变成了“数据可见型”,是答辩演示最容易讲出故事的部分。
带过的学生里,让我印象最深的一次翻车,不是系统崩了,而是演示“全年级成绩分布”时,查询超了两秒还没出结果。数据量明明只有几千条,后来一查,score_info 表压根没建联合索引,数据库对成绩表做全表扫描。从那以后我养成一个习惯:凡是写完一条带 GROUP BY 的 SQL,马上 EXPLAIN,确认索引生效再往接口里放。这个习惯不复杂,但能让你在答辩现场少出一身冷汗。希望帮到你——把这份耐心放在事务、索引和幂等这些“看不见的地方”,你的课程设计就不只是能跑,而是能讲、能展示、能经得起追问。
本文还有配套的精品资源,点击获取