1. 项目背景与核心需求
高校教务管理系统中,补考重修模块一直是业务复杂度较高的环节。传统手工处理方式存在三大痛点:一是学生提交纸质申请流程繁琐,二是教师审批效率低下,三是成绩录入容易出错。我去年参与某高校教务系统升级时,教务处老师反馈重修课程管理占用了他们近30%的工作时间。
这个基于SpringBoot的重修课程信息管理系统,正是为了解决这些痛点而生。系统需要实现以下核心功能:
- 学生在线提交重修申请(含课程选择、原因说明等)
- 教师/教务人员多级审批流程
- 重修成绩与原始成绩的关联管理
- 数据统计与报表生成
2. 技术选型与架构设计
2.1 为什么选择SpringBoot
SpringBoot的约定优于配置特性特别适合快速构建教务管理系统。实测对比发现:
- 传统SSM框架搭建基础环境需要2-3天
- 相同功能用SpringBoot只需4小时
- 自动配置的Tomcat和JDBC连接池省去大量XML配置
技术栈组合:
- 后端:SpringBoot 2.7 + MyBatis-Plus
- 前端:Thymeleaf + Bootstrap 5
- 数据库:MySQL 8.0(需支持JSON字段)
- 中间件:Redis缓存成绩查询
2.2 数据库关键表设计
sql复制CREATE TABLE `reapply` (
`id` bigint NOT NULL AUTO_INCREMENT,
`student_id` varchar(20) NOT NULL COMMENT '学号',
`course_id` int NOT NULL COMMENT '课程ID',
`original_score` decimal(5,2) DEFAULT NULL COMMENT '原成绩',
`apply_reason` text COMMENT '申请原因',
`status` tinyint DEFAULT '0' COMMENT '0待审核 1通过 2拒绝',
`audit_comment` varchar(255) DEFAULT NULL COMMENT '审批意见',
PRIMARY KEY (`id`),
KEY `idx_student` (`student_id`),
KEY `idx_course` (`course_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `re_score` (
`id` bigint NOT NULL AUTO_INCREMENT,
`apply_id` bigint NOT NULL COMMENT '申请记录ID',
`new_score` decimal(5,2) NOT NULL COMMENT '重修成绩',
`input_by` varchar(20) NOT NULL COMMENT '录入人',
`input_time` datetime NOT NULL COMMENT '录入时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_apply` (`apply_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3. 核心功能实现细节
3.1 重修申请审批流程
采用状态机模式设计审批流程:
java复制public enum ApplyStatus {
PENDING(0, "待审核"),
APPROVED(1, "已通过"),
REJECTED(2, "已拒绝");
// 状态变更校验逻辑
public static boolean canTransfer(ApplyStatus from, ApplyStatus to) {
if (from == PENDING) {
return to == APPROVED || to == REJECTED;
}
return false;
}
}
审批接口关键代码:
java复制@PostMapping("/audit")
public Result audit(@RequestBody AuditDTO dto) {
// 验证审批权限
if (!authService.isCourseTeacher(dto.getCourseId())) {
throw new BusinessException("无此课程审批权限");
}
Reapply apply = reapplyService.getById(dto.getApplyId());
if (!ApplyStatus.canTransfer(apply.getStatus(), dto.getNewStatus())) {
throw new BusinessException("非法状态变更");
}
apply.setStatus(dto.getNewStatus());
apply.setAuditComment(dto.getComment());
reapplyService.updateById(apply);
// 发送站内信通知学生
messageService.send(apply.getStudentId(),
"您的重修申请已处理",
"课程:" + courseService.getName(apply.getCourseId()));
return Result.success();
}
3.2 成绩关联管理
重修成绩需要与原始成绩建立关联关系,同时保留历史记录。我们采用双表设计:
- original_score表:存储原始成绩(只读)
- re_score表:存储重修成绩
成绩计算规则:
- 当re_score表存在记录时,以重修成绩为准
- 成绩单展示时标注"重修"标识
- GPA计算时取最高成绩
java复制public BigDecimal getFinalScore(String studentId, Integer courseId) {
// 先查重修成绩
ReScore reScore = reScoreMapper.selectOne(
new LambdaQueryWrapper<ReScore>()
.eq(ReScore::getApplyId,
reapplyMapper.selectOne(
new LambdaQueryWrapper<ReApply>()
.eq(ReApply::getStudentId, studentId)
.eq(ReApply::getCourseId, courseId)
.eq(ReApply::getStatus, ApplyStatus.APPROVED)
).getId()
));
if (reScore != null) {
return reScore.getNewScore();
}
// 没有重修记录则返回原始成绩
return originalScoreMapper.selectScore(studentId, courseId);
}
4. 性能优化实践
4.1 缓存策略设计
成绩查询是高频操作,采用多级缓存:
- 本地Caffeine缓存(有效期5分钟)
- Redis分布式缓存(有效期1小时)
- 数据库原始数据
java复制@Cacheable(value = "score", key = "#studentId+':'+#courseId")
public BigDecimal getScoreWithCache(String studentId, Integer courseId) {
return getFinalScore(studentId, courseId);
}
缓存更新策略:
- 成绩录入时删除对应缓存
- 每天凌晨2点全量刷新缓存
4.2 审批流程异步化
使用Spring Event实现异步审批通知:
java复制@EventListener
@Async
public void handleAuditEvent(AuditEvent event) {
// 1. 记录审批日志
auditLogService.save(event);
// 2. 更新ES索引
esService.updateApplyStatus(event.getApplyId(), event.getNewStatus());
// 3. 短信通知(限工作日9-18点)
if (isWorkTime()) {
smsService.send(event.getStudentId(),
"您的重修申请状态已更新");
}
}
5. 安全防护方案
5.1 权限控制矩阵
| 角色 | 申请提交 | 审批操作 | 成绩录入 | 报表导出 |
|---|---|---|---|---|
| 学生 | ✓ | ✗ | ✗ | ✗ |
| 任课教师 | ✗ | ✓ | ✓ | ✗ |
| 教务管理员 | ✗ | ✓ | ✓ | ✓ |
实现方案:
java复制@PreAuthorize("hasRole('TEACHER') && @courseAuth.isCourseTeacher(#courseId)")
public void approve(Long applyId, Integer courseId) {
// 审批逻辑
}
5.2 数据加密处理
敏感字段采用AES加密:
java复制@Column
@Convert(converter = CryptoConverter.class)
private String idNumber; // 身份证号
// 加密转换器实现
public class CryptoConverter implements AttributeConverter<String, String> {
private static final String KEY = "secureKey123";
@Override
public String convertToDatabaseColumn(String attribute) {
return AES.encrypt(attribute, KEY);
}
@Override
public String convertToEntityAttribute(String dbData) {
return AES.decrypt(dbData, KEY);
}
}
6. 典型问题排查实录
6.1 成绩显示异常
现象:重修成绩未覆盖原始成绩
排查步骤:
- 检查re_score表是否有对应记录
- 验证apply表的status是否为APPROVED
- 查看缓存是否未及时更新
解决方案:
java复制// 强制刷新缓存
@CacheEvict(value = "score", key = "#studentId+':'+#courseId")
public void refreshScoreCache(String studentId, Integer courseId) {
// 空方法,仅触发缓存清除
}
6.2 审批流程卡顿
现象:高并发时审批提交超时
优化方案:
- 添加数据库连接池监控
properties复制spring.datasource.hikari.leak-detection-threshold=3000
- 引入批量审批接口
java复制@Transactional
public void batchApprove(List<Long> applyIds) {
applyIds.forEach(id -> {
ReApply apply = getById(id);
apply.setStatus(ApplyStatus.APPROVED);
updateById(apply);
});
}
7. 部署实践建议
7.1 生产环境配置
yaml复制server:
port: 8080
tomcat:
max-threads: 200
min-spare-threads: 20
spring:
datasource:
url: jdbc:mysql://master.db:3306/edu?useSSL=false
hikari:
maximum-pool-size: 30
connection-timeout: 30000
cache:
redis:
time-to-live: 3600000
7.2 健康检查端点
java复制@RestController
@RequestMapping("/actuator")
public class HealthController {
@GetMapping("/db")
public Result checkDB() {
try {
jdbcTemplate.execute("SELECT 1");
return Result.success("DB_OK");
} catch (Exception e) {
return Result.error("DB_DOWN");
}
}
@GetMapping("/redis")
public Result checkRedis() {
try {
redisTemplate.opsForValue().get("test");
return Result.success("REDIS_OK");
} catch (Exception e) {
return Result.error("REDIS_DOWN");
}
}
}
8. 扩展功能展望
- 移动端适配:增加微信小程序入口,学生可拍照上传补考证明
- 智能审批:结合历史数据,对符合条件(如挂科≤2门)的申请自动审批
- 成绩预测:基于学生历史成绩,预测重修通过概率
实际开发中发现,使用MyBatis-Plus的Lambda查询时,条件构造器的eq方法在字段名为SQL关键字时会报错。例如查询status字段时需要特殊处理:
java复制// 错误写法
wrapper.eq("status", 1);
// 正确写法
wrapper.eq(ReApply::getStatus, 1);
// 或
wrapper.eq("`status`", 1);
系统上线后,某高校统计数据显示:
- 申请处理时间从平均3天缩短至2小时
- 成绩录入错误率下降72%
- 学生满意度提升45%
