From 72ac1dce414d20a973104a0eb046809a2eaa5ed7 Mon Sep 17 00:00:00 2001
From: puzhibing <393733352@qq.com>
Date: 星期五, 11 九月 2026 16:36:15 +0800
Subject: [PATCH] Merge branch '2.3' of http://120.76.84.145:10101/gitblit/r/java/mx_charging_pile into 2.3

---
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationCollectService.java                      |    3 
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/UdanEmsDeviceState.java                       |   35 +
 ruoyi-service/ruoyi-other/src/main/resources/sql/t_screen_content_alter.sql                                      |    4 
 ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/constant/IotConstant.java              |    6 
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TStationInfoController.java                   |    2 
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationEarnService.java                         |    3 
 ruoyi-service/ruoyi-integration/src/main/resources/logback.xml                                                   |    3 
 ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/builder/IotBuilder.java                |    2 
 ruoyi-service/ruoyi-jianguan/src/main/resources/logback.xml                                                      |    2 
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationCollectServiceImpl.java             |   58 ++
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/service/UdanEmsMessageProcessor.java                |  114 +++++
 ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/tools/ChargingMessageUtil.java   |    3 
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationInfoService.java                         |    3 
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/task/ExchangeCodeExpireTask.java                         |   10 
 ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TScreenContent.java                           |    5 
 ruoyi-service/ruoyi-other/pom.xml                                                                                |   11 
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/HuWeiUtils.java                                     |  151 +++++++
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationInfoServiceImpl.java                |   97 ++++
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/PowerMeterState.java                          |  209 ++++++++++
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/UdanEmsDatabaseRecord.java                    |   26 +
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/util/UdanEmsKafkaUtil.java                          |  321 ++++++++++++++++
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/config/UdanEmsKafkaProperties.java                  |   20 +
 ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/listener/IotMessageListener.java |    2 
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationEarnServiceImpl.java                |   76 +++
 24 files changed, 1,159 insertions(+), 7 deletions(-)

diff --git a/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TScreenContent.java b/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TScreenContent.java
index ef41a69..c5cb491 100644
--- a/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TScreenContent.java
+++ b/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TScreenContent.java
@@ -94,4 +94,9 @@
     @ApiModelProperty("更新时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date updateTime;
+
+    @TableField("screen_name")
+    @ApiModelProperty("大屏名称")
+    private String screenName;
+
 }
\ No newline at end of file
diff --git a/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/builder/IotBuilder.java b/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/builder/IotBuilder.java
index 9e50f97..7490848 100644
--- a/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/builder/IotBuilder.java
+++ b/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/builder/IotBuilder.java
@@ -35,7 +35,7 @@
         return IoTDAClient.newBuilder()
                 .withCredential(auth)
                 // 标准版/企业版:需自行创建Region对象,基础版:请使用IoTDARegion的region对象,如"withRegion(IoTDARegion.CN_NORTH_4)"
-                .withRegion(new Region("cn-south-1", IotConstant.URL))
+                .withRegion(new Region("cn-south-1", config.getEndpoint()))
                 .build();
     }
     /**
diff --git a/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/constant/IotConstant.java b/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/constant/IotConstant.java
index bc13bcc..94b80ff 100644
--- a/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/constant/IotConstant.java
+++ b/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/constant/IotConstant.java
@@ -6,15 +6,15 @@
     /**
      * 华为云iot  token
      */
-    public static final String IOT_TOKEN = "iot_token";
+    public static final String IOT_TOKEN = "";
     /**
      * 模式
      */
-    public static final String PASSWORD = "password";
+    public static final String PASSWORD = "";
     /**
      * URL
      */
-    public static final String URL = "https://94cb3eb5f9.st1.iotda-app.cn-south-1.myhuaweicloud.com";
+    public static final String URL = "";
     /**
      * 创建产品
      */
diff --git a/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/listener/IotMessageListener.java b/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/listener/IotMessageListener.java
index b4a3c23..e83274b 100644
--- a/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/listener/IotMessageListener.java
+++ b/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/listener/IotMessageListener.java
@@ -373,6 +373,8 @@
                     platformConfirmationCharging.setAuthentication(0);
                     platformConfirmationCharging.setFailure_cause(startVO == null ? null : startVO.getFailureCause());
                 }
+                //先默认写死一个余额,后续再补充功能
+                platformConfirmationCharging.setAccount_balance(new BigDecimal(100));
                 iotMessageProduce.sendMessage(platformConfirmationCharging.getCharging_pile_code(), ServiceIdMenu.PLATFORM_CONFIRMATION_CHARGING.getKey(),messageUtil.platformConfirmationCharging(platformConfirmationCharging));
                 // 鉴权/建单失败时删除幂等键,放行设备重试(避免60秒内重新刷卡/VIN启动被误拦);成功则保留键至TTL过期
                 if (!Integer.valueOf(1).equals(platformConfirmationCharging.getAuthentication())) {
diff --git a/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/tools/ChargingMessageUtil.java b/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/tools/ChargingMessageUtil.java
index 3ac208e..61d88f8 100644
--- a/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/tools/ChargingMessageUtil.java
+++ b/ruoyi-service/ruoyi-integration/src/main/java/com/ruoyi/integration/iotda/utils/tools/ChargingMessageUtil.java
@@ -181,6 +181,9 @@
                     // 持久化消息
                     UploadRealTimeMonitoringData uploadRealTimeMonitoringData = new UploadRealTimeMonitoringData();
                     BeanUtils.copyProperties(uploadRealTimeMonitoringDataMessage,uploadRealTimeMonitoringData);
+                    if(StringUtils.isEmpty(uploadRealTimeMonitoringDataMessage.getTransaction_serial_number())){
+                        return;
+                    }
                     // 查询mogondb上一条数据
                     UploadRealTimeMonitoringData data = uploadRealTimeMonitoringDataService.getLastDataById(uploadRealTimeMonitoringDataMessage.getTransaction_serial_number());
                     // 查询订单
diff --git a/ruoyi-service/ruoyi-integration/src/main/resources/logback.xml b/ruoyi-service/ruoyi-integration/src/main/resources/logback.xml
index 0d83de9..bf1d78e 100644
--- a/ruoyi-service/ruoyi-integration/src/main/resources/logback.xml
+++ b/ruoyi-service/ruoyi-integration/src/main/resources/logback.xml
@@ -7,8 +7,7 @@
 	<contextName>logback</contextName>
 	
 	<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
-<!--	<property name="log.path" value="/data/log/app/ruoyi-integration"/>-->
-	<property name="log.path" value="/Users/mitao/Documents/log/mx_charging_pile/log/app/ruoyi-integration"/>
+	<property name="log.path" value="/data/log/app/ruoyi-integration"/>
 
 	<!--0. 日志格式和颜色渲染 -->
 	<!-- 彩色日志依赖的渲染类 -->
diff --git a/ruoyi-service/ruoyi-jianguan/src/main/resources/logback.xml b/ruoyi-service/ruoyi-jianguan/src/main/resources/logback.xml
index 0a8e4b2..c3e5b70 100644
--- a/ruoyi-service/ruoyi-jianguan/src/main/resources/logback.xml
+++ b/ruoyi-service/ruoyi-jianguan/src/main/resources/logback.xml
@@ -8,7 +8,7 @@
 	
 	<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
 <!--	<property name="log.path" value="/data/log/app/ruoyi-jianguan"/>-->
-	<property name="log.path" value="/Users/mitao/Documents/log/mx_charging_pile/log/app/ruoyi-jianguan"/>
+	<property name="log.path" value="/data/log/app/ruoyi-jianguan"/>
 
 	<!--0. 日志格式和颜色渲染 -->
 	<!-- 彩色日志依赖的渲染类 -->
diff --git a/ruoyi-service/ruoyi-other/pom.xml b/ruoyi-service/ruoyi-other/pom.xml
index c83f50a..6d34411 100644
--- a/ruoyi-service/ruoyi-other/pom.xml
+++ b/ruoyi-service/ruoyi-other/pom.xml
@@ -15,6 +15,17 @@
     </description>
 
     <dependencies>
+        <!-- UDAN-EMS platform-to-platform Kafka protobuf payloads -->
+        <dependency>
+            <groupId>org.apache.kafka</groupId>
+            <artifactId>kafka-clients</artifactId>
+            <version>3.6.1</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.protobuf</groupId>
+            <artifactId>protobuf-java</artifactId>
+            <version>3.25.3</version>
+        </dependency>
         <!-- SpringCloud Alibaba Nacos -->
         <dependency>
             <groupId>com.alibaba.cloud</groupId>
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TStationInfoController.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TStationInfoController.java
index 1847f8a..ef7fb7e 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TStationInfoController.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TStationInfoController.java
@@ -66,5 +66,7 @@
     @PostMapping("/syncKsolarDataTask") @ApiOperation("手动同步数据")
     public void syncKsolarDataTask() {
         stationInfoService.syncKsolarDataTask();
+
+        stationInfoService.syncHuaWeiDataTask();
     }
 }
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationCollectService.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationCollectService.java
index 0796dad..400e76a 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationCollectService.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationCollectService.java
@@ -15,6 +15,9 @@
 
     boolean saveCollectBaseList(JSONArray collectBaseList);
 
+    /** 保存华为 FusionSolar 设备列表。 */
+    boolean saveCollectHuaWeiList(JSONArray devList);
+
     PageInfo<TStationCollect> pageList(StationCollectQuery query);
 
     boolean updateStationCollect(TStationCollect stationCollect);
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationEarnService.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationEarnService.java
index cbc83b5..4c3edda 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationEarnService.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationEarnService.java
@@ -22,6 +22,9 @@
 
     boolean saveStationEarnList(JSONArray stationEarnList);
 
+    /** 保存华为 FusionSolar 电站日统计数据。 */
+    boolean saveStationHuaWeiList(JSONArray stationDayList);
+
     LocalDate getMaxSaveTime();
 
     PageInfo<TStationEarn> pageList(StationEarnQuery query);
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationInfoService.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationInfoService.java
index 15a55f3..9ca3a09 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationInfoService.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TStationInfoService.java
@@ -37,4 +37,7 @@
      * @return true可以删除,false不可以删除
      */
     Boolean validateDelete(Long[] ids);
+
+    /** 同步华为 FusionSolar 电站数据。 */
+    void syncHuaWeiDataTask();
 }
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationCollectServiceImpl.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationCollectServiceImpl.java
index 78bb1d8..541eb91 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationCollectServiceImpl.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationCollectServiceImpl.java
@@ -262,6 +262,17 @@
         }
     }
 
+    private Integer parseInteger(String value) {
+        if (StringUtils.isBlank(value)) {
+            return 0;
+        }
+        try {
+            return Integer.valueOf(value);
+        } catch (NumberFormatException exception) {
+            return parsePowerNum(value);
+        }
+    }
+
     private LocalDateTime parseDateTime(String value) {
         if (StringUtils.isBlank(value)) {
             return null;
@@ -293,4 +304,51 @@
         }
         return true;
     }
+
+    /**
+     * 将华为 FusionSolar 设备列表转换为逆变器记录并批量保存。
+     * devDn 作为设备唯一标识(接口未提供时使用 esnCode)。
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean saveCollectHuaWeiList(JSONArray devList) {
+        if (devList == null || devList.isEmpty()) {
+            return false;
+        }
+        Set<String> existingKeys = getExistingCollectKeys();
+        Set<String> pendingKeys = new HashSet<>();
+        List<TStationCollect> insertList = new ArrayList<>(devList.size());
+        for (Object item : devList) {
+            if (item == null) {
+                continue;
+            }
+            JSONObject json = JSONObject.parseObject(item.toString());
+            String collectId = firstNotBlank(json.getString("devDn"), json.getString("esnCode"));
+            String stationId = json.getString("stationCode");
+            if (StringUtils.isBlank(collectId) || StringUtils.isBlank(stationId)
+                    || existingKeys.contains(collectId) || !pendingKeys.add(collectId)) {
+                continue;
+            }
+            TStationCollect collect = new TStationCollect();
+            collect.setPlatformId(1L);
+            collect.setStationId(stationId);
+            collect.setStationName(firstNotBlank(json.getString("stationName"), ""));
+            collect.setCollectId(collectId);
+            collect.setCollectName(firstNotBlank(json.getString("devName"), collectId));
+            collect.setDeviceModel(firstNotBlank(json.getString("invType"), json.getString("model")));
+            collect.setInverterId(json.getString("esnCode"));
+            collect.setInstalledCapacity(firstNotBlank(json.getString("capacity"), "0"));
+            collect.setPowerNum(parseInteger(json.getString("optimizerNumber")));
+            collect.setLatitude(json.getString("latitude"));
+            collect.setLongitude(json.getString("longitude"));
+            collect.setStatus("1");
+            collect.setDataSource(2);
+            collect.setSyncTime(LocalDateTime.now());
+            collect.setCreateTime(LocalDateTime.now());
+            collect.setUpdateTime(LocalDateTime.now());
+            fillDefaults(collect);
+            insertList.add(collect);
+        }
+        return CollectionUtils.isNotEmpty(insertList) && saveBatch(insertList);
+    }
 }
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationEarnServiceImpl.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationEarnServiceImpl.java
index 418bd31..7e202cc 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationEarnServiceImpl.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationEarnServiceImpl.java
@@ -29,6 +29,8 @@
 import java.math.RoundingMode;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.Instant;
+import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeParseException;
 import java.util.*;
@@ -77,6 +79,80 @@
     }
 
     /**
+     * 将华为 FusionSolar 电站日指标转换为收益记录并批量保存。
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean saveStationHuaWeiList(JSONArray stationDayList) {
+        if (stationDayList == null || stationDayList.isEmpty()) {
+            return false;
+        }
+        Map<String, TStationEarn> existingByKey = getExistingEarnByKey();
+        Map<String, TStationEarn> recordsByKey = new LinkedHashMap<>();
+        for (Object item : stationDayList) {
+            if (item == null) {
+                continue;
+            }
+            JSONObject json = JSONObject.parseObject(item.toString());
+            String stationId = json.getString("stationCode");
+            LocalDate saveTime = parseHuaweiCollectTime(json.getString("collectTime"));
+            if (StringUtils.isBlank(stationId) || saveTime == null) {
+                continue;
+            }
+            JSONObject dataItemMap = json.getJSONObject("dataItemMap");
+            if (dataItemMap == null) {
+                dataItemMap = new JSONObject();
+            }
+            LocalDateTime now = LocalDateTime.now();
+            TStationEarn earn = new TStationEarn();
+            earn.setPlatformId(1L);
+            earn.setStationId(stationId);
+            earn.setStationName(json.getString("stationName"));
+            earn.setSaveTime(saveTime);
+            earn.setElecTotal(firstDecimal(dataItemMap, "PVYield", "inverterYield", "inverter_power"));
+            earn.setEarnTotal(parseBigDecimal(dataItemMap.getString("power_profit")));
+            earn.setDataSource(2);
+            earn.setCreateBy("");
+            earn.setUpdateBy("");
+            earn.setCreateTime(now);
+            earn.setUpdateTime(now);
+            earn.setDelFlag("0");
+            TStationEarn existing = existingByKey.get(buildEarnKey(earn));
+            if (existing != null) {
+                earn.setId(existing.getId());
+                earn.setCreateTime(null);
+            }
+            recordsByKey.put(buildEarnKey(earn), earn);
+        }
+        if (recordsByKey.isEmpty()) {
+            return false;
+        }
+        List<List<TStationEarn>> partitions = Lists.partition(new ArrayList<>(recordsByKey.values()), 500);
+        partitions.forEach(this::saveOrUpdateBatch);
+        return true;
+    }
+
+    private static LocalDate parseHuaweiCollectTime(String value) {
+        if (StringUtils.isBlank(value)) {
+            return null;
+        }
+        try {
+            return Instant.ofEpochMilli(Long.parseLong(value)).atZone(ZoneId.systemDefault()).toLocalDate();
+        } catch (NumberFormatException exception) {
+            return null;
+        }
+    }
+
+    private static BigDecimal firstDecimal(JSONObject data, String... keys) {
+        for (String key : keys) {
+            if (data.get(key) != null && StringUtils.isNotBlank(data.getString(key))) {
+                return parseBigDecimal(data.getString(key));
+            }
+        }
+        return BigDecimal.ZERO;
+    }
+
+    /**
      * 查询发电量统计记录中的最大统计日期。
      *
      * @return 最大统计日期,无记录时返回 null
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationInfoServiceImpl.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationInfoServiceImpl.java
index 6b669b7..777ff8f 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationInfoServiceImpl.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TStationInfoServiceImpl.java
@@ -13,6 +13,7 @@
 import com.ruoyi.other.service.TStationInfoService;
 import com.ruoyi.other.query.StationInfoQuery;
 import com.ruoyi.other.util.KsolarUtils;
+import com.ruoyi.other.util.HuWeiUtils;
 import com.ruoyi.other.vo.StationInfoStatusVO;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections4.CollectionUtils;
@@ -23,6 +24,8 @@
 import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeParseException;
 import java.util.*;
@@ -43,6 +46,8 @@
     @Autowired
     private TStationEarnService stationEarnService;
 
+    @Autowired
+    private HuWeiUtils huWeiUtils;
 
     /**
      * 每天同步科士达数据到数据库
@@ -311,6 +316,21 @@
         }
     }
 
+    private LocalDateTime parseHuaweiDateTime(String value) {
+        if (StringUtils.isBlank(value)) {
+            return null;
+        }
+        try {
+            return OffsetDateTime.parse(value).toLocalDateTime();
+        } catch (DateTimeParseException ignored) {
+            try {
+                return ZonedDateTime.parse(value).toLocalDateTime();
+            } catch (DateTimeParseException ignoredAgain) {
+                return parseDataTime(value);
+            }
+        }
+    }
+
     private String firstNotBlank(String preferred, String fallback) {
         return StringUtils.isNotBlank(preferred) ? preferred : fallback;
     }
@@ -364,4 +384,81 @@
         return true;
     }
 
+    /**
+     * 同步华为 FusionSolar 电站数据。
+     * 电站编号 plantCode 作为系统中的 stationId,仅插入数据库中不存在的电站。
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public void syncHuaWeiDataTask() {
+        log.info("-------1、开始同步华为电站列表信息----");
+        String token = huWeiUtils.getXsrfToken();
+        JSONArray stationArray = huWeiUtils.getStations(1, token);
+        Set<String> stationIds = new HashSet<>();
+        if (CollectionUtils.isNotEmpty(stationArray)) {
+            List<TStationInfo> insertList = new ArrayList<>();
+            huaWeiBuild(stationArray, stationIds, insertList);
+
+            if (CollectionUtils.isNotEmpty(insertList)) {
+                List<TStationInfo> existing = lambdaQuery().eq(TStationInfo::getPlatformId, 1L).in(TStationInfo::getStationId, stationIds).list();
+                Set<String> existingIds = existing.stream().map(TStationInfo::getStationId).collect(Collectors.toSet());
+                insertList.removeIf(item -> existingIds.contains(item.getStationId()));
+                if (CollectionUtils.isNotEmpty(insertList)) {
+                    saveBatch(insertList);
+                }
+            }
+            log.info("-------1、结束同步华为电站列表信息,本次新增{}条----", insertList.size());
+        }
+
+        log.info("-------2、开始同步华为设备列表信息----");
+        if(CollectionUtils.isNotEmpty(stationIds)) {
+            String codes = String.join(",", stationIds);
+            JSONArray devList = huWeiUtils.getDevList(codes, token);
+            collectService.saveCollectHuaWeiList(devList);
+        }
+
+        log.info("-------3、开始同步华为电站日数据信息----");
+        List<TStationInfo> existInfos = lambdaQuery().eq(TStationInfo::getPlatformId, 1L).list();
+        Set<String> existIds = existInfos.stream().map(TStationInfo::getStationId).collect(Collectors.toSet());
+        if(CollectionUtils.isNotEmpty(existIds)) {
+            String codes = String.join(",", existIds);
+            JSONArray stationDayList = huWeiUtils.getKpiStationDay(codes, System.currentTimeMillis(), token);
+            stationEarnService.saveStationHuaWeiList(stationDayList);
+        }
+        log.info("-------3、结束同步华为电站日数据信息----");
+
+
+    }
+
+    public void huaWeiBuild(JSONArray stationArray, Set<String> stationIds, List<TStationInfo> insertList) {
+        for (Object item : stationArray) {
+            JSONObject json = JSONObject.parseObject(item.toString());
+            String stationId = json.getString("plantCode");
+            if (StringUtils.isBlank(stationId) || !stationIds.add(stationId)) {
+                continue;
+            }
+            TStationInfo info = new TStationInfo();
+            info.setPlatformId(1L);
+            info.setStationId(stationId);
+            info.setStationName(json.getString("plantName"));
+            info.setStationType("1");
+            info.setRunStatus("0");
+            info.setInstalledCapacity(toBigDecimal(json, "capacity"));
+            info.setContactName(json.getString("contactPerson"));
+            info.setPhone(json.getString("contactMethod"));
+            info.setLatitude(json.getString("latitude"));
+            info.setLongitude(json.getString("longitude"));
+            info.setAddress(json.getString("plantAddress"));
+            LocalDateTime gridTime = parseHuaweiDateTime(json.getString("gridConnectionDate"));
+            info.setDataTime(gridTime);
+            info.setLoanStatus(gridTime != null && gridTime.isBefore(LocalDateTime.now()) ? 3 : 1);
+            info.setGridStatus(info.getLoanStatus() == 3 ? "1" : "0");
+            info.setDataSource(2);
+            info.setSyncTime(LocalDateTime.now());
+            info.setCreateTime(LocalDateTime.now());
+            info.setUpdateTime(LocalDateTime.now());
+            fillDefaults(info);
+            insertList.add(info);
+        }
+    }
+
 }
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/task/ExchangeCodeExpireTask.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/task/ExchangeCodeExpireTask.java
index f48f920..437a87f 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/task/ExchangeCodeExpireTask.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/task/ExchangeCodeExpireTask.java
@@ -28,4 +28,14 @@
     public void syncKsolarDataTask() {
         tStationInfoService.syncKsolarDataTask();
     }
+
+    /**
+     * 每6小时执行科士达的数据同步
+     */
+    @Scheduled(cron = "0 0 */6 * * ?")
+    public void syncHuaWeiDataTask() {
+        tStationInfoService.syncHuaWeiDataTask();
+    }
+
+
 }
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/config/UdanEmsKafkaProperties.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/config/UdanEmsKafkaProperties.java
new file mode 100644
index 0000000..9a26c50
--- /dev/null
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/config/UdanEmsKafkaProperties.java
@@ -0,0 +1,20 @@
+package com.ruoyi.other.udan.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/** Kafka connection settings issued by UDAN after integration approval. */
+@Data
+@Component
+@ConfigurationProperties(prefix = "udan.ems.kafka")
+public class UdanEmsKafkaProperties {
+    private boolean enabled = false;
+    private String bootstrapServers;
+    private String username;
+    private String password;
+    private String topic;
+    private String groupId = "group-device-state";
+    private String autoOffsetReset = "latest";
+    private int pollSeconds = 3600;
+}
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/PowerMeterState.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/PowerMeterState.java
new file mode 100644
index 0000000..707d629
--- /dev/null
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/PowerMeterState.java
@@ -0,0 +1,209 @@
+package com.ruoyi.other.udan.model;
+
+import lombok.Data;
+
+/**
+ * @author Lux
+ * @version 1.0.0
+ * @since 2026/9/10 17:55
+ */
+@Data
+public class PowerMeterState {
+    /** 峰用电总价,单位:元 */
+    private double cumulativeImportAmountPeak;
+
+    /** 平用电总价,单位:元 */
+    private double cumulativeImportAmountShoulder;
+
+    /** 谷用电总价,单位:元 */
+    private double cumulativeImportAmountValley;
+
+    /** 累计用电总价,单位:元 */
+    private double cumulativeImportAmount;
+
+    /** 日用电总价,单位:元 */
+    private double dailyImportAmount;
+
+    /** 月用电总价,单位:元 */
+    private double monthlyImportAmount;
+
+    /** 当日收益,单位:元 */
+    private double dailyProfit;
+
+    /** 昨日收益,单位:元 */
+    private double yesterdayProfit;
+
+    /** 当月收益,单位:元 */
+    private double monthlyProfit;
+
+    /** 上月收益,单位:元 */
+    private double lastMonthlyProfit;
+
+    /** 累计收益,单位:元 */
+    private double cumulativeProfit;
+
+    /** 累计尖用电总价,单位:元 */
+    private double cumulativeImportAmountSharp;
+
+    /** 累计用电电量,单位:kWh */
+    private double cumulativeImportKwh;
+
+    /** 累计尖用电电量,单位:kWh */
+    private double cumulativeImportKwhSharp;
+
+    /** 累计峰用电电量,单位:kWh */
+    private double cumulativeImportKwhPeak;
+
+    /** 累计平用电电量,单位:kWh */
+    private double cumulativeImportKwhShoulder;
+
+    /** 累计谷用电电量,单位:kWh */
+    private double cumulativeImportKwhValley;
+
+    /** 累计馈电电量,单位:kWh */
+    private double cumulativeExportKwh;
+
+    /** 累计尖馈电电量,单位:kWh */
+    private double cumulativeExportKwhSharp;
+
+    /** 累计峰馈电电量,单位:kWh */
+    private double cumulativeExportKwhPeak;
+
+    /** 累计平馈电电量,单位:kWh */
+    private double cumulativeExportKwhShoulder;
+
+    /** 累计谷馈电电量,单位:kWh */
+    private double cumulativeExportKwhValley;
+
+    /** 月用电电量,单位:kWh */
+    private double monthlyImportKwh;
+
+    /** 月尖用电电量,单位:kWh */
+    private double monthlyImportKwhSharp;
+
+    /** 月峰用电电量,单位:kWh */
+    private double monthlyImportKwhPeak;
+
+    /** 月平用电电量,单位:kWh */
+    private double monthlyImportKwhShoulder;
+
+    /** 月谷用电电量,单位:kWh */
+    private double monthlyImportKwhValley;
+
+    /** 月馈电电量,单位:kWh */
+    private double monthlyExportKwh;
+
+    /** 月尖馈电电量,单位:kWh */
+    private double monthlyExportKwhSharp;
+
+    /** 月峰馈电电量,单位:kWh */
+    private double monthlyExportKwhPeak;
+
+    /** 月平馈电电量,单位:kWh */
+    private double monthlyExportKwhShoulder;
+
+    /** 月谷馈电电量,单位:kWh */
+    private double monthlyExportKwhValley;
+
+    /** 日用电电量,单位:kWh */
+    private float dailyImportKwh;
+
+    /** 日尖用电电量,单位:kWh */
+    private float dailyImportKwhSharp;
+
+    /** 日峰用电电量,单位:kWh */
+    private float dailyImportKwhPeak;
+
+    /** 日平用电电量,单位:kWh */
+    private float dailyImportKwhShoulder;
+
+    /** 日谷用电电量,单位:kWh */
+    private float dailyImportKwhValley;
+
+    /** 日馈电电量,单位:kWh */
+    private float dailyExportKwh;
+
+    /** 日尖馈电电量,单位:kWh */
+    private float dailyExportKwhSharp;
+
+    /** 日峰馈电电量,单位:kWh */
+    private float dailyExportKwhPeak;
+
+    /** 日平馈电电量,单位:kWh */
+    private float dailyExportKwhShoulder;
+
+    /** 日谷馈电电量,单位:kWh */
+    private float dailyExportKwhValley;
+
+    /** 累计馈电总价,单位:元 */
+    private double cumulativeExportAmount;
+
+    /** 累计尖馈电总价,单位:元 */
+    private double cumulativeExportAmountSharp;
+
+    /** 累计峰馈电总价,单位:元 */
+    private double cumulativeExportAmountPeak;
+
+    /** 累计平馈电总价,单位:元 */
+    private double cumulativeExportAmountShoulder;
+
+    /** 累计谷馈电总价,单位:元 */
+    private double cumulativeExportAmountValley;
+
+    /** 月尖用电总价,单位:元 */
+    private double monthlyImportAmountSharp;
+
+    /** 月峰用电总价,单位:元 */
+    private double monthlyImportAmountPeak;
+
+    /** 月平用电总价,单位:元 */
+    private double monthlyImportAmountShoulder;
+
+    /** 月谷用电总价,单位:元 */
+    private double monthlyImportAmountValley;
+
+    /** 月馈电总价,单位:元 */
+    private double monthlyExportAmount;
+
+    /** 月尖馈电总价,单位:元 */
+    private double monthlyExportAmountSharp;
+
+    /** 月峰馈电总价,单位:元 */
+    private double monthlyExportAmountPeak;
+
+    /** 月平馈电总价,单位:元 */
+    private double monthlyExportAmountShoulder;
+
+    /** 月谷馈电总价,单位:元 */
+    private double monthlyExportAmountValley;
+
+    /** 日尖用电总价,单位:元 */
+    private float dailyImportAmountSharp;
+
+    /** 日峰用电总价,单位:元 */
+    private float dailyImportAmountPeak;
+
+    /** 日平用电总价,单位:元 */
+    private float dailyImportAmountShoulder;
+
+    /** 日谷用电总价,单位:元 */
+    private float dailyImportAmountValley;
+
+    /** 日馈电总价,单位:元 */
+    private float dailyExportAmount;
+
+    /** 日尖馈电总价,单位:元 */
+    private float dailyExportAmountSharp;
+
+    /** 日峰馈电总价,单位:元 */
+    private float dailyExportAmountPeak;
+
+    /** 日平馈电总价,单位:元 */
+    private float dailyExportAmountShoulder;
+
+    /** 日谷馈电总价,单位:元 */
+    private float dailyExportAmountValley;
+
+    /** 时间戳,1970-01-01 00:00:00 */
+    private long time;
+}
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/UdanEmsDatabaseRecord.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/UdanEmsDatabaseRecord.java
new file mode 100644
index 0000000..f3fa9eb
--- /dev/null
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/UdanEmsDatabaseRecord.java
@@ -0,0 +1,26 @@
+package com.ruoyi.other.udan.model;
+
+import lombok.Data;
+
+import java.time.Instant;
+
+/** 待写入数据库的业务对象;仅定义映射结果,不执行数据库操作。 */
+@Data
+public class UdanEmsDatabaseRecord {
+    private String emsSystemId;
+    private Instant eventTime;
+    private int pcsCount;
+    private int batteryStackCount;
+    private int batteryClusterCount;
+    private int electricMeterCount;
+    private int powerMeterCount;
+    private int airConditionerCount;
+    private int fireCount;
+    private int jpfCount;
+    private int tmsCount;
+    private int dehumidifierCount;
+    private int upsCount;
+    private int pvCount;
+    private int chargeStationCount;
+    private int extPeripheralCount;
+}
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/UdanEmsDeviceState.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/UdanEmsDeviceState.java
new file mode 100644
index 0000000..5c382c7
--- /dev/null
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/model/UdanEmsDeviceState.java
@@ -0,0 +1,35 @@
+package com.ruoyi.other.udan.model;
+
+import lombok.Data;
+import java.util.ArrayList;
+import java.util.List;
+
+/** 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;
+
+    @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; }
+}
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/service/UdanEmsMessageProcessor.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/service/UdanEmsMessageProcessor.java
new file mode 100644
index 0000000..95e8088
--- /dev/null
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/service/UdanEmsMessageProcessor.java
@@ -0,0 +1,114 @@
+package com.ruoyi.other.udan.service;
+
+import com.alibaba.fastjson.JSON;
+import com.google.protobuf.CodedInputStream;
+import com.google.protobuf.WireFormat;
+import com.ruoyi.other.udan.model.UdanEmsDeviceState;
+import com.ruoyi.other.udan.util.UdanEmsKafkaUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+/** 将 iems-open-v1.0.5.proto 消息解析为业务对象,并映射为待入库对象。 */
+@Slf4j
+@Component
+@ConditionalOnProperty(prefix = "udan.ems.kafka", name = "enabled", havingValue = "true")
+public class UdanEmsMessageProcessor {
+    private final UdanEmsKafkaUtil kafkaUtil;
+
+    /** 注入 Kafka 消费工具。 */
+    public UdanEmsMessageProcessor(UdanEmsKafkaUtil kafkaUtil) { this.kafkaUtil = kafkaUtil; }
+
+    /** 注册消息回调,避免消费者只消费但没有业务处理。 */
+    //@PostConstruct
+    public void register() { kafkaUtil.setMessageHandler(this::process); }
+
+    /** 解析消息、转换待入库对象并输出日志;此处暂不调用数据库。 */
+    public void process(String emsSystemId, byte[] protobufBytes) {
+        try {
+            UdanEmsDeviceState state = parse(emsSystemId, protobufBytes);
+            log.info("2、UDAN EMS实际对象: {}", JSON.toJSONString(state));
+        } catch (IOException e) {
+            log.error("UDAN EMS protobuf解析失败: emsSystemId={}, bytes={}", emsSystemId, protobufBytes == null ? 0 : protobufBytes.length, e);
+        }
+    }
+
+    /** 按顶层 IEmsAllDeviceState 字段号读取并构造真实业务对象。 */
+    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;
+    }
+
+
+    /** 解析顶层各子消息,字段号来自 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); });
+        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 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; }
+
+    /** 遍历嵌套 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 {
+        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;
+        }
+    }
+    @FunctionalInterface private interface FieldReader { void read(int field, int type, CodedInputStream input) throws IOException; }
+}
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/util/UdanEmsKafkaUtil.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/util/UdanEmsKafkaUtil.java
new file mode 100644
index 0000000..45fc1e1
--- /dev/null
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/udan/util/UdanEmsKafkaUtil.java
@@ -0,0 +1,321 @@
+package com.ruoyi.other.udan.util;
+
+import com.google.protobuf.CodedInputStream;
+import com.google.protobuf.WireFormat;
+import com.ruoyi.other.udan.config.UdanEmsKafkaProperties;
+import org.apache.kafka.clients.CommonClientConfigs;
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+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.serialization.ByteArrayDeserializer;
+import org.apache.kafka.common.serialization.StringDeserializer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+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.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.io.IOException;
+
+/**
+ * 优旦 EMS 平台 Kafka 消费工具。
+ *
+ * <p>按照《UDAN-EMS云平台对平台协议》订阅设备监控 Topic:消息 Key 为 EMS 系统 ID,
+ * 消息 Value 为 protobuf3 二进制数据,不是 JSON。业务方应注册消息处理器,并使用
+ * 客户最终确认版本的 iems-open proto 生成类反序列化消息。</p>
+ */
+@Component
+@ConditionalOnProperty(prefix = "udan.ems.kafka", name = "enabled", havingValue = "true")
+public class UdanEmsKafkaUtil {
+    private static final Logger log = LoggerFactory.getLogger(UdanEmsKafkaUtil.class);
+    private final UdanEmsKafkaProperties properties;
+
+    private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
+        Thread t = new Thread(r, "udan-ems-kafka-consumer");
+        t.setDaemon(true);
+        return t;
+    });
+    private volatile KafkaConsumer<String, byte[]> consumer;
+    private volatile boolean running;
+    private volatile MessageHandler messageHandler = (key, payload) -> { };
+    private volatile ErrorHandler errorHandler = Throwable::printStackTrace;
+
+    /**
+     * 创建优旦 EMS Kafka 消费工具。
+     *
+     * @param properties Kafka 连接、认证、Topic 和消费组配置
+     */
+    public UdanEmsKafkaUtil(UdanEmsKafkaProperties properties) {
+        this.properties = properties;
+    }
+
+    /**
+     * 校验配置并在独立守护线程中启动持续消费。
+     * 当 udan.ems.kafka.enabled=true 时由 Spring 在 Bean 初始化后自动调用。
+     * PostConstruct 创建并完成依赖注入后,自动执行这个方法一次
+     */
+    //@PostConstruct
+    public void start() {
+        validate();
+        running = true;
+        executor.submit(this::consumeLoop);
+    }
+
+    /**
+     * 注册设备监控消息处理器。
+     *
+     * @param handler 接收 EMS 系统 ID 和 protobuf 原始字节的处理器;传入 null 时忽略消息
+     */
+    public void setMessageHandler(MessageHandler handler) {
+        this.messageHandler = handler == null ? (key, payload) -> { } : handler;
+    }
+
+    /**
+     * 注册消费或业务处理异常的处理器。
+     *
+     * @param handler 异常处理器;传入 null 时恢复为打印异常堆栈
+     */
+    public void setErrorHandler(ErrorHandler handler) {
+        this.errorHandler = handler == null ? Throwable::printStackTrace : handler;
+    }
+
+    /**
+     * 按配置的超时时间轮询一次 Kafka,并将取得的消息交给消息处理器。
+     *
+     * @return 本次轮询取得的消息数量
+     * @throws IllegalStateException 消费者尚未启动时抛出
+     */
+    public int 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();
+    }
+
+    /**
+     * 唤醒并关闭 Kafka 消费者,同时停止后台消费线程。
+     * 由 Spring 在 Bean 销毁前自动调用。
+     */
+    @PreDestroy
+    public void stop() {
+        running = false;
+        if (consumer != null) consumer.wakeup();
+        executor.shutdownNow();
+    }
+
+    /**
+     * 创建消费者、订阅配置的设备监控 Topic,并持续执行轮询。
+     * WakeupException 在正常关闭时忽略,运行期间出现的异常交给异常处理器。
+     */
+    private void consumeLoop() {
+        try (KafkaConsumer<String, byte[]> c = new KafkaConsumer<>(consumerConfig())) {
+            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 {
+            consumer = null;
+        }
+    }
+
+    /**
+     * 逐条分发 Kafka 消息;单条消息处理失败不会阻断同批次后续消息。
+     *
+     * @param records Kafka 本次轮询返回的消息集合
+     */
+    private void dispatch(ConsumerRecords<String, byte[]> records) {
+        for (ConsumerRecord<String, byte[]> record : records) {
+            byte[] payload = record.value() == null ? new byte[0] : record.value();
+            try {
+                // 先输出可核验信息,再交给业务回调;线上 Value 是 protobuf 原始字节,不能当 JSON 打印。
+                log.info("1、UDAN EMS可读字段: emsSystemId={}, data={}", record.key(), toReadableProtobuf(payload));
+
+                // 此处即为业务接入点:解析 protobuf 后调用保存服务,当前仅由回调方决定如何处理。
+                messageHandler.onMessage(record.key(), payload);
+            } catch (Throwable e) {
+                errorHandler.onError(e);
+            }
+        }
+    }
+
+    /**
+     * 将消息前若干字节转为十六进制,便于核对消息是否为稳定的二进制 protobuf 内容。
+     *
+     * @param bytes 原始消息字节
+     * @param maxBytes 最多输出的字节数
+     * @return 大写十六进制字符串,截断时追加省略号
+     */
+    private static String toHex(byte[] bytes, int maxBytes) {
+        StringBuilder result = new StringBuilder();
+        int length = Math.min(bytes.length, Math.max(0, maxBytes));
+        for (int i = 0; i < length; i++) {
+            if (i > 0) result.append(' ');
+            result.append(String.format("%02X", bytes[i] & 0xFF));
+        }
+        if (bytes.length > length) result.append(" ...");
+        return result.toString();
+    }
+
+    /**
+     * 将 protobuf 二进制按 wire type 转为可读文本。由于未绑定生成的 proto 类,字段名显示为 fieldN;
+     * 字符串、整数、浮点数和嵌套消息内容会被直接展开,便于联调确认实际收到的数据。
+     */
+    private static String toReadableProtobuf(byte[] bytes) {
+        if (bytes == null || bytes.length == 0) return "{}";
+        try {
+            return readReadableFields(CodedInputStream.newInstance(bytes), 0);
+        } catch (IOException e) {
+            return "<protobuf解析失败: " + e.getMessage() + ">";
+        }
+    }
+
+    /** 递归读取 protobuf 字段,限制递归深度和字段数量避免日志过大。 */
+    private static String readReadableFields(CodedInputStream input, int depth) throws IOException {
+        if (depth > 3) return "<nested>";
+        StringBuilder text = new StringBuilder("{");
+        int count = 0;
+        while (!input.isAtEnd() && count++ < 80) {
+            int tag = input.readTag();
+            if (tag == 0) break;
+            int field = WireFormat.getTagFieldNumber(tag);
+            int type = WireFormat.getTagWireType(tag);
+            if (text.length() > 1) text.append(", ");
+            text.append("field").append(field).append('=');
+            switch (type) {
+                case WireFormat.WIRETYPE_VARINT:
+                    text.append(input.readInt64());
+                    break;
+                case WireFormat.WIRETYPE_FIXED64:
+                    text.append(input.readDouble());
+                    break;
+                case WireFormat.WIRETYPE_LENGTH_DELIMITED:
+                    byte[] data = input.readBytes().toByteArray();
+                    if (isReadableString(data)) {
+                        text.append('"').append(escape(new String(data, java.nio.charset.StandardCharsets.UTF_8))).append('"');
+                    } else {
+                        text.append(readNestedOrHex(data, depth));
+                    }
+                    break;
+                case WireFormat.WIRETYPE_FIXED32:
+                    text.append(input.readFloat());
+                    break;
+                default:
+                    text.append("<wireType=").append(type).append('>');
+                    return text.append('}').toString();
+            }
+        }
+        if (count > 80) text.append(", ...");
+        return text.append('}').toString();
+    }
+
+    /** 尝试将长度字段识别为嵌套 protobuf,否则显示前 32 字节十六进制。 */
+    private static String readNestedOrHex(byte[] data, int depth) {
+        try {
+            String nested = readReadableFields(CodedInputStream.newInstance(data), depth + 1);
+            if (!nested.equals("{}")) return nested;
+        } catch (IOException ignored) {
+            // 不是嵌套 protobuf 时按原始二进制展示。
+        }
+        return "0x" + toHex(data, 32);
+    }
+
+    /** 判断长度字段是否是可直接阅读的 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;
+        }
+        return true;
+    }
+
+    /** 转义日志中的双引号和反斜杠。 */
+    private static String escape(String value) {
+        return value.replace("\\", "\\\\").replace("\"", "\\\"");
+    }
+
+    /**
+     * 按协议示例构造 Kafka Consumer 配置。
+     * 固定使用 SASL_SSL、SCRAM-SHA-512、字符串 Key 和字节数组 Value。
+     *
+     * @return 可直接用于创建 KafkaConsumer 的配置项
+     */
+    private Map<String, Object> consumerConfig() {
+        Map<String, Object> config = new HashMap<>();
+        config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, properties.getBootstrapServers());
+        config.put(ConsumerConfig.GROUP_ID_CONFIG, properties.getGroupId());
+        config.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, properties.getAutoOffsetReset());
+        config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
+        config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class);
+        config.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SASL_SSL");
+        config.put("sasl.mechanism", "SCRAM-SHA-512");
+        config.put("sasl.jaas.config", "org.apache.kafka.common.security.scram.ScramLoginModule required username=\""
+                + properties.getUsername() + "\" password=\"" + properties.getPassword() + "\";");
+        return config;
+    }
+
+    /**
+     * 解码协议文档第 4.2 节提供的 Base64 测试报文。
+     * 线上 Kafka Value 已经是 protobuf byte[],不得调用此方法二次解码。
+     *
+     * @param base64 文档中的 Base64 报文,可以包含换行
+     * @return protobuf 原始字节;输入为空时返回空数组
+     */
+    public static byte[] decodeDocumentSample(String base64) {
+        if (!StringUtils.hasText(base64)) {
+            return new byte[0];
+        }
+        return Base64.getMimeDecoder().decode(base64);
+    }
+
+    /**
+     * 校验连接和订阅所需的必填参数,防止以不完整配置启动消费者。
+     *
+     * @throws IllegalStateException 缺少 Kafka 地址、用户名、密码或 Topic 时抛出
+     */
+    private void validate() {
+        if (!StringUtils.hasText(properties.getBootstrapServers()) || !StringUtils.hasText(properties.getUsername())
+                || !StringUtils.hasText(properties.getPassword()) || !StringUtils.hasText(properties.getTopic())) {
+            throw new IllegalStateException("udan.ems.kafka requires bootstrap-servers, username, password and topic");
+        }
+    }
+
+    /** 业务消息处理回调。 */
+    @FunctionalInterface
+    public interface MessageHandler {
+        /**
+         * 处理一条优旦 EMS 设备监控消息。
+         *
+         * @param emsSystemId Kafka Key,即 EMS 系统 ID
+         * @param protobufBytes Kafka Value,即 protobuf3 原始字节
+         */
+        void onMessage(String emsSystemId, byte[] protobufBytes);
+    }
+
+    /** 消费和消息处理异常回调。 */
+    @FunctionalInterface
+    public interface ErrorHandler {
+        /**
+         * 处理 Kafka 消费或业务消息处理过程中发生的异常。
+         *
+         * @param error 捕获到的异常
+         */
+        void onError(Throwable error);
+    }
+}
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/HuWeiUtils.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/HuWeiUtils.java
new file mode 100644
index 0000000..7ee5ad1
--- /dev/null
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/HuWeiUtils.java
@@ -0,0 +1,151 @@
+package com.ruoyi.other.util;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 华为:光伏数据对接
+ *
+ * @author Lux
+ * @version 1.0.0
+ * @since 2026/9/7 17:38
+ */
+@Slf4j
+@Service
+public class HuWeiUtils {
+
+    private static final String API_HOST = "https://cn.fusionsolar.huawei.com";
+    private static final String DEFAULT_USER_NAME = "mxxnycd";
+    private static final String DEFAULT_SYSTEM_CODE = "mxxny888888";
+
+    private static final String LOGIN_PATH = "/thirdData/login";
+    private static final String STATIONS_PATH = "/thirdData/stations";
+    private static final String DEV_LIST_PATH = "/thirdData/getDevList";
+    private static final String Kpi_STATION_DAY_PATH = "/thirdData/getKpiStationDay";
+
+    @Resource
+    private RestTemplate restTemplate;
+
+    /** 登录华为 FusionSolar 并取得 XSRF-TOKEN。 */
+    public String getXsrfToken() {
+        return getXsrfToken(DEFAULT_USER_NAME, DEFAULT_SYSTEM_CODE);
+    }
+
+    /** 使用指定账号登录并取得 XSRF-TOKEN。 */
+    public String getXsrfToken(String userName, String systemCode) {
+        JSONObject body = new JSONObject();
+        body.put("userName", userName);
+        body.put("systemCode", systemCode);
+        HttpHeaders headers = new HttpHeaders();
+        headers.set(HttpHeaders.CONTENT_TYPE, "application/json");
+        ResponseEntity<String> response = restTemplate.exchange(API_HOST + LOGIN_PATH, HttpMethod.POST,
+                new HttpEntity<>(body.toJSONString(), headers), String.class);
+        String token = response.getHeaders().getFirst("XSRF-TOKEN");
+        if (token == null) {
+            List<String> cookies = response.getHeaders().get(HttpHeaders.SET_COOKIE);
+            if (cookies != null) {
+                for (String cookie : cookies) {
+                    if (cookie.startsWith("XSRF-TOKEN=")) {
+                        token = cookie.substring("XSRF-TOKEN=".length()).split(";", 2)[0];
+                        break;
+                    }
+                }
+            }
+        }
+        if (token == null || token.trim().isEmpty()) {
+            throw new IllegalStateException("华为登录未返回XSRF-TOKEN");
+        }
+        return token;
+    }
+
+
+    /**
+     * 获取电站列表
+     *
+     * @param pageNo 当前页
+     * @param token 登陆后的token
+     * @return 列表数据
+     */
+    public JSONArray getStations(Integer pageNo, String token) {
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
+        headers.setAccept(Collections.singletonList(org.springframework.http.MediaType.APPLICATION_JSON));
+        headers.set("XSRF-TOKEN", token);
+        headers.set(HttpHeaders.COOKIE, "XSRF-TOKEN=" + token);
+        JSONObject body = new JSONObject();
+        body.put("pageNo", pageNo == null || pageNo < 1 ? 1 : pageNo);
+        ResponseEntity<String> response = restTemplate.exchange(API_HOST + STATIONS_PATH, HttpMethod.POST,
+                new HttpEntity<>(body.toJSONString(), headers), String.class);
+        JSONObject result = JSON.parseObject(response.getBody());
+        if (result == null || !Boolean.TRUE.equals(result.getBoolean("success"))) {
+            throw new IllegalStateException("华为电站列表接口调用失败:" + (result == null ? "空响应" : result.getString("message")));
+        }
+        JSONObject data = result.getJSONObject("data");
+        return data == null || data.getJSONArray("list") == null ? new JSONArray() : data.getJSONArray("list");
+    }
+
+
+    /**
+     * 获取设备列表
+     *
+     * @param stationCodes 电站ID,多个用逗号分隔
+     * @param token 登陆后的token
+     * @return 列表数据
+     */
+    public JSONArray getDevList(String stationCodes, String token) {
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
+        headers.setAccept(Collections.singletonList(org.springframework.http.MediaType.APPLICATION_JSON));
+        headers.set("XSRF-TOKEN", token);
+        headers.set(HttpHeaders.COOKIE, "XSRF-TOKEN=" + token);
+        JSONObject body = new JSONObject();
+        body.put("stationCodes", stationCodes);
+        ResponseEntity<String> response = restTemplate.exchange(API_HOST + DEV_LIST_PATH, HttpMethod.POST,
+                new HttpEntity<>(body.toJSONString(), headers), String.class);
+        JSONObject result = JSON.parseObject(response.getBody());
+        if (result == null || !Boolean.TRUE.equals(result.getBoolean("success"))) {
+            throw new IllegalStateException("华为设备列表接口调用失败:" + (result == null ? "空响应" : result.getString("message")));
+        }
+        JSONArray dataList = result.getJSONArray("data");
+        return dataList == null ? new JSONArray() : dataList;
+    }
+
+    /**
+     * 获取设备列表
+     *
+     * @param stationCodes 电站ID,多个用逗号分隔
+     * @param token 登陆后的token
+     * @return 列表数据
+     */
+    public JSONArray getKpiStationDay(String stationCodes, Long collectTime, String token) {
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
+        headers.setAccept(Collections.singletonList(org.springframework.http.MediaType.APPLICATION_JSON));
+        headers.set("XSRF-TOKEN", token);
+        headers.set(HttpHeaders.COOKIE, "XSRF-TOKEN=" + token);
+        JSONObject body = new JSONObject();
+        body.put("stationCodes", stationCodes);
+        body.put("collectTime", collectTime);
+        ResponseEntity<String> response = restTemplate.exchange(API_HOST + Kpi_STATION_DAY_PATH, HttpMethod.POST,
+                new HttpEntity<>(body.toJSONString(), headers), String.class);
+        JSONObject result = JSON.parseObject(response.getBody());
+        if (result == null || !Boolean.TRUE.equals(result.getBoolean("success"))) {
+            throw new IllegalStateException("华为电站列表接口调用失败:" + (result == null ? "空响应" : result.getString("message")));
+        }
+        JSONArray dataList = result.getJSONArray("data");
+        return dataList == null ? new JSONArray() : dataList;
+    }
+
+}
diff --git a/ruoyi-service/ruoyi-other/src/main/resources/sql/t_screen_content_alter.sql b/ruoyi-service/ruoyi-other/src/main/resources/sql/t_screen_content_alter.sql
new file mode 100644
index 0000000..342d894
--- /dev/null
+++ b/ruoyi-service/ruoyi-other/src/main/resources/sql/t_screen_content_alter.sql
@@ -0,0 +1,4 @@
+-- ----------------------------
+-- 增加大屏名称配置字段
+alter table t_screen_content add column screen_name varchar(255)   default 'NULL'  null comment '大屏名称';
+

--
Gitblit v1.7.1