|
|
@@ -0,0 +1,381 @@
|
|
|
+package com.usky.cdi.service.util;
|
|
|
+
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.List;
|
|
|
+import java.util.concurrent.ThreadLocalRandom;
|
|
|
+import java.util.concurrent.atomic.AtomicInteger;
|
|
|
+import java.util.concurrent.atomic.AtomicLong;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 人防掩蔽单元人数模拟工具类(动态自适应版)
|
|
|
+ * <p>
|
|
|
+ * 核心特性:
|
|
|
+ * 1. 即时计算:根据当前时间动态返回合理总人数,无预生成/硬编码条数限制
|
|
|
+ * 2. 自适应调用管理器:跟踪调用频率,动态评估并自动扩容阈值
|
|
|
+ * 3. 分时段基准曲线:每时段独立配置基准人数与波动范围,支持平滑插值
|
|
|
+ * 4. 边界安全:防负数、防无限循环、防溢出
|
|
|
+ *
|
|
|
+ * @author fyc
|
|
|
+ * @email yuchuan.fu@chinausky.com
|
|
|
+ * @date 2026/5/30
|
|
|
+ */
|
|
|
+public class AirDefenseSimulator {
|
|
|
+
|
|
|
+ // ===================== 可配置常量 =====================
|
|
|
+ private static final int MIN_PEOPLE = 0; // 最小人数(禁止负数)
|
|
|
+ private static final int DEFAULT_MAX_CALLS = 48; // 默认最大调用次数(向下兼容)
|
|
|
+ private static final int MAX_EXPANSION_LIMIT = 288; // 扩容上限(=24h*12次/5min,即5分钟粒度全天)
|
|
|
+ private static final double EXPANSION_FACTOR = 2.0; // 每次扩容倍数
|
|
|
+ private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
|
|
|
+
|
|
|
+ // ===================== 自适应调用管理器(单例状态) =====================
|
|
|
+ /** 已调用次数计数器 */
|
|
|
+ private static final AtomicInteger callCount = new AtomicInteger(0);
|
|
|
+ /** 当前动态阈值(初始为默认值,可自动扩容) */
|
|
|
+ private static final AtomicInteger dynamicThreshold = new AtomicInteger(DEFAULT_MAX_CALLS);
|
|
|
+ /** 上次调用时间戳(用于检测异常高频调用) */
|
|
|
+ private static final AtomicLong lastCallTimestamp = new AtomicLong(0);
|
|
|
+ /** 是否已触发过扩容标志 */
|
|
|
+ private static volatile boolean expanded = false;
|
|
|
+
|
|
|
+ // ===================== 时段规则定义(基准人数 + 波动范围)=====================
|
|
|
+ /**
|
|
|
+ * 时段规则:[起始小时, 结束小时) → {基准人数, 最小波动, 最大波动}
|
|
|
+ * 基准人数代表该时段中心时刻的典型值,波动范围为±随机偏移量
|
|
|
+ */
|
|
|
+ private static final int[][] TIME_SLOT_RULES = {
|
|
|
+ // 起始H 结束H 基准 波动Min 波动Max 规则说明
|
|
|
+ {0, 5, 0, 0, 0}, // 规则1: 00:00-05:00 人数为0
|
|
|
+ {5, 6, 3, 2, 3}, // 规则2: 05:00-06:00 缓慢上升
|
|
|
+ {6, 8, 12, 4, 6}, // 规则3: 06:00-08:00 攀升至第一高点
|
|
|
+ {8, 9, 25, 2, 5}, // 规则3延:08:00-09:00 第一高点后短暂调整
|
|
|
+ {9, 11, 31, 2, 5}, // 规则4: 09:00-11:00 趋于稳定
|
|
|
+ {11, 12, 19, 1, 3}, // 规则5: 11:00-12:00 大量流出(就餐)
|
|
|
+ {12, 13, 12, 1, 3}, // 规则5延:12:00-13:00 午间低谷
|
|
|
+ {13, 14, 21, 3, 6}, // 规则6: 13:00-14:00 开始回流
|
|
|
+ {14, 15, 30, 4, 7}, // 规则7: 14:00-15:00 达到第二高点
|
|
|
+ {15, 19, 31, 2, 5}, // 规则8: 15:00-19:00 保持稳定
|
|
|
+ {19, 21, 18, 3, 6}, // 规则9: 19:00-21:00 持续流出
|
|
|
+ {21, 22, 12, 2, 5}, // 规则9延:21:00-22:00 继续清场
|
|
|
+ {22, 24, 6, 1, 3} // 规则10:22:00-24:00 逐渐降至0
|
|
|
+ };
|
|
|
+
|
|
|
+ // ===================== 公开API:获取当前时刻掩蔽单元内的总人数 =====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 【核心方法】根据当前系统时间,动态计算并返回掩蔽单元内应有的模拟总人数。
|
|
|
+ * <p>
|
|
|
+ * 无需预生成数据,即时根据24小时时间规则曲线计算出合理人数,
|
|
|
+ * 支持任意时间粒度的连续调用,通过自适应管理器自动扩容保护。
|
|
|
+ *
|
|
|
+ * @return 当前时刻的模拟人数(int, >= 0)
|
|
|
+ */
|
|
|
+ public static int getCurrentPeopleCount() {
|
|
|
+ return calculatePeopleCount(LocalDateTime.now());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 【核心方法-指定时间版本】根据指定时间,动态计算掩蔽单元内应有的模拟总人数。
|
|
|
+ *
|
|
|
+ * @param targetTime 目标时间
|
|
|
+ * @return 该时刻的模拟人数(int, >= 0)
|
|
|
+ */
|
|
|
+ public static int calculatePeopleCount(LocalDateTime targetTime) {
|
|
|
+ // 1. 自适应检查:是否需要扩容
|
|
|
+ adaptiveCheck();
|
|
|
+
|
|
|
+ int hour = targetTime.getHour();
|
|
|
+ int minute = targetTime.getMinute();
|
|
|
+
|
|
|
+ // 2. 匹配对应时段规则
|
|
|
+ int[] rule = matchTimeRule(hour);
|
|
|
+ int baseCount = rule[2];
|
|
|
+ int fluctuationMin = rule[3];
|
|
|
+ int fluctuationMax = rule[4];
|
|
|
+
|
|
|
+ // 3. 时段内线性插值:使同一小时内不同分钟的人数更平滑自然
|
|
|
+ // 特殊处理:基准为0的时段(如00:00-05:00无人期)不参与插值,强制保持0
|
|
|
+ double minuteRatio = minute / 60.0;
|
|
|
+ int interpolatedBase = (baseCount == 0) ? 0 : interpolateWithinHour(hour, baseCount, minuteRatio);
|
|
|
+
|
|
|
+ // 4. 叠加随机波动
|
|
|
+ int fluctuation = ThreadLocalRandom.current().nextInt(fluctuationMin, fluctuationMax + 1);
|
|
|
+ boolean positiveTrend = isInRisingPhase(hour);
|
|
|
+ int peopleCount = interpolatedBase + (positiveTrend ? fluctuation : -fluctuation);
|
|
|
+
|
|
|
+ // 5. 边界约束
|
|
|
+ peopleCount = Math.max(peopleCount, MIN_PEOPLE);
|
|
|
+
|
|
|
+ // 6. 记录调用
|
|
|
+ recordCall();
|
|
|
+
|
|
|
+ return peopleCount / 4;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===================== 兼容接口:生成全天模拟数据列表(动态条数)=====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成当天全天的模拟数据列表。
|
|
|
+ * <p>
|
|
|
+ * 条数由自适应管理器的当前阈值决定(默认48条/半小时粒度),
|
|
|
+ * 当业务需求更高频时可自动扩容至更细粒度(如288条/5分钟粒度)。
|
|
|
+ *
|
|
|
+ * @return 模拟数据列表
|
|
|
+ */
|
|
|
+ public static List<PeopleFlowData> generateAllDayData() {
|
|
|
+ return generateAllDayData(dynamicThreshold.get());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成全天模拟数据列表,自定义数据条数。
|
|
|
+ * <p>
|
|
|
+ * 动态扩容策略:
|
|
|
+ * - requestedCount <= DEFAULT_MAX_CALLS(48): 正常生成,半小时粒度
|
|
|
+ * - 48 < requestedCount <= MAX_EXPANSION_LIMIT(288): 自动扩容,最小粒度5分钟
|
|
|
+ * - requestedCount > 288: 截断到上限,防止内存溢出
|
|
|
+ *
|
|
|
+ * @param requestedCount 请求数据条数
|
|
|
+ * @return 模拟数据列表(实际条数可能因边界截断而小于请求值)
|
|
|
+ */
|
|
|
+ public static List<PeopleFlowData> generateAllDayData(int requestedCount) {
|
|
|
+ // 边界约束:防止非法参数导致问题
|
|
|
+ int actualCount = normalizeRequestedCount(requestedCount);
|
|
|
+
|
|
|
+ List<PeopleFlowData> dataList = new ArrayList<>(actualCount);
|
|
|
+ long totalMinutes = 24 * 60;
|
|
|
+ long intervalMinutes = totalMinutes / actualCount;
|
|
|
+
|
|
|
+ LocalDateTime currentTime = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0).withNano(0);
|
|
|
+ int lastPeople = 0;
|
|
|
+
|
|
|
+ for (int i = 0; i < actualCount; i++) {
|
|
|
+ int currentPeople = calculatePeopleCount(currentTime);
|
|
|
+
|
|
|
+ // 反推流入流出(保持数据完整性)
|
|
|
+ int delta = currentPeople - lastPeople;
|
|
|
+ int inFlow, outFlow;
|
|
|
+ if (delta >= 0) {
|
|
|
+ inFlow = delta + ThreadLocalRandom.current().nextInt(0, 2);
|
|
|
+ outFlow = ThreadLocalRandom.current().nextInt(0, 2);
|
|
|
+ } else {
|
|
|
+ inFlow = ThreadLocalRandom.current().nextInt(0, 2);
|
|
|
+ outFlow = -delta + ThreadLocalRandom.current().nextInt(0, 2);
|
|
|
+ }
|
|
|
+
|
|
|
+ String timeStr = currentTime.format(TIME_FORMATTER);
|
|
|
+ dataList.add(new PeopleFlowData(timeStr, lastPeople, inFlow, outFlow, currentPeople));
|
|
|
+
|
|
|
+ lastPeople = currentPeople;
|
|
|
+ currentTime = currentTime.plusMinutes(intervalMinutes);
|
|
|
+ }
|
|
|
+ return dataList;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===================== 自适应调用管理器 =====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 自适应检查:在每次调用前评估是否需要触发扩容
|
|
|
+ */
|
|
|
+ private static void adaptiveCheck() {
|
|
|
+ int calls = callCount.incrementAndGet();
|
|
|
+ int threshold = dynamicThreshold.get();
|
|
|
+
|
|
|
+ if (calls > threshold && !expanded) {
|
|
|
+ synchronized (AirDefenseSimulator.class) {
|
|
|
+ // 双检锁,防止并发重复扩容
|
|
|
+ if (calls > dynamicThreshold.get() && !expanded) {
|
|
|
+ int newThreshold = expandThreshold(threshold);
|
|
|
+ dynamicThreshold.set(newThreshold);
|
|
|
+ expanded = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 动态扩容策略:按指数因子扩展阈值,但不超过硬性上限
|
|
|
+ *
|
|
|
+ * @param currentThreshold 当前阈值
|
|
|
+ * @return 扩容后的新阈值
|
|
|
+ */
|
|
|
+ private static int expandThreshold(int currentThreshold) {
|
|
|
+ int newThreshold = (int) Math.min(currentThreshold * EXPANSION_FACTOR, MAX_EXPANSION_LIMIT);
|
|
|
+ return Math.max(newThreshold, currentThreshold + 1); // 至少+1保证递增
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 记录一次有效调用
|
|
|
+ */
|
|
|
+ private static void recordCall() {
|
|
|
+ lastCallTimestamp.set(System.currentTimeMillis());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 归置自适应管理器状态(测试/重启场景使用)
|
|
|
+ */
|
|
|
+ public static void resetAdaptiveManager() {
|
|
|
+ callCount.set(0);
|
|
|
+ dynamicThreshold.set(DEFAULT_MAX_CALLS);
|
|
|
+ lastCallTimestamp.set(0);
|
|
|
+ expanded = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===================== 管理器状态查询API =====================
|
|
|
+
|
|
|
+ /** 获取已调用次数 */
|
|
|
+ public static int getCallCount() { return callCount.get(); }
|
|
|
+
|
|
|
+ /** 获取当前动态阈值 */
|
|
|
+ public static int getDynamicThreshold() { return dynamicThreshold.get(); }
|
|
|
+
|
|
|
+ /** 获取扩容上限 */
|
|
|
+ public static int getMaxExpansionLimit() { return MAX_EXPANSION_LIMIT; }
|
|
|
+
|
|
|
+ /** 是否已触发过扩容 */
|
|
|
+ public static boolean isExpanded() { return expanded; }
|
|
|
+
|
|
|
+ // ===================== 私有工具方法 =====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据小时数匹配对应的时段规则
|
|
|
+ */
|
|
|
+ private static int[] matchTimeRule(int hour) {
|
|
|
+ for (int[] rule : TIME_SLOT_RULES) {
|
|
|
+ if (hour >= rule[0] && hour < rule[1]) {
|
|
|
+ return rule;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 兜底:返回午夜规则
|
|
|
+ return TIME_SLOT_RULES[0];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断当前时段是否处于人数上升期(用于决定波动的正负方向)
|
|
|
+ */
|
|
|
+ private static boolean isInRisingPhase(int hour) {
|
|
|
+ return (hour >= 5 && hour < 9) || (hour >= 13 && hour < 15);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 时段内线性插值:根据分钟位置微调基准值,实现同小时内平滑过渡
|
|
|
+ * <p>
|
|
|
+ * 例如:6:00基准15人→8:00基准30人,6:30应约为22人而非突变为30人
|
|
|
+ */
|
|
|
+ private static int interpolateWithinHour(int hour, int baseCount, double minuteRatio) {
|
|
|
+ int[] currentRule = matchTimeRule(hour);
|
|
|
+ int nextHour = (hour + 1) % 24;
|
|
|
+ int[] nextRule = matchTimeRule(nextHour);
|
|
|
+ int nextBase = nextRule[2];
|
|
|
+
|
|
|
+ // 线性插值:当前基准 + (下一基准-当前基准) * 分钟占比
|
|
|
+ int interpolated = (int) Math.round(baseCount + (nextBase - baseCount) * minuteRatio * 0.3);
|
|
|
+ return Math.max(interpolated, MIN_PEOPLE);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 规范化请求数据条数:边界条件处理,防止越界
|
|
|
+ */
|
|
|
+ private static int normalizeRequestedCount(int requestedCount) {
|
|
|
+ if (requestedCount <= 0) {
|
|
|
+ return DEFAULT_MAX_CALLS; // 非法值回退默认
|
|
|
+ }
|
|
|
+ if (requestedCount > MAX_EXPANSION_LIMIT) {
|
|
|
+ return MAX_EXPANSION_LIMIT; // 截断到上限
|
|
|
+ }
|
|
|
+ return requestedCount;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===================== 测试主方法 =====================
|
|
|
+ public static void main(String[] args) {
|
|
|
+ System.out.println("========== 自适应人防掩蔽单元人数模拟器 ==========");
|
|
|
+ System.out.println("初始阈值: " + DEFAULT_MAX_CALLS + ", 上限: " + MAX_EXPANSION_LIMIT);
|
|
|
+
|
|
|
+ // ========== 核心:60次模拟调用测试 ==========
|
|
|
+ test60Calls();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 模拟60次调用 calculatePeopleCount,每次传入不同的时间点,
|
|
|
+ * 验证自适应扩容机制和时段规则的正确性
|
|
|
+ */
|
|
|
+ private static void test60Calls() {
|
|
|
+ final int TOTAL_CALLS = 60;
|
|
|
+
|
|
|
+ // 重置管理器,确保从干净状态开始
|
|
|
+ resetAdaptiveManager();
|
|
|
+
|
|
|
+ System.out.println("\n========== 【60次模拟调用测试】开始 ==========");
|
|
|
+ System.out.printf("初始状态 → 阈值:%d, 已调用:%d, 已扩容:%s%n",
|
|
|
+ getDynamicThreshold(), getCallCount(), isExpanded());
|
|
|
+ printSeparator(70);
|
|
|
+ System.out.printf("%-6s %-8s %-10s %-15s %-10s%n", "序号", "时间点", "返回人数", "时段规则描述", "当前阈值");
|
|
|
+ printSeparator(70);
|
|
|
+
|
|
|
+ LocalDateTime baseDate = LocalDateTime.now().toLocalDate().atStartOfDay();
|
|
|
+
|
|
|
+ for (int i = 0; i < TOTAL_CALLS; i++) {
|
|
|
+ // 每24分钟推进一次(60次 × 24min = 1440min = 24小时,刚好覆盖全天)
|
|
|
+ int totalMinutesElapsed = i * 24;
|
|
|
+ int hour = (totalMinutesElapsed / 60) % 24;
|
|
|
+ int minute = totalMinutesElapsed % 60;
|
|
|
+
|
|
|
+ LocalDateTime targetTime = baseDate.plusMinutes(totalMinutesElapsed);
|
|
|
+ int peopleCount = calculatePeopleCount(targetTime);
|
|
|
+
|
|
|
+ // 获取时段描述
|
|
|
+ String timeSlotDesc = getTimeSlotDescription(hour);
|
|
|
+ String timeStr = String.format("%02d:%02d", hour, minute);
|
|
|
+
|
|
|
+ System.out.printf("%-6d %-8s %-10d %-15s %-10d%n",
|
|
|
+ i + 1, timeStr, peopleCount, timeSlotDesc, getDynamicThreshold());
|
|
|
+ }
|
|
|
+
|
|
|
+ printSeparator(70);
|
|
|
+ System.out.printf("最终状态 → 总调用:%d, 阈值:%d, 已扩容:%b%n",
|
|
|
+ getCallCount(), getDynamicThreshold(), isExpanded());
|
|
|
+
|
|
|
+ // 验证结论
|
|
|
+ if (getCallCount() >= TOTAL_CALLS) {
|
|
|
+ System.out.println("[PASS] 60次调用全部完成,自适应机制正常工作");
|
|
|
+ } else {
|
|
|
+ System.out.println("[WARN] 调用次数异常: 期望" + TOTAL_CALLS + ", 实际" + getCallCount());
|
|
|
+ }
|
|
|
+ if (getDynamicThreshold() > DEFAULT_MAX_CALLS) {
|
|
|
+ System.out.println("[PASS] 阈值已自动扩容: " + DEFAULT_MAX_CALLS + " → " + getDynamicThreshold()
|
|
|
+ + "(因调用次数超过原阈值触发)");
|
|
|
+ }
|
|
|
+ System.out.println("========== 【60次模拟调用测试】结束 ==========");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Java8兼容:打印指定长度的分隔线(替代 String.repeat())
|
|
|
+ */
|
|
|
+ private static void printSeparator(int length) {
|
|
|
+ StringBuilder sb = new StringBuilder(length);
|
|
|
+ for (int i = 0; i < length; i++) {
|
|
|
+ sb.append('─');
|
|
|
+ }
|
|
|
+ System.out.println(sb.toString());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据小时数返回时段规则描述(用于测试输出)
|
|
|
+ */
|
|
|
+ private static String getTimeSlotDescription(int hour) {
|
|
|
+ if (hour < 5) return "00-05无人";
|
|
|
+ if (hour < 6) return "05-06缓升";
|
|
|
+ if (hour < 8) return "06-08第一高点";
|
|
|
+ if (hour < 9) return "08-09调整";
|
|
|
+ if (hour < 11) return "09-11稳定";
|
|
|
+ if (hour < 12) return "11-12就餐流出";
|
|
|
+ if (hour < 13) return "12-13午间低谷";
|
|
|
+ if (hour < 14) return "13-14回流";
|
|
|
+ if (hour < 15) return "14-15第二高点";
|
|
|
+ if (hour < 19) return "15-19稳定";
|
|
|
+ if (hour < 21) return "19-21持续流出";
|
|
|
+ if (hour < 22) return "21-22清场";
|
|
|
+ return "22-00降至0";
|
|
|
+ }
|
|
|
+}
|