Java实现课表功能从零到一的完整指南与常见问题解析
引言
课表系统是教育管理中的核心组成部分,无论是学校、培训机构还是在线教育平台,都需要一个高效、可靠的课表管理功能。本文将带你从零开始,使用Java语言实现一个完整的课表系统,涵盖从需求分析、数据库设计、核心功能实现到常见问题解析的全过程。
一、需求分析与系统设计
1.1 核心功能需求
一个基础的课表系统应包含以下功能:
- 课程管理:添加、修改、删除、查询课程信息(课程名称、教师、教室、时间等)
- 排课管理:将课程安排到特定的时间段和教室
- 课表展示:按周、按天、按班级/教师展示课表
- 冲突检测:自动检测时间、教室、教师的冲突
- 用户权限:管理员、教师、学生等不同角色的权限管理
1.2 技术选型
- 后端框架:Spring Boot(快速开发、易于集成)
- 数据库:MySQL(关系型数据库,适合结构化数据)
- ORM框架:MyBatis(灵活、性能好)或 JPA(开发效率高)
- 前端:Thymeleaf(简单场景)或 Vue.js(复杂场景)
- 构建工具:Maven/Gradle
1.3 系统架构设计
采用经典的三层架构:
- 表现层:Controller处理HTTP请求
- 业务层:Service处理业务逻辑
- 数据层:DAO/Repository处理数据持久化
二、数据库设计
2.1 实体关系分析
课表系统涉及的核心实体:
- 教师(Teacher)
- 学生(Student)
- 课程(Course)
- 教室(Classroom)
- 时间段(TimeSlot)
- 排课记录(Schedule)
2.2 数据库表结构设计
-- 教师表 CREATE TABLE teacher ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, title VARCHAR(50), -- 职称 phone VARCHAR(20), email VARCHAR(100), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 学生表 CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_number VARCHAR(20) UNIQUE NOT NULL, name VARCHAR(50) NOT NULL, class_name VARCHAR(50), -- 班级 phone VARCHAR(20), email VARCHAR(100), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 课程表 CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, code VARCHAR(50) UNIQUE, -- 课程代码 credit DECIMAL(3,1), -- 学分 teacher_id BIGINT, description TEXT, FOREIGN KEY (teacher_id) REFERENCES teacher(id) ); -- 教室表 CREATE TABLE classroom ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, location VARCHAR(100), -- 位置 capacity INT, -- 容量 type VARCHAR(20), -- 类型:普通教室、实验室、多媒体教室等 equipment TEXT -- 设备信息 ); -- 时间段表(周一至周五,每天1-8节课) CREATE TABLE time_slot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, week_day INT NOT NULL, -- 1-7表示周一到周日 slot_number INT NOT NULL, -- 第几节课 start_time TIME NOT NULL, end_time TIME NOT NULL, UNIQUE KEY uk_week_slot (week_day, slot_number) ); -- 排课记录表(核心表) CREATE TABLE schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_id BIGINT NOT NULL, classroom_id BIGINT NOT NULL, time_slot_id BIGINT NOT NULL, semester VARCHAR(20) NOT NULL, -- 学期:2024-2025-1 week_range VARCHAR(50), -- 周次范围:1-16 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (course_id) REFERENCES course(id), FOREIGN KEY (classroom_id) REFERENCES classroom(id), FOREIGN KEY (time_slot_id) REFERENCES time_slot(id), UNIQUE KEY uk_course_time (course_id, time_slot_id, semester), UNIQUE KEY uk_classroom_time (classroom_id, time_slot_id, semester) ); -- 课程-学生关联表(多对多) CREATE TABLE course_student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_id BIGINT NOT NULL, student_id BIGINT NOT NULL, enrollment_date DATE DEFAULT CURRENT_DATE, FOREIGN KEY (course_id) REFERENCES course(id), FOREIGN KEY (student_id) REFERENCES student(id), UNIQUE KEY uk_course_student (course_id, student_id) ); 2.3 数据库设计要点
- 唯一约束:确保同一时间同一教室只能安排一门课程
- 外键约束:保证数据完整性
- 时间表设计:将时间抽象为独立实体,便于扩展
- 学期管理:通过学期字段区分不同学期的课表
三、项目搭建与基础配置
3.1 创建Spring Boot项目
使用Spring Initializr创建项目,添加以下依赖:
<!-- pom.xml --> <dependencies> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Boot Data JPA --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- Lombok(简化代码) --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 验证框架 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- 分页插件 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.6</version> </dependency> </dependencies> 3.2 配置文件
# application.yml server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/course_schedule?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update # 开发环境使用update,生产环境建议使用validate show-sql: true properties: hibernate: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect thymeleaf: prefix: classpath:/templates/ suffix: .html cache: false # 配置日期格式 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 # MyBatis配置(如果使用MyBatis) mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.courseschedule.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 分页配置 pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: true params: count=countSql 3.3 项目结构
src/main/java/com/example/courseschedule/ ├── config/ # 配置类 ├── controller/ # 控制器 ├── service/ # 业务逻辑 ├── repository/ # 数据访问(JPA) ├── entity/ # 实体类 ├── dto/ # 数据传输对象 ├── exception/ # 自定义异常 ├── util/ # 工具类 └── CourseScheduleApplication.java 四、核心功能实现
4.1 实体类定义(使用JPA)
// Teacher.java @Entity @Table(name = "teacher") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class Teacher { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank(message = "教师姓名不能为空") @Column(nullable = false) private String name; private String title; @Column(unique = true) private String phone; @Column(unique = true) private String email; @CreationTimestamp private LocalDateTime createdAt; @OneToMany(mappedBy = "teacher", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List<Course> courses; } // Course.java @Entity @Table(name = "course") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class Course { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank(message = "课程名称不能为空") @Column(nullable = false) private String name; @Column(unique = true) private String code; private BigDecimal credit; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "teacher_id") private Teacher teacher; @Lob private String description; @OneToMany(mappedBy = "course", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List<Schedule> schedules; @OneToMany(mappedBy = "course", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List<CourseStudent> courseStudents; } // Schedule.java(核心实体) @Entity @Table(name = "schedule") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class Schedule { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "course_id", nullable = false) private Course course; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "classroom_id", nullable = false) private Classroom classroom; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "time_slot_id", nullable = false) private TimeSlot timeSlot; @NotBlank(message = "学期不能为空") @Column(nullable = false) private String semester; private String weekRange; // 如 "1-16" 表示第1到16周 @CreationTimestamp private LocalDateTime createdAt; @UpdateTimestamp private LocalDateTime updatedAt; } // TimeSlot.java @Entity @Table(name = "time_slot") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class TimeSlot { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Min(1) @Max(7) private Integer weekDay; // 1-7表示周一到周日 @Min(1) @Max(8) private Integer slotNumber; // 第几节课 @Temporal(TemporalType.TIME) private LocalTime startTime; @Temporal(TemporalType.TIME) private LocalTime endTime; @OneToMany(mappedBy = "timeSlot", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List<Schedule> schedules; } 4.2 Repository接口
// ScheduleRepository.java public interface ScheduleRepository extends JpaRepository<Schedule, Long> { // 查询某个学期、某周、某天的课表 @Query("SELECT s FROM Schedule s WHERE s.semester = :semester AND " + "s.timeSlot.weekDay = :weekDay AND " + "s.weekRange LIKE :weekRangePattern") List<Schedule> findBySemesterAndWeekDayAndWeekRange( @Param("semester") String semester, @Param("weekDay") Integer weekDay, @Param("weekRange") String weekRangePattern); // 查询某个教师的课表 @Query("SELECT s FROM Schedule s WHERE s.course.teacher.id = :teacherId AND " + "s.semester = :semester") List<Schedule> findByTeacherIdAndSemester( @Param("teacherId") Long teacherId, @Param("semester") String semester); // 查询某个教室的课表 @Query("SELECT s FROM Schedule s WHERE s.classroom.id = :classroomId AND " + "s.semester = :semester AND " + "s.timeSlot.weekDay = :weekDay AND " + "s.timeSlot.slotNumber = :slotNumber") Optional<Schedule> findByClassroomAndTimeSlot( @Param("classroomId") Long classroomId, @Param("semester") String semester, @Param("weekDay") Integer weekDay, @Param("slotNumber") Integer slotNumber); // 检测冲突:查询同一时间、同一教室的其他课程 @Query("SELECT s FROM Schedule s WHERE s.classroom.id = :classroomId AND " + "s.semester = :semester AND " + "s.timeSlot.weekDay = :weekDay AND " + "s.timeSlot.slotNumber = :slotNumber AND " + "s.id != :scheduleId") List<Schedule> findConflicts( @Param("classroomId") Long classroomId, @Param("semester") String semester, @Param("weekDay") Integer weekDay, @Param("slotNumber") Integer slotNumber, @Param("scheduleId") Long scheduleId); // 检测教师冲突 @Query("SELECT s FROM Schedule s WHERE s.course.teacher.id = :teacherId AND " + "s.semester = :semester AND " + "s.timeSlot.weekDay = :weekDay AND " + "s.timeSlot.slotNumber = :slotNumber AND " + "s.id != :scheduleId") List<Schedule> findTeacherConflicts( @Param("teacherId") Long teacherId, @Param("semester") String semester, @Param("weekDay") Integer weekDay, @Param("slotNumber") Integer slotNumber, @Param("scheduleId") Long scheduleId); } 4.3 Service层实现
// ScheduleService.java @Service @Transactional public class ScheduleService { @Autowired private ScheduleRepository scheduleRepository; @Autowired private CourseRepository courseRepository; @Autowired private ClassroomRepository classroomRepository; @Autowired private TimeSlotRepository timeSlotRepository; /** * 创建排课记录(包含冲突检测) */ public Schedule createSchedule(ScheduleDTO scheduleDTO) { // 1. 验证数据 validateSchedule(scheduleDTO); // 2. 检测冲突 checkConflicts(scheduleDTO); // 3. 创建排课记录 Course course = courseRepository.findById(scheduleDTO.getCourseId()) .orElseThrow(() -> new EntityNotFoundException("课程不存在")); Classroom classroom = classroomRepository.findById(scheduleDTO.getClassroomId()) .orElseThrow(() -> new EntityNotFoundException("教室不存在")); TimeSlot timeSlot = timeSlotRepository.findById(scheduleDTO.getTimeSlotId()) .orElseThrow(() -> new EntityNotFoundException("时间段不存在")); Schedule schedule = Schedule.builder() .course(course) .classroom(classroom) .timeSlot(timeSlot) .semester(scheduleDTO.getSemester()) .weekRange(scheduleDTO.getWeekRange()) .build(); return scheduleRepository.save(schedule); } /** * 冲突检测 */ private void checkConflicts(ScheduleDTO scheduleDTO) { // 1. 教室冲突检测 List<Schedule> classroomConflicts = scheduleRepository.findConflicts( scheduleDTO.getClassroomId(), scheduleDTO.getSemester(), scheduleDTO.getWeekDay(), scheduleDTO.getSlotNumber(), scheduleDTO.getId() != null ? scheduleDTO.getId() : -1L ); if (!classroomConflicts.isEmpty()) { throw new ConflictException("该时间段教室已被占用"); } // 2. 教师冲突检测 Course course = courseRepository.findById(scheduleDTO.getCourseId()) .orElseThrow(() -> new EntityNotFoundException("课程不存在")); List<Schedule> teacherConflicts = scheduleRepository.findTeacherConflicts( course.getTeacher().getId(), scheduleDTO.getSemester(), scheduleDTO.getWeekDay(), scheduleDTO.getSlotNumber(), scheduleDTO.getId() != null ? scheduleDTO.getId() : -1L ); if (!teacherConflicts.isEmpty()) { throw new ConflictException("该时间段教师已有其他课程"); } } /** * 获取某学期、某周、某天的课表 */ public List<Schedule> getScheduleByDay(String semester, Integer weekDay, Integer week) { // 构建周次范围查询模式 String weekRangePattern = "%-" + week + "%"; return scheduleRepository.findBySemesterAndWeekDayAndWeekRange( semester, weekDay, weekRangePattern); } /** * 获取教师课表 */ public List<Schedule> getTeacherSchedule(Long teacherId, String semester) { return scheduleRepository.findByTeacherIdAndSemester(teacherId, semester); } /** * 获取教室课表 */ public List<Schedule> getClassroomSchedule(Long classroomId, String semester) { return scheduleRepository.findByClassroomIdAndSemester(classroomId, semester); } /** * 更新排课 */ public Schedule updateSchedule(Long id, ScheduleDTO scheduleDTO) { Schedule existingSchedule = scheduleRepository.findById(id) .orElseThrow(() -> new EntityNotFoundException("排课记录不存在")); // 更新前检测冲突 scheduleDTO.setId(id); checkConflicts(scheduleDTO); // 更新字段 if (scheduleDTO.getCourseId() != null) { Course course = courseRepository.findById(scheduleDTO.getCourseId()) .orElseThrow(() -> new EntityNotFoundException("课程不存在")); existingSchedule.setCourse(course); } if (scheduleDTO.getClassroomId() != null) { Classroom classroom = classroomRepository.findById(scheduleDTO.getClassroomId()) .orElseThrow(() -> new EntityNotFoundException("教室不存在")); existingSchedule.setClassroom(classroom); } if (scheduleDTO.getTimeSlotId() != null) { TimeSlot timeSlot = timeSlotRepository.findById(scheduleDTO.getTimeSlotId()) .orElseThrow(() -> new EntityNotFoundException("时间段不存在")); existingSchedule.setTimeSlot(timeSlot); } if (scheduleDTO.getSemester() != null) { existingSchedule.setSemester(scheduleDTO.getSemester()); } if (scheduleDTO.getWeekRange() != null) { existingSchedule.setWeekRange(scheduleDTO.getWeekRange()); } return scheduleRepository.save(existingSchedule); } /** * 删除排课 */ public void deleteSchedule(Long id) { if (!scheduleRepository.existsById(id)) { throw new EntityNotFoundException("排课记录不存在"); } scheduleRepository.deleteById(id); } /** * 验证排课数据 */ private void validateSchedule(ScheduleDTO scheduleDTO) { if (scheduleDTO.getCourseId() == null) { throw new IllegalArgumentException("课程ID不能为空"); } if (scheduleDTO.getClassroomId() == null) { throw new IllegalArgumentException("教室ID不能为空"); } if (scheduleDTO.getTimeSlotId() == null) { throw new IllegalArgumentException("时间段ID不能为空"); } if (scheduleDTO.getSemester() == null || scheduleDTO.getSemester().trim().isEmpty()) { throw new IllegalArgumentException("学期不能为空"); } // 验证周次范围格式 if (scheduleDTO.getWeekRange() != null && !scheduleDTO.getWeekRange().isEmpty()) { if (!scheduleDTO.getWeekRange().matches("\d+-\d+")) { throw new IllegalArgumentException("周次范围格式应为:起始周-结束周,如:1-16"); } } } } 4.4 Controller层实现
// ScheduleController.java @RestController @RequestMapping("/api/schedules") public class ScheduleController { @Autowired private ScheduleService scheduleService; /** * 创建排课 */ @PostMapping public ResponseEntity<Schedule> createSchedule(@Valid @RequestBody ScheduleDTO scheduleDTO) { Schedule schedule = scheduleService.createSchedule(scheduleDTO); return ResponseEntity.status(HttpStatus.CREATED).body(schedule); } /** * 获取某天课表 */ @GetMapping("/day") public ResponseEntity<List<Schedule>> getScheduleByDay( @RequestParam String semester, @RequestParam Integer weekDay, @RequestParam Integer week) { List<Schedule> schedules = scheduleService.getScheduleByDay(semester, weekDay, week); return ResponseEntity.ok(schedules); } /** * 获取教师课表 */ @GetMapping("/teacher/{teacherId}") public ResponseEntity<List<Schedule>> getTeacherSchedule( @PathVariable Long teacherId, @RequestParam String semester) { List<Schedule> schedules = scheduleService.getTeacherSchedule(teacherId, semester); return ResponseEntity.ok(schedules); } /** * 获取教室课表 */ @GetMapping("/classroom/{classroomId}") public ResponseEntity<List<Schedule>> getClassroomSchedule( @PathVariable Long classroomId, @RequestParam String semester) { List<Schedule> schedules = scheduleService.getClassroomSchedule(classroomId, semester); return ResponseEntity.ok(schedules); } /** * 更新排课 */ @PutMapping("/{id}") public ResponseEntity<Schedule> updateSchedule( @PathVariable Long id, @Valid @RequestBody ScheduleDTO scheduleDTO) { Schedule schedule = scheduleService.updateSchedule(id, scheduleDTO); return ResponseEntity.ok(schedule); } /** * 删除排课 */ @DeleteMapping("/{id}") public ResponseEntity<Void> deleteSchedule(@PathVariable Long id) { scheduleService.deleteSchedule(id); return ResponseEntity.noContent().build(); } } 4.5 DTO类定义
// ScheduleDTO.java @Data @NoArgsConstructor @AllArgsConstructor @Builder public class ScheduleDTO { private Long id; @NotNull(message = "课程ID不能为空") private Long courseId; @NotNull(message = "教室ID不能为空") private Long classroomId; @NotNull(message = "时间段ID不能为空") private Long timeSlotId; @NotBlank(message = "学期不能为空") private String semester; private String weekRange; // 用于冲突检测的辅助字段 private Integer weekDay; private Integer slotNumber; } 五、前端界面实现(Thymeleaf示例)
5.1 课表展示页面
<!-- schedule.html --> <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>课表查询</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> <style> .schedule-table { border-collapse: collapse; width: 100%; } .schedule-table th, .schedule-table td { border: 1px solid #ddd; padding: 8px; text-align: center; } .schedule-table th { background-color: #f2f2f2; } .time-slot { background-color: #e8f4f8; font-weight: bold; } .course-cell { background-color: #fff; min-height: 60px; cursor: pointer; } .course-cell:hover { background-color: #f0f8ff; } </style> </head> <body> <div class="container mt-4"> <h2 class="mb-4">课表查询</h2> <!-- 查询条件 --> <div class="card mb-4"> <div class="card-body"> <form th:action="@{/schedules}" method="get" class="row g-3"> <div class="col-md-3"> <label for="semester" class="form-label">学期</label> <select id="semester" name="semester" class="form-select"> <option value="2024-2025-1">2024-2025-1</option> <option value="2024-2025-2">2024-2025-2</option> </select> </div> <div class="col-md-3"> <label for="week" class="form-label">周次</label> <input type="number" id="week" name="week" class="form-control" min="1" max="20" value="1"> </div> <div class="col-md-3"> <label for="viewType" class="form-label">视图类型</label> <select id="viewType" name="viewType" class="form-select"> <option value="student">按学生</option> <option value="teacher">按教师</option> <option value="classroom">按教室</option> </select> </div> <div class="col-md-3"> <label for="entityId" class="form-label">ID</label> <input type="number" id="entityId" name="entityId" class="form-control"> </div> <div class="col-12"> <button type="submit" class="btn btn-primary">查询</button> </div> </form> </div> </div> <!-- 课表展示 --> <div th:if="${schedules != null}" class="card"> <div class="card-body"> <h5 th:text="'课表 - ' + ${semester} + ' 第' + ${week} + '周'"></h5> <table class="schedule-table"> <thead> <tr> <th>时间</th> <th>周一</th> <th>周二</th> <th>周三</th> <th>周四</th> <th>周五</th> <th>周六</th> <th>周日</th> </tr> </thead> <tbody> <!-- 动态生成课表 --> <tr th:each="slot : ${timeSlots}"> <td class="time-slot" th:text="${slot.startTime} + '-' + ${slot.endTime}"></td> <td th:each="day : ${1..7}" class="course-cell"> <div th:if="${scheduleMap[slot.id][day] != null}" th:with="schedule=${scheduleMap[slot.id][day]}" class="course-info"> <strong th:text="${schedule.course.name}"></strong><br> <small th:text="${schedule.course.teacher.name}"></small><br> <small th:text="${schedule.classroom.name}"></small><br> <small th:text="${schedule.weekRange}"></small> </div> </td> </tr> </tbody> </table> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> </body> </html> 5.2 Controller处理视图
// ScheduleViewController.java @Controller @RequestMapping("/schedules") public class ScheduleViewController { @Autowired private ScheduleService scheduleService; @Autowired private TimeSlotRepository timeSlotRepository; @GetMapping public String showSchedule( @RequestParam(defaultValue = "2024-2025-1") String semester, @RequestParam(defaultValue = "1") Integer week, @RequestParam(defaultValue = "student") String viewType, @RequestParam(required = false) Long entityId, Model model) { List<Schedule> schedules = new ArrayList<>(); // 根据视图类型获取数据 if ("teacher".equals(viewType) && entityId != null) { schedules = scheduleService.getTeacherSchedule(entityId, semester); } else if ("classroom".equals(viewType) && entityId != null) { schedules = scheduleService.getClassroomSchedule(entityId, semester); } else { // 默认按学生视图,需要获取学生选课信息 // 这里简化处理,返回所有课表 schedules = scheduleService.getScheduleByDay(semester, 1, week); } // 获取所有时间段 List<TimeSlot> timeSlots = timeSlotRepository.findAll(); // 构建课表映射:Map<时间段ID, Map<星期几, Schedule>> Map<Long, Map<Integer, Schedule>> scheduleMap = new HashMap<>(); for (Schedule schedule : schedules) { Long timeSlotId = schedule.getTimeSlot().getId(); Integer weekDay = schedule.getTimeSlot().getWeekDay(); scheduleMap.computeIfAbsent(timeSlotId, k -> new HashMap<>()) .put(weekDay, schedule); } model.addAttribute("schedules", schedules); model.addAttribute("timeSlots", timeSlots); model.addAttribute("scheduleMap", scheduleMap); model.addAttribute("semester", semester); model.addAttribute("week", week); return "schedule"; } } 六、常见问题解析
6.1 数据库设计相关问题
问题1:如何设计支持跨周的课程?
- 解决方案:使用
week_range字段存储周次范围,如”1-16”表示第1到16周,”1,3,5,7”表示单周课程 - 查询优化:在查询时需要解析周次范围,可以使用正则表达式或专门的周次解析工具
// 周次范围解析工具类 public class WeekRangeUtil { /** * 判断某周是否在周次范围内 */ public static boolean isWeekInRange(String weekRange, int week) { if (weekRange == null || weekRange.trim().isEmpty()) { return true; // 空表示所有周 } // 处理连续范围:1-16 if (weekRange.matches("\d+-\d+")) { String[] parts = weekRange.split("-"); int start = Integer.parseInt(parts[0]); int end = Integer.parseInt(parts[1]); return week >= start && week <= end; } // 处理离散范围:1,3,5,7 if (weekRange.matches("\d+(,\d+)*")) { String[] weeks = weekRange.split(","); for (String w : weeks) { if (Integer.parseInt(w.trim()) == week) { return true; } } return false; } // 处理混合范围:1-8,10,12-16 if (weekRange.contains(",")) { String[] parts = weekRange.split(","); for (String part : parts) { if (isWeekInRange(part.trim(), week)) { return true; } } return false; } return false; } } 问题2:如何处理教室容量与学生人数的匹配?
- 解决方案:在排课时检查教室容量是否满足课程的学生人数
- 实现:在Service层添加容量检查逻辑
// 在ScheduleService中添加容量检查 public void checkClassroomCapacity(Long classroomId, Long courseId) { Classroom classroom = classroomRepository.findById(classroomId) .orElseThrow(() -> new EntityNotFoundException("教室不存在")); Course course = courseRepository.findById(courseId) .orElseThrow(() -> new EntityNotFoundException("课程不存在")); // 获取课程的学生人数 long studentCount = courseStudentRepository.countByCourseId(courseId); if (classroom.getCapacity() < studentCount) { throw new CapacityException( String.format("教室容量不足,需要%d个座位,教室只有%d个座位", studentCount, classroom.getCapacity())); } } 6.2 冲突检测相关问题
问题1:如何高效检测时间冲突?
- 解决方案:使用数据库索引和查询优化
- 实现:在数据库表上创建合适的索引
-- 为冲突检测创建复合索引 CREATE INDEX idx_schedule_conflict ON schedule (classroom_id, semester, time_slot_id); CREATE INDEX idx_schedule_teacher_conflict ON schedule (course_id, semester, time_slot_id); -- 为周次范围查询创建索引 CREATE INDEX idx_schedule_week_range ON schedule (semester, week_range); 问题2:如何处理教师临时调课?
- 解决方案:设计调课记录表,记录调课历史
- 实现:创建调课记录表
-- 调课记录表 CREATE TABLE schedule_change ( id BIGINT PRIMARY KEY AUTO_INCREMENT, original_schedule_id BIGINT NOT NULL, new_schedule_id BIGINT, change_type ENUM('TIME_CHANGE', 'CLASSROOM_CHANGE', 'TEACHER_CHANGE', 'CANCELLATION') NOT NULL, reason TEXT, operator_id BIGINT, -- 操作人 operator_role ENUM('ADMIN', 'TEACHER', 'SYSTEM'), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (original_schedule_id) REFERENCES schedule(id), FOREIGN KEY (new_schedule_id) REFERENCES schedule(id) ); 6.3 性能优化问题
问题1:课表查询性能差
- 解决方案:
- 使用缓存(Redis)缓存常用课表
- 分页查询
- 数据库查询优化
// 使用Redis缓存课表 @Service public class ScheduleService { @Autowired private RedisTemplate<String, Object> redisTemplate; private static final String SCHEDULE_CACHE_KEY = "schedule:%s:%s:%d"; public List<Schedule> getScheduleByDay(String semester, Integer weekDay, Integer week) { String cacheKey = String.format(SCHEDULE_CACHE_KEY, semester, weekDay, week); // 尝试从缓存获取 List<Schedule> schedules = (List<Schedule>) redisTemplate.opsForValue().get(cacheKey); if (schedules != null) { return schedules; } // 缓存未命中,查询数据库 schedules = scheduleRepository.findBySemesterAndWeekDayAndWeekRange( semester, weekDay, "%-" + week + "%"); // 缓存结果(设置过期时间1小时) redisTemplate.opsForValue().set(cacheKey, schedules, 1, TimeUnit.HOURS); return schedules; } } 问题2:批量排课效率低
- 解决方案:使用批量插入和事务管理
- 实现:使用JPA的批量插入或MyBatis的批量操作
// 使用JPA批量插入 @Service @Transactional public class ScheduleService { @Autowired private EntityManager entityManager; /** * 批量创建排课 */ public void batchCreateSchedules(List<ScheduleDTO> scheduleDTOs) { int batchSize = 50; // 批量大小 for (int i = 0; i < scheduleDTOs.size(); i++) { ScheduleDTO dto = scheduleDTOs.get(i); Schedule schedule = convertToEntity(dto); entityManager.persist(schedule); // 每50条刷新一次 if (i % batchSize == 0 && i > 0) { entityManager.flush(); entityManager.clear(); // 清除缓存,防止内存溢出 } } entityManager.flush(); entityManager.clear(); } } 6.4 并发问题
问题:多个管理员同时排课导致冲突检测失效
- 解决方案:使用数据库锁或乐观锁
- 实现:使用JPA的乐观锁
// 在Schedule实体中添加版本字段 @Entity @Table(name = "schedule") @Data public class Schedule { // ... 其他字段 @Version private Long version; // 乐观锁版本号 } // 在Service层处理乐观锁异常 @Service public class ScheduleService { @Transactional public Schedule updateSchedule(Long id, ScheduleDTO scheduleDTO) { try { Schedule existingSchedule = scheduleRepository.findById(id) .orElseThrow(() -> new EntityNotFoundException("排课记录不存在")); // 更新字段 // ... return scheduleRepository.save(existingSchedule); } catch (OptimisticLockingFailureException e) { throw new ConflictException("该排课记录已被其他用户修改,请刷新后重试"); } } } 6.5 扩展性问题
问题:如何支持多种排课模式(如单双周、隔周)?
- 解决方案:设计灵活的周次规则系统
- 实现:创建周次规则表
-- 周次规则表 CREATE TABLE week_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, -- 规则名称:单周、双周、隔周等 pattern VARCHAR(200) NOT NULL, -- 规则模式:如 "1,3,5,7,9,11,13,15" description TEXT ); -- 在排课表中引用周次规则 ALTER TABLE schedule ADD COLUMN week_rule_id BIGINT; ALTER TABLE schedule ADD FOREIGN KEY (week_rule_id) REFERENCES week_rule(id); 七、测试与部署
7.1 单元测试
// ScheduleServiceTest.java @SpringBootTest @Transactional public class ScheduleServiceTest { @Autowired private ScheduleService scheduleService; @Autowired private ScheduleRepository scheduleRepository; @Test public void testCreateSchedule() { // 准备测试数据 Teacher teacher = Teacher.builder() .name("张老师") .title("教授") .build(); Course course = Course.builder() .name("Java编程") .code("CS101") .teacher(teacher) .build(); Classroom classroom = Classroom.builder() .name("A101") .capacity(50) .build(); TimeSlot timeSlot = TimeSlot.builder() .weekDay(1) // 周一 .slotNumber(1) // 第一节 .startTime(LocalTime.of(8, 0)) .endTime(LocalTime.of(9, 40)) .build(); // 创建排课 ScheduleDTO dto = ScheduleDTO.builder() .courseId(course.getId()) .classroomId(classroom.getId()) .timeSlotId(timeSlot.getId()) .semester("2024-2025-1") .weekRange("1-16") .build(); Schedule schedule = scheduleService.createSchedule(dto); // 验证 assertNotNull(schedule.getId()); assertEquals("2024-2025-1", schedule.getSemester()); } @Test public void testConflictDetection() { // 创建第一个排课 Schedule schedule1 = createTestSchedule(1L, 1L, 1L, "2024-2025-1", "1-16"); // 尝试创建冲突的排课(同一时间、同一教室) ScheduleDTO conflictDTO = ScheduleDTO.builder() .courseId(2L) // 不同课程 .classroomId(1L) // 同一教室 .timeSlotId(1L) // 同一时间段 .semester("2024-2025-1") .weekRange("1-16") .build(); // 应该抛出冲突异常 assertThrows(ConflictException.class, () -> { scheduleService.createSchedule(conflictDTO); }); } } 7.2 集成测试
// ScheduleIntegrationTest.java @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc public class ScheduleIntegrationTest { @Autowired private MockMvc mockMvc; @Test public void testCreateScheduleAPI() throws Exception { String scheduleJson = """ { "courseId": 1, "classroomId": 1, "timeSlotId": 1, "semester": "2024-2025-1", "weekRange": "1-16" } """; mockMvc.perform(post("/api/schedules") .contentType(MediaType.APPLICATION_JSON) .content(scheduleJson)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.id").exists()); } @Test public void testGetScheduleByDayAPI() throws Exception { mockMvc.perform(get("/api/schedules/day") .param("semester", "2024-2025-1") .param("weekDay", "1") .param("week", "1")) .andExpect(status().isOk()) .andExpect(jsonPath("$").isArray()); } } 7.3 部署配置
# application-prod.yml spring: datasource: url: jdbc:mysql://prod-db:3306/course_schedule?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: ${DB_USERNAME} password: ${DB_PASSWORD} jpa: hibernate: ddl-auto: validate # 生产环境使用validate,不自动修改表结构 show-sql: false redis: host: ${REDIS_HOST} port: 6379 password: ${REDIS_PASSWORD} # 生产环境关闭Thymeleaf缓存 thymeleaf: cache: true # 日志配置 logging: level: com.example.courseschedule: INFO org.hibernate.SQL: WARN file: name: /var/log/courseschedule/app.log 八、扩展功能建议
8.1 智能排课算法
// 智能排课服务 @Service public class IntelligentSchedulingService { /** * 自动排课算法(遗传算法示例) */ public List<Schedule> autoSchedule(String semester, List<Course> courses, List<Classroom> classrooms, List<TimeSlot> timeSlots) { // 1. 初始化种群 List<ScheduleSolution> population = initializePopulation(courses, classrooms, timeSlots); // 2. 迭代优化 for (int generation = 0; generation < MAX_GENERATIONS; generation++) { // 评估适应度 evaluateFitness(population); // 选择 List<ScheduleSolution> selected = selection(population); // 交叉 List<ScheduleSolution> offspring = crossover(selected); // 变异 mutate(offspring); // 更新种群 population = offspring; } // 返回最优解 return population.get(0).getSchedules(); } private double evaluateFitness(ScheduleSolution solution) { double fitness = 0; // 1. 冲突惩罚 int conflicts = countConflicts(solution.getSchedules()); fitness -= conflicts * 100; // 2. 教室利用率 double utilization = calculateClassroomUtilization(solution.getSchedules()); fitness += utilization * 10; // 3. 教师偏好 fitness += calculateTeacherPreference(solution.getSchedules()); // 4. 学生课程分布 fitness += calculateStudentDistribution(solution.getSchedules()); return fitness; } } 8.2 移动端支持
// 移动端API @RestController @RequestMapping("/api/mobile/schedules") public class MobileScheduleController { @Autowired private ScheduleService scheduleService; /** * 获取当前周课表(移动端专用) */ @GetMapping("/current-week") public ResponseEntity<Map<String, Object>> getCurrentWeekSchedule( @RequestParam String semester, @RequestParam Long userId, @RequestParam String userType) { // "student" or "teacher" Map<String, Object> result = new HashMap<>(); // 获取当前周 int currentWeek = getCurrentWeek(semester); List<Schedule> schedules; if ("teacher".equals(userType)) { schedules = scheduleService.getTeacherSchedule(userId, semester); } else { // 学生需要获取选课信息 schedules = scheduleService.getStudentSchedule(userId, semester); } // 按周次过滤 List<Schedule> currentWeekSchedules = schedules.stream() .filter(s -> WeekRangeUtil.isWeekInRange(s.getWeekRange(), currentWeek)) .collect(Collectors.toList()); // 按天分组 Map<Integer, List<Schedule>> scheduleByDay = currentWeekSchedules.stream() .collect(Collectors.groupingBy(s -> s.getTimeSlot().getWeekDay())); result.put("currentWeek", currentWeek); result.put("schedules", scheduleByDay); result.put("weekDays", Arrays.asList("周一", "周二", "周三", "周四", "周五", "周六", "周日")); return ResponseEntity.ok(result); } } 8.3 数据分析与报表
// 课表分析服务 @Service public class ScheduleAnalysisService { @Autowired private ScheduleRepository scheduleRepository; /** * 教室使用率分析 */ public Map<String, Object> analyzeClassroomUtilization(String semester, Integer week) { List<Schedule> schedules = scheduleRepository.findBySemesterAndWeek(semester, week); Map<Long, Integer> classroomUsage = new HashMap<>(); Map<Long, String> classroomNames = new HashMap<>(); for (Schedule schedule : schedules) { Long classroomId = schedule.getClassroom().getId(); classroomUsage.put(classroomId, classroomUsage.getOrDefault(classroomId, 0) + 1); classroomNames.put(classroomId, schedule.getClassroom().getName()); } // 计算使用率 Map<String, Double> utilizationRate = new HashMap<>(); for (Map.Entry<Long, Integer> entry : classroomUsage.entrySet()) { double rate = (double) entry.getValue() / 8 * 100; // 假设每天8节课 utilizationRate.put(classroomNames.get(entry.getKey()), rate); } Map<String, Object> result = new HashMap<>(); result.put("utilizationRate", utilizationRate); result.put("totalSchedules", schedules.size()); result.put("averageUtilization", utilizationRate.values().stream() .mapToDouble(Double::doubleValue).average().orElse(0)); return result; } } 九、总结
本文详细介绍了使用Java实现课表系统的完整过程,从需求分析、数据库设计到核心功能实现,再到常见问题解析和扩展建议。通过这个指南,你可以:
- 理解课表系统的核心架构:三层架构、实体关系设计
- 掌握关键技术:Spring Boot、JPA、MySQL、Thymeleaf
- 解决常见问题:冲突检测、性能优化、并发处理
- 扩展系统功能:智能排课、移动端支持、数据分析
关键要点回顾
- 数据库设计是基础:合理的表结构设计能大大简化后续开发
- 冲突检测是核心:时间、教室、教师的冲突检测必须准确高效
- 性能优化是关键:缓存、索引、批量操作能显著提升系统性能
- 测试覆盖是保障:单元测试和集成测试确保系统稳定性
下一步建议
- 逐步实现:从基础功能开始,逐步添加高级特性
- 代码审查:定期进行代码审查,确保代码质量
- 用户反馈:收集用户反馈,持续改进系统
- 安全加固:添加认证授权、输入验证等安全措施
通过本指南,你应该能够独立开发一个功能完整的课表系统。如果遇到具体问题,可以参考文中的解决方案,或根据实际需求进行调整和扩展。
支付宝扫一扫
微信扫一扫