From 0a09097095dae59f73fea5c051a0b223ecc229fc Mon Sep 17 00:00:00 2001 From: "Claude Opus 4.6" Date: Tue, 17 Mar 2026 19:34:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=8E=92=E8=AF=BE?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E6=94=AF=E6=8C=81=E4=BB=A3=E7=A0=81=20-=20?= =?UTF-8?q?=E8=AF=BE=E7=A8=8B=E7=B1=BB=E5=9E=8B=E6=9E=9A=E4=B8=BE=E3=80=81?= =?UTF-8?q?=E5=86=B2=E7=AA=81=E6=A3=80=E6=B5=8B=E3=80=81=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E6=8F=90=E9=86=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: - LessonTypeEnum: 7种课程类型枚举(导入课、集体课、五大领域课) - ScheduleConflictService: 排课冲突检测服务 - ScheduleReminderTask: 排课提醒定时任务 - ScheduleConfig: 排课相关配置 - 新增DTO: ScheduleCreateByClassesRequest, CalendarViewResponse等 Co-Authored-By: Claude Opus 4.6 --- .../common/config/ScheduleConfig.java | 14 ++ .../ScheduleCreateByClassesRequest.java | 55 ++++++ .../dto/response/CalendarViewResponse.java | 62 +++++++ .../dto/response/ConflictCheckResult.java | 76 ++++++++ .../platform/dto/response/LessonTypeInfo.java | 27 +++ .../platform/enums/LessonTypeEnum.java | 92 ++++++++++ .../service/ScheduleConflictService.java | 71 ++++++++ .../impl/ScheduleConflictServiceImpl.java | 164 +++++++++++++++++ .../platform/task/ScheduleReminderTask.java | 171 ++++++++++++++++++ 9 files changed, 732 insertions(+) create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/config/ScheduleConfig.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/ScheduleCreateByClassesRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/response/CalendarViewResponse.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/response/ConflictCheckResult.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/response/LessonTypeInfo.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/enums/LessonTypeEnum.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/ScheduleConflictService.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/impl/ScheduleConflictServiceImpl.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/task/ScheduleReminderTask.java diff --git a/reading-platform-java/src/main/java/com/reading/platform/common/config/ScheduleConfig.java b/reading-platform-java/src/main/java/com/reading/platform/common/config/ScheduleConfig.java new file mode 100644 index 0000000..a9d0ca8 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/common/config/ScheduleConfig.java @@ -0,0 +1,14 @@ +package com.reading.platform.common.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * 定时任务配置 + * 启用 Spring Scheduling 支持 + */ +@Configuration +@EnableScheduling +public class ScheduleConfig { + +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/ScheduleCreateByClassesRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/ScheduleCreateByClassesRequest.java new file mode 100644 index 0000000..369075d --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/ScheduleCreateByClassesRequest.java @@ -0,0 +1,55 @@ +package com.reading.platform.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDate; +import java.util.List; + +/** + * 批量创建排课请求DTO(按班级) + */ +@Schema(description = "批量创建排课请求") +@Data +public class ScheduleCreateByClassesRequest { + + @Schema(description = "课程包ID") + @NotNull(message = "课程包ID不能为空") + private Long coursePackageId; + + @Schema(description = "课程ID") + @NotNull(message = "课程ID不能为空") + private Long courseId; + + @Schema(description = "课程类型") + @NotBlank(message = "课程类型不能为空") + private String lessonType; + + @Schema(description = "班级ID列表") + @NotEmpty(message = "班级ID列表不能为空") + private List classIds; + + @Schema(description = "教师ID") + @NotNull(message = "教师ID不能为空") + private Long teacherId; + + @Schema(description = "排课日期") + @NotNull(message = "排课日期不能为空") + private LocalDate scheduledDate; + + @Schema(description = "时间段 (如:09:00-10:00)") + @NotBlank(message = "时间段不能为空") + private String scheduledTime; + + @Schema(description = "重复方式 (NONE/WEEKLY/BIWEEKLY)") + private String repeatType = "NONE"; + + @Schema(description = "重复截止日期") + private LocalDate repeatEndDate; + + @Schema(description = "备注") + private String note; +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/response/CalendarViewResponse.java b/reading-platform-java/src/main/java/com/reading/platform/dto/response/CalendarViewResponse.java new file mode 100644 index 0000000..85eb5d3 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/response/CalendarViewResponse.java @@ -0,0 +1,62 @@ +package com.reading.platform.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +/** + * 日历视图响应DTO + */ +@Schema(description = "日历视图响应") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CalendarViewResponse { + + @Schema(description = "开始日期") + private LocalDate startDate; + + @Schema(description = "结束日期") + private LocalDate endDate; + + @Schema(description = "排课数据 (key: 日期字符串 YYYY-MM-DD, value: 排课列表)") + private Map> schedules; + + /** + * 日期排课项 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class DayScheduleItem { + + @Schema(description = "排课ID") + private Long id; + + @Schema(description = "班级名称") + private String className; + + @Schema(description = "课程包名称") + private String coursePackageName; + + @Schema(description = "课程类型名称") + private String lessonTypeName; + + @Schema(description = "教师名称") + private String teacherName; + + @Schema(description = "时间段") + private String scheduledTime; + + @Schema(description = "状态") + private String status; + } +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/response/ConflictCheckResult.java b/reading-platform-java/src/main/java/com/reading/platform/dto/response/ConflictCheckResult.java new file mode 100644 index 0000000..49fc21e --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/response/ConflictCheckResult.java @@ -0,0 +1,76 @@ +package com.reading.platform.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 冲突检测结果 + */ +@Data +@Schema(description = "冲突检测结果") +public class ConflictCheckResult { + + @Schema(description = "是否存在冲突") + private Boolean hasConflict; + + @Schema(description = "冲突类型列表") + private List conflicts = new ArrayList<>(); + + @Schema(description = "冲突信息描述") + private String message; + + /** + * 冲突详情 + */ + @Data + @Schema(description = "冲突详情") + public static class ConflictInfo { + + @Schema(description = "冲突类型 (TEACHER/CLASS)") + private String type; + + @Schema(description = "冲突的资源 ID") + private Long resourceId; + + @Schema(description = "冲突的资源名称") + private String resourceName; + + @Schema(description = "冲突的排课 ID") + private Long schedulePlanId; + + @Schema(description = "冲突的排课名称") + private String scheduleName; + + @Schema(description = "冲突的日期") + private String scheduledDate; + + @Schema(description = "冲突的时间段") + private String scheduledTime; + + } + + /** + * 创建无冲突结果 + */ + public static ConflictCheckResult noConflict() { + ConflictCheckResult result = new ConflictCheckResult(); + result.setHasConflict(false); + result.setMessage("无时间冲突"); + return result; + } + + /** + * 创建有冲突结果 + */ + public static ConflictCheckResult withConflicts(List conflicts) { + ConflictCheckResult result = new ConflictCheckResult(); + result.setHasConflict(true); + result.setConflicts(conflicts); + result.setMessage("检测到时间冲突"); + return result; + } + +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/response/LessonTypeInfo.java b/reading-platform-java/src/main/java/com/reading/platform/dto/response/LessonTypeInfo.java new file mode 100644 index 0000000..e5ee0f6 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/response/LessonTypeInfo.java @@ -0,0 +1,27 @@ +package com.reading.platform.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 课程类型信息DTO + */ +@Schema(description = "课程类型信息") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LessonTypeInfo { + + @Schema(description = "课程类型代码") + private String lessonType; + + @Schema(description = "课程类型名称") + private String lessonTypeName; + + @Schema(description = "该类型下的课程数量") + private Long count; +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/enums/LessonTypeEnum.java b/reading-platform-java/src/main/java/com/reading/platform/enums/LessonTypeEnum.java new file mode 100644 index 0000000..32a5dbd --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/enums/LessonTypeEnum.java @@ -0,0 +1,92 @@ +package com.reading.platform.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 课程类型枚举 + */ +public enum LessonTypeEnum { + + /** + * 导入课 + */ + INTRODUCTION("导入课", "INTRODUCTION"), + + /** + * 集体课 + */ + COLLECTIVE("集体课", "COLLECTIVE"), + + /** + * 语言课(五大领域) + */ + LANGUAGE("语言课", "LANGUAGE"), + + /** + * 社会课(五大领域) + */ + SOCIETY("社会课", "SOCIETY"), + + /** + * 科学课(五大领域) + */ + SCIENCE("科学课", "SCIENCE"), + + /** + * 艺术课(五大领域) + */ + ART("艺术课", "ART"), + + /** + * 健康课(五大领域) + */ + HEALTH("健康课", "HEALTH"); + + /** + * 描述 + */ + private final String description; + + /** + * 代码 + */ + @EnumValue + @JsonValue + private final String code; + + LessonTypeEnum(String description, String code) { + this.description = description; + this.code = code; + } + + public String getDescription() { + return description; + } + + public String getCode() { + return code; + } + + /** + * 根据代码获取枚举 + */ + public static LessonTypeEnum fromCode(String code) { + if (code == null) { + return null; + } + for (LessonTypeEnum type : values()) { + if (type.code.equals(code)) { + return type; + } + } + return null; + } + + /** + * 验证是否有效的课程类型代码 + */ + public static boolean isValidCode(String code) { + return fromCode(code) != null; + } +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/ScheduleConflictService.java b/reading-platform-java/src/main/java/com/reading/platform/service/ScheduleConflictService.java new file mode 100644 index 0000000..d250158 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/service/ScheduleConflictService.java @@ -0,0 +1,71 @@ +package com.reading.platform.service; + +import com.reading.platform.dto.response.ConflictCheckResult; +import com.reading.platform.entity.SchedulePlan; + +import java.time.LocalDate; +import java.util.List; + +/** + * 排课冲突检测服务接口 + */ +public interface ScheduleConflictService { + + /** + * 检测排课冲突 + * + * @param tenantId 租户 ID + * @param classId 班级 ID + * @param teacherId 教师 ID + * @param scheduledDate 排课日期 + * @param scheduledTime 时间段 (如:09:00-10:00) + * @param excludeId 排除的排课 ID (用于更新时排除自身) + * @return 冲突检测结果 + */ + ConflictCheckResult checkConflict(Long tenantId, Long classId, Long teacherId, + LocalDate scheduledDate, String scheduledTime, Long excludeId); + + /** + * 检测排课冲突 (无排除 ID) + */ + default ConflictCheckResult checkConflict(Long tenantId, Long classId, Long teacherId, + LocalDate scheduledDate, String scheduledTime) { + return checkConflict(tenantId, classId, teacherId, scheduledDate, scheduledTime, null); + } + + /** + * 检测教师时间冲突 + * + * @param tenantId 租户 ID + * @param teacherId 教师 ID + * @param scheduledDate 排课日期 + * @param scheduledTime 时间段 + * @param excludeId 排除的排课 ID + * @return 冲突的排课列表 + */ + List checkTeacherConflict(Long tenantId, Long teacherId, + LocalDate scheduledDate, String scheduledTime, Long excludeId); + + /** + * 检测班级时间冲突 + * + * @param tenantId 租户 ID + * @param classId 班级 ID + * @param scheduledDate 排课日期 + * @param scheduledTime 时间段 + * @param excludeId 排除的排课 ID + * @return 冲突的排课列表 + */ + List checkClassConflict(Long tenantId, Long classId, + LocalDate scheduledDate, String scheduledTime, Long excludeId); + + /** + * 判断两个时间段是否重叠 + * + * @param time1 时间段1 (如:09:00-10:00) + * @param time2 时间段2 (如:09:30-10:30) + * @return 是否重叠 + */ + boolean isTimeOverlap(String time1, String time2); + +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/ScheduleConflictServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/ScheduleConflictServiceImpl.java new file mode 100644 index 0000000..bc9d265 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/ScheduleConflictServiceImpl.java @@ -0,0 +1,164 @@ +package com.reading.platform.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.reading.platform.dto.response.ConflictCheckResult; +import com.reading.platform.entity.SchedulePlan; +import com.reading.platform.mapper.SchedulePlanMapper; +import com.reading.platform.service.ScheduleConflictService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 排课冲突检测服务实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ScheduleConflictServiceImpl implements ScheduleConflictService { + + private final SchedulePlanMapper schedulePlanMapper; + + @Override + public ConflictCheckResult checkConflict(Long tenantId, Long classId, Long teacherId, + LocalDate scheduledDate, String scheduledTime, Long excludeId) { + log.debug("检测排课冲突: tenantId={}, classId={}, teacherId={}, date={}, time={}, excludeId={}", + tenantId, classId, teacherId, scheduledDate, scheduledTime, excludeId); + + List conflicts = new ArrayList<>(); + + // 检测教师时间冲突 + List teacherConflicts = checkTeacherConflict(tenantId, teacherId, + scheduledDate, scheduledTime, excludeId); + for (SchedulePlan plan : teacherConflicts) { + ConflictCheckResult.ConflictInfo info = new ConflictCheckResult.ConflictInfo(); + info.setType("TEACHER"); + info.setResourceId(teacherId); + info.setResourceName("教师"); + info.setSchedulePlanId(plan.getId()); + info.setScheduleName(plan.getName()); + info.setScheduledDate(plan.getScheduledDate().toString()); + info.setScheduledTime(plan.getScheduledTime()); + conflicts.add(info); + } + + // 检测班级时间冲突 + List classConflicts = checkClassConflict(tenantId, classId, + scheduledDate, scheduledTime, excludeId); + for (SchedulePlan plan : classConflicts) { + ConflictCheckResult.ConflictInfo info = new ConflictCheckResult.ConflictInfo(); + info.setType("CLASS"); + info.setResourceId(classId); + info.setResourceName("班级"); + info.setSchedulePlanId(plan.getId()); + info.setScheduleName(plan.getName()); + info.setScheduledDate(plan.getScheduledDate().toString()); + info.setScheduledTime(plan.getScheduledTime()); + conflicts.add(info); + } + + if (conflicts.isEmpty()) { + return ConflictCheckResult.noConflict(); + } else { + return ConflictCheckResult.withConflicts(conflicts); + } + } + + @Override + public List checkTeacherConflict(Long tenantId, Long teacherId, + LocalDate scheduledDate, String scheduledTime, Long excludeId) { + if (teacherId == null) { + return new ArrayList<>(); + } + + // 查询该教师当天所有排课 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SchedulePlan::getTenantId, tenantId) + .eq(SchedulePlan::getTeacherId, teacherId) + .eq(SchedulePlan::getScheduledDate, scheduledDate) + .ne(SchedulePlan::getStatus, "cancelled"); + + if (excludeId != null) { + wrapper.ne(SchedulePlan::getId, excludeId); + } + + List plans = schedulePlanMapper.selectList(wrapper); + + // 过滤出时间重叠的排课 + List conflicts = new ArrayList<>(); + for (SchedulePlan plan : plans) { + if (isTimeOverlap(scheduledTime, plan.getScheduledTime())) { + conflicts.add(plan); + } + } + + return conflicts; + } + + @Override + public List checkClassConflict(Long tenantId, Long classId, + LocalDate scheduledDate, String scheduledTime, Long excludeId) { + if (classId == null) { + return new ArrayList<>(); + } + + // 查询该班级当天所有排课 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SchedulePlan::getTenantId, tenantId) + .eq(SchedulePlan::getClassId, classId) + .eq(SchedulePlan::getScheduledDate, scheduledDate) + .ne(SchedulePlan::getStatus, "cancelled"); + + if (excludeId != null) { + wrapper.ne(SchedulePlan::getId, excludeId); + } + + List plans = schedulePlanMapper.selectList(wrapper); + + // 过滤出时间重叠的排课 + List conflicts = new ArrayList<>(); + for (SchedulePlan plan : plans) { + if (isTimeOverlap(scheduledTime, plan.getScheduledTime())) { + conflicts.add(plan); + } + } + + return conflicts; + } + + @Override + public boolean isTimeOverlap(String time1, String time2) { + if (time1 == null || time2 == null) { + return false; + } + + try { + // 解析时间段 (格式: HH:mm-HH:mm) + String[] parts1 = time1.split("-"); + String[] parts2 = time2.split("-"); + + if (parts1.length != 2 || parts2.length != 2) { + log.warn("时间段格式不正确: {} 或 {}", time1, time2); + return false; + } + + LocalTime start1 = LocalTime.parse(parts1[0].trim()); + LocalTime end1 = LocalTime.parse(parts1[1].trim()); + LocalTime start2 = LocalTime.parse(parts2[0].trim()); + LocalTime end2 = LocalTime.parse(parts2[1].trim()); + + // 时间重叠判断: start1 < end2 && start2 < end1 + return start1.isBefore(end2) && start2.isBefore(end1); + + } catch (Exception e) { + log.error("解析时间段失败: {} 或 {}", time1, time2, e); + return false; + } + } + +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/task/ScheduleReminderTask.java b/reading-platform-java/src/main/java/com/reading/platform/task/ScheduleReminderTask.java new file mode 100644 index 0000000..f52f764 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/task/ScheduleReminderTask.java @@ -0,0 +1,171 @@ +package com.reading.platform.task; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.reading.platform.entity.SchedulePlan; +import com.reading.platform.entity.Teacher; +import com.reading.platform.mapper.SchedulePlanMapper; +import com.reading.platform.mapper.TeacherMapper; +import com.reading.platform.service.NotificationService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +/** + * 排课提醒定时任务 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ScheduleReminderTask { + + private final SchedulePlanMapper schedulePlanMapper; + private final TeacherMapper teacherMapper; + private final NotificationService notificationService; + + /** + * 每天早上 7:00 发送当天排课提醒 + * cron: 秒 分 时 日 月 周 + */ + @Scheduled(cron = "0 0 7 * * ?") + public void sendDailyScheduleReminder() { + log.info("开始发送每日排课提醒..."); + + LocalDate today = LocalDate.now(); + + // 查询当天所有未取消且未发送提醒的排课 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SchedulePlan::getScheduledDate, today) + .ne(SchedulePlan::getStatus, "cancelled") + .eq(SchedulePlan::getReminderSent, 0); + + List plans = schedulePlanMapper.selectList(wrapper); + log.info("找到 {} 条待发送提醒的排课", plans.size()); + + int successCount = 0; + int failCount = 0; + + for (SchedulePlan plan : plans) { + try { + sendReminder(plan, "daily"); + // 标记已发送 + plan.setReminderSent(1); + plan.setReminderSentAt(LocalDateTime.now()); + schedulePlanMapper.updateById(plan); + successCount++; + } catch (Exception e) { + log.error("发送排课提醒失败: planId={}, error={}", plan.getId(), e.getMessage(), e); + failCount++; + } + } + + log.info("每日排课提醒发送完成: 成功={}, 失败={}", successCount, failCount); + } + + /** + * 每 30 分钟检查即将开始的排课,发送提醒 + * 在课程开始前 30 分钟发送提醒 + */ + @Scheduled(cron = "0 0/30 * * * ?") + public void sendUpcomingScheduleReminder() { + log.debug("检查即将开始的排课..."); + + LocalDate today = LocalDate.now(); + LocalTime now = LocalTime.now(); + LocalTime after30Minutes = now.plusMinutes(30); + + // 查询今天、未取消、未发送即将提醒、开始时间在接下来 30 分钟内的排课 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SchedulePlan::getScheduledDate, today) + .ne(SchedulePlan::getStatus, "cancelled"); + + List plans = schedulePlanMapper.selectList(wrapper); + + for (SchedulePlan plan : plans) { + try { + LocalTime startTime = parseStartTime(plan.getScheduledTime()); + if (startTime == null) { + continue; + } + + // 判断是否在接下来 30 分钟内开始 + if (startTime.isAfter(now) && !startTime.isAfter(after30Minutes)) { + // 检查是否已发送过即将开始提醒(使用 reminderSent 字段值为 2 表示已发送即将开始提醒) + if (plan.getReminderSent() == null || plan.getReminderSent() < 2) { + sendReminder(plan, "upcoming"); + // 标记已发送即将开始提醒(设置为 2) + plan.setReminderSent(2); + plan.setReminderSentAt(LocalDateTime.now()); + schedulePlanMapper.updateById(plan); + log.info("发送即将开始提醒: planId={}, time={}", plan.getId(), plan.getScheduledTime()); + } + } + } catch (Exception e) { + log.error("处理即将开始排课失败: planId={}, error={}", plan.getId(), e.getMessage()); + } + } + } + + /** + * 发送提醒通知 + */ + private void sendReminder(SchedulePlan plan, String reminderType) { + String title; + String content; + + if ("daily".equals(reminderType)) { + title = "今日课程提醒"; + content = String.format("您有课程「%s」安排在今天 %s,请提前做好准备。", + plan.getName(), plan.getScheduledTime()); + } else { + title = "课程即将开始"; + content = String.format("您的课程「%s」将在 30 分钟内(%s)开始,请做好准备。", + plan.getName(), plan.getScheduledTime()); + } + + // 发送给教师 + if (plan.getTeacherId() != null) { + Teacher teacher = teacherMapper.selectById(plan.getTeacherId()); + if (teacher != null) { + notificationService.createNotification( + plan.getTenantId(), + 0L, + "SYSTEM", + title, + content, + "SCHEDULE", + "TEACHER", + plan.getTeacherId() + ); + } + } + + // 可以扩展:发送给班级相关的人员(如班主任等) + } + + /** + * 解析开始时间 + */ + private LocalTime parseStartTime(String scheduledTime) { + if (scheduledTime == null || scheduledTime.isEmpty()) { + return null; + } + + try { + String[] parts = scheduledTime.split("-"); + if (parts.length >= 1) { + return LocalTime.parse(parts[0].trim()); + } + } catch (Exception e) { + log.warn("解析时间失败: {}", scheduledTime); + } + + return null; + } + +}