| | |
| | | import com.alibaba.fastjson2.JSONObject; |
| | | |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.Set; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | | * 围栏运营策略时间范围索引工具类 |
| | | */ |
| | | public final class OperationStrategyIndex { |
| | | private final Map<Integer, List<TimeInterval>> allowedByType; |
| | | private final List<List<TimeInterval>> strategyIntervals; |
| | | |
| | | /** |
| | | * 构造索引(内部使用)。 |
| | | * |
| | | * @param allowedByType 按 type 聚合后的允许时间区间 |
| | | * @param strategyIntervals 策略组时间区间列表(每个策略组已 merge) |
| | | */ |
| | | private OperationStrategyIndex(Map<Integer, List<TimeInterval>> allowedByType) { |
| | | this.allowedByType = allowedByType; |
| | | private OperationStrategyIndex(List<List<TimeInterval>> strategyIntervals) { |
| | | this.strategyIntervals = strategyIntervals == null ? Collections.emptyList() : strategyIntervals; |
| | | } |
| | | |
| | | /** |
| | | * 解析围栏 {@code operation_strategy} JSON 字符串为运营时间范围索引。 |
| | | * <p> |
| | | * 说明:会按 {@code type} 聚合并合并时间段,最终用于“时间覆盖范围”判断。 |
| | | * 说明:会按“策略组”聚合并合并时间段;点位只要被任意一个策略组的时间段完整覆盖,则为正常。 |
| | | * </p> |
| | | * |
| | | * @param operationStrategyJson 围栏运营策略 JSON(数组字符串) |
| | |
| | | */ |
| | | public static OperationStrategyIndex parse(String operationStrategyJson) { |
| | | if (operationStrategyJson == null || operationStrategyJson.isEmpty()) { |
| | | return new OperationStrategyIndex(Collections.emptyMap()); |
| | | return new OperationStrategyIndex(Collections.emptyList()); |
| | | } |
| | | try { |
| | | JSONArray jsonArray = JSONArray.parseArray(operationStrategyJson); |
| | | if (jsonArray == null || jsonArray.isEmpty()) { |
| | | return new OperationStrategyIndex(Collections.emptyMap()); |
| | | return new OperationStrategyIndex(Collections.emptyList()); |
| | | } |
| | | Map<Integer, List<TimeInterval>> map = new java.util.HashMap<>(); |
| | | 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; |
| | | } |
| | | Integer type = obj.getInteger("type"); |
| | | JSONArray timeArray = obj.getJSONArray("time"); |
| | | if (type == null || timeArray == null || timeArray.isEmpty()) { |
| | | if (timeArray == null || timeArray.isEmpty()) { |
| | | continue; |
| | | } |
| | | List<TimeInterval> intervals = TimeInterval.parseRanges(timeArray); |
| | | if (intervals.isEmpty()) { |
| | | continue; |
| | | List<TimeInterval> intervals = TimeInterval.parseRangesDedup(timeArray); |
| | | List<TimeInterval> merged = TimeInterval.merge(intervals); |
| | | if (!merged.isEmpty()) { |
| | | groups.add(merged); |
| | | } |
| | | map.merge(type, intervals, (a, b) -> { |
| | | List<TimeInterval> merged = new java.util.ArrayList<>(a.size() + b.size()); |
| | | merged.addAll(a); |
| | | merged.addAll(b); |
| | | return TimeInterval.merge(merged); |
| | | }); |
| | | } |
| | | map.replaceAll((k, v) -> TimeInterval.merge(v)); |
| | | return new OperationStrategyIndex(map); |
| | | return new OperationStrategyIndex(groups); |
| | | } catch (Exception e) { |
| | | return new OperationStrategyIndex(Collections.emptyMap()); |
| | | return new OperationStrategyIndex(Collections.emptyList()); |
| | | } |
| | | } |
| | | |
| | |
| | | * @return 时间区间列表;解析失败返回空列表 |
| | | */ |
| | | public static List<TimeInterval> parsePointEffectiveTimePeriod(String jsonArrayStr) { |
| | | return TimeInterval.parseJsonRanges(jsonArrayStr); |
| | | return TimeInterval.parseJsonRangesDedup(jsonArrayStr); |
| | | } |
| | | |
| | | /** |
| | |
| | | * @return Union 区间列表(已 merge 并排序) |
| | | */ |
| | | public List<TimeInterval> unionAllowed() { |
| | | if (allowedByType == null || allowedByType.isEmpty()) { |
| | | if (strategyIntervals == null || strategyIntervals.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | List<TimeInterval> union = allowedByType.values().stream() |
| | | List<TimeInterval> union = strategyIntervals.stream() |
| | | .filter(Objects::nonNull) |
| | | .flatMap(List::stream) |
| | | .collect(Collectors.toList()); |
| | |
| | | } |
| | | |
| | | /** |
| | | * 判断点位的时间段是否完全落在围栏 Union 运营时间覆盖范围内。 |
| | | * 判断点位的时间段是否为正常。 |
| | | * <p> |
| | | * 规则:只要点位的全部运营时间段被围栏“任意一个策略组”的时间段范围完整覆盖,则为正常。 |
| | | * </p> |
| | | * |
| | | * @param pointIntervals 点位时间区间列表 |
| | | * @return true 表示点位每段时间都被围栏 Union 覆盖 |
| | | * @return true 表示正常;false 表示不在任何一个策略组覆盖范围内 |
| | | */ |
| | | public boolean isPointScheduleWithin(List<TimeInterval> pointIntervals) { |
| | | if (pointIntervals == null || pointIntervals.isEmpty()) { |
| | | return true; |
| | | } |
| | | return TimeInterval.isWithin(pointIntervals, this.unionAllowed()); |
| | | if (strategyIntervals == null || strategyIntervals.isEmpty()) { |
| | | return false; |
| | | } |
| | | for (List<TimeInterval> groupIntervals : strategyIntervals) { |
| | | if (TimeInterval.isWithin(pointIntervals, groupIntervals)) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | |
| | | * @param jsonArrayStr JSON 数组字符串(例如 ["08:00-12:00","13:00-18:00"]) |
| | | * @return merge 后的区间列表;解析失败返回空列表 |
| | | */ |
| | | static List<TimeInterval> parseJsonRanges(String jsonArrayStr) { |
| | | static List<TimeInterval> parseJsonRangesDedup(String jsonArrayStr) { |
| | | if (jsonArrayStr == null || jsonArrayStr.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | |
| | | if (arr == null || arr.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | return merge(parseRanges(arr)); |
| | | return merge(parseRangesDedup(arr)); |
| | | } catch (Exception e) { |
| | | return Collections.emptyList(); |
| | | } |
| | |
| | | * 解析 JSONArray 中的时间段为区间列表(不做 merge)。 |
| | | * |
| | | * @param timeArray 时间段数组(元素形如 "HH:mm-HH:mm") |
| | | * @return 区间列表(跨天会拆分为两段) |
| | | * @return 区间列表 |
| | | */ |
| | | static List<TimeInterval> parseRanges(JSONArray timeArray) { |
| | | List<TimeInterval> result = new java.util.ArrayList<>(); |
| | |
| | | } |
| | | |
| | | /** |
| | | * 解析单个时间段字符串为区间列表(支持跨天拆分:如 22:00-02:00)。 |
| | | * 解析 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 < start,则视为非法配置并忽略该时间段。 |
| | | * </p> |
| | | */ |
| | | private static List<TimeInterval> parseRange(String range) { |
| | | if (range == null || !range.contains("-")) { |
| | |
| | | return Collections.emptyList(); |
| | | } |
| | | if (end < start) { |
| | | List<TimeInterval> list = new java.util.ArrayList<>(2); |
| | | list.add(new TimeInterval(start, 23 * 60 + 59)); |
| | | list.add(new TimeInterval(0, end)); |
| | | return list; |
| | | return Collections.emptyList(); |
| | | } |
| | | return java.util.Collections.singletonList(new TimeInterval(start, end)); |
| | | } |
| | |
| | | * main 自测:使用固定样例打印 old/new Union、是否修改/缩短,以及点位在旧/新范围内判断结果。 |
| | | */ |
| | | public static void main(String[] args) { |
| | | String oldJson = "[{\"name\": \"策略组1\", \"time\": [\"08:00-23:59\"], \"type\": 0, \"typeStr\": []}, {\"name\": \"策略组2\", \"time\": [\"07:00-23:59\"], \"type\": 2, \"typeStr\": \"\"}, {\"name\": \"策略组3\", \"time\": [\"02:00-23:59\"], \"type\": 1, \"typeStr\": \"\"}]"; |
| | | String newJson = "[{\"name\": \"策略组1\", \"time\": [\"08:00-23:59\"], \"type\": 0, \"typeStr\": []}, {\"name\": \"策略组2\", \"time\": [\"07:00-23:59\"], \"type\": 2, \"typeStr\": \"\"}, {\"name\": \"策略组3\", \"time\": [\"02:00-20:59\"], \"type\": 1, \"typeStr\": \"\"}]"; |
| | | String pointJson = "[\"07:00-13:00\", \"13:00-23:59\"]"; |
| | | 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 oldIndex = OperationStrategyIndex.parse(oldJson); |
| | | OperationStrategyIndex newIndex = OperationStrategyIndex.parse(newJson); |
| | | OperationStrategyIndex index = OperationStrategyIndex.parse(strategyJson); |
| | | List<TimeInterval> pointIntervals = OperationStrategyIndex.parsePointEffectiveTimePeriod(pointJson); |
| | | |
| | | System.out.println("oldUnion=" + oldIndex.unionAllowed()); |
| | | System.out.println("newUnion=" + newIndex.unionAllowed()); |
| | | System.out.println("equalsNormalized=" + oldIndex.equalsNormalized(newIndex)); |
| | | System.out.println("hasAnyShrinkComparedTo=" + oldIndex.hasAnyShrinkComparedTo(newIndex)); |
| | | boolean inOld = oldIndex.isPointScheduleWithin(pointIntervals); |
| | | boolean inNew = newIndex.isPointScheduleWithin(pointIntervals); |
| | | System.out.println("point(inOld,inNew)=(" + inOld + "," + inNew + ")"); |
| | | boolean shouldDisable = !newIndex.isPointScheduleWithin(pointIntervals); |
| | | System.out.println("shouldDisableByUnion=" + shouldDisable); |
| | | System.out.println("pointIntervals=" + pointIntervals); |
| | | System.out.println("fenceUnion=" + index.unionAllowed()); |
| | | boolean ok = index.isPointScheduleWithin(pointIntervals); |
| | | System.out.println("isNormal=" + ok); |
| | | } |
| | | } |