WangHan
2024-09-12 d5855a4926926698b740bc6c7ba489de47adb68b
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
package tech.powerjob.server.core.scheduler.auxiliary.impl;
 
import com.google.common.collect.Sets;
import lombok.Data;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.stereotype.Component;
import tech.powerjob.common.enums.TimeExpressionType;
import tech.powerjob.common.serialize.JsonUtils;
import tech.powerjob.common.utils.CollectionUtils;
import tech.powerjob.common.utils.CommonUtils;
import tech.powerjob.server.common.utils.TimeUtils;
import tech.powerjob.server.core.scheduler.auxiliary.TimeOfDay;
import tech.powerjob.server.core.scheduler.auxiliary.TimingStrategyHandler;
 
import java.io.Serializable;
import java.util.Calendar;
import java.util.Date;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
 
/**
 * DailyTimeIntervalStrategyHandler
 * @author 550w
 * @date 2027/02/15
 */
@Component
public class DailyTimeIntervalStrategyHandler implements TimingStrategyHandler {
 
    /**
     * 使用中国星期!!!
     */
    private static final Set<Integer> ALL_DAY = Sets.newHashSet(1, 2, 3, 4, 5, 6, 7);
 
    @Override
    public TimeExpressionType supportType() {
        return TimeExpressionType.DAILY_TIME_INTERVAL;
    }
 
    @Override
    @SneakyThrows
    public void validate(String timeExpression) {
        DailyTimeIntervalExpress ep = JsonUtils.parseObject(timeExpression, DailyTimeIntervalExpress.class);
        CommonUtils.requireNonNull(ep.interval, "interval can't be null or empty in DailyTimeIntervalExpress");
        CommonUtils.requireNonNull(ep.startTimeOfDay, "startTimeOfDay can't be null or empty in DailyTimeIntervalExpress");
        CommonUtils.requireNonNull(ep.endTimeOfDay, "endTimeOfDay can't be null or empty in DailyTimeIntervalExpress");
 
        TimeOfDay startTime = TimeOfDay.from(ep.startTimeOfDay);
        TimeOfDay endTime = TimeOfDay.from(ep.endTimeOfDay);
 
        if (endTime.before(startTime)) {
            throw new IllegalArgumentException("endTime should after startTime!");
        }
 
        if (StringUtils.isNotEmpty(ep.intervalUnit)) {
            TimeUnit.valueOf(ep.intervalUnit);
        }
    }
 
    @Override
    @SneakyThrows
    public Long calculateNextTriggerTime(Long preTriggerTime, String timeExpression, Long startTime, Long endTime) {
        DailyTimeIntervalExpress ep = JsonUtils.parseObject(timeExpression, DailyTimeIntervalExpress.class);
 
        // 未开始状态下,用起点算调度时间
        if (startTime != null && startTime > System.currentTimeMillis() && preTriggerTime < startTime) {
            return calculateInRangeTime(startTime, ep);
        }
 
        // 间隔时间
        TimeUnit timeUnit = Optional.ofNullable(ep.intervalUnit).map(TimeUnit::valueOf).orElse(TimeUnit.SECONDS);
        long interval = timeUnit.toMillis(ep.interval);
 
        Long ret = calculateInRangeTime(preTriggerTime + interval, ep);
        if (ret == null || ret <= Optional.ofNullable(endTime).orElse(Long.MAX_VALUE)) {
            return ret;
        }
        return null;
    }
 
    /**
     * 计算最近一次在范围中的时间
     * @param time 当前时间基准,可能直接返回该时间作为结果
     * @param ep 表达式
     * @return 最近一次在范围中的时间
     */
    static Long calculateInRangeTime(Long time, DailyTimeIntervalExpress ep) {
 
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date(time));
 
        int year = calendar.get(Calendar.YEAR);
        // 月份 + 1,转为熟悉的 1~12 月
        int month = calendar.get(Calendar.MONTH) + 1;
        int day = calendar.get(Calendar.DAY_OF_MONTH);
 
        // 判断是否符合"日"的执行条件
        int week = TimeUtils.calculateWeek(year, month, day);
        Set<Integer> targetDays = CollectionUtils.isEmpty(ep.daysOfWeek) ? ALL_DAY : ep.daysOfWeek;
        // 未包含情况下,将时间改写为符合条件日的 00:00 分,重新开始递归(这部分应该有性能更优的写法,不过这个调度模式应该很难触发瓶颈,先简单好用的实现)
        if (!targetDays.contains(week)) {
            simpleSetCalendar(calendar, 0, 0, 0);
            Date tomorrowZero = DateUtils.addDays(calendar.getTime(), 1);
            return calculateInRangeTime(tomorrowZero.getTime(), ep);
        }
 
        // 范围的开始时间
        TimeOfDay rangeStartTime = TimeOfDay.from(ep.startTimeOfDay);
        simpleSetCalendar(calendar, rangeStartTime.getHour(), rangeStartTime.getMinute(), rangeStartTime.getSecond());
        long todayStartTs = calendar.getTimeInMillis();
 
        // 未开始
        if (time < todayStartTs) {
            return todayStartTs;
        }
 
        TimeOfDay rangeEndTime = TimeOfDay.from(ep.endTimeOfDay);
        simpleSetCalendar(calendar, rangeEndTime.getHour(), rangeEndTime.getMinute(), rangeEndTime.getSecond());
        long todayEndTs = calendar.getTimeInMillis();
 
        // 范围之间
        if (time <= todayEndTs) {
            return time;
        }
 
        // 已结束,重新计算第二天时间
        simpleSetCalendar(calendar, 0, 0, 0);
        return calculateInRangeTime(DateUtils.addDays(calendar.getTime(), 1).getTime(), ep);
    }
 
    private static void simpleSetCalendar(Calendar calendar, int h, int m, int s) {
        calendar.set(Calendar.SECOND, s);
        calendar.set(Calendar.MINUTE, m);
        calendar.set(Calendar.HOUR_OF_DAY, h);
        calendar.set(Calendar.MILLISECOND, 0);
    }
 
    @Data
    static class DailyTimeIntervalExpress implements Serializable {
 
        /**
         * 时间间隔
         */
        private Long interval;
        /**
         * 每天激活的时间起点,格式为:18:30:00 代表 18点30分00秒激活
         */
        private String startTimeOfDay;
        /**
         * 每日激活的时间终点,格式同上
         */
        private String endTimeOfDay;
 
        /* ************ 非必填字段 ************ */
        /**
         * 时间单位,默认秒
         */
        private String intervalUnit;
        /**
         * 每周的哪几天激活,空代表每天都激活
         */
        private Set<Integer> daysOfWeek;
    }
}