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<String, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();
|
|
/**
|
* 初始化定时任务
|
* 从数据库加载所有定时任务并进行调度
|
*/
|
@PostConstruct
|
public void init() {
|
log.info("空调定时任务调度器初始化开始");
|
|
// 查询所有有效的定时任务
|
AirConditionerSchedule query = new AirConditionerSchedule();
|
query.setStatus("0"); // 状态正常的任务
|
List<AirConditionerSchedule> 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<String, ScheduledFuture<?>> entry : scheduledTasks.entrySet()) {
|
entry.getValue().cancel(false);
|
}
|
scheduledTasks.clear();
|
log.info("空调定时任务调度器已关闭,所有任务已取消");
|
}
|
|
/**
|
* 查询空调定时任务列表
|
*
|
* @param airConditionerSchedule 空调定时任务信息
|
* @return 空调定时任务集合
|
*/
|
@Override
|
public List<AirConditionerSchedule> 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<AirConditionerSchedule> 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<String, ScheduledFuture<?>> entry : scheduledTasks.entrySet()) {
|
entry.getValue().cancel(false);
|
}
|
scheduledTasks.clear();
|
|
// 查询所有有效的定时任务并重新调度
|
AirConditionerSchedule query = new AirConditionerSchedule();
|
query.setStatus("0"); // 状态正常的任务
|
List<AirConditionerSchedule> 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类型表示,但在与数据库交互时,
|
* 只有时分秒部分会被保存和读取。
|
*/
|
}
|