feat: 添加排课功能支持代码 - 课程类型枚举、冲突检测、定时提醒
新增功能: - LessonTypeEnum: 7种课程类型枚举(导入课、集体课、五大领域课) - ScheduleConflictService: 排课冲突检测服务 - ScheduleReminderTask: 排课提醒定时任务 - ScheduleConfig: 排课相关配置 - 新增DTO: ScheduleCreateByClassesRequest, CalendarViewResponse等 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
eb1b1a3153
commit
0a09097095
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
@ -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<Long> 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;
|
||||||
|
}
|
||||||
@ -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<String, List<DayScheduleItem>> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<ConflictInfo> 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<ConflictInfo> conflicts) {
|
||||||
|
ConflictCheckResult result = new ConflictCheckResult();
|
||||||
|
result.setHasConflict(true);
|
||||||
|
result.setConflicts(conflicts);
|
||||||
|
result.setMessage("检测到时间冲突");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<SchedulePlan> 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<SchedulePlan> 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);
|
||||||
|
|
||||||
|
}
|
||||||
@ -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<ConflictCheckResult.ConflictInfo> conflicts = new ArrayList<>();
|
||||||
|
|
||||||
|
// 检测教师时间冲突
|
||||||
|
List<SchedulePlan> 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<SchedulePlan> 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<SchedulePlan> checkTeacherConflict(Long tenantId, Long teacherId,
|
||||||
|
LocalDate scheduledDate, String scheduledTime, Long excludeId) {
|
||||||
|
if (teacherId == null) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询该教师当天所有排课
|
||||||
|
LambdaQueryWrapper<SchedulePlan> 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<SchedulePlan> plans = schedulePlanMapper.selectList(wrapper);
|
||||||
|
|
||||||
|
// 过滤出时间重叠的排课
|
||||||
|
List<SchedulePlan> conflicts = new ArrayList<>();
|
||||||
|
for (SchedulePlan plan : plans) {
|
||||||
|
if (isTimeOverlap(scheduledTime, plan.getScheduledTime())) {
|
||||||
|
conflicts.add(plan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return conflicts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SchedulePlan> checkClassConflict(Long tenantId, Long classId,
|
||||||
|
LocalDate scheduledDate, String scheduledTime, Long excludeId) {
|
||||||
|
if (classId == null) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询该班级当天所有排课
|
||||||
|
LambdaQueryWrapper<SchedulePlan> 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<SchedulePlan> plans = schedulePlanMapper.selectList(wrapper);
|
||||||
|
|
||||||
|
// 过滤出时间重叠的排课
|
||||||
|
List<SchedulePlan> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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<SchedulePlan> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(SchedulePlan::getScheduledDate, today)
|
||||||
|
.ne(SchedulePlan::getStatus, "cancelled")
|
||||||
|
.eq(SchedulePlan::getReminderSent, 0);
|
||||||
|
|
||||||
|
List<SchedulePlan> 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<SchedulePlan> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(SchedulePlan::getScheduledDate, today)
|
||||||
|
.ne(SchedulePlan::getStatus, "cancelled");
|
||||||
|
|
||||||
|
List<SchedulePlan> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user