xuhy
2026-03-14 cf7fde24bf5a20ca8328729406f85ee16984da7a
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
package com.xiaopeng.map.utils;
 
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
 
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
 
/**
 * 围栏运营策略时间范围索引工具类
 */
public final class OperationStrategyIndex {
    private final List<List<TimeInterval>> strategyIntervals;
 
    /**
     * 构造索引(内部使用)。
     *
     * @param strategyIntervals 策略组时间区间列表(每个策略组已 merge)
     */
    private OperationStrategyIndex(List<List<TimeInterval>> strategyIntervals) {
        this.strategyIntervals = strategyIntervals == null ? Collections.emptyList() : strategyIntervals;
    }
 
    /**
     * 解析围栏 {@code operation_strategy} JSON 字符串为运营时间范围索引。
     * <p>
     * 说明:会按“策略组”聚合并合并时间段;点位只要被任意一个策略组的时间段完整覆盖,则为正常。
     * </p>
     *
     * @param operationStrategyJson 围栏运营策略 JSON(数组字符串)
     * @return 索引对象;解析失败/空入参返回空索引
     */
    public static OperationStrategyIndex parse(String operationStrategyJson) {
        if (operationStrategyJson == null || operationStrategyJson.isEmpty()) {
            return new OperationStrategyIndex(Collections.emptyList());
        }
        try {
            JSONArray jsonArray = JSONArray.parseArray(operationStrategyJson);
            if (jsonArray == null || jsonArray.isEmpty()) {
                return new OperationStrategyIndex(Collections.emptyList());
            }
            List<List<TimeInterval>> groups = new java.util.ArrayList<>();
            for (int i = 0; i < jsonArray.size(); i++) {
                JSONObject obj = jsonArray.getJSONObject(i);
                if (obj == null) {
                    continue;
                }
                JSONArray timeArray = obj.getJSONArray("time");
                if (timeArray == null || timeArray.isEmpty()) {
                    continue;
                }
                List<TimeInterval> intervals = TimeInterval.parseRangesDedup(timeArray);
                List<TimeInterval> merged = TimeInterval.merge(intervals);
                if (!merged.isEmpty()) {
                    groups.add(merged);
                }
            }
            return new OperationStrategyIndex(groups);
        } catch (Exception e) {
            return new OperationStrategyIndex(Collections.emptyList());
        }
    }
 
    /**
     * 解析点位 {@code effective_time_period} JSON 数组字符串为时间区间列表(并合并)。
     *
     * @param jsonArrayStr 点位生效时间段 JSON(数组字符串)
     * @return 时间区间列表;解析失败返回空列表
     */
    public static List<TimeInterval> parsePointEffectiveTimePeriod(String jsonArrayStr) {
        return TimeInterval.parseJsonRangesDedup(jsonArrayStr);
    }
 
    /**
     * 判断两个围栏策略索引的 Union 运营时间覆盖范围是否一致。
     * <p>
     * 注意:不关心 JSON 的原始顺序与分组,只比较合并后的 Union 覆盖范围是否相同。
     * </p>
     *
     * @param other 待比较索引
     * @return true 表示 Union 覆盖范围一致
     */
    public boolean equalsNormalized(OperationStrategyIndex other) {
        if (other == null) {
            return false;
        }
        return equalsIntervals(this.unionAllowed(), other.unionAllowed());
    }
 
    /**
     * 判断相对新索引是否发生“运营时间范围缩短”。
     * <p>
     * 缩短定义:新 Union 覆盖范围被旧 Union 完整覆盖,且与旧 Union 不相等(newUnion ⊂ oldUnion)。
     * </p>
     *
     * @param newIndex 新索引(编辑后的围栏策略)
     * @return true 表示发生缩短
     */
    public boolean hasAnyShrinkComparedTo(OperationStrategyIndex newIndex) {
        if (newIndex == null) {
            return false;
        }
        List<TimeInterval> oldUnion = this.unionAllowed();
        List<TimeInterval> newUnion = newIndex.unionAllowed();
        if (equalsIntervals(oldUnion, newUnion)) {
            return false;
        }
        return TimeInterval.isWithin(newUnion, oldUnion);
    }
 
    /**
     * 获取当前索引的 Union 运营时间覆盖范围(合并后的时间区间列表)。
     *
     * @return Union 区间列表(已 merge 并排序)
     */
    public List<TimeInterval> unionAllowed() {
        if (strategyIntervals == null || strategyIntervals.isEmpty()) {
            return Collections.emptyList();
        }
        List<TimeInterval> union = strategyIntervals.stream()
                .filter(Objects::nonNull)
                .flatMap(List::stream)
                .collect(Collectors.toList());
        return TimeInterval.merge(union);
    }
 
    /**
     * 判断点位的时间段是否为正常。
     * <p>
     * 规则:只要点位的全部运营时间段被围栏“任意一个策略组”的时间段范围完整覆盖,则为正常。
     * </p>
     *
     * @param pointIntervals 点位时间区间列表
     * @return true 表示正常;false 表示不在任何一个策略组覆盖范围内
     */
    public boolean isPointScheduleWithin(List<TimeInterval> pointIntervals) {
        if (pointIntervals == null || pointIntervals.isEmpty()) {
            return true;
        }
        if (strategyIntervals == null || strategyIntervals.isEmpty()) {
            return false;
        }
        for (List<TimeInterval> groupIntervals : strategyIntervals) {
            if (TimeInterval.isWithin(pointIntervals, groupIntervals)) {
                return true;
            }
        }
        return false;
    }
 
    /**
     * 兼容旧接口:忽略 {@code effectivePeriod},统一按每日(Union 覆盖范围)判断。
     *
     * @param effectivePeriod 点位生效周期(不再使用)
     * @param pointIntervals  点位时间区间列表
     * @return true 表示点位时间段被围栏 Union 覆盖
     */
    public boolean isPointScheduleWithin(Integer effectivePeriod, List<TimeInterval> pointIntervals) {
        return isPointScheduleWithin(pointIntervals);
    }
 
    /**
     * 区间列表归一化:merge 合并并排序。
     */
    private static List<TimeInterval> normalizeIntervals(List<TimeInterval> intervals) {
        return TimeInterval.merge(intervals);
    }
 
    /**
     * 比较两组区间在归一化(merge)后的覆盖范围是否完全一致。
     */
    private static boolean equalsIntervals(List<TimeInterval> a, List<TimeInterval> b) {
        List<TimeInterval> x = normalizeIntervals(a);
        List<TimeInterval> y = normalizeIntervals(b);
        if (x.size() != y.size()) {
            return false;
        }
        for (int i = 0; i < x.size(); i++) {
            TimeInterval xi = x.get(i);
            TimeInterval yi = y.get(i);
            if (xi.startMinute != yi.startMinute || xi.endMinute != yi.endMinute) {
                return false;
            }
        }
        return true;
    }
 
    public static final class TimeInterval {
        private final int startMinute; // inclusive
        private final int endMinute;   // inclusive
 
        /**
         * 构造时间区间(闭区间,单位:分钟)。
         */
        private TimeInterval(int startMinute, int endMinute) {
            this.startMinute = startMinute;
            this.endMinute = endMinute;
        }
 
        /**
         * 解析 JSON 数组字符串为时间区间列表,并做 merge 合并。
         *
         * @param jsonArrayStr JSON 数组字符串(例如 ["08:00-12:00","13:00-18:00"])
         * @return merge 后的区间列表;解析失败返回空列表
         */
        static List<TimeInterval> parseJsonRangesDedup(String jsonArrayStr) {
            if (jsonArrayStr == null || jsonArrayStr.isEmpty()) {
                return Collections.emptyList();
            }
            try {
                JSONArray arr = JSONArray.parseArray(jsonArrayStr);
                if (arr == null || arr.isEmpty()) {
                    return Collections.emptyList();
                }
                return merge(parseRangesDedup(arr));
            } catch (Exception e) {
                return Collections.emptyList();
            }
        }
 
        /**
         * 解析 JSONArray 中的时间段为区间列表(不做 merge)。
         *
         * @param timeArray 时间段数组(元素形如 "HH:mm-HH:mm")
         * @return 区间列表
         */
        static List<TimeInterval> parseRanges(JSONArray timeArray) {
            List<TimeInterval> result = new java.util.ArrayList<>();
            for (int i = 0; i < timeArray.size(); i++) {
                String range = timeArray.getString(i);
                result.addAll(parseRange(range));
            }
            return result;
        }
 
        /**
         * 解析 JSONArray 中的时间段为区间列表(先按字符串去重,再解析;不做 merge)。
         */
        static List<TimeInterval> parseRangesDedup(JSONArray timeArray) {
            if (timeArray == null || timeArray.isEmpty()) {
                return Collections.emptyList();
            }
            Set<String> uniq = new LinkedHashSet<>();
            for (int i = 0; i < timeArray.size(); i++) {
                String range = timeArray.getString(i);
                if (range != null && !range.isEmpty()) {
                    uniq.add(range);
                }
            }
            List<TimeInterval> result = new java.util.ArrayList<>();
            for (String range : uniq) {
                result.addAll(parseRange(range));
            }
            return result;
        }
 
        /**
         * 解析单个时间段字符串为区间列表。
         * <p>
         * 注意:业务约束不会出现跨天时间段;若出现 end &lt; start,则视为非法配置并忽略该时间段。
         * </p>
         */
        private static List<TimeInterval> parseRange(String range) {
            if (range == null || !range.contains("-")) {
                return Collections.emptyList();
            }
            String[] parts = range.split("-");
            if (parts.length != 2) {
                return Collections.emptyList();
            }
            Integer start = parseMinute(parts[0].trim());
            Integer end = parseMinute(parts[1].trim());
            if (start == null || end == null) {
                return Collections.emptyList();
            }
            if (end < start) {
                return Collections.emptyList();
            }
            return java.util.Collections.singletonList(new TimeInterval(start, end));
        }
 
        /**
         * 将 HH:mm 解析为分钟数(0-1439),非法格式返回 null。
         */
        private static Integer parseMinute(String hhmm) {
            if (hhmm == null || hhmm.length() != 5 || hhmm.charAt(2) != ':') {
                return null;
            }
            try {
                int hour = Integer.parseInt(hhmm.substring(0, 2));
                int minute = Integer.parseInt(hhmm.substring(3, 5));
                if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
                    return null;
                }
                return hour * 60 + minute;
            } catch (Exception e) {
                return null;
            }
        }
 
        /**
         * 合并重叠/相邻区间,并按起始分钟排序。
         */
        static List<TimeInterval> merge(List<TimeInterval> intervals) {
            if (intervals == null || intervals.isEmpty()) {
                return Collections.emptyList();
            }
            List<TimeInterval> sorted = intervals.stream()
                    .filter(Objects::nonNull)
                    .sorted(java.util.Comparator.comparingInt(a -> a.startMinute))
                    .collect(Collectors.toList());
            if (sorted.isEmpty()) {
                return Collections.emptyList();
            }
            List<TimeInterval> merged = new java.util.ArrayList<>();
            TimeInterval cur = sorted.get(0);
            for (int i = 1; i < sorted.size(); i++) {
                TimeInterval next = sorted.get(i);
                if (next.startMinute <= cur.endMinute + 1) {
                    cur = new TimeInterval(cur.startMinute, Math.max(cur.endMinute, next.endMinute));
                } else {
                    merged.add(cur);
                    cur = next;
                }
            }
            merged.add(cur);
            return merged;
        }
 
        /**
         * 判断 pointIntervals 是否完全被 allowedIntervals 覆盖(每段都需被某个 allowed 段完全包含)。
         */
        static boolean isWithin(List<TimeInterval> pointIntervals, List<TimeInterval> allowedIntervals) {
            if (pointIntervals == null || pointIntervals.isEmpty()) {
                return true;
            }
            List<TimeInterval> allowed = merge(allowedIntervals);
            if (allowed.isEmpty()) {
                return false;
            }
            for (TimeInterval p : pointIntervals) {
                boolean covered = false;
                for (TimeInterval a : allowed) {
                    if (a.startMinute <= p.startMinute && a.endMinute >= p.endMinute) {
                        covered = true;
                        break;
                    }
                }
                if (!covered) {
                    return false;
                }
            }
            return true;
        }
 
        /**
         * 字符串化:便于 main 输出与排查(格式:startMinute-endMinute)。
         */
        @Override
        public String toString() {
            return startMinute + "-" + endMinute;
        }
    }
 
    /**
     * main 自测:使用固定样例打印 old/new Union、是否修改/缩短,以及点位在旧/新范围内判断结果。
     */
    public static void main(String[] args) {
        String pointJson = "[\"05:00-12:00\", \"13:00-23:59\"]";
        String strategyJson = "[{\"name\": \"策略组1\", \"time\": [\"06:00-12:00\", \"13:00-23:59\"], \"type\": 0, \"typeStr\": []}, {\"name\": \"策略组2\", \"time\": [\"05:00-12:00\", \"12:00-23:00\"], \"type\": 2, \"typeStr\": \"\"}, {\"name\": \"策略组3\", \"time\": [\"09:00-23:59\"], \"type\": 1, \"typeStr\": \"\"}]";
 
        OperationStrategyIndex index = OperationStrategyIndex.parse(strategyJson);
        List<TimeInterval> pointIntervals = OperationStrategyIndex.parsePointEffectiveTimePeriod(pointJson);
 
        System.out.println("pointIntervals=" + pointIntervals);
        System.out.println("fenceUnion=" + index.unionAllowed());
        boolean ok = index.isPointScheduleWithin(pointIntervals);
        System.out.println("isNormal=" + ok);
    }
}