简介:这是一套基于SpringBoot的在线学籍系统源码,面向计算机、电子信息工程等专业的学习者,可作为高分毕业设计、课程设计或期末大作业的参考项目。系统采用B/S与MVC架构,技术栈涵盖Java、SpringBoot、Mybatis、Ajax、Vue、Mysql、Maven等,开发环境为IDEA、JDK1.8、Maven3.6与Mysql5.7,部署于Tomcat8.0/9.0,浏览器兼容谷歌、Edge与火狐。压缩包共422个文件,约9.14MB,包含107个java后端源码、44个vue前端组件、21个js脚本、14个xml配置、7个css样式及若干svg、jpg、png等静态资源,另有bat启动脚本与yml、properties配置文件,结构完整、层次清晰。目前已有61人浏览学习。所有源码均经过严格测试,可直接下载运行,便于读者快速理解学籍管理模块的前后端交互逻辑、数据库设计与接口组织方式,适合作为二次开发与功能扩展的基础工程。
1. 在线学籍系统代码:从一张学生表到能跑通的 Java 后端
每年开学季,教务老师最头疼的不是排课,而是学籍数据对不上:新生名单在 Excel 里、转专业记录在另一个系统、休复学审批还躺在纸质档案袋里。所谓在线学籍系统代码,本质就是用 Java 把「学生身份」这件事从纸质台账搬到一个能查、能改、能追溯的数据库里。它要解决的核心问题只有三个:学籍状态怎么存、状态变更怎么留痕、多角色怎么按权限读写。适合谁看?正在做课程设计的学生、被临时抽调做教务模块的 Java 工程师、以及想拿一个真实业务练 Spring Boot + MyBatis 的开发者。这篇不聊虚的,从建表到接口到排错,一步步把能跑的代码摆出来。
2. 学籍领域建模:先想清楚状态机再写实体类
2.1 学籍状态为什么不能只用一个 status 字段
新手最容易翻车的地方,是把学籍状态设计成一个status字段,值从 0 到 5 随便定。等业务跑起来就会发现:转专业和休学是两回事,休学又分「休学中」和「休学到期未复学」,一个字段根本表达不了。学籍的本质是一个状态机,常见状态包括:在籍、休学、复学、转专业、退学、毕业。每个状态之间的流转是有方向的,比如「退学」是终态,不能再转回「在籍」。
我一般会拆成两张表:student存基础身份信息,student_status_log存每一次状态变更。当前状态不冗余存,而是取日志表里最新一条。这样做的代价是查询要多一次 join,但换来的是完整的变更追溯——教务查「这个学生什么时候休的学、谁审批的」,一条 SQL 就出来了。
-- 学生基础表:只存不随状态变化的信息 CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号,业务主键', name VARCHAR(50) NOT NULL, id_card VARCHAR(18) NOT NULL COMMENT '身份证号,用于唯一性校验', major_code VARCHAR(20) NOT NULL COMMENT '专业代码', grade_year INT NOT NULL COMMENT '入学年份', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_id_card (id_card) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 状态变更日志:每次变更插一条,永不更新 CREATE TABLE student_status_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL, from_status VARCHAR(20) COMMENT '变更前状态,首次为空', to_status VARCHAR(20) NOT NULL COMMENT '变更后状态', reason VARCHAR(200) COMMENT '变更原因', operator VARCHAR(50) NOT NULL COMMENT '操作人工号', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_student_no (student_no, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:student表用student_no做业务唯一键,id_card单独加唯一索引防止重复录入。student_status_log只插入不更新,from_status允许为空是因为首次建档没有前状态。参数上,student_no长度给 20 够用,reason给 200 字符能放下「因个人身体原因申请休学一学期」这类描述。注意utf8mb4不是utf8,否则学生姓名里的生僻字会变成问号,这是血泪经验。
2.2 用 Java 枚举把状态流转规则钉死
状态机如果只靠数据库字段约束,代码里到处写 if-else 判断,早晚会乱。正确做法是用 Java 枚举定义合法流转,非法流转直接抛异常。
public enum StudentStatus { ENROLLED("在籍"), SUSPENDED("休学"), RESUMED("复学"), TRANSFERRED("转专业"), WITHDRAWN("退学"), GRADUATED("毕业"); private final String label; StudentStatus(String label) { this.label = label; } public String getLabel() { return label; } // 定义合法流转:key 是当前状态,value 是允许转到的状态集合 private static final Map<StudentStatus, Set<StudentStatus>> TRANSITIONS = Map.of( ENROLLED, Set.of(SUSPENDED, TRANSFERRED, WITHDRAWN, GRADUATED), SUSPENDED, Set.of(RESUMED, WITHDRAWN), RESUMED, Set.of(SUSPENDED, TRANSFERRED, WITHDRAWN, GRADUATED), TRANSFERRED,Set.of(SUSPENDED, WITHDRAWN, GRADUATED), WITHDRAWN, Set.of(), // 终态 GRADUATED, Set.of() // 终态 ); public boolean canTransferTo(StudentStatus target) { return TRANSITIONS.getOrDefault(this, Set.of()).contains(target); } }逻辑说明:TRANSITIONS用Map.of静态初始化,把「哪些状态能转到哪些状态」集中在一处。canTransferTo在 service 层调用,不合法就抛业务异常。参数上注意WITHDRAWN和GRADUATED的 value 是空集合,表示终态不可再流转。这样写的好处是,以后加新状态只改枚举,不用满项目搜 if-else。常见误用是把流转规则写在 Controller 里,结果前端传什么状态就存什么,数据迟早脏掉。
3. 用 Spring Boot + MyBatis 把学籍接口跑起来
3.1 最小可运行工程的三层结构
一个能跑的在线学籍系统代码,不需要一上来就微服务。Spring Boot 单体 + MyBatis 足够撑起一个学院的学籍管理。目录结构按 controller / service / mapper / entity 四层分,别把 SQL 写在 service 里。
// StudentController.java:只做参数校验和路由,不写业务 @RestController @RequestMapping("/api/student") public class StudentController { @Autowired private StudentService studentService; // 查询学生当前学籍状态 @GetMapping("/{studentNo}/status") public Result<StudentStatusVO> getStatus(@PathVariable String studentNo) { return Result.ok(studentService.getCurrentStatus(studentNo)); } // 变更学籍状态 @PostMapping("/{studentNo}/status") public Result<Void> changeStatus(@PathVariable String studentNo, @RequestBody @Valid StatusChangeDTO dto) { studentService.changeStatus(studentNo, dto); return Result.ok(); } }逻辑说明:Controller 只负责接参和返回,@Valid触发 DTO 上的校验注解。Result是统一返回包装类,ok()和fail()两个静态方法就够。参数上,studentNo走路径变量而不是请求体,因为它是资源标识。注意别在 Controller 里直接调 Mapper,那样事务控制会失控。
3.2 状态变更的 Service 事务与并发处理
状态变更必须在一个事务里完成「查当前状态 → 校验流转 → 插日志」,否则并发下会出现两个请求都读到「在籍」,然后都插入变更日志。
@Service public class StudentService { @Autowired private StudentStatusLogMapper logMapper; @Transactional(rollbackFor = Exception.class) public void changeStatus(String studentNo, StatusChangeDTO dto) { // 1. 查最新状态,加行锁防止并发 StudentStatusLog latest = logMapper.selectLatestForUpdate(studentNo); StudentStatus current = latest == null ? StudentStatus.ENROLLED : StudentStatus.valueOf(latest.getToStatus()); // 2. 校验流转合法性 StudentStatus target = StudentStatus.valueOf(dto.getToStatus()); if (!current.canTransferTo(target)) { throw new BizException("不允许从 " + current.getLabel() + " 变更为 " + target.getLabel()); } // 3. 插入变更日志 StudentStatusLog log = new StudentStatusLog(); log.setStudentNo(studentNo); log.setFromStatus(current.name()); log.setToStatus(target.name()); log.setReason(dto.getReason()); log.setOperator(dto.getOperator()); logMapper.insert(log); } }逻辑说明:selectLatestForUpdate对应的 SQL 要带FOR UPDATE,锁住该学生最新那条日志行,保证同一学生的状态变更串行执行。参数上rollbackFor = Exception.class确保任何异常都回滚,别用默认的只回滚 RuntimeException。常见坑是忘了加锁,压测时两个请求同时改状态,日志表里出现两条 from_status 相同的记录,追溯就断了。
<!-- StudentStatusLogMapper.xml 关键片段 --> <select id="selectLatestForUpdate" resultType="StudentStatusLog"> SELECT * FROM student_status_log WHERE student_no = #{studentNo} ORDER BY created_at DESC, id DESC LIMIT 1 FOR UPDATE </select>逻辑说明:排序用created_at DESC, id DESC双字段,因为同一秒内可能插入多条,靠 id 兜底。FOR UPDATE只在事务内生效,所以必须配合@Transactional。参数上LIMIT 1不能省,否则锁住全表。
3.3 批量导入新生名单的接口写法
教务最常用的功能是批量导入。别用循环单条插入,几千个学生能把数据库打满。
@PostMapping("/batch-import") public Result<Integer> batchImport(@RequestParam("file") MultipartFile file) { List<Student> students = ExcelParser.parse(file); // 解析 Excel // 分批插入,每批 500 条 int total = 0; for (int i = 0; i < students.size(); i += 500) { List<Student> batch = students.subList(i, Math.min(i + 500, students.size())); total += studentMapper.batchInsert(batch); } return Result.ok(total); }逻辑说明:分批大小 500 是经验值,太小事务次数多,太大 SQL 包体超限。batchInsert对应 MyBatis 的<foreach>拼接。参数上注意 Excel 解析要处理空行和重复学号,重复的直接跳过并记录到错误列表返回给前端。常见误用是整表一次性 insert,MySQL 默认max_allowed_packet是 4MB,超过就报错。
4. 学籍系统避坑排查:那些让数据对不上的细节
4.1 现象:学生姓名显示成问号
原因:建表时用了utf8而不是utf8mb4,MySQL 的utf8最多 3 字节,存不下 4 字节的生僻字。解决:建表和连接串都改成utf8mb4,JDBC URL 加characterEncoding=utf8mb4。已经建好的表用ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4迁移。
4.2 现象:状态变更日志出现重复的 from_status
原因:并发请求同时读到同一个当前状态,都没加锁。解决:selectLatestForUpdate加FOR UPDATE,并确保调用方在事务内。如果用的是读写分离,注意FOR UPDATE必须走主库。
4.3 现象:批量导入到一半报 max_allowed_packet
原因:一次性拼接的 SQL 太大。解决:分批 500 条,或者调大 MySQL 的max_allowed_packet。我一般两个都做,分批是代码层面的保险,调参数是运维层面的兜底。
4.4 现象:转专业后查不到新专业课程
原因:学籍状态变了,但选课系统读的还是旧的major_code。解决:状态变更时同步更新student表的major_code,或者让选课系统读状态日志里的专业字段。推荐后者,因为专业变更也是历史的一部分。
4.5 现象:退学学生还能登录系统
原因:只改了学籍状态,没联动账号状态。解决:在changeStatus里加一个钩子,状态变为WITHDRAWN时调用账号服务禁用登录。别把账号逻辑写死在学籍 service 里,用事件或接口解耦。
5. 学籍代码的进阶玩法:用状态日志做数据校验与审计
学籍系统跑起来之后,真正体现价值的地方不是增删改查,而是用状态日志做审计和自检。我一般会加一个定时任务,每天凌晨跑一遍全量校验:遍历每个学生的状态日志,按时间顺序重放,看最终状态和student表里冗余的当前状态是否一致。不一致就告警。这个校验能抓出大部分手工改库、程序 bug 导致的数据漂移。
@Scheduled(cron = "0 0 3 * * ?") public void auditStatusConsistency() { List<String> studentNos = studentMapper.selectAllStudentNos(); for (String no : studentNos) { List<StudentStatusLog> logs = logMapper.selectByStudentNoOrderByTime(no); StudentStatus replayed = StudentStatus.ENROLLED; for (StudentStatusLog log : logs) { StudentStatus target = StudentStatus.valueOf(log.getToStatus()); if (!replayed.canTransferTo(target)) { alertService.send("学号 " + no + " 状态日志非法流转: " + replayed + " -> " + target); break; } replayed = target; } } }逻辑说明:replayed从ENROLLED开始,按日志时间顺序重放,遇到非法流转就告警。参数上 cron 选凌晨 3 点,避开教务使用高峰。这个任务不需要事务,只读不写。注意日志量大时全量重放会慢,可以只校验最近 30 天有变更的学生。
另一个进阶技巧是把状态日志导出成审计报表。教务每年要报学籍异动统计,与其手工翻记录,不如直接按to_status和created_at分组查询。一条 SQL 就能出「本学年休学人数」「转专业人数」的月报。这比任何花哨的前端都实用。
最后说个我自己的习惯:每次改学籍相关的代码,先在测试库把状态日志清空,从建档开始手动走一遍「在籍 → 休学 → 复学 → 转专业 → 毕业」全流程,看日志是不是一条不多一条不少。这个笨办法帮我挡掉过好几次上线事故。学籍数据不像订单,错了可以重下,一个学生的状态错了,可能影响他毕业。希望帮到你。
本文还有配套的精品资源,点击获取