baoshiwei
2025-06-05 eb54eea87c39f35b5d0476f146bfd29d7c7841be
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
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类型表示,但在与数据库交互时,
     * 只有时分秒部分会被保存和读取。
     */
}