1. 项目概述
这个基于SpringBoot+Vue的自动排课系统,是我去年为本地一所培训机构开发的实战项目。排课问题在教育机构日常运营中一直是个令人头疼的难题——需要考虑教室资源、教师时间、课程类型等多维度的约束条件。传统手工排课往往需要教务人员花费数小时反复调整,还经常出现冲突漏排的情况。
我们开发的这套系统,通过算法自动处理80%的基础排课工作,将原本需要半天的手工排课压缩到10分钟内完成。系统上线后,该机构的教务效率提升了300%,家长投诉率下降了45%。下面我就从技术架构和核心实现两个维度,详细拆解这个项目的开发要点。
2. 技术架构设计
2.1 前后端分离架构
系统采用现在主流的SpringBoot+Vue前后端分离架构:
- 后端:SpringBoot 2.7 + MyBatis-Plus + Redis
- 前端:Vue 3 + Element Plus + ECharts
- 通信:RESTful API + WebSocket(用于实时通知)
选择这种架构主要基于三点考虑:
- 培训机构可能存在多校区协同办公需求,分离架构更利于后期扩展
- Vue的响应式特性非常适合排课这种需要频繁交互的场景
- SpringBoot的自动配置特性可以快速搭建稳定的后端服务
2.2 数据库设计
核心表结构设计如下(简化版):
sql复制CREATE TABLE `course` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '课程名称',
`duration` int NOT NULL COMMENT '课时长度(分钟)',
`type` tinyint NOT NULL COMMENT '课程类型(1:一对一 2:小班课 3:大班课)',
PRIMARY KEY (`id`)
);
CREATE TABLE `teacher` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL,
`available_time` json DEFAULT NULL COMMENT '可用时间段JSON',
PRIMARY KEY (`id`)
);
CREATE TABLE `classroom` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL,
`capacity` int NOT NULL COMMENT '容纳人数',
PRIMARY KEY (`id`)
);
CREATE TABLE `schedule` (
`id` bigint NOT NULL AUTO_INCREMENT,
`course_id` bigint NOT NULL,
`teacher_id` bigint NOT NULL,
`classroom_id` bigint NOT NULL,
`start_time` datetime NOT NULL,
`end_time` datetime NOT NULL,
`status` tinyint DEFAULT '0' COMMENT '0:未开始 1:进行中 2:已结束',
PRIMARY KEY (`id`)
);
特别注意:available_time字段使用JSON类型存储教师的可用时间段,这是为了适应不同教师可能有复杂的时间安排需求。例如:
json复制{ "weekdays": ["Mon", "Wed", "Fri"], "timeRanges": [ {"start": "09:00", "end": "12:00"}, {"start": "14:00", "end": "18:00"} ] }
3. 核心算法实现
3.1 排课约束条件
自动排课本质上是一个多约束条件的优化问题,我们的算法需要处理以下约束:
-
硬约束(必须满足):
- 教师时间冲突
- 教室时间冲突
- 教室容量限制
- 课程时间长度匹配
-
软约束(尽量满足):
- 同一教师的课程尽量集中安排
- 热门时段(如周末)教室利用率最大化
- 相邻课程之间保留适当的间隔时间
3.2 基于遗传算法的实现
我们采用改进的遗传算法来解决这个问题,核心流程如下:
java复制// 遗传算法主流程
public List<Schedule> generateTimetable(List<Course> courses,
List<Teacher> teachers,
List<Classroom> classrooms,
LocalDate startDate,
LocalDate endDate) {
// 1. 初始化种群
Population population = initPopulation(50, courses, teachers, classrooms, startDate, endDate);
// 2. 迭代优化
for (int i = 0; i < 100; i++) {
// 评估适应度
population.calculateFitness();
// 选择优秀个体
Population newPopulation = selection(population);
// 交叉变异
crossover(newPopulation);
mutation(newPopulation);
population = newPopulation;
}
// 3. 返回最优解
return population.getBestIndividual().getSchedules();
}
关键参数说明:
- 种群大小:50(经过测试,这个数值在保证多样性的同时不会导致性能问题)
- 迭代次数:100(实际测试发现超过80代后优化效果提升不明显)
- 适应度函数:综合考虑冲突数、软约束满足度等指标
3.3 冲突检测优化
排课过程中最耗时的操作是冲突检测,我们对此做了专门优化:
java复制// 使用时间区间树加速冲突检测
public class IntervalTree {
private Node root;
public boolean hasConflict(Interval interval) {
return hasConflict(root, interval);
}
private boolean hasConflict(Node node, Interval interval) {
if (node == null) return false;
if (node.interval.overlaps(interval)) {
return true;
}
if (node.left != null && node.left.maxEnd >= interval.start) {
return hasConflict(node.left, interval);
}
return hasConflict(node.right, interval);
}
// 插入、删除等方法省略...
}
实测表明,使用区间树后冲突检测效率提升了8倍,这对于需要频繁检测冲突的遗传算法尤为重要。
4. 前端交互实现
4.1 可视化排课界面
使用Vue+Element Plus实现的可拖拽排课界面:
vue复制<template>
<div class="timetable">
<div class="header">
<div v-for="day in weekDays" :key="day" class="day-header">{{day}}</div>
</div>
<div class="body">
<div v-for="day in weekDays" :key="day" class="day-column">
<div
v-for="slot in timeSlots"
:key="slot"
class="time-slot"
@drop="onDrop($event, day, slot)"
@dragover.prevent>
<ScheduleCard
v-if="hasSchedule(day, slot)"
:schedule="getSchedule(day, slot)"
draggable
@dragstart="onDragStart"/>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
weekDays: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
timeSlots: Array.from({length: 12}, (_, i) => i + 8), // 8:00 - 20:00
schedules: [] // 从后端获取的数据
}
},
methods: {
onDrop(event, day, slot) {
const scheduleId = event.dataTransfer.getData('scheduleId');
// 调用API更新排课时间
this.updateScheduleTime(scheduleId, day, slot);
},
// 其他方法省略...
}
}
</script>
4.2 实时冲突提示
利用WebSocket实现实时冲突检测:
javascript复制// 前端WebSocket连接
const socket = new WebSocket(`wss://${location.host}/api/schedule/ws`);
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'CONFLICT') {
this.$notify({
title: '排课冲突',
message: `检测到冲突:${data.message}`,
type: 'warning'
});
}
};
// 后端SpringBoot实现
@GetMapping("/schedule/ws")
public void handleWebSocket(Session session) {
// 当排课变更时广播冲突检测结果
scheduleService.addChangeListener((changeEvent) -> {
List<Conflict> conflicts = detectConflicts(changeEvent);
if (!conflicts.isEmpty()) {
session.getAsyncRemote().sendText(
JSON.toJSONString(Map.of(
"type", "CONFLICT",
"message", conflicts.get(0).getMessage()
))
);
}
});
}
5. 系统部署与优化
5.1 性能优化方案
在实际运行中发现几个性能瓶颈及解决方案:
-
排课生成速度慢:
- 问题:当课程数量超过100时,单次排课需要30秒以上
- 解决:引入并行计算,将种群评估分配到多个线程
java复制// 使用并行流加速适应度计算 population.getIndividuals().parallelStream() .forEach(Individual::calculateFitness); -
数据库查询压力大:
- 问题:排课页面加载时需要查询大量关联数据
- 解决:使用Redis缓存热点数据
java复制@Cacheable(value = "teacher", key = "#id") public Teacher getTeacherById(Long id) { return teacherMapper.selectById(id); }
5.2 安全防护措施
教育系统对数据安全有较高要求,我们实施了以下措施:
- 接口权限控制:
java复制@PreAuthorize("hasRole('ADMIN') || #teacherId == authentication.principal.id")
@GetMapping("/teachers/{teacherId}/schedules")
public List<Schedule> getTeacherSchedules(@PathVariable Long teacherId) {
return scheduleService.getByTeacherId(teacherId);
}
- 敏感数据脱敏:
javascript复制// 在前端对手机号等敏感信息显示时做脱敏处理
filters: {
phoneFilter(value) {
return value.replace(/^(\d{3})\d{4}(\d+)/, '$1****$2');
}
}
6. 常见问题与解决方案
在实际运行中,我们遇到了几个典型问题:
-
排课结果不理想:
- 现象:算法有时会产生明显不合理的排课方案
- 原因:软约束权重设置不当
- 解决:引入机器学习动态调整权重
python复制# 使用简单线性回归调整权重 def adjust_weights(historical_data): X = historical_data['constraint_violations'] y = historical_data['user_adjustments'] model = LinearRegression().fit(X, y) return model.coef_ -
前端卡顿:
- 现象:当排课数据量大时页面响应缓慢
- 原因:Vue响应式数据过多
- 解决:使用虚拟滚动优化
vue复制<RecycleScroller class="scroller" :items="largeScheduleList" :item-size="56" key-field="id"> <template v-slot="{ item }"> <ScheduleItem :schedule="item"/> </template> </RecycleScroller> -
并发修改冲突:
- 现象:多人同时修改排课导致数据不一致
- 解决:引入乐观锁机制
java复制@Update("UPDATE schedule SET ..., version = version + 1 WHERE id = #{id} AND version = #{version}") int updateWithVersion(Schedule schedule);
这个项目让我深刻体会到,一个好的排课系统不仅需要强大的算法支持,还需要考虑实际业务场景中的各种细节问题。比如我们最初没有考虑教师通勤时间,导致有的老师需要在两节课之间匆忙赶路,后来加入了"教师最小间隔时间"的约束才解决这个问题。
