Pārlūkot izejas kodu

Merge branch 'vpp-zyj' of uskycloud/usky-modules into master

hanzhengyi 4 dienas atpakaļ
vecāks
revīzija
b33a6fd891
25 mainītis faili ar 812 papildinājumiem un 113 dzēšanām
  1. 6 0
      service-job/pom.xml
  2. 11 0
      service-job/src/main/java/com/ruoyi/job/task/RyTask.java
  3. 12 0
      service-vpp/service-vpp-api/src/main/java/com/usky/vpp/RemoteVppTaskService.java
  4. 22 0
      service-vpp/service-vpp-api/src/main/java/com/usky/vpp/factory/RemoteVppTaskFactory.java
  5. 6 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppTsdbConstants.java
  6. 18 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/api/ServiceVppTaskApi.java
  7. 15 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ResourceController.java
  8. 39 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/DmpDeviceStatus.java
  9. 3 4
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDevice.java
  10. 4 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppSite.java
  11. 9 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/mapper/DmpDeviceStatusMapper.java
  12. 6 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppResourceOverviewService.java
  13. 12 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppSiteCompletionRateTaskService.java
  14. 0 3
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppCapabilityEvalServiceImpl.java
  15. 64 24
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDeviceServiceImpl.java
  16. 246 14
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResourceOverviewServiceImpl.java
  17. 278 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppSiteCompletionRateTaskServiceImpl.java
  18. 4 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppSiteServiceImpl.java
  19. 6 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/EnergyTodayVO.java
  20. 13 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResourceCurvePointVO.java
  21. 18 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResourceCurveSeriesVO.java
  22. 17 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResourceOverviewRealtimeCurveVO.java
  23. 3 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/SiteListVO.java
  24. 0 3
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppCapabilityEvalHelper.java
  25. 0 65
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppResourceMockHelper.java

+ 6 - 0
service-job/pom.xml

@@ -94,6 +94,12 @@
             <version>0.0.1</version>
             <scope>compile</scope>
         </dependency>
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>service-vpp-api</artifactId>
+            <version>0.0.1</version>
+            <scope>compile</scope>
+        </dependency>
     </dependencies>
 
     <build>

+ 11 - 0
service-job/src/main/java/com/ruoyi/job/task/RyTask.java

@@ -7,6 +7,7 @@ import com.usky.eg.RemoteEgService;
 import com.usky.fire.RemotePlanSonService;
 import com.usky.meeting.RemoteMeetingService;
 import com.usky.ems.RemoteEmsTaskService;
+import com.usky.vpp.RemoteVppTaskService;
 import com.usky.fire.RemoteFireService;
 import com.usky.iot.RemoteIotTaskService;
 import com.usky.pm.RemotePmService;
@@ -47,6 +48,10 @@ public class RyTask {
     @Autowired
     private RemotePlanSonService remotePlanSonService;
 
+    @Autowired
+    private RemoteVppTaskService remoteVppTaskService;
+
+
     public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i) {
         System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i));
     }
@@ -140,4 +145,10 @@ public class RyTask {
         remotePlanSonService.addPlanSon();
     }
 
+    // 刷新站点平均完成率
+    public void refreshSiteAvgCompletionRate() {
+        System.out.println("refreshSiteAvgCompletionRate start......");
+        remoteVppTaskService.refreshSiteAvgCompletionRate();
+	}
+
 }

+ 12 - 0
service-vpp/service-vpp-api/src/main/java/com/usky/vpp/RemoteVppTaskService.java

@@ -0,0 +1,12 @@
+package com.usky.vpp;
+
+import com.usky.vpp.factory.RemoteVppTaskFactory;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+
+@FeignClient(contextId = "remoteVppTaskService", value = "service-vpp", fallbackFactory = RemoteVppTaskFactory.class)
+public interface RemoteVppTaskService {
+
+    @GetMapping("/refreshSiteAvgCompletionRate")
+    void refreshSiteAvgCompletionRate();
+}

+ 22 - 0
service-vpp/service-vpp-api/src/main/java/com/usky/vpp/factory/RemoteVppTaskFactory.java

@@ -0,0 +1,22 @@
+package com.usky.vpp.factory;
+
+import com.usky.common.core.exception.FeignBadRequestException;
+import com.usky.vpp.RemoteVppTaskService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.cloud.openfeign.FallbackFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class RemoteVppTaskFactory implements FallbackFactory<RemoteVppTaskService> {
+
+    private static final Logger log = LoggerFactory.getLogger(RemoteVppTaskFactory.class);
+
+    @Override
+    public RemoteVppTaskService create(Throwable throwable) {
+        log.error("VPP 定时任务服务调用失败: {}", throwable.getMessage());
+        return () -> {
+            throw new FeignBadRequestException(500, "定时刷新站点平均完成率异常" + throwable.getMessage());
+        };
+    }
+}

+ 6 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppTsdbConstants.java

@@ -45,5 +45,11 @@ public final class VppTsdbConstants {
     /** 正向总有功电能(用电累计) */
     public static final String METRIC_EPP = "epp";
 
+    /** 总有功功率 kW */
+    public static final String METRIC_P = "p";
+
+    /** 储能 SOC % */
+    public static final String METRIC_SOC = "soc";
+
     public static final String TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
 }

+ 18 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/api/ServiceVppTaskApi.java

@@ -0,0 +1,18 @@
+package com.usky.vpp.controller.api;
+
+import com.usky.vpp.RemoteVppTaskService;
+import com.usky.vpp.service.VppSiteCompletionRateTaskService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class ServiceVppTaskApi implements RemoteVppTaskService {
+
+    @Autowired
+    private VppSiteCompletionRateTaskService siteCompletionRateTaskService;
+
+    @Override
+    public void refreshSiteAvgCompletionRate() {
+        siteCompletionRateTaskService.refreshSiteAvgCompletionRate();
+    }
+}

+ 15 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ResourceController.java

@@ -8,6 +8,7 @@ import com.usky.vpp.service.VppResourceOverviewService;
 import com.usky.vpp.service.VppResourcePointService;
 import com.usky.vpp.service.vo.AlarmLevelStatVO;
 import com.usky.vpp.service.vo.EnergyTodayVO;
+import com.usky.vpp.service.vo.ResourceOverviewRealtimeCurveVO;
 import com.usky.vpp.service.vo.ResourceBoardDetailVO;
 import com.usky.vpp.service.vo.ResourceBoardItemVO;
 import com.usky.vpp.service.vo.ResourcePointListVO;
@@ -73,6 +74,20 @@ public class ResourceController {
         return ApiResult.success(vppResourceOverviewService.getEnergyToday());
     }
 
+    /**
+     * 首页 - 实时曲线
+     * <p>租户下设备按时间点 TSDB 汇总:实时负荷、光伏出力、储能 SOC。</p>
+     *
+     * @param startTime 开始时间 yyyy-MM-dd HH:mm:ss,默认当日 00:00:00
+     * @param endTime   结束时间 yyyy-MM-dd HH:mm:ss,默认当前时刻
+     */
+    @GetMapping("/overview/realtime-curves")
+    public ApiResult<ResourceOverviewRealtimeCurveVO> resourceOverviewRealtimeCurves(
+            @RequestParam(value = "startTime", required = false) String startTime,
+            @RequestParam(value = "endTime", required = false) String endTime) {
+        return ApiResult.success(vppResourceOverviewService.getRealtimeCurves(startTime, endTime));
+    }
+
 //    /**
 //     * 资源概览 - 实时告警统计
 //     * <p>按紧急/重要/一般统计告警数量,含跳转告警列表路径(模拟数据)。</p>

+ 39 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/DmpDeviceStatus.java

@@ -0,0 +1,39 @@
+package com.usky.vpp.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 物联网设备状态表(dmp_device_status)
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("dmp_device_status")
+public class DmpDeviceStatus implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    private String deviceId;
+
+    private Integer productId;
+
+    /** 1:在线,2:离线 */
+    private Integer deviceStatus;
+
+    private LocalDateTime lastOnlineTime;
+
+    private LocalDateTime lastOfflineTime;
+
+    private String productCode;
+
+    private String deviceUuid;
+}

+ 3 - 4
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDevice.java

@@ -36,10 +36,6 @@ public class VppDevice implements Serializable {
     private String model;
     @TableField("rated_power_kw")
     private BigDecimal ratedPowerKw;
-    @TableField("comm_status")
-    private Integer commStatus;
-    @TableField("run_status")
-    private Integer runStatus;
     @TableField("firmware_version")
     private String firmwareVersion;
     @TableField("last_online_at")
@@ -61,4 +57,7 @@ public class VppDevice implements Serializable {
     private Integer deleteFlag;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
+    @TableField(exist = false)
+    /** 1:在线,2:离线 */
+    private Integer deviceStatus;
 }

+ 4 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppSite.java

@@ -62,4 +62,8 @@ public class VppSite implements Serializable {
     private LocalDateTime deletedAt;
     private String accountNo;
     private String streetTown;
+    @TableField("avg_completion_rate")
+    private BigDecimal avgCompletionRate;
+    @TableField("run_status")
+    private Integer runStatus;
 }

+ 9 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/mapper/DmpDeviceStatusMapper.java

@@ -0,0 +1,9 @@
+package com.usky.vpp.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.usky.vpp.domain.DmpDeviceStatus;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface DmpDeviceStatusMapper extends BaseMapper<DmpDeviceStatus> {
+}

+ 6 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppResourceOverviewService.java

@@ -2,6 +2,7 @@ package com.usky.vpp.service;
 
 import com.usky.vpp.service.vo.AlarmLevelStatVO;
 import com.usky.vpp.service.vo.EnergyTodayVO;
+import com.usky.vpp.service.vo.ResourceOverviewRealtimeCurveVO;
 import com.usky.vpp.service.vo.ResourceTypeStatVO;
 
 import java.util.List;
@@ -21,6 +22,11 @@ public interface VppResourceOverviewService {
      */
     EnergyTodayVO getEnergyToday();
 
+    /**
+     * 资源概览实时曲线(租户下设备 TSDB 按时间点汇总)
+     */
+    ResourceOverviewRealtimeCurveVO getRealtimeCurves(String startTime, String endTime);
+
     /**
      * 实时告警统计(紧急/重要/一般,含跳转路径,模拟数据)
      */

+ 12 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppSiteCompletionRateTaskService.java

@@ -0,0 +1,12 @@
+package com.usky.vpp.service;
+
+/**
+ * 站点平均完成率定时汇总
+ */
+public interface VppSiteCompletionRateTaskService {
+
+    /**
+     * 根据已结束需求响应事件的参与完成率,汇总更新各站点 avg_completion_rate。
+     */
+    void refreshSiteAvgCompletionRate();
+}

+ 0 - 3
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppCapabilityEvalServiceImpl.java

@@ -315,9 +315,6 @@ public class VppCapabilityEvalServiceImpl implements VppCapabilityEvalService {
             if (device == null || !StringUtils.hasText(device.getDeviceUuid())) {
                 continue;
             }
-            if (device.getCommStatus() != null && device.getCommStatus() == 0) {
-                continue;
-            }
             uuids.add(device.getDeviceUuid().trim());
         }
         return new ArrayList<>(uuids);

+ 64 - 24
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDeviceServiceImpl.java

@@ -6,9 +6,10 @@ import com.usky.common.core.bean.CommonPage;
 import com.usky.common.core.exception.BusinessException;
 import com.usky.common.core.util.UUIDUtils;
 import com.usky.common.security.utils.SecurityUtils;
-import com.usky.iot.RemoteIotService;
 import com.usky.iot.RemoteIotTaskService;
+import com.usky.vpp.domain.DmpDeviceStatus;
 import com.usky.vpp.domain.VppDevice;
+import com.usky.vpp.mapper.DmpDeviceStatusMapper;
 import com.usky.vpp.mapper.VppDeviceMapper;
 import com.usky.vpp.service.VppDeviceService;
 import com.usky.vpp.service.VppSiteService;
@@ -18,19 +19,21 @@ import com.usky.vpp.util.VppPageHelper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
 import org.springframework.util.StringUtils;
 
+import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 @Service
 public class VppDeviceServiceImpl implements VppDeviceService {
 
-    private static final int COMM_OFFLINE = 0;
-    private static final int RUN_STOPPED = 0;
-
     @Autowired
     private VppDeviceMapper deviceMapper;
     @Autowired
+    private DmpDeviceStatusMapper dmpDeviceStatusMapper;
+    @Autowired
     private VppSiteService siteService;
     @Autowired
     private RemoteIotTaskService remoteIotTaskService;
@@ -38,7 +41,7 @@ public class VppDeviceServiceImpl implements VppDeviceService {
     @Override
     public CommonPage<VppDevice> pageDevice(Map<String, Object> params) {
         Page<VppDevice> page = VppPageHelper.of(params);
-        return toCommonPage(deviceMapper.selectPage(page, buildQueryWrapper(params)));
+        return selectDevicePage(page, buildQueryWrapper(params), params);
     }
 
     @Override
@@ -47,7 +50,15 @@ public class VppDeviceServiceImpl implements VppDeviceService {
         Page<VppDevice> page = VppPageHelper.of(params);
         LambdaQueryWrapper<VppDevice> wrapper = buildQueryWrapper(params)
                 .eq(VppDevice::getSiteId, siteId);
-        return toCommonPage(deviceMapper.selectPage(page, wrapper));
+        return selectDevicePage(page, wrapper, params);
+    }
+
+    private CommonPage<VppDevice> selectDevicePage(Page<VppDevice> page,
+                                                   LambdaQueryWrapper<VppDevice> wrapper,
+                                                   Map<String, Object> params) {
+        Page<VppDevice> result = deviceMapper.selectPage(page, wrapper);
+        fillDeviceStatus(result.getRecords(), params);
+        return toCommonPage(result);
     }
 
     @Override
@@ -84,16 +95,11 @@ public class VppDeviceServiceImpl implements VppDeviceService {
         device.setDeviceUuid(UUIDUtils.uuid().substring(0, 16));
         applyRequest(device, request);
         device.setTenantId(SecurityUtils.getTenantId());
-        if (device.getCommStatus() == null) {
-            device.setCommStatus(COMM_OFFLINE);
-        }
-        if (device.getRunStatus() == null) {
-            device.setRunStatus(RUN_STOPPED);
-        }
         VppAuditHelper.fillCreate(device);
         deviceMapper.insert(device);
 
-        remoteIotTaskService.addDeviceInfo(request.getProductCode(),device.getDeviceUuid(),device.getDeviceCode(),device.getDeviceName(),"",2);
+        remoteIotTaskService.addDeviceInfo(request.getProductCode(), device.getDeviceUuid(), device.getDeviceCode(),
+                device.getDeviceName(), "", 2);
         return device;
     }
 
@@ -120,6 +126,7 @@ public class VppDeviceServiceImpl implements VppDeviceService {
     private LambdaQueryWrapper<VppDevice> buildQueryWrapper(Map<String, Object> params) {
         LambdaQueryWrapper<VppDevice> wrapper = new LambdaQueryWrapper<VppDevice>()
                 .eq(VppDevice::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(VppDevice::getTenantId, SecurityUtils.getTenantId())
                 .orderByDesc(VppDevice::getCreateTime);
         if (params == null) {
             return wrapper;
@@ -139,13 +146,52 @@ public class VppDeviceServiceImpl implements VppDeviceService {
         if (params.get("deviceType") != null) {
             wrapper.eq(VppDevice::getDeviceType, params.get("deviceType").toString());
         }
-        if (params.get("commStatus") != null) {
-            wrapper.eq(VppDevice::getCommStatus, Integer.parseInt(params.get("commStatus").toString()));
+        applyRunStatusFilter(wrapper, params);
+        return wrapper;
+    }
+
+    private void applyRunStatusFilter(LambdaQueryWrapper<VppDevice> wrapper, Map<String, Object> params) {
+        Integer runStatus = resolveRunStatus(params);
+        if (runStatus == null) {
+            return;
         }
-        if (params.get("runStatus") != null) {
-            wrapper.eq(VppDevice::getRunStatus, Integer.parseInt(params.get("runStatus").toString()));
+        wrapper.isNotNull(VppDevice::getDeviceUuid)
+                .ne(VppDevice::getDeviceUuid, "")
+                .apply("EXISTS (SELECT 1 FROM dmp_device_status ds WHERE ds.device_uuid = vpp_device.device_uuid AND ds.device_status = {0})", runStatus);
+    }
+
+    private Integer resolveRunStatus(Map<String, Object> params) {
+        if (params == null || params.get("runStatus") == null) {
+            return null;
         }
-        return wrapper;
+        return Integer.parseInt(params.get("runStatus").toString());
+    }
+
+    private void fillDeviceStatus(List<VppDevice> devices, Map<String, Object> params) {
+        if (CollectionUtils.isEmpty(devices)) {
+            return;
+        }
+        Integer runStatus = resolveRunStatus(params);
+        if (runStatus != null) {
+            devices.forEach(device -> device.setDeviceStatus(runStatus));
+            return;
+        }
+        List<String> deviceUuids = devices.stream()
+                .map(VppDevice::getDeviceUuid)
+                .filter(StringUtils::hasText)
+                .distinct()
+                .collect(Collectors.toList());
+        if (deviceUuids.isEmpty()) {
+            return;
+        }
+        Map<String, Integer> statusByUuid = dmpDeviceStatusMapper.selectList(
+                        new LambdaQueryWrapper<DmpDeviceStatus>()
+                                .select(DmpDeviceStatus::getDeviceUuid, DmpDeviceStatus::getDeviceStatus)
+                                .in(DmpDeviceStatus::getDeviceUuid, deviceUuids))
+                .stream()
+                .filter(status -> StringUtils.hasText(status.getDeviceUuid()))
+                .collect(Collectors.toMap(DmpDeviceStatus::getDeviceUuid, DmpDeviceStatus::getDeviceStatus, (a, b) -> a));
+        devices.forEach(device -> device.setDeviceStatus(statusByUuid.get(device.getDeviceUuid())));
     }
 
     private void validateDeviceRequest(DeviceRequest request, Long excludeId) {
@@ -178,12 +224,6 @@ public class VppDeviceServiceImpl implements VppDeviceService {
         device.setManufacturer(request.getManufacturer());
         device.setModel(request.getModel());
         device.setRatedPowerKw(request.getRatedPowerKw());
-        if (request.getCommStatus() != null) {
-            device.setCommStatus(request.getCommStatus());
-        }
-        if (request.getRunStatus() != null) {
-            device.setRunStatus(request.getRunStatus());
-        }
         device.setFirmwareVersion(request.getFirmwareVersion());
         device.setGatewayId(request.getGatewayId());
         device.setRemark(request.getRemark());

+ 246 - 14
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResourceOverviewServiceImpl.java

@@ -1,6 +1,7 @@
 package com.usky.vpp.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.common.core.exception.BusinessException;
 import com.usky.common.security.utils.SecurityUtils;
 import com.usky.vpp.constant.VppTsdbConstants;
 import com.usky.vpp.domain.VppDevice;
@@ -11,6 +12,9 @@ import com.usky.vpp.service.VppResourceOverviewService;
 import com.usky.vpp.service.VppTsdbQueryService;
 import com.usky.vpp.service.vo.AlarmLevelStatVO;
 import com.usky.vpp.service.vo.EnergyTodayVO;
+import com.usky.vpp.service.vo.ResourceCurvePointVO;
+import com.usky.vpp.service.vo.ResourceCurveSeriesVO;
+import com.usky.vpp.service.vo.ResourceOverviewRealtimeCurveVO;
 import com.usky.vpp.service.vo.ResourceTypeStatVO;
 import com.usky.vpp.util.VppAuditHelper;
 import com.usky.vpp.util.VppResourceMockHelper;
@@ -23,9 +27,13 @@ import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -39,6 +47,8 @@ import java.util.stream.Collectors;
 public class VppResourceOverviewServiceImpl implements VppResourceOverviewService {
 
     private static final List<String> RESOURCE_TYPES = Arrays.asList("PV", "ESS", "EVCS", "IND_LOAD", "COM_BLDG");
+    private static final DateTimeFormatter TIME_HH_MM = DateTimeFormatter.ofPattern("HH:mm");
+    private static final DateTimeFormatter TSDB_TIME = DateTimeFormatter.ofPattern(VppTsdbConstants.TIME_FORMAT);
 
     @Autowired
     private VppResourcePointMapper resourcePointMapper;
@@ -64,6 +74,27 @@ public class VppResourceOverviewServiceImpl implements VppResourceOverviewServic
         return buildAlarmStatsMock();
     }
 
+    @Override
+    public ResourceOverviewRealtimeCurveVO getRealtimeCurves(String startTime, String endTime) {
+        LocalDateTime end = parseDateTime(endTime, LocalDateTime.now());
+        LocalDateTime start = parseDateTime(startTime, LocalDate.now().atStartOfDay());
+        if (start.isAfter(end)) {
+            throw new BusinessException("开始时间不能晚于结束时间");
+        }
+
+        List<VppDevice> devices = listActiveDevices();
+        Map<Long, String> deviceResourceTypeMap = buildDeviceResourceTypeMap();
+        List<String> allDeviceUuids = resolveDeviceUuids(devices);
+        List<String> pvDeviceUuids = resolveDeviceUuidsByResourceType(devices, deviceResourceTypeMap, "PV");
+        List<String> essDeviceUuids = resolveDeviceUuidsByResourceType(devices, deviceResourceTypeMap, "ESS");
+
+        ResourceOverviewRealtimeCurveVO curve = new ResourceOverviewRealtimeCurveVO();
+        curve.setLoadCurve(buildPowerCurveSeries("实时负荷曲线", "kW", allDeviceUuids, start, end));
+        curve.setPvOutputCurve(buildPowerCurveSeries("光伏出力曲线", "kW", pvDeviceUuids, start, end));
+        curve.setEssSocCurve(buildSocCurveSeries("储能SOC曲线", essDeviceUuids, start, end));
+        return curve;
+    }
+
     private List<VppResourcePoint> listActiveResources() {
         return resourcePointMapper.selectList(
                 new LambdaQueryWrapper<VppResourcePoint>()
@@ -71,6 +102,176 @@ public class VppResourceOverviewServiceImpl implements VppResourceOverviewServic
                         .eq(VppResourcePoint::getTenantId, SecurityUtils.getTenantId()));
     }
 
+    private List<VppDevice> listActiveDevices() {
+        return deviceMapper.selectList(
+                new LambdaQueryWrapper<VppDevice>()
+                        .eq(VppDevice::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .eq(VppDevice::getTenantId, SecurityUtils.getTenantId()));
+    }
+
+    private Map<Long, String> buildDeviceResourceTypeMap() {
+        Map<Long, String> deviceResourceTypeMap = new HashMap<>();
+        for (VppResourcePoint resource : listActiveResources()) {
+            if (resource.getDeviceId() == null || !StringUtils.hasText(resource.getResourceType())) {
+                continue;
+            }
+            deviceResourceTypeMap.putIfAbsent(resource.getDeviceId(), resource.getResourceType().toUpperCase());
+        }
+        return deviceResourceTypeMap;
+    }
+
+    private ResourceCurveSeriesVO buildPowerCurveSeries(String name, String unit, List<String> deviceUuids,
+                                                        LocalDateTime start, LocalDateTime end) {
+        Map<LocalDateTime, BigDecimal> aggregated = queryAggregatedSum(deviceUuids, start, end,
+                Collections.singletonList(VppTsdbConstants.METRIC_P));
+        return toCurveSeries(name, unit, aggregated);
+    }
+
+    private ResourceCurveSeriesVO buildSocCurveSeries(String name, List<String> deviceUuids,
+                                                      LocalDateTime start, LocalDateTime end) {
+        Map<LocalDateTime, BigDecimal> aggregated = queryAggregatedAverage(deviceUuids, start, end,
+                Collections.singletonList(VppTsdbConstants.METRIC_SOC));
+        return toCurveSeries(name, "%", aggregated);
+    }
+
+    private Map<LocalDateTime, BigDecimal> queryAggregatedSum(List<String> deviceUuids, LocalDateTime start,
+                                                              LocalDateTime end, List<String> metrics) {
+        if (CollectionUtils.isEmpty(deviceUuids)) {
+            return Collections.emptyMap();
+        }
+        Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> history =
+                vppTsdbQueryService.queryDeviceMetricHistory(deviceUuids, start, end, metrics);
+        return aggregateSumByTime(history, metrics.get(0));
+    }
+
+    private Map<LocalDateTime, BigDecimal> queryAggregatedAverage(List<String> deviceUuids, LocalDateTime start,
+                                                                  LocalDateTime end, List<String> metrics) {
+        if (CollectionUtils.isEmpty(deviceUuids)) {
+            return Collections.emptyMap();
+        }
+        Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> history =
+                vppTsdbQueryService.queryDeviceMetricHistory(deviceUuids, start, end, metrics);
+        return aggregateAverageByTime(history, metrics.get(0));
+    }
+
+    private Map<LocalDateTime, BigDecimal> aggregateSumByTime(
+            Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> history, String metric) {
+        if (CollectionUtils.isEmpty(history)) {
+            return Collections.emptyMap();
+        }
+        Map<LocalDateTime, BigDecimal> result = new TreeMap<>();
+        for (Map<String, TreeMap<LocalDateTime, BigDecimal>> deviceMetrics : history.values()) {
+            TreeMap<LocalDateTime, BigDecimal> series = deviceMetrics.get(metric);
+            if (series == null || series.isEmpty()) {
+                continue;
+            }
+            for (Map.Entry<LocalDateTime, BigDecimal> entry : series.entrySet()) {
+                result.merge(entry.getKey(), entry.getValue(), BigDecimal::add);
+            }
+        }
+        return result;
+    }
+
+    private Map<LocalDateTime, BigDecimal> aggregateAverageByTime(
+            Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> history, String metric) {
+        if (CollectionUtils.isEmpty(history)) {
+            return Collections.emptyMap();
+        }
+        Map<LocalDateTime, List<BigDecimal>> samples = new TreeMap<>();
+        for (Map<String, TreeMap<LocalDateTime, BigDecimal>> deviceMetrics : history.values()) {
+            TreeMap<LocalDateTime, BigDecimal> series = deviceMetrics.get(metric);
+            if (series == null || series.isEmpty()) {
+                continue;
+            }
+            for (Map.Entry<LocalDateTime, BigDecimal> entry : series.entrySet()) {
+                samples.computeIfAbsent(entry.getKey(), key -> new ArrayList<>()).add(entry.getValue());
+            }
+        }
+        Map<LocalDateTime, BigDecimal> result = new TreeMap<>();
+        samples.forEach((timestamp, values) -> {
+            BigDecimal avg = averageValues(values);
+            if (avg != null) {
+                result.put(timestamp, avg);
+            }
+        });
+        return result;
+    }
+
+    private BigDecimal averageValues(List<BigDecimal> values) {
+        if (CollectionUtils.isEmpty(values)) {
+            return null;
+        }
+        List<BigDecimal> nonNullValues = values.stream()
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+        if (nonNullValues.isEmpty()) {
+            return null;
+        }
+        BigDecimal sum = nonNullValues.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
+        return sum.divide(BigDecimal.valueOf(nonNullValues.size()), 4, RoundingMode.HALF_UP);
+    }
+
+    private ResourceCurveSeriesVO toCurveSeries(String name, String unit, Map<LocalDateTime, BigDecimal> aggregated) {
+        ResourceCurveSeriesVO series = new ResourceCurveSeriesVO();
+        series.setName(name);
+        series.setUnit(unit);
+        List<ResourceCurvePointVO> points = new ArrayList<>();
+        for (Map.Entry<LocalDateTime, BigDecimal> entry : aggregated.entrySet()) {
+            ResourceCurvePointVO point = new ResourceCurvePointVO();
+            point.setTime(entry.getKey().format(TIME_HH_MM));
+            point.setValue(entry.getValue().setScale(2, RoundingMode.HALF_UP));
+            points.add(point);
+        }
+        series.setPoints(points);
+        series.setCurrentValue(aggregated.entrySet().stream()
+                .max(Map.Entry.comparingByKey())
+                .map(entry -> entry.getValue().setScale(2, RoundingMode.HALF_UP))
+                .orElse(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)));
+        return series;
+    }
+
+    private List<String> resolveDeviceUuids(List<VppDevice> devices) {
+        if (CollectionUtils.isEmpty(devices)) {
+            return Collections.emptyList();
+        }
+        return devices.stream()
+                .map(VppDevice::getDeviceUuid)
+                .filter(StringUtils::hasText)
+                .map(String::trim)
+                .distinct()
+                .collect(Collectors.toList());
+    }
+
+    private List<String> resolveDeviceUuidsByResourceType(List<VppDevice> devices,
+                                                          Map<Long, String> deviceResourceTypeMap,
+                                                          String resourceType) {
+        if (CollectionUtils.isEmpty(devices) || !StringUtils.hasText(resourceType)) {
+            return Collections.emptyList();
+        }
+        String targetType = resourceType.toUpperCase();
+        Set<String> uuids = new LinkedHashSet<>();
+        for (VppDevice device : devices) {
+            if (device.getId() == null || !StringUtils.hasText(device.getDeviceUuid())) {
+                continue;
+            }
+            if (targetType.equals(deviceResourceTypeMap.get(device.getId()))) {
+                uuids.add(device.getDeviceUuid().trim());
+            }
+        }
+        return new ArrayList<>(uuids);
+    }
+
+    private LocalDateTime parseDateTime(String value, LocalDateTime defaultValue) {
+        if (!StringUtils.hasText(value)) {
+            return defaultValue;
+        }
+        try {
+            return LocalDateTime.parse(value.trim(), TSDB_TIME);
+        } catch (DateTimeParseException ex) {
+            throw new BusinessException("时间格式无效,请使用 yyyy-MM-dd HH:mm:ss");
+        }
+    }
+
     private List<ResourceTypeStatVO> buildTypeStats(List<VppResourcePoint> resources) {
         Map<String, ResourceTypeStatVO> statMap = new LinkedHashMap<>();
         for (String type : RESOURCE_TYPES) {
@@ -114,28 +315,59 @@ public class VppResourceOverviewServiceImpl implements VppResourceOverviewServic
         List<String> pvDeviceUuids = resolveDeviceUuids(pvResources, deviceMap);
         List<String> loadDeviceUuids = resolveDeviceUuids(loadResources, deviceMap);
 
-        LocalDateTime startTime = LocalDate.now().atStartOfDay();
-        LocalDateTime endTime = LocalDateTime.now();
+        LocalDateTime todayStart = LocalDate.now().atStartOfDay();
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime yesterdayStart = todayStart.minusDays(1);
+        LocalDateTime yesterdayEnd = yesterdayStart.plus(ChronoUnit.MINUTES.between(todayStart, now), ChronoUnit.MINUTES);
 
-        BigDecimal totalGeneration = queryTodayEnergy(pvDeviceUuids, startTime, endTime,
+        BigDecimal totalGeneration = queryTodayEnergy(pvDeviceUuids, todayStart, now,
+                Collections.singletonList(VppTsdbConstants.METRIC_EDAY), true);
+        BigDecimal totalConsumption = queryTodayEnergy(loadDeviceUuids, todayStart, now,
+                Collections.singletonList(VppTsdbConstants.METRIC_EPP), false);
+        BigDecimal yesterdayGeneration = queryTodayEnergy(pvDeviceUuids, yesterdayStart, yesterdayEnd,
                 Collections.singletonList(VppTsdbConstants.METRIC_EDAY), true);
-        BigDecimal totalConsumption = queryTodayEnergy(loadDeviceUuids, startTime, endTime,
+        BigDecimal yesterdayConsumption = queryTodayEnergy(loadDeviceUuids, yesterdayStart, yesterdayEnd,
                 Collections.singletonList(VppTsdbConstants.METRIC_EPP), false);
 
+        BigDecimal totalGenerationScaled = totalGeneration.setScale(2, RoundingMode.HALF_UP);
+        BigDecimal totalConsumptionScaled = totalConsumption.setScale(2, RoundingMode.HALF_UP);
+        BigDecimal greenEnergyRatio = calculateGreenEnergyRatio(totalGenerationScaled, totalConsumptionScaled);
+        BigDecimal yesterdayGreenEnergyRatio = calculateGreenEnergyRatio(
+                yesterdayGeneration.setScale(2, RoundingMode.HALF_UP),
+                yesterdayConsumption.setScale(2, RoundingMode.HALF_UP));
+
         EnergyTodayVO energy = new EnergyTodayVO();
-        energy.setTotalGenerationKwh(totalGeneration.setScale(2, RoundingMode.HALF_UP));
-        energy.setTotalConsumptionKwh(totalConsumption.setScale(2, RoundingMode.HALF_UP));
-        if (energy.getTotalConsumptionKwh().compareTo(BigDecimal.ZERO) > 0) {
-            BigDecimal ratio = energy.getTotalGenerationKwh()
-                    .multiply(BigDecimal.valueOf(100))
-                    .divide(energy.getTotalConsumptionKwh(), 2, RoundingMode.HALF_UP);
-            energy.setGreenEnergyRatioPercent(ratio.min(BigDecimal.valueOf(100)));
-        } else {
-            energy.setGreenEnergyRatioPercent(BigDecimal.ZERO);
-        }
+        energy.setTotalGenerationKwh(totalGenerationScaled);
+        energy.setTotalGenerationKwhRingRatio(calculateRingRatio(totalGeneration, yesterdayGeneration));
+        energy.setTotalConsumptionKwh(totalConsumptionScaled);
+        energy.setTotalConsumptionKwhRingRatio(calculateRingRatio(totalConsumption, yesterdayConsumption));
+        energy.setGreenEnergyRatioPercent(greenEnergyRatio);
+        energy.setGreenEnergyRatioPercentRingRatio(calculateRingRatio(greenEnergyRatio, yesterdayGreenEnergyRatio));
         return energy;
     }
 
+    private BigDecimal calculateGreenEnergyRatio(BigDecimal generationKwh, BigDecimal consumptionKwh) {
+        if (consumptionKwh == null || consumptionKwh.compareTo(BigDecimal.ZERO) <= 0) {
+            return BigDecimal.ZERO;
+        }
+        BigDecimal generation = generationKwh != null ? generationKwh : BigDecimal.ZERO;
+        return generation.multiply(BigDecimal.valueOf(100))
+                .divide(consumptionKwh, 2, RoundingMode.HALF_UP)
+                .min(BigDecimal.valueOf(100));
+    }
+
+    private BigDecimal calculateRingRatio(BigDecimal current, BigDecimal previous) {
+        if (current == null) {
+            current = BigDecimal.ZERO;
+        }
+        if (previous == null || previous.compareTo(BigDecimal.ZERO) == 0) {
+            return BigDecimal.ZERO;
+        }
+        return current.subtract(previous)
+                .multiply(BigDecimal.valueOf(100))
+                .divide(previous, 2, RoundingMode.HALF_UP);
+    }
+
     private BigDecimal queryTodayEnergy(List<String> deviceUuids, LocalDateTime startTime, LocalDateTime endTime,
                                           List<String> metrics, boolean useLatestValue) {
         if (CollectionUtils.isEmpty(deviceUuids)) {

+ 278 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppSiteCompletionRateTaskServiceImpl.java

@@ -0,0 +1,278 @@
+package com.usky.vpp.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.vpp.domain.VppDevice;
+import com.usky.vpp.domain.VppDrEvent;
+import com.usky.vpp.domain.VppDrParticipation;
+import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.domain.VppSite;
+import com.usky.vpp.mapper.VppDeviceMapper;
+import com.usky.vpp.mapper.VppDrEventMapper;
+import com.usky.vpp.mapper.VppDrParticipationMapper;
+import com.usky.vpp.mapper.VppResourcePointMapper;
+import com.usky.vpp.mapper.VppSiteMapper;
+import com.usky.vpp.service.VppSiteCompletionRateTaskService;
+import com.usky.vpp.service.VppTsdbQueryService;
+import com.usky.vpp.util.VppAuditHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Service
+public class VppSiteCompletionRateTaskServiceImpl implements VppSiteCompletionRateTaskService {
+
+    private static final Logger log = LoggerFactory.getLogger(VppSiteCompletionRateTaskServiceImpl.class);
+
+    private static final int EVENT_STATUS_ENDED = 3;
+    private static final int SITE_STATUS_ONLINE = 0;
+    private static final int SITE_STATUS_OFFLINE = 1;
+    private static final int SITE_STATUS_FAULT = 2;
+    private static final int TSDB_DEVICE_STATUS_OFFLINE = 0;
+    private static final int TSDB_DEVICE_STATUS_ONLINE = 1;
+
+    @Autowired
+    private VppDrEventMapper drEventMapper;
+    @Autowired
+    private VppDrParticipationMapper participationMapper;
+    @Autowired
+    private VppResourcePointMapper resourcePointMapper;
+    @Autowired
+    private VppDeviceMapper deviceMapper;
+    @Autowired
+    private VppSiteMapper siteMapper;
+    @Autowired
+    private VppTsdbQueryService vppTsdbQueryService;
+
+    @Override
+    public void refreshSiteAvgCompletionRate() {
+        log.info("refreshSiteAvgCompletionRate start");
+
+        Map<Long, BigDecimal> rateList = loadResourceCompletionRateMap();
+        List<VppResourcePoint> resources = loadActiveResources();
+        if (resources.isEmpty()) {
+            log.info("refreshSiteAvgCompletionRate skipped: no active resource points");
+            return;
+        }
+
+        Map<Long, List<VppResourcePoint>> siteResourcesMap = groupResourcesBySite(resources);
+        Map<Long, VppDevice> deviceMap = loadDeviceMap(resources);
+        Map<String, Map<String, BigDecimal>> tsdbMetricsMap = loadTsdbMetricsMap(deviceMap.values());
+
+        int updatedCount = 0;
+        for (Map.Entry<Long, List<VppResourcePoint>> entry : siteResourcesMap.entrySet()) {
+            Long siteId = entry.getKey();
+            List<VppResourcePoint> siteResources = entry.getValue();
+
+            Integer runStatus = resolveSiteRunStatus(siteResources, deviceMap, tsdbMetricsMap);
+            BigDecimal avgRate = averageCompletionRate(siteResources, rateList);
+
+            LambdaUpdateWrapper<VppSite> updateWrapper = new LambdaUpdateWrapper<VppSite>()
+                    .eq(VppSite::getId, siteId)
+                    .set(VppSite::getRunStatus, runStatus);
+            if (avgRate != null) {
+                updateWrapper.set(VppSite::getAvgCompletionRate, avgRate);
+            }
+            siteMapper.update(null, updateWrapper);
+            updatedCount++;
+        }
+
+        log.info("refreshSiteAvgCompletionRate finished, updatedSites={}", updatedCount);
+    }
+
+    private Map<Long, BigDecimal> loadResourceCompletionRateMap() {
+        LambdaQueryWrapper<VppDrEvent> eventWrapper = new LambdaQueryWrapper<VppDrEvent>()
+                .select(VppDrEvent::getId)
+                .eq(VppDrEvent::getEventStatus, EVENT_STATUS_ENDED)
+                .eq(VppDrEvent::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+        Integer tenantId = SecurityUtils.getTenantId();
+        if (tenantId != null) {
+            eventWrapper.eq(VppDrEvent::getTenantId, tenantId);
+        }
+        List<Long> endedEventIds = drEventMapper.selectList(eventWrapper)
+                .stream()
+                .map(VppDrEvent::getId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+        if (endedEventIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+
+        List<VppDrParticipation> participations = participationMapper.selectList(
+                new LambdaQueryWrapper<VppDrParticipation>()
+                        .select(VppDrParticipation::getResourceId, VppDrParticipation::getCompletionRate)
+                        .in(VppDrParticipation::getEventId, endedEventIds)
+                        .isNotNull(VppDrParticipation::getResourceId)
+                        .isNotNull(VppDrParticipation::getCompletionRate)
+                        .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        if (CollectionUtils.isEmpty(participations)) {
+            return Collections.emptyMap();
+        }
+
+        Map<Long, List<BigDecimal>> groupedRates = new HashMap<>();
+        for (VppDrParticipation participation : participations) {
+            groupedRates.computeIfAbsent(participation.getResourceId(), key -> new ArrayList<>())
+                    .add(participation.getCompletionRate());
+        }
+
+        Map<Long, BigDecimal> rateList = new HashMap<>();
+        groupedRates.forEach((resourceId, rates) -> {
+            BigDecimal avg = averageValues(rates);
+            if (avg != null) {
+                rateList.put(resourceId, avg);
+            }
+        });
+        return rateList;
+    }
+
+    private List<VppResourcePoint> loadActiveResources() {
+        LambdaQueryWrapper<VppResourcePoint> wrapper = new LambdaQueryWrapper<VppResourcePoint>()
+                .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+        Integer tenantId = SecurityUtils.getTenantId();
+        if (tenantId != null) {
+            wrapper.eq(VppResourcePoint::getTenantId, tenantId);
+        }
+        return resourcePointMapper.selectList(wrapper);
+    }
+
+    private Map<Long, List<VppResourcePoint>> groupResourcesBySite(List<VppResourcePoint> resources) {
+        Map<Long, List<VppResourcePoint>> siteResourcesMap = new HashMap<>();
+        for (VppResourcePoint resource : resources) {
+            if (resource.getSiteId() == null) {
+                continue;
+            }
+            siteResourcesMap.computeIfAbsent(resource.getSiteId(), key -> new ArrayList<>()).add(resource);
+        }
+        return siteResourcesMap;
+    }
+
+    private Map<Long, VppDevice> loadDeviceMap(List<VppResourcePoint> resources) {
+        Set<Long> deviceIds = resources.stream()
+                .map(VppResourcePoint::getDeviceId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        if (deviceIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        return deviceMapper.selectBatchIds(deviceIds).stream()
+                .filter(device -> !VppAuditHelper.isDeleted(device.getDeleteFlag()))
+                .collect(Collectors.toMap(VppDevice::getId, device -> device, (a, b) -> a));
+    }
+
+    private Map<String, Map<String, BigDecimal>> loadTsdbMetricsMap(Collection<VppDevice> devices) {
+        List<String> deviceUuids = devices.stream()
+                .map(VppDevice::getDeviceUuid)
+                .filter(StringUtils::hasText)
+                .distinct()
+                .collect(Collectors.toList());
+        if (deviceUuids.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        return vppTsdbQueryService.queryLatestMetrics(deviceUuids,
+                Collections.singletonList("device_status"));
+    }
+
+    private Integer resolveSiteRunStatus(List<VppResourcePoint> siteResources,
+                                         Map<Long, VppDevice> deviceMap,
+                                         Map<String, Map<String, BigDecimal>> tsdbMetricsMap) {
+        List<Integer> deviceStatuses = new ArrayList<>();
+        for (VppResourcePoint resource : siteResources) {
+            VppDevice device = resolveDeviceForResource(resource, deviceMap);
+            Map<String, BigDecimal> tsdbMetrics = resolveTsdbMetrics(device, tsdbMetricsMap);
+            deviceStatuses.add(resolveDeviceOnlineStatus(tsdbMetrics));
+        }
+        return resolveSiteRunStatus(deviceStatuses);
+    }
+
+    private VppDevice resolveDeviceForResource(VppResourcePoint resource, Map<Long, VppDevice> deviceMap) {
+        if (resource.getDeviceId() == null) {
+            return null;
+        }
+        VppDevice device = deviceMap.get(resource.getDeviceId());
+        return matchesResourceSite(device, resource.getSiteId()) ? device : null;
+    }
+
+    private boolean matchesResourceSite(VppDevice device, Long siteId) {
+        return device != null && siteId != null && siteId.equals(device.getSiteId());
+    }
+
+    private Map<String, BigDecimal> resolveTsdbMetrics(VppDevice device,
+                                                       Map<String, Map<String, BigDecimal>> tsdbMetricsMap) {
+        if (device == null || !StringUtils.hasText(device.getDeviceUuid())) {
+            return Collections.emptyMap();
+        }
+        Map<String, BigDecimal> metrics = tsdbMetricsMap.get(device.getDeviceUuid());
+        return metrics != null ? metrics : Collections.emptyMap();
+    }
+
+    private Integer resolveDeviceOnlineStatus(Map<String, BigDecimal> tsdbMetrics) {
+        if (CollectionUtils.isEmpty(tsdbMetrics) || tsdbMetrics.get("device_status") == null) {
+            return null;
+        }
+        int status = tsdbMetrics.get("device_status").intValue();
+        if (status == TSDB_DEVICE_STATUS_OFFLINE) {
+            return TSDB_DEVICE_STATUS_OFFLINE;
+        }
+        if (status == TSDB_DEVICE_STATUS_ONLINE) {
+            return TSDB_DEVICE_STATUS_ONLINE;
+        }
+        return null;
+    }
+
+    private Integer resolveSiteRunStatus(List<Integer> deviceStatuses) {
+        if (CollectionUtils.isEmpty(deviceStatuses)) {
+            return SITE_STATUS_FAULT;
+        }
+        boolean allOnline = deviceStatuses.stream().allMatch(status -> status != null && status == TSDB_DEVICE_STATUS_ONLINE);
+        boolean allOffline = deviceStatuses.stream().allMatch(status -> status != null && status == TSDB_DEVICE_STATUS_OFFLINE);
+        if (allOnline) {
+            return SITE_STATUS_ONLINE;
+        }
+        if (allOffline) {
+            return SITE_STATUS_OFFLINE;
+        }
+        return SITE_STATUS_FAULT;
+    }
+
+    private BigDecimal averageCompletionRate(List<VppResourcePoint> siteResources, Map<Long, BigDecimal> rateList) {
+        if (CollectionUtils.isEmpty(siteResources) || CollectionUtils.isEmpty(rateList)) {
+            return null;
+        }
+        List<BigDecimal> matchedRates = siteResources.stream()
+                .map(VppResourcePoint::getId)
+                .map(rateList::get)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+        return averageValues(matchedRates);
+    }
+
+    private BigDecimal averageValues(List<BigDecimal> values) {
+        if (CollectionUtils.isEmpty(values)) {
+            return null;
+        }
+        List<BigDecimal> nonNullValues = values.stream()
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+        if (nonNullValues.isEmpty()) {
+            return null;
+        }
+        BigDecimal sum = nonNullValues.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
+        return sum.divide(BigDecimal.valueOf(nonNullValues.size()), 4, RoundingMode.HALF_UP);
+    }
+}

+ 4 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppSiteServiceImpl.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.common.core.exception.BusinessException;
+import com.usky.common.security.utils.SecurityUtils;
 import com.usky.vpp.domain.VppCustomer;
 import com.usky.vpp.domain.VppSite;
 import com.usky.vpp.domain.VppSiteConfig;
@@ -158,6 +159,7 @@ public class VppSiteServiceImpl implements VppSiteService {
     private LambdaQueryWrapper<VppSite> buildSiteWrapper(Map<String, Object> params) {
         LambdaQueryWrapper<VppSite> wrapper = new LambdaQueryWrapper<VppSite>()
                 .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(VppSite::getTenantId, SecurityUtils.getTenantId())
                 .orderByDesc(VppSite::getCreateTime);
         if (params != null) {
             if (params.get("customerId") != null) {
@@ -241,6 +243,8 @@ public class VppSiteServiceImpl implements VppSiteService {
                     .collect(Collectors.toList()));
             vo.setAccountNo(site.getAccountNo());
             vo.setStreetTown(site.getStreetTown());
+            vo.setAvgCompletionRate(site.getAvgCompletionRate());
+            vo.setRunStatus(site.getRunStatus());
             voList.add(vo);
         }
         return new CommonPage<>(voList, page.getTotal(), page.getCurrent(), page.getSize());

+ 6 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/EnergyTodayVO.java

@@ -12,8 +12,14 @@ public class EnergyTodayVO {
 
     /** 总发电量 kWh */
     private BigDecimal totalGenerationKwh;
+    /** 总发电量环比 % */
+    private BigDecimal totalGenerationKwhRingRatio;
     /** 总用电量 kWh */
     private BigDecimal totalConsumptionKwh;
+    /** 总用电量环比 % */
+    private BigDecimal totalConsumptionKwhRingRatio;
     /** 绿电消纳比例 % */
     private BigDecimal greenEnergyRatioPercent;
+    /** 绿电消纳比例环比 % */
+    private BigDecimal greenEnergyRatioPercentRingRatio;
 }

+ 13 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResourceCurvePointVO.java

@@ -0,0 +1,13 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class ResourceCurvePointVO {
+
+    /** 时刻 HH:mm */
+    private String time;
+    private BigDecimal value;
+}

+ 18 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResourceCurveSeriesVO.java

@@ -0,0 +1,18 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+public class ResourceCurveSeriesVO {
+
+    /** 曲线名称 */
+    private String name;
+    /** 单位 kW / % */
+    private String unit;
+    /** 当前值(最新时间点) */
+    private BigDecimal currentValue;
+    private List<ResourceCurvePointVO> points;
+}

+ 17 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResourceOverviewRealtimeCurveVO.java

@@ -0,0 +1,17 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+/**
+ * 资源概览实时曲线(负荷 / 光伏出力 / 储能 SOC)
+ */
+@Data
+public class ResourceOverviewRealtimeCurveVO {
+
+    /** 实时负荷曲线 kW */
+    private ResourceCurveSeriesVO loadCurve;
+    /** 光伏出力曲线 kW */
+    private ResourceCurveSeriesVO pvOutputCurve;
+    /** 储能 SOC 曲线 % */
+    private ResourceCurveSeriesVO essSocCurve;
+}

+ 3 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/SiteListVO.java

@@ -33,4 +33,7 @@ public class SiteListVO {
     private LocalDateTime createTime;
     private String accountNo;
     private String streetTown;
+    private BigDecimal avgCompletionRate;
+    /** 站点状态 0 在线 1 离线 2 故障 */
+    private Integer runStatus;
 }

+ 0 - 3
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppCapabilityEvalHelper.java

@@ -198,9 +198,6 @@ public final class VppCapabilityEvalHelper {
         BigDecimal total = BigDecimal.ZERO;
         for (VppResourcePoint resource : resources) {
             VppDevice device = findDevice(devices, resource.getDeviceId());
-            if (device != null && device.getCommStatus() != null && device.getCommStatus() == 0) {
-                continue;
-            }
             BigDecimal cap = resource.getCapacityKw() != null ? resource.getCapacityKw() : BigDecimal.valueOf(100);
             double typeFactor = typeLoadFactor(resource.getResourceType());
             double noise = 0.85 + pseudoRandom(seed, resource.getId() != null ? resource.getId() : 0L, minuteOfDay, seriesSalt) * 0.3;

+ 0 - 65
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppResourceMockHelper.java

@@ -16,27 +16,6 @@ public final class VppResourceMockHelper {
     private VppResourceMockHelper() {
     }
 
-    public static BigDecimal mockRealtimePowerKw(VppResourcePoint resource, VppDevice device) {
-        BigDecimal base = resource.getCapacityKw() != null ? resource.getCapacityKw() : BigDecimal.valueOf(100);
-        double factor = 0.3 + pseudoRandom(resource.getId(), 0) * 0.6;
-        if (device != null && device.getCommStatus() != null && device.getCommStatus() == 0) {
-            return BigDecimal.ZERO;
-        }
-        return base.multiply(BigDecimal.valueOf(factor)).setScale(2, RoundingMode.HALF_UP);
-    }
-
-    public static BigDecimal mockVoltageV(VppResourcePoint resource) {
-        return BigDecimal.valueOf(380 + pseudoRandom(resource.getId(), 1) * 20).setScale(1, RoundingMode.HALF_UP);
-    }
-
-    public static BigDecimal mockCurrentA(VppResourcePoint resource, BigDecimal powerKw, BigDecimal voltageV) {
-        if (powerKw == null || voltageV == null || voltageV.compareTo(BigDecimal.ZERO) == 0) {
-            return BigDecimal.ZERO;
-        }
-        double current = powerKw.doubleValue() * 1000 / (voltageV.doubleValue() * 1.732);
-        return BigDecimal.valueOf(current).setScale(2, RoundingMode.HALF_UP);
-    }
-
     public static BigDecimal mockSocPercent(VppResourcePoint resource) {
         if (!"ESS".equalsIgnoreCase(resource.getResourceType())) {
             return null;
@@ -53,31 +32,6 @@ public final class VppResourceMockHelper {
                 .setScale(2, RoundingMode.HALF_UP);
     }
 
-    public static boolean mockAbnormal(VppResourcePoint resource, VppDevice device) {
-        if (device != null) {
-            if (device.getCommStatus() != null && device.getCommStatus() == 0) {
-                return true;
-            }
-            if (device.getRunStatus() != null && device.getRunStatus() == 2) {
-                return true;
-            }
-        }
-        return resource.getId() != null && resource.getId() % 11 == 0;
-    }
-
-    public static String mockAbnormalReason(VppResourcePoint resource, VppDevice device) {
-        if (device != null && device.getCommStatus() != null && device.getCommStatus() == 0) {
-            return "设备通信离线";
-        }
-        if (device != null && device.getRunStatus() != null && device.getRunStatus() == 2) {
-            return "设备运行故障";
-        }
-        if ("ESS".equalsIgnoreCase(resource.getResourceType())) {
-            return "SOC 低于告警阈值";
-        }
-        return "实时功率超出告警阈值";
-    }
-
     public static String resourceTypeLabel(String resourceType) {
         if (resourceType == null) {
             return "未知";
@@ -98,25 +52,6 @@ public final class VppResourceMockHelper {
         }
     }
 
-    public static String runStatusLabel(VppDevice device, boolean abnormal) {
-        if (abnormal) {
-            return "异常";
-        }
-        if (device == null) {
-            return "未知";
-        }
-        if (device.getCommStatus() != null && device.getCommStatus() == 0) {
-            return "离线";
-        }
-        if (device.getRunStatus() != null && device.getRunStatus() == 1) {
-            return "运行";
-        }
-        if (device.getRunStatus() != null && device.getRunStatus() == 0) {
-            return "停机";
-        }
-        return "正常";
-    }
-
     private static double pseudoRandom(Long seed, int salt) {
         if (seed == null) {
             return ThreadLocalRandom.current().nextDouble();