Forráskód Böngészése

掩蔽人数代码提交

fuyuchuan 1 napja
szülő
commit
7271b4a8cd

+ 4 - 2
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseDataTransferService.java

@@ -245,7 +245,9 @@ public class BaseDataTransferService {
         try {
             Map<Integer, Integer> userIdToName = new HashMap<>();
             userIdToName.put(702, 31);
-            userIdToName.put(703, 33);
+            // 20:人员统计-掩蔽人数双目统计摄像机
+            // 33:人员闯入-人员闯入监测传感器
+            userIdToName.put(703, 20);
             userIdToName.put(704, 11);
             userIdToName.put(705, 11);
             userIdToName.put(707, 19);
@@ -278,7 +280,7 @@ public class BaseDataTransferService {
             Gson gson = new Gson();
             String topic = "base/sensorInfo";
             System.out.println(gson.toJson(map));
-//            log.info("发送智能监管物联设施信息,Topic: {}, SensorID: {}", topic, vo.getSensorID());
+            log.info("发送智能监管物联设施信息,Topic: {}, SensorID: {}", topic, vo.getDeviceId());
             MqttConnectionTool.MqttGateway gateway = mqttConnectionTool.connectOrRefresh(vo.getUserName(), vo.getPassword());
             gateway.sendToMqtt(topic, gson.toJson(map));
 

+ 87 - 9
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/IotDataTransferService.java

@@ -4,16 +4,11 @@ import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.StringUtils;
-import com.usky.cdi.domain.CdiDefenseProject;
-import com.usky.cdi.domain.CdiDeliveryLog;
-import com.usky.cdi.domain.DmpDevice;
-import com.usky.cdi.domain.DmpProduct;
-import com.usky.cdi.mapper.CdiDefenseProjectMapper;
-import com.usky.cdi.mapper.CdiDeliveryLogMapper;
-import com.usky.cdi.mapper.DmpDeviceMapper;
-import com.usky.cdi.mapper.DmpProductMapper;
+import com.usky.cdi.domain.*;
+import com.usky.cdi.mapper.*;
 import com.usky.cdi.service.enums.MqttTopics;
 import com.usky.cdi.service.mqtt.MqttConnectionTool;
+import com.usky.cdi.service.util.AirDefenseSimulator;
 import com.usky.cdi.service.util.DeviceDataQuery;
 import com.usky.cdi.service.util.SnowflakeIdGenerator;
 import com.usky.cdi.service.vo.IotDataTransferVO;
@@ -37,6 +32,7 @@ import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ThreadLocalRandom;
 import java.util.stream.Collectors;
 
 /**
@@ -92,6 +88,9 @@ public class IotDataTransferService {
     @Autowired
     private CdiDeliveryLogMapper cdiDeliveryLogMapper;
 
+    @Autowired
+    private BaseBuildUnitMapper baseBuildUnitMapper;
+
     @PostConstruct
     public void init() {
         this.idGenerator = new SnowflakeIdGenerator(workerId, dataCenterId);
@@ -387,6 +386,81 @@ public class IotDataTransferService {
         return cdiDefenseProject.getTenantId();
     }
 
+    /**
+     * 发送人员统计情况(703)
+     *
+     * @return 推送结果,包含成功数和失败数
+     **/
+    public Map<String, Integer> sendPersonCount(IotDataTransferVO transferVO) {
+        Map<String, Integer> result = new HashMap<>();
+        result.put("successCount", 0);
+        result.put("failureCount", 0);
+
+        if (!validateMqttGateway(transferVO.getUsername())) {
+            return result;
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        long startTime = System.currentTimeMillis();
+
+        // 模拟人员统计
+        log.info("开始执行【人员统计】数据推送,工程ID:{}", transferVO.getEngineeringId());
+        try {
+            LambdaQueryWrapper<BaseBuildUnit> buildUnitQuery = new LambdaQueryWrapper<>();
+            buildUnitQuery.eq(BaseBuildUnit::getTenantId, transferVO.getTenantId());
+            List<BaseBuildUnit> buildUnitList = baseBuildUnitMapper.selectList(buildUnitQuery);
+
+            // 修复3:单元为空,打印日志+标记失败,不静默返回
+            if (buildUnitList.isEmpty()) {
+                log.warn("【人员统计】未查询到建筑单元,租户ID:{},推送终止", transferVO.getTenantId());
+                result.put("failureCount", 1);
+                return result;
+            }
+
+            log.info("【人员统计】获取到建筑单元数量:{}", buildUnitList.size());
+            for (BaseBuildUnit buildUnit : buildUnitList) {
+                int peopleCount = AirDefenseSimulator.calculatePeopleCount(now);
+                HeadcountVO headcountVO = new HeadcountVO();
+                headcountVO.setDataPacketID(generateDataPacketID());
+                headcountVO.setEngineeringID(transferVO.getEngineeringId());
+                headcountVO.setSensorValue(peopleCount);
+                // headcountVO.setSensorID(buildUnit.getId());
+                headcountVO.setDataEndTime(now.minusMinutes(ThreadLocalRandom.current().nextInt(1, 2)));
+                headcountVO.setPublishTime(now);
+                headcountVO.setUnitName(buildUnit.getUnitName());
+                headcountVO.setFloor(buildUnit.getFloor());
+
+                try {
+                    sendMqttMessage(
+                            MqttTopics.IotInfo.PERSON.getTopic(),
+                            headcountVO,
+                            MqttTopics.IotInfo.PERSON.getDesc(),
+                            transferVO.getUsername()
+                    );
+                    result.put("successCount", result.get("successCount") + 1);
+                    log.info("【人员统计】推送成功,单元:{},人数:{}", buildUnit.getUnitName(), peopleCount);
+                } catch (Exception e) {
+                    log.warn("【人员统计】数据推送失败,单元:{},异常:{}", buildUnit.getUnitName(), e.getMessage());
+                    result.put("failureCount", result.get("failureCount") + 1);
+                }
+            }
+
+            // 修复4:统计推送完成,打印汇总日志
+            log.info("【人员统计】推送完成,成功:{},失败:{}", result.get("successCount"), result.get("failureCount"));
+            // 可选:和闯入一样保存汇总日志(推荐加上)
+            long endTime = System.currentTimeMillis();
+            saveLog(transferVO, now, startTime, endTime, buildUnitList.size(),
+                    result.get("successCount"), result.get("failureCount"), 0,
+                    result.get("successCount") > 0 ? 1 : 0, SecurityUtils.getUsername());
+
+        } catch (Exception e) {
+            // 修复5:全局异常兜底,绝不丢失
+            log.error("【人员统计】推送发生全局异常", e);
+            result.put("failureCount", result.getOrDefault("failureCount", 0) + 1);
+        }
+        return result;
+    }
+
     /**
      * 发送人员闯入情况(703)
      *
@@ -403,7 +477,6 @@ public class IotDataTransferService {
 
         LocalDateTime now = LocalDateTime.now();
         long startTime = System.currentTimeMillis();
-
         try {
             List<JSONObject> deviceData = deviceDataQuery.getDeviceData(transferVO);
             Integer deviceType = transferVO.getDeviceType();
@@ -1083,6 +1156,7 @@ public class IotDataTransferService {
             transferVO.setDevices(devices);
             transferVO.setEngineeringId(engineeringId);
             transferVO.setUsername(username); // 保存当前任务的用户名
+            transferVO.setTenantId(tenantId);
             transferList.add(transferVO);
         });
 
@@ -1128,6 +1202,10 @@ public class IotDataTransferService {
                     result = sendWaterLeak(transferVO);
                     break;
                 case 703:
+                    if (transferVO.getEngineeringId() == 3101100024L) {
+                        result = sendPersonCount(transferVO);
+                        break;
+                    }
                     result = sendPersonPresence(transferVO);
                     break;
                 case 704:

+ 381 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/AirDefenseSimulator.java

@@ -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";
+    }
+}

+ 49 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/PeopleFlowData.java

@@ -0,0 +1,49 @@
+package com.usky.cdi.service.util;
+
+/**
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/5/30
+ */
+/**
+ * 人防掩蔽单元人员流量数据实体
+ */
+public class PeopleFlowData {
+    // 采样时间(HH:mm)
+    private String sampleTime;
+    // 上一时刻人数
+    private int lastPeopleCount;
+    // 本期监测入口流入人数
+    private int inFlow;
+    // 本期全出口流出人数
+    private int outFlow;
+    // 当前现有人数
+    private int currentPeopleCount;
+
+    // 构造器
+    public PeopleFlowData(String sampleTime, int lastPeopleCount, int inFlow, int outFlow, int currentPeopleCount) {
+        this.sampleTime = sampleTime;
+        this.lastPeopleCount = lastPeopleCount;
+        this.inFlow = inFlow;
+        this.outFlow = outFlow;
+        this.currentPeopleCount = currentPeopleCount;
+    }
+
+    // toString(方便控制台打印)
+    @Override
+    public String toString() {
+        return "采样时间:" + sampleTime +
+                " | 上一时刻人数:" + lastPeopleCount +
+                " | 本期流入:" + inFlow +
+                " | 本期流出:" + outFlow +
+                " | 当前现有人数:" + currentPeopleCount;
+    }
+
+    // Getter & Setter
+    public String getSampleTime() {return sampleTime;}
+    public int getLastPeopleCount() {return lastPeopleCount;}
+    public int getInFlow() {return inFlow;}
+    public int getOutFlow() {return outFlow;}
+    public int getCurrentPeopleCount() {return currentPeopleCount;}
+}

+ 6 - 1
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/IotDataTransferVO.java

@@ -25,9 +25,14 @@ public class IotDataTransferVO {
      * 产品ID
      */
     private Integer deviceType;
-    
+
     /**
      * MQTT用户名
      */
     private String username;
+
+    /**
+     * 租户ID
+     */
+    private Integer tenantId;
 }

+ 54 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/info/HeadcountVO.java

@@ -0,0 +1,54 @@
+package com.usky.cdi.service.vo.info;
+
+import com.usky.cdi.service.vo.base.BaseEnvMonitorPushVO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 人员统计情况推送VO(兼容2021版)
+ * 用途:每半小时上报最新人员统计状态,设防时段超过掩蔽人数需同步作为告警事件上传
+ * MQTT Topic:iotInfo/personPresence
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2025/11/20
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class HeadcountVO extends BaseEnvMonitorPushVO {
+
+    private static final long serialVersionUID = 1L;
+
+    // ====================== 专属字段(父类已包含公共字段)======================
+    /**
+     * 当前掩蔽人员数量(必填)
+     * 类型:Int,长度1(取值参考 SensorValueEnum)
+     */
+    private Integer sensorValue;
+
+    /**
+     * 当前单元名称(必填)
+     * 类型:String,长度1-30
+     */
+    private String unitName;
+
+    /**
+     * 当前楼层名称(必填)
+     * 类型:String,长度1-10
+     */
+    private String floor;
+
+    // ====================== 实现父类抽象方法 ======================
+    @Override
+    public Number getSensorValue() {
+        return sensorValue;
+    }
+
+    @Override
+    protected void validateSensorValue() {
+        // 1. 非空校验
+        if (sensorValue == null) {
+            throw new IllegalArgumentException("当前单元掩蔽人数(sensorValue)为必填项");
+        }
+
+    }
+}