教务管理一直是教育机构日常运营中的核心环节,而课表管理又是教务工作中最复杂、最容易出错的模块之一。传统的手工排课方式存在诸多痛点:教务老师需要手动核对几十甚至上百个班级、教师、教室的时间安排,稍有不慎就会出现时间冲突;当遇到教师请假、教室维修等突发情况时,调整课表更是费时费力;学生和教师查询课表也不够便捷,经常需要跑到教务办公室或公告栏查看。
我在实际参与某高校教务系统升级项目时,亲眼目睹了教务老师用Excel表格手工排课的痛苦场景——他们需要同时打开七八个窗口,反复核对各种约束条件,排完一个年级的课表往往需要耗费一整天时间。这种低效的现状促使我开发了这套基于SpringBoot+Vue的课表管理系统。
系统主要解决以下核心问题:
后端选择SpringBoot的三大理由:
前端选择Vue.js的关键因素:
数据库选择MySQL的实践考量:
采用前后端分离架构,通过清晰的职责划分提升系统可维护性:
code复制┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Vue前端 │ │ SpringBoot │ │ MySQL │
│ (用户交互层) │◄──►│ (业务逻辑层) │◄──►│ (数据持久层) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
前后端交互规范:
特别设计:为解决课表冲突检测的高并发问题,专门设计了乐观锁机制:
java复制@Transactional
public boolean scheduleCourse(CourseSchedule schedule) {
// 检查冲突时添加行锁
int conflictCount = courseMapper.checkConflict(
schedule.getTeacherId(),
schedule.getClassroomId(),
schedule.getTimeSlot());
if(conflictCount > 0) {
throw new RuntimeException("存在时间冲突");
}
return courseMapper.insert(schedule) > 0;
}
课程表(course)的进阶设计:
sql复制CREATE TABLE `course` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL COMMENT '课程名称',
`credit` TINYINT NOT NULL DEFAULT 1 COMMENT '学分',
`max_student` SMALLINT DEFAULT 50 COMMENT '最大选课人数',
`time_json` JSON NOT NULL COMMENT '上课时间安排',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '0-禁用 1-正常',
`version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (`id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
time_json字段的典型值示例:
json复制{
"weeks": [1,2,3,4,5,6,7,8],
"dayOfWeek": 3,
"sections": [1,2],
"classroom": "A301"
}
在开发过程中通过EXPLAIN分析发现三个关键优化点:
sql复制ALTER TABLE `course_schedule`
ADD INDEX `idx_teacher_time` (`teacher_id`, `week`, `day_of_week`, `section`);
sql复制-- 优化前(需要回表)
SELECT * FROM course WHERE status = 1;
-- 优化后(使用覆盖索引)
SELECT id, name FROM course WHERE status = 1;
选课操作的事务处理典型代码:
java复制@Transactional(rollbackFor = Exception.class)
public Result selectCourse(Long studentId, Long courseId) {
// 1. 检查课程可选人数
Course course = courseMapper.selectById(courseId);
if (course.getSelected() >= course.getMaxStudent()) {
throw new RuntimeException("课程已满");
}
// 2. 检查是否已选
Integer count = selectionMapper.selectCount(
new QueryWrapper<Selection>()
.eq("student_id", studentId)
.eq("course_id", courseId));
if (count > 0) {
throw new RuntimeException("不能重复选课");
}
// 3. 插入选课记录
Selection selection = new Selection();
selection.setStudentId(studentId);
selection.setCourseId(courseId);
selectionMapper.insert(selection);
// 4. 更新已选人数
courseMapper.updateSelected(courseId, 1);
return Result.success();
}
课表系统的核心难点在于冲突检测,我们实现了三维冲突检测模型:
关键实现代码:
java复制public ConflictCheckResult checkConflict(CourseSchedule schedule) {
// 检查教师时间冲突
int teacherConflict = scheduleMapper.countTeacherConflict(
schedule.getTeacherId(),
schedule.getWeek(),
schedule.getDayOfWeek(),
schedule.getSection());
// 检查教室冲突
int roomConflict = scheduleMapper.countRoomConflict(
schedule.getClassroomId(),
schedule.getWeek(),
schedule.getDayOfWeek(),
schedule.getSection());
// 构建返回结果
ConflictCheckResult result = new ConflictCheckResult();
result.setHasConflict(teacherConflict > 0 || roomConflict > 0);
result.setTeacherConflict(teacherConflict > 0);
result.setRoomConflict(roomConflict > 0);
return result;
}
前端使用FullCalendar组件实现课表可视化:
vue复制<template>
<full-calendar
:options="calendarOptions"
:events="courseEvents"
@eventClick="handleEventClick"
/>
</template>
<script>
export default {
data() {
return {
calendarOptions: {
initialView: 'timeGridWeek',
slotMinTime: '08:00:00',
slotMaxTime: '22:00:00',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'timeGridWeek,timeGridDay'
}
},
courseEvents: []
}
},
methods: {
loadCourses() {
this.$api.get('/api/schedule').then(res => {
this.courseEvents = res.data.map(item => ({
id: item.id,
title: `${item.courseName}@${item.classroom}`,
start: this.buildDateTime(item.week, item.dayOfWeek, item.startSection),
end: this.buildDateTime(item.week, item.dayOfWeek, item.endSection),
extendedProps: {
teacher: item.teacherName
}
}));
});
},
buildDateTime(week, dayOfWeek, section) {
// 将周次、星期、节次转换为具体日期时间
}
}
}
</script>
基于RBAC模型的权限控制:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/teacher/**").hasAnyRole("TEACHER", "ADMIN")
.antMatchers("/student/**").hasAnyRole("STUDENT", "TEACHER", "ADMIN")
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
推荐使用Docker Compose进行一键部署:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: course_system
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
backend:
build: ./backend
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/course_system
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
ports:
- "8080:8080"
depends_on:
- mysql
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- backend
volumes:
mysql_data:
java复制@Cacheable(value = "courses", key = "#id")
public Course getCourseById(Long id) {
return courseMapper.selectById(id);
}
@CacheEvict(value = "courses", key = "#course.id")
public void updateCourse(Course course) {
courseMapper.updateById(course);
}
properties复制spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.connection-timeout=2000
问题1:选课人数统计不准确
问题2:课表冲突检测失效
问题3:JWT令牌过期处理
javascript复制// 前端响应拦截器
service.interceptors.response.use(
response => response,
error => {
if (error.response.status === 401) {
// 跳转到登录页
router.push('/login?redirect=' + encodeURIComponent(route.fullPath));
}
return Promise.reject(error);
}
);
推荐配置ELK日志系统:
properties复制logging.file.name=logs/application.log
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n
关键监控指标:
在实际使用过程中,我总结了几个有价值的扩展方向:
一个简单的遗传算法排课示例:
python复制def generate_timetable(population_size=100, generations=500):
population = [random_schedule() for _ in range(population_size)]
for _ in range(generations):
# 评估适应度
graded = [(fitness(schedule), schedule) for schedule in population]
graded = [x[1] for x in sorted(graded, key=lambda x: x[0], reverse=True)]
# 选择前10%作为精英
elite = graded[:int(population_size*0.1)]
# 交叉和变异
children = []
while len(children) < population_size - len(elite):
parent1, parent2 = random.choices(graded[:50], k=2)
child = crossover(parent1, parent2)
child = mutate(child)
children.append(child)
population = elite + children
return max(population, key=fitness)
在开发这个项目的过程中,最深刻的体会是:一个看似简单的课表系统,背后需要考虑的边界条件和异常情况远超预期。比如处理跨周课程、节假日调课、教师临时调课等场景时,都需要在系统设计阶段就预留足够的灵活性。建议后续开发者在数据库设计时,尽量采用JSON字段存储可变属性,同时在前端做好相应的编辑组件。