yanderong
2026-09-15 868b19dddc1a28cdcf2703dac7a07df4b0b74221
feat: 储能数据同步,新运维大楼大屏的配置
1个文件已删除
1个文件已添加
9个文件已修改
966 ■■■■■ 已修改文件
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/emind/domain/EmindStation.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/emind/service/EmindStationManagementService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/config/UdanEmsKafkaProperties.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/EmsState.java 488 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/PowerMeterState.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/UdanEmsDatabaseRecord.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/UdanEmsDeviceState.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/service/UdanEmsMessageProcessor.java 319 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/util/UdanEmsKafkaUtil.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/resources/mapper/other/EmindStationMapper.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/resources/sql/t_screen_content_alter.sql 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/emind/domain/EmindStation.java
@@ -85,4 +85,8 @@
    /** Last update time. */
    @ApiModelProperty("Last update time")
    private LocalDateTime updateTime;
    /**
     * 电站设备编号
     */
    private String stationCode;
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/emind/service/EmindStationManagementService.java
@@ -48,13 +48,14 @@
        station.setMaxPower(input.getMaxPower());
        station.setCapacity(input.getCapacity());
        station.setCommissioningTime(input.getCommissioningTime());
        station.setPlatformName(EmindStation.DEFAULT_PLATFORM_NAME);
        station.setPlatformName(input.getPlatformName());
        station.setSourceType(EmindStation.SOURCE_MANUAL);
        station.setManagementDeleted(0);
        station.setDelFlag(0);
        station.setSyncedAt(now);
        station.setCreateTime(now);
        station.setUpdateTime(now);
        station.setStationCode(input.getStationCode());
        mapper.insert(station);
        return station;
    }
@@ -80,6 +81,7 @@
        station.setCapacity(input.getCapacity());
        station.setCommissioningTime(input.getCommissioningTime());
        station.setUpdateTime(LocalDateTime.now(clock));
        station.setStationCode(input.getStationCode());
        mapper.updateById(station);
        return station;
    }
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/config/UdanEmsKafkaProperties.java
@@ -16,5 +16,5 @@
    private String topic;
    private String groupId = "group-device-state";
    private String autoOffsetReset = "latest";
    private int pollSeconds = 3600;
    private int pollSeconds = 3600; // 1个小时
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/EmsState.java
New file
@@ -0,0 +1,488 @@
package com.ruoyi.other.udan.model;
import lombok.Data;
import java.io.Serializable;
/**
 * 优旦储能-设备
 * @author Lux
 * @version 1.0.0
 * @since 2026/9/14 17:59
 */
@Data
public class EmsState implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 设备id (可关联系统配置的电站设备号)
     * 只能由字母、数字和下划线组成,长度不超过50个字节
     */
    private String deviceId;
    /**
     * 系统状态
     */
    private String emsStatus;
    /**
     * 需量
     */
    private Float demandPower;
    /**
     * 4G卡
     */
    private String emsSimCard;
    /**
     * 时间
     * 1970-01-01T00:00:00Z 以来的毫秒数
     */
    private Long time;
    /**
     * proto协议版本
     */
    private String protoProtocolVersion;
    /**
     * 运行状态
     */
    private EmsRunStatus emsRunStatus;
    /**
     * 工作状态
     */
    private EmsWorkStatus emsWorkStatus;
    /**
     * 故障状态
     */
    private ErrorStatus faultStatus;
    /**
     * 并离网状态
     */
    private String onGridStatus;
    /**
     * 控制源
     */
    private ControlSrc controlSrc;
    /**
     * 指令模式
     */
    private EmuModeCode emuModeCode;
    /**
     * 计划模式
     */
    private EmuAutoMode emuAutoMod;
    /**
     * 防逆流控制
     */
    private YesNo overDischargeProtect;
    /**
     * 防过载控制
     */
    private YesNo overChargeProtect;
    /**
     * 需量控制
     */
    private YesNo demandControl;
    /**
     * 动态增容控制
     */
    private YesNo capacityExpansionModeCtrl;
    /**
     * 日充电量
     * 单位:kWh
     */
    private Float dailyChargeEnergyKwh;
    /**
     * 日放电量
     * 单位:kWh
     */
    private Float dailyDischargeEnergyKwh;
    /**
     * 月充电量
     * 单位:kWh
     */
    private Float monthlyChargeEnergyKwh;
    /**
     * 月放电量
     * 单位:kWh
     */
    private Float monthlyDischargeEnergyKwh;
    /**
     * 累计充电量
     * 单位:kWh
     */
    private Double totalChargeEnergyKwh;
    /**
     * 累计放电量
     * 单位:kWh
     */
    private Double totalDischargeEnergyKwh;
    /**
     * PCS总有功功率
     * 单位:kW
     */
    private Float pcsSumsActivePower;
    /**
     * PCS总视在功率
     * 单位:kVar
     */
    private Float pcsSumsApparentPower;
    /**
     * 直流母线总电压
     * 单位:V
     */
    private Float dcBusVoltage;
    /**
     * 直流母线总电流
     * 单位:A
     */
    private Float dcBusCurrent;
    /**
     * 总SOC
     * 单位:%
     */
    private Float sumsSoc;
    /**
     * 电池总节数
     */
    private Integer cellSums;
    /**
     * 电池平均温度
     * 单位:°C
     */
    private Float batteryAverageTempr;
    /**
     * 电池最大温差
     * 单位:°C
     */
    private Float batteryMaxTemprDiff;
    /**
     * 最高单体电压值
     * 单位:mV
     */
    private Integer maxCellVoltage;
    /**
     * 最低单体电压值
     * 单位:mV
     */
    private Integer minCellVoltage;
    /**
     * 系统最大允许充电功率
     * 单位:kW
     */
    private Float systemMaxAllowChgPower;
    /**
     * 系统最大允许放电功率
     * 单位:kW
     */
    private Float systemMaxAllowDisPower;
    /**
     * 可充电量
     * 单位:kWh
     */
    private Float rechargeableKwh;
    /**
     * 可放电量
     * 单位:kWh
     */
    private Float dischargeableKwh;
    /**
     * 当前时刻最大允许充电功率(汇总)
     * 单位:kW
     */
    private Float currentMaxAllowChgPower;
    /**
     * 当前时刻最大允许放电功率(汇总)
     * 单位:kW
     */
    private Float currentMaxAllowDisPower;
    /**
     * SOC上限
     * 单位:%
     */
    private Float socUpperLimit;
    /**
     * SOC下限
     * 单位:%
     */
    private Float socLowerLimit;
    /**
     * 最大功率放电时长
     * 单位:s
     */
    private Integer maxPowerDischargeDuration;
    /**
     * 最大功率充电时长
     * 单位:s
     */
    private Integer maxPowerChargeDuration;
    /**
     * 与主机通讯状态
     */
    private String masterCommStatus;
    /**
     * 并网点正向有功电能
     * 单位:kWh
     */
    private Double gridImportActiveEnergyKwh;
    /**
     * 并网点反向有功电能
     * 单位:kWh
     */
    private Double gridExportActiveEnergyKwh;
    /**
     * 直流侧总功率
     * 单位:kW
     */
    private Float dcTotalPower;
    /**
     * 负载功率
     * 正放负充
     * 单位:kW
     */
    private Float loadTotalPower;
    /**
     * 储能侧总功率
     * 正放负充
     * 单位:kW
     */
    private Float energyStoreTotalPower;
    /**
     * 高压侧电网功率
     * 正放负充
     * 单位:kW
     */
    private Float highSideGridPower;
    /**
     * 低压侧电网功率
     * 正放负充
     * 单位:kW
     */
    private Float lowSideGridPower;
    /**
     * 光伏功率
     * 正放负充
     * 单位:kW
     */
    private Float pvPower;
    /**
     * 总SOH
     * 单位:%
     */
    private Float sumsSoh;
    public enum ErrorStatus {
        ERROR_STATUS_UNKNOWN(0), ERROR_STATUS_OCCURRING(1), ERROR_STATUS_NONE(2);
        private final int code;
        ErrorStatus(int code) { this.code = code; }
        public int getCode() { return code; }
    }
    /**
     * EMS运行状态
     */
    public enum EmsRunStatus {
        /**
         * 预留
         */
        EMS_RUN_STATUS_UNKNOWN(0),
        /**
         * 停止
         */
        EMS_RUN_STATUS_STOPPED(1),
        /**
         * 待机
         */
        EMS_RUN_STATUS_IDLE(2),
        /**
         * 运行
         */
        EMS_RUN_STATUS_RUNNING(3),
        /**
         * 测试
         */
        EMS_RUN_STATUS_TEST(4);
        private final int code;
        EmsRunStatus(int code) {
            this.code = code;
        }
        public int getCode() {
            return code;
        }
    }
    /**
     * EMS工作状态
     */
    public enum EmsWorkStatus {
        EMS_WORK_STATUS_UNKNOWN(0),
        EMS_WORK_STATUS_CHARGING(1),
        EMS_WORK_STATUS_DISCHARGING(2),
        EMS_WORK_STATUS_IDLE(3);
        private final int code;
        EmsWorkStatus(int code) {
            this.code = code;
        }
        public int getCode() {
            return code;
        }
    }
    /**
     * 控制源
     */
    public enum ControlSrc {
        CONTROL_SRC_UNKNOWN(0),
        CONTROL_SRC_LOCAL(1),
        CONTROL_SRC_REMOTE(2),
        CONTROL_SRC_OTHER(3);
        private final int code;
        ControlSrc(int code) {
            this.code = code;
        }
        public int getCode() {
            return code;
        }
    }
    /**
     * 指令模式
     */
    public enum EmuModeCode {
        EMU_MODE_CODE_UNKNOWN(0),
        EMU_MODE_CODE_SUB_SYSTEM(1),
        EMU_MODE_CODE_PLAN(2),
        EMU_MODE_CODE_GENERAL_INSTRUCTION(3);
        private final int code;
        EmuModeCode(int code) {
            this.code = code;
        }
        public int getCode() {
            return code;
        }
    }
    /**
     * 计划模式
     */
    public enum EmuAutoMode {
        EMU_AUTO_MODE_UNKNOWN(0),
        EMU_AUTO_MODE_24_HOUR_SCHEDULED_MODE(1),
        EMU_AUTO_MODE_DEMAND_RESPONSE_SCHEDULED_MODE(2);
        private final int code;
        EmuAutoMode(int code) {
            this.code = code;
        }
        public int getCode() {
            return code;
        }
    }
    /**
     * 保留
     */
    public enum YesNo {
        YES_NO_UNKNOWN(0), // 保留
        YES_NO_YES(1), // 是
        YES_NO_NO(2); // 否
        private final int code;
        YesNo(int code) {
            this.code = code;
        }
        public int getCode() {
            return code;
        }
    }
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/PowerMeterState.java
@@ -2,13 +2,18 @@
import lombok.Data;
import java.io.Serializable;
/**
 * @author Lux
 * @version 1.0.0
 * @since 2026/9/10 17:55
 */
@Data
public class PowerMeterState {
public class PowerMeterState implements Serializable {
    private static final long serialVersionUID = 1L;
    /** 峰用电总价,单位:元 */
    private double cumulativeImportAmountPeak;
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/UdanEmsDatabaseRecord.java
File was deleted
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/UdanEmsDeviceState.java
@@ -1,35 +1,60 @@
package com.ruoyi.other.udan.model;
import com.ruoyi.other.emind.dto.EmindIncomeHistoryDto;
import lombok.Data;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/** iems-open-v1.0.5.proto 中 IEmsAllDeviceState 对应的业务对象。 */
@Data
public class UdanEmsDeviceState {
    private String emsSystemId;
    private long time;
    private EmsState ems;
    private List<PcsState> pcsStates = new ArrayList<>();
    private List<BatteryStackState> batteryStackStates = new ArrayList<>();
    private List<BatteryClusterState> batteryClusterStates = new ArrayList<>();
    private List<ElectricMeterState> electricMeterStates = new ArrayList<>();
    private List<PvState> pvStates = new ArrayList<>();
    private List<ChargeStationState> chargeStationStates = new ArrayList<>();
    private int powerMeterCount;
    private int airConditionerCount;
    private int fireCount;
    private int jpfCount;
    private int tmsCount;
    private int dehumidifierCount;
    private int upsCount;
    private int extPeripheralCount;
    private String emsDeviceId;
    private EmsState emsState;
    private PowerMeterState powerMeterState;
    @Data public static class EmsState { private String deviceId; private int emsStatus; private float demandPower; private long time; private String protoProtocolVersion; private int emsRunStatus; private int emsWorkStatus; private int faultStatus; private int onGridStatus; private float dailyChargeEnergyKwh; private float dailyDischargeEnergyKwh; private float monthlyChargeEnergyKwh; private float monthlyDischargeEnergyKwh; private double totalChargeEnergyKwh; private double totalDischargeEnergyKwh; private float pcsSumsActivePower; private float sumsSoc; private float sumsSoh; private float dcBusVoltage; private float dcBusCurrent; }
    @Data public static class PcsState { private long time; private String deviceId; private int errorStatus; private int alarmStatus; private int runStatus; private int onGridStatus; private float acTotalActivePower; private float acTotalReactivePower; private float acTotalApparentPower; private float acTotalPowerFactor; private List<Float> dcTotalVoltage = new ArrayList<>(); private List<Float> dcCurrent = new ArrayList<>(); private List<Float> dcPower = new ArrayList<>(); private float dailyChargeEnergyKwh; private float dailyDischargeEnergyKwh; private double totalChargeEnergyKwh; private double totalDischargeEnergyKwh; private int controlMode; private int workStatus; private float ambientTemp; private int commStatus; }
    @Data public static class BatteryStackState { private long time; private String deviceId; private int workStatus; private int errorStatus; private float tv; private float tc; private float soh; private float soc; private float rechargeableCapacityKwh; private float dischargeableCapacityKwh; private double cumulativeDischargeKwh; private double cumulativeChargeKwh; private int avgCellVoltageMv; private int avgCellTemp; private int batteryStatus; private int commStatus; }
    @Data public static class BatteryClusterState { private long time; private String deviceId; private String stackDeviceId; private int workStatus; private float tv; private float tc; private float soh; private float soc; private float rechargeableCapacityKwh; private float dischargeableCapacityKwh; private double cumulativeChargeKwh; private double cumulativeDischargeKwh; private int avgCellTemp; private int avgCellVoltMv; private int commStatus; }
    @Data public static class ElectricMeterState { private long time; private String deviceId; private int meterType; private double cumulativeImportKwh; private double cumulativeExportKwh; private float acTotalActivePower; private float aPhaseActivePower; private float bPhaseActivePower; private float cPhaseActivePower; private int commStatus; }
    @Data public static class PvState { private String deviceId; private int commStatus; private float activePower; private float reactivePower; private float powerFactor; private float dailyPowerGeneration; private double totalPowerGeneration; private int runStatus; private int errorStatus; private int alarmStatus; }
    @Data public static class ChargeStationState { private String deviceId; private long time; private int runStatus; private int commStatus; }
    public List<EmindIncomeHistoryDto> convert(Long stationId) {
        List<EmindIncomeHistoryDto> dailyList = new ArrayList<>();
        if(Objects.nonNull(this.getPowerMeterState())) {
            EmindIncomeHistoryDto daily = new EmindIncomeHistoryDto();
            daily.setStationId(stationId);
            Optional.of(getPowerMeterState().getTime()).ifPresent(t -> {
                LocalDate localDate = Instant.ofEpochMilli(t).atZone(ZoneId.systemDefault()).toLocalDate();
                daily.setDate(localDate);
            });
            daily.setPrice(getPowerMeterState().getDailyImportAmount());
            daily.setPriceName("");
            Optional.of(getPowerMeterState().getDailyProfit()).map(String::valueOf).map(BigDecimal::new).ifPresent(daily::setIncome);
            Optional.of(getPowerMeterState().getDailyExportKwhValley()).map(String::valueOf).map(BigDecimal::new).ifPresent(daily::setDischargedEnergyValley);
            Optional.of(getPowerMeterState().getDailyExportKwhSharp()).map(String::valueOf).map(BigDecimal::new).ifPresent(daily::setDischargedEnergySharp);
            Optional.of(getPowerMeterState().getDailyExportKwhPeak()).map(String::valueOf).map(BigDecimal::new).ifPresent(daily::setDischargedEnergyPeak);
            Optional.of(getPowerMeterState().getDailyExportKwhShoulder()).map(String::valueOf).map(BigDecimal::new).ifPresent(daily::setDischargedEnergyFlat);
            Optional.of(getPowerMeterState().getDailyExportKwh()).map(String::valueOf).map(BigDecimal::new).ifPresent(daily::setDischargedEnergy);
            Optional.of(getPowerMeterState().getDailyImportKwhValley()).map(String::valueOf).map(BigDecimal::new).ifPresent(daily::setChargedEnergyValley);
            Optional.of(getPowerMeterState().getDailyImportKwhSharp()).map(String::valueOf).map(BigDecimal::new).ifPresent(daily::setChargedEnergySharp);
            Optional.of(getPowerMeterState().getDailyImportKwhPeak()).map(String::valueOf).map(BigDecimal::new).ifPresent(daily::setChargedEnergyPeak);
            Optional.of(getPowerMeterState().getDailyImportKwhShoulder()).map(String::valueOf).map(BigDecimal::new).ifPresent(daily::setChargedEnergyFlat);
            Optional.of(getPowerMeterState().getDailyImportKwh()).map(String::valueOf).map(BigDecimal::new).ifPresent(daily::setChargedEnergy);
            dailyList.add(daily);
        }
        return dailyList;
    }
    // @Data public static class EmsState { private String deviceId; private int emsStatus; private float demandPower; private long time; private String protoProtocolVersion; private int emsRunStatus; private int emsWorkStatus; private int faultStatus; private int onGridStatus; private float dailyChargeEnergyKwh; private float dailyDischargeEnergyKwh; private float monthlyChargeEnergyKwh; private float monthlyDischargeEnergyKwh; private double totalChargeEnergyKwh; private double totalDischargeEnergyKwh; private float pcsSumsActivePower; private float sumsSoc; private float sumsSoh; private float dcBusVoltage; private float dcBusCurrent; }
   // @Data public static class PcsState { private long time; private String deviceId; private int errorStatus; private int alarmStatus; private int runStatus; private int onGridStatus; private float acTotalActivePower; private float acTotalReactivePower; private float acTotalApparentPower; private float acTotalPowerFactor; private List<Float> dcTotalVoltage = new ArrayList<>(); private List<Float> dcCurrent = new ArrayList<>(); private List<Float> dcPower = new ArrayList<>(); private float dailyChargeEnergyKwh; private float dailyDischargeEnergyKwh; private double totalChargeEnergyKwh; private double totalDischargeEnergyKwh; private int controlMode; private int workStatus; private float ambientTemp; private int commStatus; }
//    @Data public static class BatteryStackState { private long time; private String deviceId; private int workStatus; private int errorStatus; private float tv; private float tc; private float soh; private float soc; private float rechargeableCapacityKwh; private float dischargeableCapacityKwh; private double cumulativeDischargeKwh; private double cumulativeChargeKwh; private int avgCellVoltageMv; private int avgCellTemp; private int batteryStatus; private int commStatus; }
//    @Data public static class BatteryClusterState { private long time; private String deviceId; private String stackDeviceId; private int workStatus; private float tv; private float tc; private float soh; private float soc; private float rechargeableCapacityKwh; private float dischargeableCapacityKwh; private double cumulativeChargeKwh; private double cumulativeDischargeKwh; private int avgCellTemp; private int avgCellVoltMv; private int commStatus; }
//    @Data public static class ElectricMeterState { private long time; private String deviceId; private int meterType; private double cumulativeImportKwh; private double cumulativeExportKwh; private float acTotalActivePower; private float aPhaseActivePower; private float bPhaseActivePower; private float cPhaseActivePower; private int commStatus; }
//    @Data public static class PvState { private String deviceId; private int commStatus; private float activePower; private float reactivePower; private float powerFactor; private float dailyPowerGeneration; private double totalPowerGeneration; private int runStatus; private int errorStatus; private int alarmStatus; }
//    @Data public static class ChargeStationState { private String deviceId; private long time; private int runStatus; private int commStatus; }
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/service/UdanEmsMessageProcessor.java
@@ -3,112 +3,281 @@
import com.alibaba.fastjson.JSON;
import com.google.protobuf.CodedInputStream;
import com.google.protobuf.WireFormat;
import com.ruoyi.other.emind.domain.EmindStation;
import com.ruoyi.other.emind.dto.EmindIncomeHistoryDto;
import com.ruoyi.other.emind.mapper.EmindStationMapper;
import com.ruoyi.other.emind.mapper.EmindStorageDashboardMapper;
import com.ruoyi.other.udan.model.EmsState;
import com.ruoyi.other.udan.model.PowerMeterState;
import com.ruoyi.other.udan.model.UdanEmsDeviceState;
import com.ruoyi.other.udan.util.UdanEmsKafkaUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
/** 将 iems-open-v1.0.5.proto 消息解析为业务对象,并映射为待入库对象。 */
/** 将 IEmsAllDeviceState 中模型已有的 EMS 和能耗计字段转换为业务对象。 */
@Slf4j
@Component
@ConditionalOnProperty(prefix = "udan.ems.kafka", name = "enabled", havingValue = "true")
public class UdanEmsMessageProcessor {
    private final UdanEmsKafkaUtil kafkaUtil;
    private final EmindStorageDashboardMapper dashboardMapper;
    private final EmindStationMapper stationMapper;
    /** 注入 Kafka 消费工具。 */
    public UdanEmsMessageProcessor(UdanEmsKafkaUtil kafkaUtil) { this.kafkaUtil = kafkaUtil; }
    public UdanEmsMessageProcessor(UdanEmsKafkaUtil kafkaUtil, EmindStorageDashboardMapper dashboardMapper, EmindStationMapper stationMapper) {
        this.kafkaUtil = kafkaUtil;
        this.dashboardMapper = dashboardMapper;
        this.stationMapper = stationMapper;
    }
    /** 注册消息回调,避免消费者只消费但没有业务处理。 */
    //@PostConstruct
    public void register() { kafkaUtil.setMessageHandler(this::process); }
    @PostConstruct
    public void register() {
        kafkaUtil.setMessageHandler(this::process);
    }
    /** 解析消息、转换待入库对象并输出日志;此处暂不调用数据库。 */
    public void process(String emsSystemId, byte[] protobufBytes) {
    public void process(String id, byte[] bytes, String emsDeviceId) {
        try {
            UdanEmsDeviceState state = parse(emsSystemId, protobufBytes);
            log.info("2、UDAN EMS实际对象: {}", JSON.toJSONString(state));
            LocalDateTime now = LocalDateTime.now();
            UdanEmsDeviceState udanEmsDeviceState = parse(id, emsDeviceId, bytes);
            if(StringUtils.isNotEmpty(udanEmsDeviceState.getEmsDeviceId())) {
                log.info("2、UDAN EMS实际对象: {}", JSON.toJSONString(udanEmsDeviceState));
                List<EmindStation>  stations = stationMapper.selectRemoteActiveStations();
                EmindStation station = stations.stream().filter(s -> udanEmsDeviceState.getEmsDeviceId().equals(s.getStationCode())).findFirst().orElse(null);
                if(Objects.nonNull(station)) {
                    List<EmindIncomeHistoryDto> daily = udanEmsDeviceState.convert(station.getStationId());
                    dashboardMapper.upsertDaily(station.getStationId(), daily, now);
                }else{
                    log.info("UDAN EMS数据同步错误-电站信息不存在!");
                }
            }
        } catch (IOException e) {
            log.error("UDAN EMS protobuf解析失败: emsSystemId={}, bytes={}", emsSystemId, protobufBytes == null ? 0 : protobufBytes.length, e);
            log.error("UDAN EMS protobuf解析失败: emsSystemId={}, bytes={}", id,
                    bytes == null ? 0 : bytes.length, e);
        }
    }
    /** 按顶层 IEmsAllDeviceState 字段号读取并构造真实业务对象。 */
    /** 兼容不带 Header 的直接调用。 */
    public void process(String id, byte[] bytes) {
        process(id, bytes, null);
    }
    public UdanEmsDeviceState parse(String id, String emsDeviceId, byte[] bytes) throws IOException {
        UdanEmsDeviceState state = new UdanEmsDeviceState();
        state.setEmsSystemId(id);
        state.setEmsDeviceId(emsDeviceId);
        read(bytes, (field, type, in) -> {
            if (field == 1 && type == 0) state.setTime(in.readUInt64());
            else if (type == 2 && field == 2) state.setEmsState(parseEms(in.readBytes().toByteArray()));
            else if (type == 2 && field == 8) state.setPowerMeterState(parsePowerMeter(in.readBytes().toByteArray()));
            else skip(in, type);
        });
        return state;
    }
    /** 兼容不带 Header 的直接解析调用。 */
    public UdanEmsDeviceState parse(String id, byte[] bytes) throws IOException {
        UdanEmsDeviceState s = new UdanEmsDeviceState();
        s.setEmsSystemId(id);
        CodedInputStream in = CodedInputStream.newInstance(bytes == null ? new byte[0] : bytes);
        while (!in.isAtEnd()) {
            int tag = in.readTag(); if (tag == 0) break;
            int field = WireFormat.getTagFieldNumber(tag); int type = WireFormat.getTagWireType(tag);
            if (field == 1 && type == WireFormat.WIRETYPE_VARINT) s.setTime(in.readUInt64());
            else if (type == WireFormat.WIRETYPE_LENGTH_DELIMITED) parseChild(s, field, in.readBytes().toByteArray());
            else in.skipField(tag);
        }
        return s;
        return parse(id, null, bytes);
    }
    /** 解析顶层各子消息,字段号来自 iems-open-v1.0.5.proto。 */
    private void parseChild(UdanEmsDeviceState s, int field, byte[] data) throws IOException {
        switch (field) {
            case 2: s.setEms(parseEms(data)); break;
            case 4: s.getPcsStates().add(parsePcs(data)); break;
            case 5: s.getBatteryStackStates().add(parseStack(data)); break;
            case 6: s.getBatteryClusterStates().add(parseCluster(data)); break;
            case 7: s.getElectricMeterStates().add(parseMeter(data)); break;
            case 8: s.setPowerMeterCount(s.getPowerMeterCount() + 1); break;
            case 9: s.setAirConditionerCount(s.getAirConditionerCount() + 1); break;
            case 11: s.setFireCount(s.getFireCount() + 1); break;
            case 13: s.setJpfCount(s.getJpfCount() + 1); break;
            case 14: s.setTmsCount(s.getTmsCount() + 1); break;
            case 18: s.setDehumidifierCount(s.getDehumidifierCount() + 1); break;
            case 19: s.setUpsCount(s.getUpsCount() + 1); break;
            case 20: s.getPvStates().add(parsePv(data)); break;
            case 21: s.getChargeStationStates().add(parseChargeStation(data)); break;
            case 23: s.setExtPeripheralCount(s.getExtPeripheralCount() + 1); break;
            default: break;
        }
    }
    /** 解析 EMS 状态对象中的实际字段。 */
    private UdanEmsDeviceState.EmsState parseEms(byte[] b) throws IOException {
        UdanEmsDeviceState.EmsState x = new UdanEmsDeviceState.EmsState();
        read(b, (f, t, in) -> { if (t == 2 && f == 1) x.setDeviceId(in.readString()); else if (t == 5 && f == 18) x.setDemandPower(in.readFloat()); else if (t == 2 && f == 21) x.setProtoProtocolVersion(in.readString()); else if (t == 5 && f == 33) x.setDailyChargeEnergyKwh(in.readFloat()); else if (t == 5 && f == 34) x.setDailyDischargeEnergyKwh(in.readFloat()); else if (t == 1 && f == 37) x.setTotalChargeEnergyKwh(in.readDouble()); else if (t == 1 && f == 38) x.setTotalDischargeEnergyKwh(in.readDouble()); else if (t == 5 && f == 43) x.setSumsSoc(in.readFloat()); else if (t == 5 && f == 69) x.setSumsSoh(in.readFloat()); else if (t == 5 && f == 41) x.setDcBusVoltage(in.readFloat()); else if (t == 5 && f == 42) x.setDcBusCurrent(in.readFloat()); else skip(in, t); });
    private EmsState parseEms(byte[] bytes) throws IOException {
        EmsState x = new EmsState();
        read(bytes, (f, t, in) -> {
            switch (f) {
                case 1: x.setDeviceId(str(in,t)); break; case 9: x.setEmsStatus(enumName(in,t,"RunStatus")); break;
                case 18: x.setDemandPower(f32(in,t)); break; case 20: x.setTime(u64(in,t)); break; case 21: x.setProtoProtocolVersion(str(in,t)); break;
                case 22: x.setEmsRunStatus(enumAt(EmsState.EmsRunStatus.values(),in,t)); break; case 23: x.setEmsWorkStatus(enumAt(EmsState.EmsWorkStatus.values(),in,t)); break;
                case 24: x.setFaultStatus(enumAt(EmsState.ErrorStatus.values(),in,t)); break; case 25: x.setOnGridStatus(enumName(in,t,"GridStatus")); break;
                case 26: x.setControlSrc(enumAt(EmsState.ControlSrc.values(),in,t)); break; case 27: x.setEmuModeCode(enumAt(EmsState.EmuModeCode.values(),in,t)); break;
                case 28: x.setEmuAutoMod(enumAt(EmsState.EmuAutoMode.values(),in,t)); break; case 29: x.setOverDischargeProtect(enumAt(EmsState.YesNo.values(),in,t)); break;
                case 30: x.setOverChargeProtect(enumAt(EmsState.YesNo.values(),in,t)); break; case 31: x.setDemandControl(enumAt(EmsState.YesNo.values(),in,t)); break; case 32: x.setCapacityExpansionModeCtrl(enumAt(EmsState.YesNo.values(),in,t)); break;
                case 33: x.setDailyChargeEnergyKwh(f32(in,t)); break; case 34: x.setDailyDischargeEnergyKwh(f32(in,t)); break; case 35: x.setMonthlyChargeEnergyKwh(f32(in,t)); break; case 36: x.setMonthlyDischargeEnergyKwh(f32(in,t)); break;
                case 37: x.setTotalChargeEnergyKwh(f64(in,t)); break; case 38: x.setTotalDischargeEnergyKwh(f64(in,t)); break; case 39: x.setPcsSumsActivePower(f32(in,t)); break; case 40: x.setPcsSumsApparentPower(f32(in,t)); break;
                case 41: x.setDcBusVoltage(f32(in,t)); break; case 42: x.setDcBusCurrent(f32(in,t)); break; case 43: x.setSumsSoc(f32(in,t)); break; case 44: x.setCellSums(u32(in,t)); break;
                case 45: x.setBatteryAverageTempr(f32(in,t)); break; case 46: x.setBatteryMaxTemprDiff(f32(in,t)); break; case 47: x.setMaxCellVoltage(u32(in,t)); break; case 48: x.setMinCellVoltage(u32(in,t)); break;
                case 49: x.setSystemMaxAllowChgPower(f32(in,t)); break; case 50: x.setSystemMaxAllowDisPower(f32(in,t)); break; case 51: x.setRechargeableKwh(f32(in,t)); break; case 52: x.setDischargeableKwh(f32(in,t)); break;
                case 53: x.setCurrentMaxAllowChgPower(f32(in,t)); break; case 54: x.setCurrentMaxAllowDisPower(f32(in,t)); break; case 55: x.setSocUpperLimit(f32(in,t)); break; case 56: x.setSocLowerLimit(f32(in,t)); break;
                case 57: x.setMaxPowerDischargeDuration(u32(in,t)); break; case 58: x.setMaxPowerChargeDuration(u32(in,t)); break; case 60: x.setMasterCommStatus(enumName(in,t,"CommStatus")); break;
                case 61: x.setGridImportActiveEnergyKwh(f64(in,t)); break; case 62: x.setGridExportActiveEnergyKwh(f64(in,t)); break; case 63: x.setDcTotalPower(f32(in,t)); break; case 64: x.setLoadTotalPower(f32(in,t)); break;
                case 65: x.setEnergyStoreTotalPower(f32(in,t)); break; case 66: x.setHighSideGridPower(f32(in,t)); break; case 67: x.setLowSideGridPower(f32(in,t)); break; case 68: x.setPvPower(f32(in,t)); break; case 69: x.setSumsSoh(f32(in,t)); break;
                default: skip(in,t);
            }
        });
        return x;
    }
    /** 解析 PCS 实际字段。 */
    private UdanEmsDeviceState.PcsState parsePcs(byte[] b) throws IOException {
        UdanEmsDeviceState.PcsState x = new UdanEmsDeviceState.PcsState();
        read(b, (f, t, in) -> { if (t == 2 && f == 2) x.setDeviceId(in.readString()); else if (t == 5 && f == 13) x.setAcTotalActivePower(in.readFloat()); else if (t == 5 && f == 21) x.setAcTotalReactivePower(in.readFloat()); else if (t == 5 && f == 22) x.setAcTotalApparentPower(in.readFloat()); else if (t == 5 && f == 23) x.setAcTotalPowerFactor(in.readFloat()); else if (t == 5 && f == 17) x.setDailyChargeEnergyKwh(in.readFloat()); else if (t == 5 && f == 18) x.setDailyDischargeEnergyKwh(in.readFloat()); else if (t == 1 && f == 19) x.setTotalChargeEnergyKwh(in.readDouble()); else if (t == 1 && f == 20) x.setTotalDischargeEnergyKwh(in.readDouble()); else if (t == 5 && f == 59) x.setAmbientTemp(in.readFloat()); else skip(in, t); });
        return x;
    private PowerMeterState parsePowerMeter(byte[] bytes) throws IOException {
        PowerMeterState value = new PowerMeterState();
        read(bytes, (field, type, input) -> {
            switch (field) {
                case 1: value.setCumulativeImportAmountPeak(f64(input, type)); break;
                case 2: value.setCumulativeImportAmountShoulder(f64(input, type)); break;
                case 3: value.setCumulativeImportAmountValley(f64(input, type)); break;
                case 4: value.setCumulativeImportAmount(f64(input, type)); break;
                case 5: value.setDailyImportAmount((float) f64(input, type)); break;
                case 6: value.setMonthlyImportAmount(f64(input, type)); break;
                case 7: value.setDailyProfit(f64(input, type)); break;
                case 8: value.setYesterdayProfit(f64(input, type)); break;
                case 9: value.setMonthlyProfit(f64(input, type)); break;
                case 10: value.setLastMonthlyProfit(f64(input, type)); break;
                case 11: value.setCumulativeProfit(f64(input, type)); break;
                case 12: value.setCumulativeImportAmountSharp(f64(input, type)); break;
                case 15: value.setCumulativeImportKwh(f64(input, type)); break;
                case 16: value.setCumulativeImportKwhSharp(f64(input, type)); break;
                case 17: value.setCumulativeImportKwhPeak(f64(input, type)); break;
                case 18: value.setCumulativeImportKwhShoulder(f64(input, type)); break;
                case 19: value.setCumulativeImportKwhValley(f64(input, type)); break;
                case 20: value.setCumulativeExportKwh(f64(input, type)); break;
                case 21: value.setCumulativeExportKwhSharp(f64(input, type)); break;
                case 22: value.setCumulativeExportKwhPeak(f64(input, type)); break;
                case 23: value.setCumulativeExportKwhShoulder(f64(input, type)); break;
                case 24: value.setCumulativeExportKwhValley(f64(input, type)); break;
                case 25: value.setMonthlyImportKwh(f64(input, type)); break;
                case 26: value.setMonthlyImportKwhSharp(f64(input, type)); break;
                case 27: value.setMonthlyImportKwhPeak(f64(input, type)); break;
                case 28: value.setMonthlyImportKwhShoulder(f64(input, type)); break;
                case 29: value.setMonthlyImportKwhValley(f64(input, type)); break;
                case 30: value.setMonthlyExportKwh(f64(input, type)); break;
                case 31: value.setMonthlyExportKwhSharp(f64(input, type)); break;
                case 32: value.setMonthlyExportKwhPeak(f64(input, type)); break;
                case 33: value.setMonthlyExportKwhShoulder(f64(input, type)); break;
                case 34: value.setMonthlyExportKwhValley(f64(input, type)); break;
                case 35: value.setDailyImportKwh(f32(input, type)); break;
                case 36: value.setDailyImportKwhSharp(f32(input, type)); break;
                case 37: value.setDailyImportKwhPeak(f32(input, type)); break;
                case 38: value.setDailyImportKwhShoulder(f32(input, type)); break;
                case 39: value.setDailyImportKwhValley(f32(input, type)); break;
                case 40: value.setDailyExportKwh(f32(input, type)); break;
                case 41: value.setDailyExportKwhSharp(f32(input, type)); break;
                case 42: value.setDailyExportKwhPeak(f32(input, type)); break;
                case 43: value.setDailyExportKwhShoulder(f32(input, type)); break;
                case 44: value.setDailyExportKwhValley(f32(input, type)); break;
                case 45: value.setCumulativeExportAmount(f64(input, type)); break;
                case 46: value.setCumulativeExportAmountSharp(f64(input, type)); break;
                case 47: value.setCumulativeExportAmountPeak(f64(input, type)); break;
                case 48: value.setCumulativeExportAmountShoulder(f64(input, type)); break;
                case 49: value.setCumulativeExportAmountValley(f64(input, type)); break;
                case 50: value.setMonthlyImportAmountSharp(f64(input, type)); break;
                case 51: value.setMonthlyImportAmountPeak(f64(input, type)); break;
                case 52: value.setMonthlyImportAmountShoulder(f64(input, type)); break;
                case 53: value.setMonthlyImportAmountValley(f64(input, type)); break;
                case 54: value.setMonthlyExportAmount(f64(input, type)); break;
                case 55: value.setMonthlyExportAmountSharp(f64(input, type)); break;
                case 56: value.setMonthlyExportAmountPeak(f64(input, type)); break;
                case 57: value.setMonthlyExportAmountShoulder(f64(input, type)); break;
                case 58: value.setMonthlyExportAmountValley(f64(input, type)); break;
                case 59: value.setDailyImportAmountSharp(f32(input, type)); break;
                case 60: value.setDailyImportAmountPeak(f32(input, type)); break;
                case 61: value.setDailyImportAmountShoulder(f32(input, type)); break;
                case 62: value.setDailyImportAmountValley(f32(input, type)); break;
                case 63: value.setDailyExportAmount(f32(input, type)); break;
                case 64: value.setDailyExportAmountSharp(f32(input, type)); break;
                case 65: value.setDailyExportAmountPeak(f32(input, type)); break;
                case 66: value.setDailyExportAmountShoulder(f32(input, type)); break;
                case 67: value.setDailyExportAmountValley(f32(input, type)); break;
                case 68: value.setTime(u64(input, type)); break;
                default: skip(input, type);
            }
        });
        return value;
    }
    /** 解析电池堆实际字段。 */
    private UdanEmsDeviceState.BatteryStackState parseStack(byte[] b) throws IOException { UdanEmsDeviceState.BatteryStackState x = new UdanEmsDeviceState.BatteryStackState(); read(b, (f,t,in)-> { if(t==2&&f==2)x.setDeviceId(in.readString()); else if(t==5&&f==7)x.setTv(in.readFloat()); else if(t==5&&f==8)x.setTc(in.readFloat()); else if(t==5&&f==9)x.setSoh(in.readFloat()); else if(t==5&&f==10)x.setSoc(in.readFloat()); else if(t==5&&f==31)x.setRechargeableCapacityKwh(in.readFloat()); else if(t==5&&f==32)x.setDischargeableCapacityKwh(in.readFloat()); else if(t==1&&f==33)x.setCumulativeDischargeKwh(in.readDouble()); else if(t==1&&f==34)x.setCumulativeChargeKwh(in.readDouble()); else skip(in,t); }); return x; }
    /** 解析电池簇实际字段。 */
    private UdanEmsDeviceState.BatteryClusterState parseCluster(byte[] b) throws IOException { UdanEmsDeviceState.BatteryClusterState x = new UdanEmsDeviceState.BatteryClusterState(); read(b,(f,t,in)->{if(t==2&&f==2)x.setDeviceId(in.readString());else if(t==2&&f==3)x.setStackDeviceId(in.readString());else if(t==5&&f==7)x.setTv(in.readFloat());else if(t==5&&f==8)x.setTc(in.readFloat());else if(t==5&&f==9)x.setSoh(in.readFloat());else if(t==5&&f==10)x.setSoc(in.readFloat());else skip(in,t);});return x; }
    /** 解析电表实际字段。 */
    private UdanEmsDeviceState.ElectricMeterState parseMeter(byte[] b) throws IOException { UdanEmsDeviceState.ElectricMeterState x = new UdanEmsDeviceState.ElectricMeterState(); read(b,(f,t,in)->{if(t==2&&f==2)x.setDeviceId(in.readString());else if(t==1&&f==17)x.setCumulativeImportKwh(in.readDouble());else if(t==1&&f==22)x.setCumulativeExportKwh(in.readDouble());else if(t==5&&f==54)x.setAcTotalActivePower(in.readFloat());else skip(in,t);});return x; }
    /** 解析光伏实际字段。 */
    private UdanEmsDeviceState.PvState parsePv(byte[] b) throws IOException { UdanEmsDeviceState.PvState x = new UdanEmsDeviceState.PvState(); read(b,(f,t,in)->{if(t==2&&f==1)x.setDeviceId(in.readString());else if(t==5&&f==18)x.setActivePower(in.readFloat());else if(t==5&&f==19)x.setReactivePower(in.readFloat());else if(t==5&&f==20)x.setPowerFactor(in.readFloat());else if(t==5&&f==21)x.setDailyPowerGeneration(in.readFloat());else if(t==1&&f==22)x.setTotalPowerGeneration(in.readDouble());else skip(in,t);});return x; }
    /** 解析充电桩实际字段。 */
    private UdanEmsDeviceState.ChargeStationState parseChargeStation(byte[] b) throws IOException { UdanEmsDeviceState.ChargeStationState x = new UdanEmsDeviceState.ChargeStationState(); read(b,(f,t,in)->{if(t==2&&f==1)x.setDeviceId(in.readString());else if(t==0&&f==2)x.setTime(in.readUInt64());else skip(in,t);});return x; }
    private void read(byte[] bytes, Reader reader) throws IOException {
        CodedInputStream input = CodedInputStream.newInstance(bytes == null ? new byte[0] : bytes);
        while (!input.isAtEnd()) {
            int tag = input.readTag();
            if (tag == 0) {
                break;
            }
            reader.read(WireFormat.getTagFieldNumber(tag), WireFormat.getTagWireType(tag), input);
        }
    }
    /** 遍历嵌套 protobuf 字段,并把字段号、线类型和读取器交给解析器。 */
    private void read(byte[] b, FieldReader reader) throws IOException { CodedInputStream in=CodedInputStream.newInstance(b); while(!in.isAtEnd()){int tag=in.readTag();if(tag==0)break;reader.read(WireFormat.getTagFieldNumber(tag),WireFormat.getTagWireType(tag),in);} }
    /** 跳过当前不需要映射的字段。 */
    private void skip(CodedInputStream in, int type) throws IOException {
    private static String str(CodedInputStream input, int type) throws IOException {
        if (type == WireFormat.WIRETYPE_LENGTH_DELIMITED) {
            return input.readString();
        }
        skip(input, type);
        return null;
    }
    private static long u64(CodedInputStream input, int type) throws IOException {
        if (type == WireFormat.WIRETYPE_VARINT) {
            return input.readUInt64();
        }
        skip(input, type);
        return 0L;
    }
    private static int u32(CodedInputStream input, int type) throws IOException {
        return (int) u64(input, type);
    }
    private static float f32(CodedInputStream input, int type) throws IOException {
        if (type == WireFormat.WIRETYPE_FIXED32) {
            return input.readFloat();
        }
        skip(input, type);
        return 0F;
    }
    private static double f64(CodedInputStream input, int type) throws IOException {
        if (type == WireFormat.WIRETYPE_FIXED64) {
            return input.readDouble();
        }
        skip(input, type);
        return 0D;
    }
    private static String enumName(CodedInputStream input, int type, String prefix) throws IOException {
        if (type != WireFormat.WIRETYPE_VARINT) {
            skip(input, type);
            return null;
        }
        int number = input.readInt32();
        if ("RunStatus".equals(prefix)) {
            return new String[]{"RUN_STATUS_UNKNOWN", "RUN_STATUS_STOPPED", "RUN_STATUS_RUNNING"}[number < 0 || number > 2 ? 0 : number];
        }
        if ("GridStatus".equals(prefix)) {
            return new String[]{"GRID_STATUS_UNKNOWN", "GRID_STATUS_ON_GRID", "GRID_STATUS_OFF_GRID"}[number < 0 || number > 2 ? 0 : number];
        }
        if ("CommStatus".equals(prefix)) {
            return new String[]{"COMM_STATUS_UNKNOWN", "COMM_STATUS_DISCONNECTED", "COMM_STATUS_CONNECTED"}[number < 0 || number > 2 ? 0 : number];
        }
        return prefix + "_" + number;
    }
    private static <T> T enumAt(T[] values, CodedInputStream input, int type) throws IOException {
        int number = (int) u64(input, type);
        return number >= 0 && number < values.length ? values[number] : null;
    }
    private static void skip(CodedInputStream input, int type) throws IOException {
        switch (type) {
            case WireFormat.WIRETYPE_VARINT: in.readInt64(); break;
            case WireFormat.WIRETYPE_FIXED64: in.readFixed64(); break;
            case WireFormat.WIRETYPE_LENGTH_DELIMITED: in.readBytes(); break;
            case WireFormat.WIRETYPE_FIXED32: in.readFixed32(); break;
            default: break;
            case WireFormat.WIRETYPE_VARINT:
                input.readInt64();
                break;
            case WireFormat.WIRETYPE_FIXED64:
                input.readFixed64();
                break;
            case WireFormat.WIRETYPE_LENGTH_DELIMITED:
                input.readBytes();
                break;
            case WireFormat.WIRETYPE_FIXED32:
                input.readFixed32();
                break;
            default:
                throw new IOException("Unsupported protobuf wire type: " + type);
        }
    }
    @FunctionalInterface private interface FieldReader { void read(int field, int type, CodedInputStream input) throws IOException; }
    @FunctionalInterface
    private interface Reader {
        void read(int field, int type, CodedInputStream input) throws IOException;
    }
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/util/UdanEmsKafkaUtil.java
@@ -8,7 +8,7 @@
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.errors.WakeupException;
import org.apache.kafka.common.header.Header;
import org.apache.kafka.common.serialization.ByteArrayDeserializer;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.slf4j.Logger;
@@ -20,13 +20,11 @@
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.time.Duration;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
 * 优旦 EMS 平台 Kafka 消费工具。
@@ -48,7 +46,7 @@
    });
    private volatile KafkaConsumer<String, byte[]> consumer;
    private volatile boolean running;
    private volatile MessageHandler messageHandler = (key, payload) -> { };
    private volatile MessageHandler messageHandler = (key, payload, emsDeviceId) -> { };
    private volatile ErrorHandler errorHandler = Throwable::printStackTrace;
    /**
@@ -65,7 +63,7 @@
     * 当 udan.ems.kafka.enabled=true 时由 Spring 在 Bean 初始化后自动调用。
     * PostConstruct 创建并完成依赖注入后,自动执行这个方法一次
     */
    //@PostConstruct
    @PostConstruct
    public void start() {
        validate();
        running = true;
@@ -78,7 +76,7 @@
     * @param handler 接收 EMS 系统 ID 和 protobuf 原始字节的处理器;传入 null 时忽略消息
     */
    public void setMessageHandler(MessageHandler handler) {
        this.messageHandler = handler == null ? (key, payload) -> { } : handler;
        this.messageHandler = handler == null ? (key, payload, emsDeviceId) -> { } : handler;
    }
    /**
@@ -93,16 +91,15 @@
    /**
     * 按配置的超时时间轮询一次 Kafka,并将取得的消息交给消息处理器。
     *
     * @return 本次轮询取得的消息数量
     * @throws IllegalStateException 消费者尚未启动时抛出
     */
    public int pollOnce() {
    public void pollOnce() {
        if (consumer == null) {
            throw new IllegalStateException("Kafka consumer has not started");
        }
        ConsumerRecords<String, byte[]> records = consumer.poll(Duration.ofSeconds(Math.max(1, properties.getPollSeconds())));
        dispatch(records);
        return records.count();
        records.count();
    }
    /**
@@ -125,8 +122,6 @@
            consumer = c;
            c.subscribe(Collections.singletonList(properties.getTopic()));
            while (running) pollOnce();
        } catch (WakeupException ignored) {
            if (running) errorHandler.onError(ignored);
        } catch (Throwable e) {
            if (running) errorHandler.onError(e);
        } finally {
@@ -147,11 +142,23 @@
                log.info("1、UDAN EMS可读字段: emsSystemId={}, data={}", record.key(), toReadableProtobuf(payload));
                // 此处即为业务接入点:解析 protobuf 后调用保存服务,当前仅由回调方决定如何处理。
                messageHandler.onMessage(record.key(), payload);
                messageHandler.onMessage(record.key(), payload, getEmsDeviceId(record));
            } catch (Throwable e) {
                errorHandler.onError(e);
            }
        }
    }
    /** 获取消息 Header 中的 EMS 设备 ID。优先使用 emsDeviceId,兼容单 Header 报文。 */
    private String getEmsDeviceId(ConsumerRecord<String, byte[]> record) {
        Header header = record.headers().lastHeader("ems_device_id");
        if (header == null && !Objects.isNull(record.headers())) {
            header = record.headers().iterator().next();
        }
        if (header == null || header.value() == null) {
            return null;
        }
        return new String(header.value(), StandardCharsets.UTF_8);
    }
    /**
@@ -237,7 +244,6 @@
    /** 判断长度字段是否是可直接阅读的 UTF-8 文本。 */
    private static boolean isReadableString(byte[] data) {
        if (data.length == 0) return true;
        for (byte value : data) {
            int c = value & 0xFF;
            if (c < 0x20 || c > 0x7E) return false;
@@ -304,8 +310,9 @@
         *
         * @param emsSystemId Kafka Key,即 EMS 系统 ID
         * @param protobufBytes Kafka Value,即 protobuf3 原始字节
         * @param emsDeviceId Kafka Header 中的 EMS 设备 ID
         */
        void onMessage(String emsSystemId, byte[] protobufBytes);
        void onMessage(String emsSystemId, byte[] protobufBytes, String emsDeviceId);
    }
    /** 消费和消息处理异常回调。 */
ruoyi-service/ruoyi-other/src/main/resources/mapper/other/EmindStationMapper.xml
@@ -54,7 +54,7 @@
    <select id="selectManagementPage" resultMap="stationResultMap">
        SELECT id, station_id, name, address, province, city, longitude, latitude,
               max_power, capacity, commissioning_time, platform_name, source_type,
               management_deleted, del_flag, synced_at, create_time, update_time
               management_deleted, del_flag, synced_at, create_time, update_time, station_code
        FROM t_emind_station
        WHERE del_flag = 0 AND management_deleted = 0
        <if test="query.platformName != null and query.platformName != ''">
ruoyi-service/ruoyi-other/src/main/resources/sql/t_screen_content_alter.sql
@@ -1,4 +1,8 @@
-- ----------------------------
-- 增加大屏名称配置字段
-- 1、增加大屏名称配置字段
alter table t_screen_content add column screen_name varchar(255)   default 'NULL'  null comment '大屏名称';
-- 2、增加储能电站表设备编号
alter table t_emind_station add column station_code varchar(255)   default 'NULL'  null comment '设备编号';