package com.zhitan.airconditioner.service.impl; import java.text.SimpleDateFormat; import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; import com.zhitan.airconditioner.domain.AirConditionerSchedule; import com.zhitan.airconditioner.mapper.AirConditionerScheduleMapper; import com.zhitan.airconditioner.service.IAirConditionerScheduleService; import com.zhitan.airconditioner.service.IAirConditionerService; import com.zhitan.common.utils.DateTimeUtil; import com.zhitan.common.utils.DateUtils; import com.zhitan.common.utils.SecurityUtils; import com.zhitan.system.service.ISysHolidayService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; /** * 空调定时任务Service业务层处理 * * @author zhitan */ /** * 空调定时任务Service业务层处理 * * @author zhitan */ @Service public class AirConditionerScheduleServiceImpl implements IAirConditionerScheduleService { private static final Logger log = LoggerFactory.getLogger(AirConditionerScheduleServiceImpl.class); @Autowired private AirConditionerScheduleMapper airConditionerScheduleMapper; @Autowired private IAirConditionerService airConditionerService; @Autowired private TaskScheduler taskScheduler; @Autowired private ISysHolidayService sysHolidayService; // 存储所有调度的任务,key为任务ID,value为调度的Future对象 private final Map> scheduledTasks = new ConcurrentHashMap<>(); /** * 初始化定时任务 * 从数据库加载所有定时任务并进行调度 */ @PostConstruct public void init() { log.info("空调定时任务调度器初始化开始"); // 查询所有有效的定时任务 AirConditionerSchedule query = new AirConditionerSchedule(); query.setStatus("0"); // 状态正常的任务 List schedules = airConditionerScheduleMapper.selectAirConditionerScheduleList(query); if (schedules.isEmpty()) { log.info("没有找到需要调度的空调定时任务"); return; } log.info("找到{}个需要调度的空调定时任务", schedules.size()); // 为每个任务创建调度 for (AirConditionerSchedule schedule : schedules) { scheduleTask(schedule); } log.info("空调定时任务调度器已启动,使用基于数据库的精确调度机制"); } /** * 应用关闭时取消所有定时任务 */ @PreDestroy public void destroy() { for (Map.Entry> entry : scheduledTasks.entrySet()) { entry.getValue().cancel(false); } scheduledTasks.clear(); log.info("空调定时任务调度器已关闭,所有任务已取消"); } /** * 查询空调定时任务列表 * * @param airConditionerSchedule 空调定时任务信息 * @return 空调定时任务集合 */ @Override public List selectAirConditionerScheduleList(AirConditionerSchedule airConditionerSchedule) { return airConditionerScheduleMapper.selectAirConditionerScheduleList(airConditionerSchedule); } /** * 查询空调定时任务信息 * * @param id 空调定时任务ID * @return 空调定时任务信息 */ @Override public AirConditionerSchedule selectAirConditionerScheduleById(Long id) { return airConditionerScheduleMapper.selectAirConditionerScheduleById(id); } /** * 根据空调ID查询定时任务列表 * * @param airConditionerId 空调ID * @return 定时任务列表 */ @Override public List selectAirConditionerScheduleByAirConditionerId(Long airConditionerId) { return airConditionerScheduleMapper.selectAirConditionerScheduleByAirConditionerId(airConditionerId); } /** * 新增空调定时任务 * * @param airConditionerSchedule 空调定时任务信息 * @return 结果 */ @Override public int insertAirConditionerSchedule(AirConditionerSchedule airConditionerSchedule) { airConditionerSchedule.setCreateTime(DateUtils.getNowDate()); airConditionerSchedule.setCreateBy(SecurityUtils.getUsername()); int rows = airConditionerScheduleMapper.insertAirConditionerSchedule(airConditionerSchedule); if (rows > 0) { // 重新调度任务,使新添加的定时任务生效 rescheduleTask(); } return rows; } /** * 修改空调定时任务 * * @param airConditionerSchedule 空调定时任务信息 * @return 结果 */ @Override public int updateAirConditionerSchedule(AirConditionerSchedule airConditionerSchedule) { airConditionerSchedule.setUpdateTime(DateUtils.getNowDate()); airConditionerSchedule.setUpdateBy(SecurityUtils.getUsername()); int rows = airConditionerScheduleMapper.updateAirConditionerSchedule(airConditionerSchedule); if (rows > 0) { // 重新调度任务,使修改后的定时任务生效 rescheduleTask(); } return rows; } /** * 批量删除空调定时任务 * * @param ids 需要删除的空调定时任务ID * @return 结果 */ @Override public int deleteAirConditionerScheduleByIds(Long[] ids) { int rows = airConditionerScheduleMapper.deleteAirConditionerScheduleByIds(ids); if (rows > 0) { // 重新调度任务,使删除操作生效 rescheduleTask(); } return rows; } /** * 删除空调定时任务信息 * * @param id 空调定时任务ID * @return 结果 */ @Override public int deleteAirConditionerScheduleById(Long id) { int rows = airConditionerScheduleMapper.deleteAirConditionerScheduleById(id); if (rows > 0) { // 重新调度任务,使删除操作生效 rescheduleTask(); } return rows; } /** * 根据空调ID删除定时任务 * * @param airConditionerId 空调ID * @return 结果 */ @Override public int deleteAirConditionerScheduleByAirConditionerId(String airConditionerId) { int rows = airConditionerScheduleMapper.deleteAirConditionerScheduleByAirConditionerId(airConditionerId); if (rows > 0) { // 重新调度任务,使删除操作生效 rescheduleTask(); } return rows; } /** * 执行空调定时任务 * 此方法保留用于兼容接口,在新的实现中不再使用 * 新的实现为每个具体的时间点创建独立的调度任务,不再需要每分钟检查一次 */ @Override public void executeScheduleTasks() { log.info("executeScheduleTasks方法已被调用,但在新的实现中不再使用"); log.info("当前使用基于数据库的精确定时任务调度机制,为每个时间点创建独立的调度任务"); // 如果有必要,可以在这里重新调度所有任务 // rescheduleTask(); } /** * 重新调度任务 * 当定时任务发生变化时(如添加、修改、删除定时任务)调用此方法 * 取消所有当前的调度任务并重新创建 */ public void rescheduleTask() { // 取消所有现有的调度任务 for (Map.Entry> entry : scheduledTasks.entrySet()) { entry.getValue().cancel(false); } scheduledTasks.clear(); // 查询所有有效的定时任务并重新调度 AirConditionerSchedule query = new AirConditionerSchedule(); query.setStatus("0"); // 状态正常的任务 List schedules = airConditionerScheduleMapper.selectAirConditionerScheduleList(query); if (schedules.isEmpty()) { log.info("没有找到需要调度的空调定时任务"); return; } log.info("重新调度{}个空调定时任务", schedules.size()); // 为每个任务创建调度 for (AirConditionerSchedule schedule : schedules) { scheduleTask(schedule); } log.info("空调定时任务已重新调度完成"); } /** * 为单个定时任务创建调度 * 分别为开机时间和关机时间创建独立的调度任务 * 开机指令只在法定工作日添加定时任务,关机指令每天都添加定时任务 * * @param schedule 定时任务信息 */ private void scheduleTask(AirConditionerSchedule schedule) { if (schedule == null || schedule.getId() == null) { log.error("无法调度空或无ID的定时任务"); return; } try { // 为开机时间创建调度 if (schedule.getStartTime() != null) { // 开机指令只在法定工作日添加定时任务 scheduleSpecificTask(schedule, schedule.getStartTime(), true); } // 为关机时间创建调度 if (schedule.getOffTime() != null) { // 关机指令每天都添加定时任务 scheduleSpecificTask(schedule, schedule.getOffTime(), false); } } catch (Exception e) { log.error("为定时任务创建调度失败,任务ID:{},错误信息:{}", schedule.getId(), e.getMessage()); } } /** * 为特定时间点创建调度任务 * * @param schedule 定时任务信息 * @param targetTime 目标时间点 * @param isStartTime 是否为开机时间(true为开机,false为关机) */ private void scheduleSpecificTask(AirConditionerSchedule schedule, Date targetTime, boolean isStartTime) { // 计算下一次执行时间 Date nextExecutionTime = calculateNextExecutionTime(targetTime); if (nextExecutionTime == null) { log.error("计算下一次执行时间失败,任务ID:{}", schedule.getId()); return; } // 如果是开机任务,检查执行日期是否为工作日 if (isStartTime) { // 如果不是工作日,则计算下一个工作日 while (!isWorkDay(nextExecutionTime)) { // 不是工作日,跳到下一天再检查 nextExecutionTime = DateUtils.addDays(nextExecutionTime, 1); // 更新时间部分(保持原来的时分秒不变) Calendar cal = Calendar.getInstance(); cal.setTime(nextExecutionTime); Calendar targetCal = Calendar.getInstance(); targetCal.setTime(targetTime); cal.set(Calendar.HOUR_OF_DAY, targetCal.get(Calendar.HOUR_OF_DAY)); cal.set(Calendar.MINUTE, targetCal.get(Calendar.MINUTE)); cal.set(Calendar.SECOND, targetCal.get(Calendar.SECOND)); nextExecutionTime = cal.getTime(); } log.info("开机任务已调整为下一个工作日执行,执行时间:{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(nextExecutionTime)); } // 创建任务ID,格式为:任务ID + "_start"或"_off" String taskKey = schedule.getId() + (isStartTime ? "_start" : "_off"); // 取消已存在的相同任务(如果有) ScheduledFuture existingTask = scheduledTasks.get(taskKey); if (existingTask != null) { existingTask.cancel(false); scheduledTasks.remove(taskKey); } // 创建新的调度任务 ScheduledFuture future = taskScheduler.schedule( () -> executeSpecificTask(schedule, isStartTime), nextExecutionTime ); // 保存调度任务 scheduledTasks.put(taskKey, future); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); log.info("已为空调ID:{}创建{}定时任务,执行时间:{}", schedule.getAirConditionerId(), isStartTime ? "开机" : "关机", sdf.format(nextExecutionTime)); } /** * 计算下一次执行时间 * 如果今天的执行时间已过,则安排在明天的同一时间执行 * * @param targetTime 目标时间 * @return 下一次执行时间 */ private Date calculateNextExecutionTime(Date targetTime) { if (targetTime == null) { return null; } // 将Date转换为LocalTime(只保留时分秒) LocalTime targetLocalTime = targetTime.toInstant() .atZone(ZoneId.systemDefault()) .toLocalTime(); // 获取当前日期和时间 LocalDateTime now = LocalDateTime.now(); LocalDate today = now.toLocalDate(); // 创建今天的目标执行时间 LocalDateTime targetDateTime = LocalDateTime.of(today, targetLocalTime); // 如果目标时间已过,安排在明天的同一时间执行 if (targetDateTime.isBefore(now)) { targetDateTime = targetDateTime.plusDays(1); } // 转换回Date类型 return Date.from(targetDateTime.atZone(ZoneId.systemDefault()).toInstant()); } /** * 执行特定的定时任务 * * @param schedule 定时任务信息 * @param isStartTime 是否为开机时间(true为开机,false为关机) */ private void executeSpecificTask(AirConditionerSchedule schedule, boolean isStartTime) { try { if (isStartTime) { // 开机操作 - 根据季节自动判断制热或制冷模式 String controlMode = determineControlModeByMonth(); log.info("执行空调开机任务,空调ID:{},控制模式:{}", schedule.getAirConditionerId(), controlMode); // 执行空调开机控制 airConditionerService.controlAirConditioner( schedule.getAirConditionerId(), controlMode, // 0制冷或1制热 "1" // 自动操作 ); } else { // 关机操作 log.info("执行空调关机任务,空调ID:{}", schedule.getAirConditionerId()); // 执行空调关机控制 airConditionerService.controlAirConditioner( schedule.getAirConditionerId(), "2", // 关机 "1" // 自动操作 ); } // 任务执行完成后,重新调度下一天的同一时间点 // 获取原始的时间点 Date originalTime = isStartTime ? schedule.getStartTime() : schedule.getOffTime(); // 重新调度 scheduleSpecificTask(schedule, originalTime, isStartTime); } catch (Exception e) { log.error("执行空调{}任务失败,空调ID:{},错误信息:{}", isStartTime ? "开机" : "关机", schedule.getAirConditionerId(), e.getMessage()); } } /** * 判断日期是否为工作日 * 工作日判断逻辑: * 1. 如果是周一至周五,且不是法定节假日,则为工作日 * 2. 如果是周六日,但是为调休工作日,则为工作日 * * @param date 需要判断的日期 * @return 如果是工作日返回true,否则返回false */ private boolean isWorkDay(Date date) { // 调用节假日服务进行精确的工作日判断 return sysHolidayService.isWorkDay(date); } /** * 根据月份自动判断制热或制冷模式 * 5-9月为夏季,使用制冷模式 * 11-3月为冬季,使用制热模式 * 4月和10月为过渡季节,根据当前温度判断 * * @return 控制模式(0制冷 1制热) */ private String determineControlModeByMonth() { // 获取当前月份 int currentMonth = DateTimeUtil.getMonthOfYear(new Date()); // 根据月份判断季节 if (currentMonth >= 5 && currentMonth <= 9) { // 夏季使用制冷模式 return "0"; } else if ((currentMonth >= 11 && currentMonth <= 12) || (currentMonth >= 1 && currentMonth <= 3)) { // 冬季使用制热模式 return "1"; } else { // 过渡季节,默认使用制冷模式 // 实际应用中可以根据当前温度判断 return "0"; } } /** * 注意:对于只存储时分秒的时间字段(如开始时间和结束时间), * 数据库应该使用TIME类型而非DATETIME或TIMESTAMP类型。 * * MySQL中的TIME类型格式为'HH:MM:SS',只存储时间部分,不包含日期信息。 * 这样可以减少存储空间,并且更符合业务需求。 * * 在Java实体类中,虽然使用Date类型表示,但在与数据库交互时, * 只有时分秒部分会被保存和读取。 */ }