1. 项目背景与核心需求
高校教师工作量统计一直是教务管理中的痛点。传统的人工统计方式不仅效率低下,而且容易出错。以某省属高校为例,教务员每月需要手动汇总来自7个学院的教师课时、科研分、行政工作量等数据,仅核对环节就要花费3-5个工作日。更棘手的是,不同院系对"标准课时"的折算系数存在差异(如实验课按1.5倍计算),跨院系授课的教师经常反映统计结果不准确。
这个SpringBoot项目正是为了解决这些痛点而生。系统需要实现三个核心功能:
- 多维度工作量自动汇总(教学、科研、行政)
- 差异化折算规则配置(支持院系自定义系数)
- 可视化数据看板(个人/院系/全校维度)
关键设计决策:采用规则引擎Drools处理复杂的折算逻辑,而非硬编码。这样当教务政策调整时,管理员只需修改规则文件,无需重新部署系统。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构设计
2.1 整体技术栈
- 前端:Vue3 + Element Plus(适合管理后台的组件库)
- 后端:SpringBoot 2.7 + MyBatis-Plus(简化CRUD)
- 数据库:MySQL 8.0(事务型场景)+ Redis(缓存课表数据)
- 规则引擎:Drools 7.73(处理工作量折算规则)
2.2 核心表结构设计
sql复制CREATE TABLE `teacher_workload` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`teacher_id` VARCHAR(20) NOT NULL COMMENT '工号',
`course_type` TINYINT NOT NULL COMMENT '1理论课 2实验课 3实践课',
`class_hours` DECIMAL(5,2) NOT NULL COMMENT '原始课时',
`adjust_coefficient` DECIMAL(3,2) DEFAULT 1.0 COMMENT '折算系数',
`final_hours` DECIMAL(5,2) GENERATED ALWAYS AS (`class_hours`*`adjust_coefficient`) STORED,
`semester` CHAR(6) NOT NULL COMMENT '如202302',
PRIMARY KEY (`id`),
KEY `idx_teacher_semester` (`teacher_id`,`semester`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
建表技巧:使用生成列自动计算最终课时,避免应用层重复计算。针对高频查询建立联合索引。
3. 核心功能实现
3.1 动态规则配置
通过Drools规则文件实现系数动态调整:
drl复制rule "ExperimentalCourseCoefficient"
when
$workload : TeacherWorkload(courseType == 2)
then
$workload.setAdjustCoefficient(1.5);
end
实际项目中,我们将这些规则存储在数据库,通过以下接口动态加载:
java复制@PostMapping("/rule/update")
public Result updateRule(@RequestBody RuleDTO dto) {
KieSession kieSession = kieContainer.newKieSession();
kieSession.insert(dto.getWorkloadData());
kieSession.fireAllRules();
// 更新数据库...
}
3.2 批量导入优化
处理Excel导入时常见的性能问题:
java复制// 使用EasyExcel的监听器模式
public class WorkloadDataListener extends AnalysisEventListener<WorkloadDTO> {
private static final int BATCH_COUNT = 300;
private List<WorkloadDTO> cachedList = new ArrayList<>(BATCH_COUNT);
@Override
public void invoke(WorkloadDTO data, AnalysisContext context) {
cachedList.add(data);
if (cachedList.size() >= BATCH_COUNT) {
saveBatch();
cachedList.clear();
}
}
private void saveBatch() {
// 使用MyBatis-Plus的saveBatch
workloadService.saveBatch(cachedList);
}
}
踩坑记录:初期直接使用POI解析大文件导致OOM,改为流式读取后内存占用从2GB降至200MB左右。
4. 典型问题解决方案
4.1 并发计算冲突
当多个教务员同时提交数据时,可能出现重复计算。我们采用两种策略:
- 数据库层面:对teacher_semester字段加唯一索引
- 应用层面:使用Redis分布式锁
java复制public boolean calculateWorkload(String teacherId, String semester) {
String lockKey = "lock:" + teacherId + ":" + semester;
try {
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
// 执行计算逻辑
}
} finally {
redisTemplate.delete(lockKey);
}
}
4.2 历史数据追溯
为满足审计要求,设计历史版本表:
sql复制CREATE TABLE `workload_history` (
`id` BIGINT NOT NULL,
`origin_id` BIGINT NOT NULL COMMENT '原记录ID',
`snapshot_data` JSON NOT NULL COMMENT '历史数据快照',
`operation_type` TINYINT NOT NULL COMMENT '1新增 2修改 3删除',
`operator` VARCHAR(20) NOT NULL,
`operate_time` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_origin` (`origin_id`)
);
使用MyBatis拦截器自动记录变更:
java复制@Intercepts({
@Signature(type= Executor.class, method="update",
args={MappedStatement.class, Object.class})
})
public class AuditInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) {
// 解析SQL类型和参数
// 生成历史记录
return invocation.proceed();
}
}
5. 性能优化实践
5.1 统计查询加速
对于全院工作量统计这类高频复杂查询,采用预聚合方案:
- 使用Spring Scheduler每天凌晨计算各院系汇总数据
- 结果存入统计表,查询时直接读取
- 配合Redis缓存热门查询
java复制@Scheduled(cron = "0 0 3 * * ?")
public void dailyAggregate() {
List<Department> departments = departmentMapper.selectList(null);
departments.forEach(dept -> {
WorkloadStats stats = workloadMapper.aggregateByDept(dept.getId());
statsMapper.insertOrUpdate(stats);
});
}
5.2 前端渲染优化
当数据量超过5000条时,Element UI表格会出现明显卡顿。解决方案:
- 采用虚拟滚动技术
- 后端分页配合前端缓存
vue复制<template>
<el-table-v2
:columns="columns"
:data="data"
:width="1000"
:height="500"
:estimated-row-height="50"
:buffer-size="20"
/>
</template>
6. 安全防护措施
6.1 数据权限控制
通过自定义注解实现院系数据隔离:
java复制@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataPermission {
String deptField() default "department_id";
}
@Aspect
@Component
public class DataPermissionAspect {
@Before("@annotation(dp)")
public void before(JoinPoint jp, DataPermission dp) {
String userDept = getCurrentUserDept();
String sql = String.format("%s = '%s'", dp.deptField(), userDept);
DataPermissionHelper.setDataPermission(sql);
}
}
6.2 文件上传防护
针对教务员上传Excel文件的安全处理:
java复制@PostMapping("/import")
public Result importWorkload(@RequestParam MultipartFile file) {
// 1. 校验文件类型
if (!"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
.equals(file.getContentType())) {
throw new BusinessException("仅支持xlsx格式");
}
// 2. 病毒扫描
if (virusScanner.scan(file.getBytes())) {
throw new BusinessException("文件安全检测未通过");
}
// 3. 使用临时文件处理
File tempFile = File.createTempFile("import_", ".xlsx");
file.transferTo(tempFile);
try {
return doImport(tempFile);
} finally {
Files.deleteIfExists(tempFile.toPath());
}
}
7. 部署与监控
7.1 多环境配置
使用Spring Profiles区分环境:
yaml复制# application-dev.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/workload_dev
username: dev_user
password: dev123
# application-prod.yml
spring:
datasource:
url: jdbc:mysql://prod-db:3306/workload
username: ${DB_USER}
password: ${DB_PWD}
7.2 Prometheus监控
暴露关键指标:
java复制@RestController
public class MetricsController {
private final Counter importCounter = Counter.build()
.name("workload_import_total")
.help("Total imported records")
.register();
@PostMapping("/import")
public Result importData(@RequestBody List<Workload> data) {
importCounter.inc(data.size());
// 处理导入...
}
}
配置Grafana看板监控:
- 导入成功率
- 规则引擎执行耗时
- 并发计算任务数
8. 项目演进方向
在实际使用中,我们收集到院系教务员的反馈后,逐步增加了以下功能:
- 移动端审批:企业微信/钉钉集成
- 智能预警:当教师工作量连续两周低于标准时触发提醒
- 数据对比:支持跨学期工作量趋势分析
对于计算机专业毕设来说,可以考虑的扩展方向:
- 加入教师能力画像(基于工作量数据)
- 实现自动排课与工作量预测的联动
- 采用微服务架构拆分统计模块与规则引擎
这个项目让我深刻体会到,即使是看似简单的管理系统,在真实业务场景下也会面临各种技术挑战。最值得分享的经验是:在初期设计时就要为规则变化留好扩展点,教育领域的政策调整远比我们想象的频繁。
