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> strategyIntervals; /** * 构造索引(内部使用)。 * * @param strategyIntervals 策略组时间区间列表(每个策略组已 merge) */ private OperationStrategyIndex(List> strategyIntervals) { this.strategyIntervals = strategyIntervals == null ? Collections.emptyList() : strategyIntervals; } /** * 解析围栏 {@code operation_strategy} JSON 字符串为运营时间范围索引。 *

* 说明:会按“策略组”聚合并合并时间段;点位只要被任意一个策略组的时间段完整覆盖,则为正常。 *

* * @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> 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 intervals = TimeInterval.parseRangesDedup(timeArray); List 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 parsePointEffectiveTimePeriod(String jsonArrayStr) { return TimeInterval.parseJsonRangesDedup(jsonArrayStr); } /** * 判断两个围栏策略索引的 Union 运营时间覆盖范围是否一致。 *

* 注意:不关心 JSON 的原始顺序与分组,只比较合并后的 Union 覆盖范围是否相同。 *

* * @param other 待比较索引 * @return true 表示 Union 覆盖范围一致 */ public boolean equalsNormalized(OperationStrategyIndex other) { if (other == null) { return false; } return equalsIntervals(this.unionAllowed(), other.unionAllowed()); } /** * 判断相对新索引是否发生“运营时间范围缩短”。 *

* 缩短定义:新 Union 覆盖范围被旧 Union 完整覆盖,且与旧 Union 不相等(newUnion ⊂ oldUnion)。 *

* * @param newIndex 新索引(编辑后的围栏策略) * @return true 表示发生缩短 */ public boolean hasAnyShrinkComparedTo(OperationStrategyIndex newIndex) { if (newIndex == null) { return false; } List oldUnion = this.unionAllowed(); List newUnion = newIndex.unionAllowed(); if (equalsIntervals(oldUnion, newUnion)) { return false; } return TimeInterval.isWithin(newUnion, oldUnion); } /** * 获取当前索引的 Union 运营时间覆盖范围(合并后的时间区间列表)。 * * @return Union 区间列表(已 merge 并排序) */ public List unionAllowed() { if (strategyIntervals == null || strategyIntervals.isEmpty()) { return Collections.emptyList(); } List union = strategyIntervals.stream() .filter(Objects::nonNull) .flatMap(List::stream) .collect(Collectors.toList()); return TimeInterval.merge(union); } /** * 判断点位的时间段是否为正常。 *

* 规则:只要点位的全部运营时间段被围栏“任意一个策略组”的时间段范围完整覆盖,则为正常。 *

* * @param pointIntervals 点位时间区间列表 * @return true 表示正常;false 表示不在任何一个策略组覆盖范围内 */ public boolean isPointScheduleWithin(List pointIntervals) { if (pointIntervals == null || pointIntervals.isEmpty()) { return true; } if (strategyIntervals == null || strategyIntervals.isEmpty()) { return false; } for (List 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 pointIntervals) { return isPointScheduleWithin(pointIntervals); } /** * 区间列表归一化:merge 合并并排序。 */ private static List normalizeIntervals(List intervals) { return TimeInterval.merge(intervals); } /** * 比较两组区间在归一化(merge)后的覆盖范围是否完全一致。 */ private static boolean equalsIntervals(List a, List b) { List x = normalizeIntervals(a); List 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 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 parseRanges(JSONArray timeArray) { List 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 parseRangesDedup(JSONArray timeArray) { if (timeArray == null || timeArray.isEmpty()) { return Collections.emptyList(); } Set 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 result = new java.util.ArrayList<>(); for (String range : uniq) { result.addAll(parseRange(range)); } return result; } /** * 解析单个时间段字符串为区间列表。 *

* 注意:业务约束不会出现跨天时间段;若出现 end < start,则视为非法配置并忽略该时间段。 *

*/ private static List 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 merge(List intervals) { if (intervals == null || intervals.isEmpty()) { return Collections.emptyList(); } List sorted = intervals.stream() .filter(Objects::nonNull) .sorted(java.util.Comparator.comparingInt(a -> a.startMinute)) .collect(Collectors.toList()); if (sorted.isEmpty()) { return Collections.emptyList(); } List 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 pointIntervals, List allowedIntervals) { if (pointIntervals == null || pointIntervals.isEmpty()) { return true; } List 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-21:59\"]"; String strategyJson = "[{\"name\": \"策略组1\", \"time\": [\"06:00-22:00\", \"22:00-23:59\"], \"type\": 0, \"typeStr\": []}, {\"name\": \"策略组2\", \"time\": [\"06: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 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); } }