瀏覽代碼

大屏接口&响应事件、邀约代码调整

fuyuchuan 1 周之前
父節點
當前提交
cff4ca2947
共有 18 個文件被更改,包括 954 次插入154 次删除
  1. 24 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppSocialBenefitConstants.java
  2. 32 12
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DashboardController.java
  3. 8 26
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrEvent.java
  4. 19 10
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrInvitation.java
  5. 20 7
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDashboardService.java
  6. 588 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDashboardServiceImpl.java
  7. 0 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrEventIngestServiceImpl.java
  8. 36 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrInvitationServiceImpl.java
  9. 9 50
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrServiceImpl.java
  10. 9 7
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrSubsidyPredictionServiceImpl.java
  11. 30 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DashboardCurveVO.java
  12. 28 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DashboardMapVO.java
  13. 107 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DashboardSummaryVO.java
  14. 6 16
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrEventDetailVO.java
  15. 6 14
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrEventRequest.java
  16. 11 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInvitationRequest.java
  17. 11 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInvitationVO.java
  18. 10 9
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_schema.sql

+ 24 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppSocialBenefitConstants.java

@@ -0,0 +1,24 @@
+package com.usky.vpp.constant;
+
+import java.math.BigDecimal;
+
+/**
+ * 大屏社会效益折算系数(按响应电量 MWh)
+ */
+public final class VppSocialBenefitConstants {
+
+    /** 1 MWh → 减碳量(吨 CO₂) */
+    public static final BigDecimal CARBON_TON_PER_MWH = new BigDecimal("0.785");
+
+    /** 1 MWh → 节约标准煤(吨) */
+    public static final BigDecimal COAL_TON_PER_MWH = new BigDecimal("0.404");
+
+    /** 1 吨 CO₂ → 等效植树(棵) */
+    public static final BigDecimal TREES_PER_CARBON_TON = new BigDecimal("1.44");
+
+    /** 1 MWh → 等效绿证(张) */
+    public static final BigDecimal GREEN_CERT_PER_MWH = BigDecimal.ONE;
+
+    private VppSocialBenefitConstants() {
+    }
+}

+ 32 - 12
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DashboardController.java

@@ -2,12 +2,18 @@ package com.usky.vpp.controller.web;
 
 import com.usky.common.core.bean.ApiResult;
 import com.usky.vpp.service.VppDashboardService;
+import com.usky.vpp.service.vo.DashboardCurveVO;
+import com.usky.vpp.service.vo.DashboardMapVO;
+import com.usky.vpp.service.vo.DashboardSummaryVO;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
 
 /**
- * 虚拟电厂 - Dashboard 接口
- * 网关前缀: /prod-api/service-vpp
+ * 虚拟电厂 - 运行管理监控大屏
+ * <p>网关前缀: /prod-api/service-vpp</p>
  */
 @RestController
 @RequestMapping("/dashboard")
@@ -16,16 +22,30 @@ public class DashboardController {
     @Autowired
     private VppDashboardService vppDashboardService;
 
-    @GetMapping(value = "/summary")
-    public ApiResult<Object> summary(@RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+    /**
+     * 大屏汇总 KPI
+     * <p>租户隔离;削峰/填谷/补贴/当年调控/社会效益/收益排名仅统计已结束事件。</p>
+     */
+    @GetMapping("/summary")
+    public ApiResult<DashboardSummaryVO> summary() {
+        return ApiResult.success(vppDashboardService.getSummary());
     }
-    @GetMapping(value = "/map")
-    public ApiResult<Object> mapResources(@RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+
+    /**
+     * 地图站点点位(含经纬度与装机容量)
+     */
+    @GetMapping("/map")
+    public ApiResult<DashboardMapVO> mapResources() {
+        return ApiResult.success(vppDashboardService.getMap());
     }
-    @GetMapping(value = "/curve")
-    public ApiResult<Object> curve(@RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+
+    /**
+     * 当日用电负荷曲线(基础功率 vs 负荷功率)
+     *
+     * @param date 统计日期 yyyy-MM-dd,默认当天
+     */
+    @GetMapping("/curve")
+    public ApiResult<DashboardCurveVO> curve(@RequestParam(value = "date", required = false) String date) {
+        return ApiResult.success(vppDashboardService.getCurve(date));
     }
 }

+ 8 - 26
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrEvent.java

@@ -46,32 +46,14 @@ public class VppDrEvent implements Serializable {
     /** 出清规模 kW */
     @TableField("cleared_capacity_kw")
     private BigDecimal clearedCapacityKw;
-    /** 申报出清容量 kW */
-    @TableField("declared_cleared_capacity_kw")
-    private BigDecimal declaredClearedCapacityKw;
-    /** 实际出清 kW(电网审核通过) */
-    @TableField("actual_cleared_capacity_kw")
-    private BigDecimal actualClearedCapacityKw;
-    /** 预估响应容量 kW(响应完成后本平台评估) */
-    @TableField("estimated_response_capacity_kw")
-    private BigDecimal estimatedResponseCapacityKw;
-    /** 实际响应容量 kW(电网评估最终结果) */
-    @TableField("actual_response_capacity_kw")
-    private BigDecimal actualResponseCapacityKw;
-    /**
-     * 响应完成率 = 实际响应容量 / 邀约规模;展示时 ×100%
-     */
-    @TableField("response_completion_rate")
-    private BigDecimal responseCompletionRate;
-    /** 是否中标 0否 1是(单选,集中标记) */
-    @TableField("is_winning_bid")
-    private Integer isWinningBid;
-    /** 备注附件 URL */
-    @TableField("remark_attachment_url")
-    private String remarkAttachmentUrl;
-    /** 备注附件名称 */
-    @TableField("remark_attachment_name")
-    private String remarkAttachmentName;
+    /** 备注 */
+    private String remark;
+    /** 附件 URL */
+    @TableField("attachment_url")
+    private String attachmentUrl;
+    /** 附件名称 */
+    @TableField("attachment_name")
+    private String attachmentName;
     @TableField("subsidy_price")
     private BigDecimal subsidyPrice;
     @TableField("event_status")

+ 19 - 10
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrInvitation.java

@@ -30,6 +30,8 @@ public class VppDrInvitation implements Serializable {
     private Long drEventId;
     @TableField("customer_id")
     private Long customerId;
+    @TableField("site_id")
+    private Long siteId;
     @TableField("execute_start_date")
     private LocalDate executeStartDate;
     @TableField("execute_end_date")
@@ -47,6 +49,23 @@ public class VppDrInvitation implements Serializable {
     /** 申报出清容量 kW */
     @TableField("declared_capacity_kw")
     private BigDecimal declaredCapacityKw;
+    /** 实际出清 kW(电网审核通过) */
+    @TableField("actual_cleared_capacity_kw")
+    private BigDecimal actualClearedCapacityKw;
+    /** 预估响应容量 kW(响应完成后本平台评估) */
+    @TableField("estimated_response_capacity_kw")
+    private BigDecimal estimatedResponseCapacityKw;
+    /** 实际响应容量 kW(电网评估最终结果) */
+    @TableField("actual_response_capacity_kw")
+    private BigDecimal actualResponseCapacityKw;
+    /**
+     * 响应完成率 = 实际响应容量 / 邀约规模(需求容量);展示时 ×100%
+     */
+    @TableField("response_completion_rate")
+    private BigDecimal responseCompletionRate;
+    /** 是否中标 0否 1是(单选,集中标记) */
+    @TableField("is_winning_bid")
+    private Integer isWinningBid;
     @TableField("reply_status")
     private Integer replyStatus;
     @TableField("response_status")
@@ -74,14 +93,4 @@ public class VppDrInvitation implements Serializable {
     private Integer deleteFlag;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
-    @TableField("site_id")
-    private long site_id;
-    @TableField("actual_cleared_capacity_kw")
-    private BigDecimal actual_cleared_capacity_kw;
-    @TableField("estimated_response_capacity_kw")
-    private BigDecimal estimated_response_capacity_kw;
-    @TableField("actual_response_capacity_kw")
-    private BigDecimal actual_response_capacity_kw;
-    @TableField("is_winning_bid")
-    private int is_winning_bid;
 }

+ 20 - 7
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDashboardService.java

@@ -1,15 +1,28 @@
 package com.usky.vpp.service;
 
-import com.usky.common.core.bean.CommonPage;
-
-import java.util.Map;
+import com.usky.vpp.service.vo.DashboardCurveVO;
+import com.usky.vpp.service.vo.DashboardMapVO;
+import com.usky.vpp.service.vo.DashboardSummaryVO;
 
 /**
- * VppDashboardService 业务接口
+ * 运行管理监控大屏
  */
 public interface VppDashboardService {
 
-    default Object stub(String action, Map<String, Object> params) {
-        return null;
-    }
+    /**
+     * 大屏汇总 KPI(租户隔离;DR 相关仅统计已结束事件)
+     */
+    DashboardSummaryVO getSummary();
+
+    /**
+     * 当日用电负荷曲线(基础功率 vs 负荷功率)
+     *
+     * @param date yyyy-MM-dd,为空默认当天
+     */
+    DashboardCurveVO getCurve(String date);
+
+    /**
+     * 地图站点点位
+     */
+    DashboardMapVO getMap();
 }

+ 588 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDashboardServiceImpl.java

@@ -1,11 +1,598 @@
 package com.usky.vpp.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.vpp.constant.VppSocialBenefitConstants;
+import com.usky.vpp.domain.VppCustomer;
+import com.usky.vpp.domain.VppDevice;
+import com.usky.vpp.domain.VppDrEvaluation;
+import com.usky.vpp.domain.VppDrEvent;
+import com.usky.vpp.domain.VppDrInvitation;
+import com.usky.vpp.domain.VppDrParticipation;
+import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.domain.VppSite;
+import com.usky.vpp.mapper.VppCustomerMapper;
+import com.usky.vpp.mapper.VppDeviceMapper;
+import com.usky.vpp.mapper.VppDrEvaluationMapper;
+import com.usky.vpp.mapper.VppDrEventMapper;
+import com.usky.vpp.mapper.VppDrInvitationMapper;
+import com.usky.vpp.mapper.VppDrParticipationMapper;
+import com.usky.vpp.mapper.VppResourcePointMapper;
+import com.usky.vpp.mapper.VppSiteMapper;
+import com.usky.vpp.service.VppCapabilityEvalService;
 import com.usky.vpp.service.VppDashboardService;
+import com.usky.vpp.service.vo.CapabilityEvalSummaryVO;
+import com.usky.vpp.service.vo.CapabilityLoadCurveVO;
+import com.usky.vpp.service.vo.CapabilityLoadPointVO;
+import com.usky.vpp.service.vo.DashboardCurveVO;
+import com.usky.vpp.service.vo.DashboardMapVO;
+import com.usky.vpp.service.vo.DashboardSummaryVO;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppCapabilityEvalHelper;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.Year;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
- * VppDashboardService 默认实现(待按业务完善)
+ * 运行管理监控大屏
+ * <p>租户隔离;DR 相关指标仅统计 event_status=已结束 的历史事件。</p>
  */
 @Service
 public class VppDashboardServiceImpl implements VppDashboardService {
+
+    private static final int EVENT_STATUS_ENDED = 3;
+    private static final int EVENT_TYPE_PEAK = 1;
+    private static final int EVENT_TYPE_VALLEY = 2;
+    private static final int REPLY_ACCEPT = 1;
+    private static final int COMM_ONLINE = 1;
+    private static final int COMM_OFFLINE = 0;
+    private static final int RUN_FAULT = 2;
+    private static final int IS_CONTROL = 1;
+    private static final int IS_WINNING = 1;
+    private static final int SCALE = 4;
+    private static final int MONEY_SCALE = 2;
+    private static final BigDecimal KW_PER_MW = new BigDecimal("1000");
+
+    @Autowired
+    private VppSiteMapper siteMapper;
+    @Autowired
+    private VppCustomerMapper customerMapper;
+    @Autowired
+    private VppResourcePointMapper resourcePointMapper;
+    @Autowired
+    private VppDeviceMapper deviceMapper;
+    @Autowired
+    private VppDrEventMapper eventMapper;
+    @Autowired
+    private VppDrEvaluationMapper evaluationMapper;
+    @Autowired
+    private VppDrInvitationMapper invitationMapper;
+    @Autowired
+    private VppDrParticipationMapper participationMapper;
+    @Autowired
+    private VppCapabilityEvalService capabilityEvalService;
+
+    @Override
+    public DashboardSummaryVO getSummary() {
+        Integer tenantId = SecurityUtils.getTenantId();
+        DashboardSummaryVO vo = new DashboardSummaryVO();
+
+        List<VppSite> sites = listSites(tenantId);
+        List<VppResourcePoint> resources = listResources(tenantId);
+        List<VppDevice> devices = listDevices(tenantId);
+        List<VppDrEvent> endedEvents = listEndedEvents(tenantId);
+        Map<Long, VppDrEvaluation> evaluationMap = loadEvaluationMap(endedEvents);
+        List<VppDrInvitation> endedInvitations = listInvitationsByEvents(endedEvents, tenantId);
+        Map<Long, List<VppDrInvitation>> invitationsByEvent = endedInvitations.stream()
+                .filter(i -> i.getDrEventId() != null)
+                .collect(Collectors.groupingBy(VppDrInvitation::getDrEventId));
+
+        fillAccess(vo.getAccess(), sites, resources);
+        fillBasic(vo.getBasic(), tenantId, resources, endedEvents, evaluationMap, invitationsByEvent);
+        fillDeviceStatus(vo.getDeviceStatus(), devices);
+        fillSubsidy(vo.getSubsidy(), evaluationMap);
+        fillYearRegulation(vo.getYearRegulation(), tenantId, endedEvents, evaluationMap, invitationsByEvent);
+        fillSocialBenefit(vo.getSocialBenefit(), endedEvents, evaluationMap, invitationsByEvent);
+        fillEarningsRank(vo, sites, resources, endedEvents, evaluationMap, endedInvitations);
+        return vo;
+    }
+
+    @Override
+    public DashboardCurveVO getCurve(String date) {
+        CapabilityLoadCurveVO source = capabilityEvalService.getLoadCurve(null, date);
+        DashboardCurveVO curve = new DashboardCurveVO();
+        curve.setStatDate(source.getDate());
+        curve.setDataSource(source.getDataSource());
+        if (!CollectionUtils.isEmpty(source.getPoints())) {
+            List<DashboardCurveVO.Point> points = new ArrayList<>(source.getPoints().size());
+            for (CapabilityLoadPointVO p : source.getPoints()) {
+                DashboardCurveVO.Point point = new DashboardCurveVO.Point();
+                point.setTime(p.getTime());
+                point.setBaselineKw(p.getBaselineLoadKw());
+                point.setLoadKw(p.getActualLoadKw());
+                points.add(point);
+            }
+            curve.setPoints(points);
+        }
+        return curve;
+    }
+
+    @Override
+    public DashboardMapVO getMap() {
+        Integer tenantId = SecurityUtils.getTenantId();
+        List<VppSite> sites = listSites(tenantId);
+        List<VppResourcePoint> resources = listResources(tenantId);
+        Map<Long, BigDecimal> capacityBySite = new HashMap<>();
+        for (VppResourcePoint resource : resources) {
+            if (resource.getSiteId() == null) {
+                continue;
+            }
+            BigDecimal cap = nz(resource.getCapacityKw());
+            capacityBySite.merge(resource.getSiteId(), cap, BigDecimal::add);
+        }
+
+        DashboardMapVO map = new DashboardMapVO();
+        for (VppSite site : sites) {
+            if (site.getLongitude() == null || site.getLatitude() == null) {
+                continue;
+            }
+            DashboardMapVO.SitePoint point = new DashboardMapVO.SitePoint();
+            point.setSiteId(site.getId());
+            point.setSiteName(site.getSiteName());
+            point.setLongitude(site.getLongitude());
+            point.setLatitude(site.getLatitude());
+            point.setProvince(site.getProvince());
+            point.setCity(site.getCity());
+            point.setCapacityKw(capacityBySite.getOrDefault(site.getId(), BigDecimal.ZERO)
+                    .setScale(SCALE, RoundingMode.HALF_UP));
+            map.getSites().add(point);
+        }
+        return map;
+    }
+
+    // ---------- access / basic / device ----------
+
+    private void fillAccess(DashboardSummaryVO.Access access, List<VppSite> sites, List<VppResourcePoint> resources) {
+        access.setSiteCount((long) sites.size());
+        BigDecimal total = BigDecimal.ZERO;
+        BigDecimal pv = BigDecimal.ZERO;
+        BigDecimal ess = BigDecimal.ZERO;
+        BigDecimal adjustable = BigDecimal.ZERO;
+        for (VppResourcePoint resource : resources) {
+            BigDecimal cap = nz(resource.getCapacityKw());
+            total = total.add(cap);
+            String category = VppCapabilityEvalHelper.resolveCategory(resource.getResourceType());
+            if (VppCapabilityEvalHelper.CATEGORY_GENERATION.equals(category)) {
+                pv = pv.add(cap);
+            } else if (VppCapabilityEvalHelper.CATEGORY_STORAGE.equals(category)) {
+                ess = ess.add(cap);
+            }
+            if (Objects.equals(resource.getIsControl(), IS_CONTROL)) {
+                adjustable = adjustable.add(cap);
+            }
+        }
+        access.setTotalCapacityKw(total.setScale(SCALE, RoundingMode.HALF_UP));
+        access.setPvCapacityKw(pv.setScale(SCALE, RoundingMode.HALF_UP));
+        access.setEssCapacityKw(ess.setScale(SCALE, RoundingMode.HALF_UP));
+        access.setAdjustableCapacityKw(adjustable.setScale(SCALE, RoundingMode.HALF_UP));
+    }
+
+    private void fillBasic(DashboardSummaryVO.Basic basic, Integer tenantId,
+                           List<VppResourcePoint> resources,
+                           List<VppDrEvent> endedEvents, Map<Long, VppDrEvaluation> evaluationMap,
+                           Map<Long, List<VppDrInvitation>> invitationsByEvent) {
+        long customerCount = customerMapper.selectCount(
+                new LambdaQueryWrapper<VppCustomer>()
+                        .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .eq(tenantId != null, VppCustomer::getTenantId, tenantId));
+        basic.setCustomerCount(customerCount);
+
+        BigDecimal loadCapacity = BigDecimal.ZERO;
+        Set<Long> controlDeviceIds = new HashSet<>();
+        long controlResourceWithoutDevice = 0L;
+        BigDecimal maxUp = BigDecimal.ZERO;
+        BigDecimal maxDown = BigDecimal.ZERO;
+        for (VppResourcePoint resource : resources) {
+            if (VppCapabilityEvalHelper.CATEGORY_LOAD.equals(
+                    VppCapabilityEvalHelper.resolveCategory(resource.getResourceType()))) {
+                loadCapacity = loadCapacity.add(nz(resource.getCapacityKw()));
+            }
+            if (Objects.equals(resource.getIsControl(), IS_CONTROL)) {
+                if (resource.getDeviceId() != null) {
+                    controlDeviceIds.add(resource.getDeviceId());
+                } else {
+                    controlResourceWithoutDevice++;
+                }
+                maxUp = maxUp.add(nz(resource.getMaxUpKw()));
+                maxDown = maxDown.add(nz(resource.getMinDownKw()));
+            }
+        }
+        basic.setLoadCapacityKw(loadCapacity.setScale(SCALE, RoundingMode.HALF_UP));
+        basic.setAdjustableDeviceCount(controlDeviceIds.size() + controlResourceWithoutDevice);
+
+        BigDecimal peak = BigDecimal.ZERO;
+        BigDecimal valley = BigDecimal.ZERO;
+        for (VppDrEvent event : endedEvents) {
+            BigDecimal responseKw = resolveEventActualResponseKw(
+                    event, evaluationMap.get(event.getId()), invitationsByEvent.get(event.getId()));
+            if (Objects.equals(event.getEventType(), EVENT_TYPE_PEAK)) {
+                peak = peak.add(responseKw);
+            } else if (Objects.equals(event.getEventType(), EVENT_TYPE_VALLEY)) {
+                valley = valley.add(responseKw);
+            }
+        }
+        basic.setPeakShaveTotalKw(peak.setScale(SCALE, RoundingMode.HALF_UP));
+        basic.setValleyFillTotalKw(valley.setScale(SCALE, RoundingMode.HALF_UP));
+
+        CapabilityEvalSummaryVO capability = capabilityEvalService.getSummary(null);
+        if (capability != null) {
+            basic.setMaxUpKw(nz(capability.getMaxUpKw()).setScale(SCALE, RoundingMode.HALF_UP));
+            basic.setMaxDownKw(nz(capability.getMaxDownKw()).setScale(SCALE, RoundingMode.HALF_UP));
+        } else {
+            basic.setMaxUpKw(maxUp.setScale(SCALE, RoundingMode.HALF_UP));
+            basic.setMaxDownKw(maxDown.setScale(SCALE, RoundingMode.HALF_UP));
+        }
+    }
+
+    private void fillDeviceStatus(DashboardSummaryVO.DeviceStatus status, List<VppDevice> devices) {
+        long online = 0;
+        long offline = 0;
+        long fault = 0;
+        for (VppDevice device : devices) {
+            if (Objects.equals(device.getRunStatus(), RUN_FAULT)) {
+                fault++;
+            } else if (Objects.equals(device.getCommStatus(), COMM_OFFLINE)
+                    || device.getCommStatus() == null) {
+                offline++;
+            } else if (Objects.equals(device.getCommStatus(), COMM_ONLINE)) {
+                online++;
+            } else {
+                offline++;
+            }
+        }
+        status.setTotal((long) devices.size());
+        status.setOnline(online);
+        status.setOffline(offline);
+        status.setFault(fault);
+    }
+
+    private void fillSubsidy(DashboardSummaryVO.Subsidy subsidy, Map<Long, VppDrEvaluation> evaluationMap) {
+        BigDecimal total = BigDecimal.ZERO;
+        for (VppDrEvaluation evaluation : evaluationMap.values()) {
+            total = total.add(nz(evaluation.getSubsidyAmount()));
+        }
+        subsidy.setTotalAmount(total.setScale(MONEY_SCALE, RoundingMode.HALF_UP));
+    }
+
+    private void fillYearRegulation(DashboardSummaryVO.YearRegulation yearRegulation, Integer tenantId,
+                                    List<VppDrEvent> endedEvents, Map<Long, VppDrEvaluation> evaluationMap,
+                                    Map<Long, List<VppDrInvitation>> invitationsByEvent) {
+        int year = Year.now().getValue();
+        yearRegulation.setYear(year);
+        LocalDateTime yearStart = LocalDate.of(year, 1, 1).atStartOfDay();
+        LocalDateTime yearEnd = LocalDate.of(year, 12, 31).atTime(23, 59, 59);
+
+        List<VppDrEvent> yearEvents = endedEvents.stream()
+                .filter(e -> e.getStartTime() != null
+                        && !e.getStartTime().isBefore(yearStart)
+                        && !e.getStartTime().isAfter(yearEnd))
+                .collect(Collectors.toList());
+        Set<Long> yearEventIds = yearEvents.stream().map(VppDrEvent::getId).collect(Collectors.toSet());
+
+        List<VppDrInvitation> yearInvitations = yearEventIds.isEmpty()
+                ? Collections.emptyList()
+                : yearEventIds.stream()
+                .flatMap(id -> invitationsByEvent.getOrDefault(id, Collections.emptyList()).stream())
+                .collect(Collectors.toList());
+
+        List<BigDecimal> deviationRates = new ArrayList<>();
+        List<BigDecimal> completionRates = new ArrayList<>();
+        BigDecimal effectiveKw = BigDecimal.ZERO;
+        BigDecimal totalClearedKw = BigDecimal.ZERO;
+        BigDecimal durationHour = BigDecimal.ZERO;
+        boolean anyWinning = yearInvitations.stream()
+                .anyMatch(i -> Objects.equals(i.getIsWinningBid(), IS_WINNING));
+
+        for (VppDrInvitation invitation : yearInvitations) {
+            BigDecimal actual = resolveInvitationActualResponseKw(invitation);
+            BigDecimal cleared = nz(invitation.getActualClearedCapacityKw());
+            BigDecimal inviteScale = nz(invitation.getDemandCapacityKw());
+
+            if (cleared.compareTo(BigDecimal.ZERO) > 0) {
+                BigDecimal absDiff = actual.subtract(cleared).abs();
+                BigDecimal rate = BigDecimal.ONE.subtract(
+                        absDiff.divide(cleared, SCALE, RoundingMode.HALF_UP));
+                if (rate.compareTo(BigDecimal.ZERO) < 0) {
+                    rate = BigDecimal.ZERO;
+                }
+                deviationRates.add(rate);
+            }
+
+            BigDecimal completion = invitation.getResponseCompletionRate();
+            if (completion == null && inviteScale.compareTo(BigDecimal.ZERO) > 0) {
+                completion = actual.divide(inviteScale, SCALE, RoundingMode.HALF_UP);
+            }
+            if (completion != null) {
+                completionRates.add(completion);
+            }
+
+            boolean countAsEffective = anyWinning
+                    ? Objects.equals(invitation.getIsWinningBid(), IS_WINNING)
+                    : actual.compareTo(BigDecimal.ZERO) > 0;
+            if (countAsEffective) {
+                effectiveKw = effectiveKw.add(actual);
+            }
+            totalClearedKw = totalClearedKw.add(cleared.compareTo(BigDecimal.ZERO) > 0 ? cleared : inviteScale);
+        }
+
+        for (VppDrEvent event : yearEvents) {
+            durationHour = durationHour.add(resolveDurationHour(event, evaluationMap.get(event.getId())));
+            // 无邀约时回退事件出清规模
+            if (!invitationsByEvent.containsKey(event.getId())
+                    || invitationsByEvent.get(event.getId()).isEmpty()) {
+                totalClearedKw = totalClearedKw.add(nz(event.getClearedCapacityKw()).compareTo(BigDecimal.ZERO) > 0
+                        ? nz(event.getClearedCapacityKw())
+                        : nz(event.getTargetCapacityKw()));
+            }
+        }
+
+        yearRegulation.setExecutionDeviationRate(avg(deviationRates));
+        yearRegulation.setResponseCompletionRate(avg(completionRates));
+        yearRegulation.setEffectiveCapacityMw(toMw(effectiveKw));
+        yearRegulation.setTotalCapacityMw(toMw(totalClearedKw));
+        yearRegulation.setTotalDurationHour(durationHour.setScale(SCALE, RoundingMode.HALF_UP));
+        yearRegulation.setInvitationCount((long) yearInvitations.size());
+        yearRegulation.setParticipationCount(yearInvitations.stream()
+                .filter(i -> Objects.equals(i.getReplyStatus(), REPLY_ACCEPT))
+                .count());
+    }
+
+    private void fillSocialBenefit(DashboardSummaryVO.SocialBenefit socialBenefit,
+                                   List<VppDrEvent> endedEvents,
+                                   Map<Long, VppDrEvaluation> evaluationMap,
+                                   Map<Long, List<VppDrInvitation>> invitationsByEvent) {
+        BigDecimal responseMwh = BigDecimal.ZERO;
+        for (VppDrEvent event : endedEvents) {
+            VppDrEvaluation evaluation = evaluationMap.get(event.getId());
+            BigDecimal kw = resolveEventActualResponseKw(event, evaluation, invitationsByEvent.get(event.getId()));
+            BigDecimal hours = resolveDurationHour(event, evaluation);
+            responseMwh = responseMwh.add(kw.multiply(hours).divide(KW_PER_MW, 8, RoundingMode.HALF_UP));
+        }
+
+        BigDecimal carbon = responseMwh.multiply(VppSocialBenefitConstants.CARBON_TON_PER_MWH)
+                .setScale(SCALE, RoundingMode.HALF_UP);
+        BigDecimal coal = responseMwh.multiply(VppSocialBenefitConstants.COAL_TON_PER_MWH)
+                .setScale(SCALE, RoundingMode.HALF_UP);
+        long trees = carbon.multiply(VppSocialBenefitConstants.TREES_PER_CARBON_TON)
+                .setScale(0, RoundingMode.HALF_UP).longValue();
+        long greenCert = responseMwh.multiply(VppSocialBenefitConstants.GREEN_CERT_PER_MWH)
+                .setScale(0, RoundingMode.HALF_UP).longValue();
+
+        socialBenefit.setCarbonReductionTon(carbon);
+        socialBenefit.setCoalSavedTon(coal);
+        socialBenefit.setEquivalentTrees(trees);
+        socialBenefit.setEquivalentGreenCert(greenCert);
+    }
+
+    private void fillEarningsRank(DashboardSummaryVO vo, List<VppSite> sites,
+                                  List<VppResourcePoint> resources,
+                                  List<VppDrEvent> endedEvents,
+                                  Map<Long, VppDrEvaluation> evaluationMap,
+                                  List<VppDrInvitation> endedInvitations) {
+        if (endedEvents.isEmpty()) {
+            vo.setEarningsRank(emptyRank(sites));
+            return;
+        }
+
+        Set<Long> eventIds = endedEvents.stream().map(VppDrEvent::getId).collect(Collectors.toSet());
+        Map<Long, Long> resourceSiteMap = resources.stream()
+                .filter(r -> r.getId() != null && r.getSiteId() != null)
+                .collect(Collectors.toMap(VppResourcePoint::getId, VppResourcePoint::getSiteId, (a, b) -> a));
+
+        List<VppDrParticipation> participations = participationMapper.selectList(
+                new LambdaQueryWrapper<VppDrParticipation>()
+                        .in(VppDrParticipation::getEventId, eventIds)
+                        .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+
+        Map<Long, Set<Long>> eventSiteIds = new HashMap<>();
+        for (VppDrParticipation participation : participations) {
+            Long siteId = resourceSiteMap.get(participation.getResourceId());
+            if (siteId == null) {
+                continue;
+            }
+            eventSiteIds.computeIfAbsent(participation.getEventId(), k -> new LinkedHashSet<>()).add(siteId);
+        }
+
+        for (VppDrInvitation invitation : endedInvitations) {
+            if (invitation.getSiteId() == null || invitation.getSiteId() <= 0) {
+                continue;
+            }
+            eventSiteIds.computeIfAbsent(invitation.getDrEventId(), k -> new LinkedHashSet<>())
+                    .add(invitation.getSiteId());
+        }
+
+        Map<Long, BigDecimal> earningsBySite = new HashMap<>();
+        for (VppSite site : sites) {
+            earningsBySite.put(site.getId(), BigDecimal.ZERO);
+        }
+        for (VppDrEvent event : endedEvents) {
+            VppDrEvaluation evaluation = evaluationMap.get(event.getId());
+            BigDecimal amount = evaluation != null ? nz(evaluation.getSubsidyAmount()) : BigDecimal.ZERO;
+            if (amount.compareTo(BigDecimal.ZERO) <= 0) {
+                continue;
+            }
+            Set<Long> siteIds = eventSiteIds.getOrDefault(event.getId(), Collections.emptySet());
+            if (siteIds.isEmpty()) {
+                continue;
+            }
+            BigDecimal share = amount.divide(BigDecimal.valueOf(siteIds.size()), 8, RoundingMode.HALF_UP);
+            for (Long siteId : siteIds) {
+                earningsBySite.merge(siteId, share, BigDecimal::add);
+            }
+        }
+
+        Map<Long, String> siteNameMap = sites.stream()
+                .collect(Collectors.toMap(VppSite::getId, VppSite::getSiteName, (a, b) -> a));
+        List<DashboardSummaryVO.EarningsRankItem> rank = earningsBySite.entrySet().stream()
+                .map(e -> {
+                    DashboardSummaryVO.EarningsRankItem item = new DashboardSummaryVO.EarningsRankItem();
+                    item.setSiteId(e.getKey());
+                    item.setSiteName(siteNameMap.getOrDefault(e.getKey(), String.valueOf(e.getKey())));
+                    item.setEarnings(e.getValue().setScale(MONEY_SCALE, RoundingMode.HALF_UP));
+                    return item;
+                })
+                .sorted(Comparator.comparing(DashboardSummaryVO.EarningsRankItem::getEarnings).reversed())
+                .collect(Collectors.toList());
+        vo.setEarningsRank(rank);
+    }
+
+    private List<DashboardSummaryVO.EarningsRankItem> emptyRank(List<VppSite> sites) {
+        return sites.stream().map(site -> {
+            DashboardSummaryVO.EarningsRankItem item = new DashboardSummaryVO.EarningsRankItem();
+            item.setSiteId(site.getId());
+            item.setSiteName(site.getSiteName());
+            item.setEarnings(BigDecimal.ZERO.setScale(MONEY_SCALE, RoundingMode.HALF_UP));
+            return item;
+        }).collect(Collectors.toList());
+    }
+
+    // ---------- data loaders ----------
+
+    private List<VppSite> listSites(Integer tenantId) {
+        return siteMapper.selectList(new LambdaQueryWrapper<VppSite>()
+                .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(tenantId != null, VppSite::getTenantId, tenantId));
+    }
+
+    private List<VppResourcePoint> listResources(Integer tenantId) {
+        return resourcePointMapper.selectList(new LambdaQueryWrapper<VppResourcePoint>()
+                .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(tenantId != null, VppResourcePoint::getTenantId, tenantId));
+    }
+
+    private List<VppDevice> listDevices(Integer tenantId) {
+        return deviceMapper.selectList(new LambdaQueryWrapper<VppDevice>()
+                .eq(VppDevice::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(tenantId != null, VppDevice::getTenantId, tenantId));
+    }
+
+    private List<VppDrEvent> listEndedEvents(Integer tenantId) {
+        return eventMapper.selectList(new LambdaQueryWrapper<VppDrEvent>()
+                .eq(VppDrEvent::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(VppDrEvent::getEventStatus, EVENT_STATUS_ENDED)
+                .eq(tenantId != null, VppDrEvent::getTenantId, tenantId));
+    }
+
+    private List<VppDrInvitation> listInvitationsByEvents(List<VppDrEvent> events, Integer tenantId) {
+        if (events.isEmpty()) {
+            return Collections.emptyList();
+        }
+        Set<Long> eventIds = events.stream().map(VppDrEvent::getId).collect(Collectors.toSet());
+        return invitationMapper.selectList(new LambdaQueryWrapper<VppDrInvitation>()
+                .in(VppDrInvitation::getDrEventId, eventIds)
+                .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(tenantId != null, VppDrInvitation::getTenantId, tenantId));
+    }
+
+    private Map<Long, VppDrEvaluation> loadEvaluationMap(List<VppDrEvent> events) {
+        if (events.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        Set<Long> eventIds = events.stream().map(VppDrEvent::getId).collect(Collectors.toSet());
+        return evaluationMapper.selectList(new LambdaQueryWrapper<VppDrEvaluation>()
+                        .in(VppDrEvaluation::getEventId, eventIds)
+                        .eq(VppDrEvaluation::getDeleteFlag, VppAuditHelper.NOT_DELETED))
+                .stream()
+                .collect(Collectors.toMap(VppDrEvaluation::getEventId, e -> e, (a, b) -> a));
+    }
+
+    // ---------- helpers ----------
+
+    private static BigDecimal resolveEventActualResponseKw(VppDrEvent event, VppDrEvaluation evaluation,
+                                                           List<VppDrInvitation> invitations) {
+        if (!CollectionUtils.isEmpty(invitations)) {
+            BigDecimal sum = BigDecimal.ZERO;
+            boolean any = false;
+            for (VppDrInvitation invitation : invitations) {
+                BigDecimal kw = resolveInvitationActualResponseKw(invitation);
+                if (kw.compareTo(BigDecimal.ZERO) > 0
+                        || invitation.getActualResponseCapacityKw() != null
+                        || invitation.getEstimatedResponseCapacityKw() != null) {
+                    sum = sum.add(kw);
+                    any = true;
+                }
+            }
+            if (any) {
+                return sum;
+            }
+        }
+        if (evaluation != null && evaluation.getActualCapacityKw() != null) {
+            return nz(evaluation.getActualCapacityKw());
+        }
+        return BigDecimal.ZERO;
+    }
+
+    private static BigDecimal resolveInvitationActualResponseKw(VppDrInvitation invitation) {
+        if (invitation.getActualResponseCapacityKw() != null) {
+            return nz(invitation.getActualResponseCapacityKw());
+        }
+        if (invitation.getEstimatedResponseCapacityKw() != null) {
+            return nz(invitation.getEstimatedResponseCapacityKw());
+        }
+        return BigDecimal.ZERO;
+    }
+
+    private static BigDecimal resolveDurationHour(VppDrEvent event, VppDrEvaluation evaluation) {
+        if (evaluation != null && evaluation.getResponseDurationMin() != null
+                && evaluation.getResponseDurationMin() > 0) {
+            return BigDecimal.valueOf(evaluation.getResponseDurationMin())
+                    .divide(BigDecimal.valueOf(60), SCALE, RoundingMode.HALF_UP);
+        }
+        if (event.getStartTime() != null && event.getEndTime() != null
+                && event.getEndTime().isAfter(event.getStartTime())) {
+            long minutes = ChronoUnit.MINUTES.between(event.getStartTime(), event.getEndTime());
+            if (minutes > 0) {
+                return BigDecimal.valueOf(minutes)
+                        .divide(BigDecimal.valueOf(60), SCALE, RoundingMode.HALF_UP);
+            }
+        }
+        return BigDecimal.ZERO;
+    }
+
+    private static BigDecimal avg(List<BigDecimal> values) {
+        if (values == null || values.isEmpty()) {
+            return null;
+        }
+        BigDecimal sum = BigDecimal.ZERO;
+        for (BigDecimal value : values) {
+            sum = sum.add(value);
+        }
+        return sum.divide(BigDecimal.valueOf(values.size()), SCALE, RoundingMode.HALF_UP);
+    }
+
+    private static BigDecimal toMw(BigDecimal kw) {
+        return nz(kw).divide(KW_PER_MW, SCALE, RoundingMode.HALF_UP);
+    }
+
+    private static BigDecimal nz(BigDecimal value) {
+        return value == null ? BigDecimal.ZERO : value;
+    }
 }

+ 0 - 2
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrEventIngestServiceImpl.java

@@ -82,7 +82,6 @@ public class VppDrEventIngestServiceImpl implements VppDrEventIngestService {
             BigDecimal total = sumListLoad(list);
             if (total.compareTo(BigDecimal.ZERO) > 0) {
                 event.setClearedCapacityKw(total);
-                event.setActualClearedCapacityKw(total);
             }
         }
         if (event.getEventStatus() == null || event.getEventStatus() == EVENT_STATUS_PENDING) {
@@ -203,7 +202,6 @@ public class VppDrEventIngestServiceImpl implements VppDrEventIngestService {
                 BigDecimal cleared = VppUnEventParser.sumResourceLoadKw(resources, true);
                 if (cleared.compareTo(BigDecimal.ZERO) > 0) {
                     event.setClearedCapacityKw(cleared);
-                    event.setActualClearedCapacityKw(cleared);
                 }
                 unDrSyncService.syncTargetResources(event, resources, true);
                 event.setEventStatus(EVENT_STATUS_DECLARED);

+ 36 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrInvitationServiceImpl.java

@@ -123,6 +123,12 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
         invitation.setResponseType(request.getResponseType() != null ? request.getResponseType() : event.getResponseType());
         invitation.setDemandCapacityKw(request.getDemandCapacityKw());
         invitation.setDeclaredCapacityKw(request.getDeclaredCapacityKw() != null ? request.getDeclaredCapacityKw() : BigDecimal.ZERO);
+        invitation.setActualClearedCapacityKw(request.getActualClearedCapacityKw());
+        invitation.setEstimatedResponseCapacityKw(request.getEstimatedResponseCapacityKw());
+        invitation.setActualResponseCapacityKw(request.getActualResponseCapacityKw());
+        invitation.setIsWinningBid(request.getIsWinningBid());
+        invitation.setResponseCompletionRate(resolveCompletionRate(request, invitation.getDemandCapacityKw()));
+        invitation.setSiteId(request.getSiteId());
         invitation.setReplyStatus(REPLY_PENDING);
         invitation.setResponseStatus(RESPONSE_PENDING);
         invitation.setSmsNotifyStatus(SMS_NOT_SENT);
@@ -161,6 +167,22 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
         if (request.getDeclaredCapacityKw() != null) {
             invitation.setDeclaredCapacityKw(request.getDeclaredCapacityKw());
         }
+        if (request.getActualClearedCapacityKw() != null) {
+            invitation.setActualClearedCapacityKw(request.getActualClearedCapacityKw());
+        }
+        if (request.getEstimatedResponseCapacityKw() != null) {
+            invitation.setEstimatedResponseCapacityKw(request.getEstimatedResponseCapacityKw());
+        }
+        if (request.getActualResponseCapacityKw() != null) {
+            invitation.setActualResponseCapacityKw(request.getActualResponseCapacityKw());
+        }
+        if (request.getIsWinningBid() != null) {
+            invitation.setIsWinningBid(request.getIsWinningBid());
+        }
+        if (request.getSiteId() != null) {
+            invitation.setSiteId(request.getSiteId());
+        }
+        invitation.setResponseCompletionRate(resolveCompletionRate(request, invitation.getDemandCapacityKw()));
         if (request.getTransactionType() != null) {
             invitation.setTransactionType(request.getTransactionType());
         }
@@ -505,6 +527,20 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
         return vo;
     }
 
+    /**
+     * 优先用请求显式完成率;否则按 实际响应容量 / 需求容量(邀约规模) 计算。
+     */
+    private BigDecimal resolveCompletionRate(DrInvitationRequest request, BigDecimal demandCapacityKw) {
+        if (request == null) {
+            return null;
+        }
+        if (request.getResponseCompletionRate() != null) {
+            return request.getResponseCompletionRate();
+        }
+        BigDecimal actual = request.getActualResponseCapacityKw();
+        return VppDrParticipationHelper.calcResponseCompletionRate(actual, demandCapacityKw);
+    }
+
     private String resolveTemplateCode(DrInvitationNotifyRequest request) {
         if (request != null && StringUtils.hasText(request.getTemplateCode())) {
             return request.getTemplateCode().trim();

+ 9 - 50
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrServiceImpl.java

@@ -151,15 +151,9 @@ public class VppDrServiceImpl implements VppDrService {
         event.setInviteScope(request.getInviteScope());
         event.setTargetCapacityKw(request.getTargetCapacityKw());
         event.setClearedCapacityKw(request.getClearedCapacityKw());
-        event.setDeclaredClearedCapacityKw(request.getDeclaredClearedCapacityKw());
-        event.setActualClearedCapacityKw(request.getActualClearedCapacityKw());
-        event.setEstimatedResponseCapacityKw(request.getEstimatedResponseCapacityKw());
-        event.setActualResponseCapacityKw(request.getActualResponseCapacityKw());
-        event.setResponseCompletionRate(VppDrParticipationHelper.calcResponseCompletionRate(
-                request.getActualResponseCapacityKw(), request.getTargetCapacityKw()));
-        event.setIsWinningBid(request.getIsWinningBid());
-        event.setRemarkAttachmentUrl(request.getRemarkAttachmentUrl());
-        event.setRemarkAttachmentName(request.getRemarkAttachmentName());
+        event.setRemark(request.getRemark());
+        event.setAttachmentUrl(request.getAttachmentUrl());
+        event.setAttachmentName(request.getAttachmentName());
         event.setSubsidyPrice(request.getSubsidyPrice());
         event.setEventStatus(EVENT_STATUS_PENDING);
         VppAuditHelper.fillCreate(event);
@@ -189,15 +183,9 @@ public class VppDrServiceImpl implements VppDrService {
         event.setInviteScope(request.getInviteScope());
         event.setTargetCapacityKw(request.getTargetCapacityKw());
         event.setClearedCapacityKw(request.getClearedCapacityKw());
-        event.setDeclaredClearedCapacityKw(request.getDeclaredClearedCapacityKw());
-        event.setActualClearedCapacityKw(request.getActualClearedCapacityKw());
-        event.setEstimatedResponseCapacityKw(request.getEstimatedResponseCapacityKw());
-        event.setActualResponseCapacityKw(request.getActualResponseCapacityKw());
-        event.setResponseCompletionRate(VppDrParticipationHelper.calcResponseCompletionRate(
-                request.getActualResponseCapacityKw(), request.getTargetCapacityKw()));
-        event.setIsWinningBid(request.getIsWinningBid());
-        event.setRemarkAttachmentUrl(request.getRemarkAttachmentUrl());
-        event.setRemarkAttachmentName(request.getRemarkAttachmentName());
+        event.setRemark(request.getRemark());
+        event.setAttachmentUrl(request.getAttachmentUrl());
+        event.setAttachmentName(request.getAttachmentName());
         event.setSubsidyPrice(request.getSubsidyPrice());
         VppAuditHelper.fillUpdate(event);
         eventMapper.updateById(event);
@@ -344,7 +332,6 @@ public class VppDrServiceImpl implements VppDrService {
                 participationMapper.insert(participation);
             }
 
-            event.setDeclaredClearedCapacityKw(request.getDeclaredCapacityKw());
             event.setEventStatus(EVENT_STATUS_DECLARED);
             VppAuditHelper.fillUpdate(event);
             eventMapper.updateById(event);
@@ -598,9 +585,6 @@ public class VppDrServiceImpl implements VppDrService {
             executionMapper.updateById(execution);
         }
 
-        event.setEstimatedResponseCapacityKw(totalActual);
-        event.setResponseCompletionRate(VppDrParticipationHelper.calcResponseCompletionRate(
-                totalActual, event.getTargetCapacityKw()));
         event.setEventStatus(EVENT_STATUS_ENDED);
         VppAuditHelper.fillUpdate(event);
         eventMapper.updateById(event);
@@ -845,26 +829,6 @@ public class VppDrServiceImpl implements VppDrService {
         if (request.getClearedCapacityKw() != null && request.getClearedCapacityKw().compareTo(BigDecimal.ZERO) < 0) {
             throw new BusinessException("出清规模不能为负数");
         }
-        if (request.getDeclaredClearedCapacityKw() != null
-                && request.getDeclaredClearedCapacityKw().compareTo(BigDecimal.ZERO) < 0) {
-            throw new BusinessException("申报出清容量不能为负数");
-        }
-        if (request.getActualClearedCapacityKw() != null
-                && request.getActualClearedCapacityKw().compareTo(BigDecimal.ZERO) < 0) {
-            throw new BusinessException("实际出清不能为负数");
-        }
-        if (request.getEstimatedResponseCapacityKw() != null
-                && request.getEstimatedResponseCapacityKw().compareTo(BigDecimal.ZERO) < 0) {
-            throw new BusinessException("预估响应容量不能为负数");
-        }
-        if (request.getActualResponseCapacityKw() != null
-                && request.getActualResponseCapacityKw().compareTo(BigDecimal.ZERO) < 0) {
-            throw new BusinessException("实际响应容量不能为负数");
-        }
-        if (request.getIsWinningBid() != null
-                && request.getIsWinningBid() != 0 && request.getIsWinningBid() != 1) {
-            throw new BusinessException("是否中标无效,取值 0否 1是");
-        }
         if (request.getDeclareDeadline() != null && request.getStartTime() != null
                 && request.getDeclareDeadline().isAfter(request.getStartTime())) {
             throw new BusinessException("申报截止时间不能晚于响应开始时间");
@@ -920,14 +884,9 @@ public class VppDrServiceImpl implements VppDrService {
         vo.setInviteScope(event.getInviteScope());
         vo.setTargetCapacityKw(event.getTargetCapacityKw());
         vo.setClearedCapacityKw(event.getClearedCapacityKw());
-        vo.setDeclaredClearedCapacityKw(event.getDeclaredClearedCapacityKw());
-        vo.setActualClearedCapacityKw(event.getActualClearedCapacityKw());
-        vo.setEstimatedResponseCapacityKw(event.getEstimatedResponseCapacityKw());
-        vo.setActualResponseCapacityKw(event.getActualResponseCapacityKw());
-        vo.setResponseCompletionRate(event.getResponseCompletionRate());
-        vo.setIsWinningBid(event.getIsWinningBid());
-        vo.setRemarkAttachmentUrl(event.getRemarkAttachmentUrl());
-        vo.setRemarkAttachmentName(event.getRemarkAttachmentName());
+        vo.setRemark(event.getRemark());
+        vo.setAttachmentUrl(event.getAttachmentUrl());
+        vo.setAttachmentName(event.getAttachmentName());
         vo.setSubsidyPrice(event.getSubsidyPrice());
         vo.setEventStatus(event.getEventStatus());
         vo.setParticipations(loadParticipationVos(event.getId()));

+ 9 - 7
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrSubsidyPredictionServiceImpl.java

@@ -108,7 +108,7 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
 
         // 批量查询站点名称
         Set<Long> siteIds = invitations.stream()
-                .map(VppDrInvitation::getSite_id)
+                .map(VppDrInvitation::getSiteId)
                 .filter(Objects::nonNull)
                 .collect(Collectors.toSet());
         Map<Long, String> siteNameMap = siteIds.isEmpty() ? Collections.emptyMap()
@@ -147,8 +147,10 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         VppDrSubsidyPrediction p = new VppDrSubsidyPrediction();
         p.setInvitationId(invitation.getId());
         p.setDrEventId(event.getId());
-        p.setSiteId(invitation.getSite_id());
-        p.setSiteName(siteNameMap.getOrDefault(invitation.getSite_id(), null));
+        p.setSiteId(invitation.getSiteId());
+        p.setSiteName(invitation.getSiteId() == null
+                ? null
+                : siteNameMap.getOrDefault(invitation.getSiteId(), null));
         p.setEventStartTime(event.getStartTime());
         p.setEventEndTime(event.getEndTime());
 
@@ -164,15 +166,15 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         p.setSubsidyPrice(event.getSubsidyPrice());
 
         // 预估响应容量
-        p.setEstimatedCapacityKw(invitation.getEstimated_response_capacity_kw());
+        p.setEstimatedCapacityKw(invitation.getEstimatedResponseCapacityKw());
 
         // 实际出清
-        p.setClearedCapacityKw(invitation.getActual_cleared_capacity_kw());
+        p.setClearedCapacityKw(invitation.getActualClearedCapacityKw());
 
         // 预估完成率 = 预估容量 / 实际出清;实际出清为NULL或0时记为0
         BigDecimal completionRate = BigDecimal.ZERO;
-        BigDecimal estimatedKw = invitation.getEstimated_response_capacity_kw();
-        BigDecimal actualKw = invitation.getActual_cleared_capacity_kw();
+        BigDecimal estimatedKw = invitation.getEstimatedResponseCapacityKw();
+        BigDecimal actualKw = invitation.getActualClearedCapacityKw();
         if (estimatedKw != null && actualKw != null && actualKw.compareTo(BigDecimal.ZERO) > 0) {
             completionRate = estimatedKw.divide(actualKw, 4, RoundingMode.HALF_UP);
         }

+ 30 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DashboardCurveVO.java

@@ -0,0 +1,30 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 运行管理监控大屏 - 当日用电负荷曲线
+ */
+@Data
+public class DashboardCurveVO {
+
+    /** 统计日期 yyyy-MM-dd */
+    private String statDate;
+    /** 数据来源:tsdb / mock */
+    private String dataSource;
+    private List<Point> points = new ArrayList<>();
+
+    @Data
+    public static class Point {
+        /** 时刻 HH:mm */
+        private String time;
+        /** 基础功率 kW */
+        private BigDecimal baselineKw;
+        /** 负荷功率 kW */
+        private BigDecimal loadKw;
+    }
+}

+ 28 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DashboardMapVO.java

@@ -0,0 +1,28 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 运行管理监控大屏 - 地图站点
+ */
+@Data
+public class DashboardMapVO {
+
+    private List<SitePoint> sites = new ArrayList<>();
+
+    @Data
+    public static class SitePoint {
+        private Long siteId;
+        private String siteName;
+        private BigDecimal longitude;
+        private BigDecimal latitude;
+        private String province;
+        private String city;
+        /** 站点下资源装机容量合计 kW */
+        private BigDecimal capacityKw = BigDecimal.ZERO;
+    }
+}

+ 107 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DashboardSummaryVO.java

@@ -0,0 +1,107 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 运行管理监控大屏 - 汇总 KPI
+ */
+@Data
+public class DashboardSummaryVO {
+
+    private Access access = new Access();
+    private Basic basic = new Basic();
+    private DeviceStatus deviceStatus = new DeviceStatus();
+    private Subsidy subsidy = new Subsidy();
+    private YearRegulation yearRegulation = new YearRegulation();
+    private SocialBenefit socialBenefit = new SocialBenefit();
+    private List<EarningsRankItem> earningsRank = new ArrayList<>();
+
+    @Data
+    public static class Access {
+        /** 接入站点数 */
+        private Long siteCount = 0L;
+        /** 接入装机容量 kW */
+        private BigDecimal totalCapacityKw = BigDecimal.ZERO;
+        /** 接入光伏 kW */
+        private BigDecimal pvCapacityKw = BigDecimal.ZERO;
+        /** 接入储能资源 kW */
+        private BigDecimal essCapacityKw = BigDecimal.ZERO;
+        /** 接入可调资源 kW */
+        private BigDecimal adjustableCapacityKw = BigDecimal.ZERO;
+    }
+
+    @Data
+    public static class Basic {
+        /** 接入用户数量 */
+        private Long customerCount = 0L;
+        /** 负荷容量 kW(负荷类资源) */
+        private BigDecimal loadCapacityKw = BigDecimal.ZERO;
+        /** 可调设备数量 */
+        private Long adjustableDeviceCount = 0L;
+        /** 累计削峰量 kW(仅已结束事件) */
+        private BigDecimal peakShaveTotalKw = BigDecimal.ZERO;
+        /** 累计填谷量 kW(仅已结束事件) */
+        private BigDecimal valleyFillTotalKw = BigDecimal.ZERO;
+        /** 最大上调功率 kW */
+        private BigDecimal maxUpKw = BigDecimal.ZERO;
+        /** 最大下调功率 kW */
+        private BigDecimal maxDownKw = BigDecimal.ZERO;
+    }
+
+    @Data
+    public static class DeviceStatus {
+        private Long total = 0L;
+        private Long online = 0L;
+        private Long offline = 0L;
+        private Long fault = 0L;
+    }
+
+    @Data
+    public static class Subsidy {
+        /** 累计补贴总额(元,仅已结束事件评价) */
+        private BigDecimal totalAmount = BigDecimal.ZERO;
+    }
+
+    @Data
+    public static class YearRegulation {
+        private Integer year;
+        /** 执行偏差率 0~1 */
+        private BigDecimal executionDeviationRate;
+        /** 响应完成率 0~1 */
+        private BigDecimal responseCompletionRate;
+        /** 有效调控容量 MW */
+        private BigDecimal effectiveCapacityMw = BigDecimal.ZERO;
+        /** 总调控容量 MW */
+        private BigDecimal totalCapacityMw = BigDecimal.ZERO;
+        /** 总调控时长 H */
+        private BigDecimal totalDurationHour = BigDecimal.ZERO;
+        /** 总邀约次数 */
+        private Long invitationCount = 0L;
+        /** 总参与次数 */
+        private Long participationCount = 0L;
+    }
+
+    @Data
+    public static class SocialBenefit {
+        /** 减碳量(吨) */
+        private BigDecimal carbonReductionTon = BigDecimal.ZERO;
+        /** 节约标准煤(吨) */
+        private BigDecimal coalSavedTon = BigDecimal.ZERO;
+        /** 等效植树量(棵) */
+        private Long equivalentTrees = 0L;
+        /** 等效绿证数(张) */
+        private Long equivalentGreenCert = 0L;
+    }
+
+    @Data
+    public static class EarningsRankItem {
+        private Long siteId;
+        private String siteName;
+        /** 收益(元) */
+        private BigDecimal earnings = BigDecimal.ZERO;
+    }
+}

+ 6 - 16
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrEventDetailVO.java

@@ -27,22 +27,12 @@ public class DrEventDetailVO {
     private BigDecimal targetCapacityKw;
     /** 出清规模 kW */
     private BigDecimal clearedCapacityKw;
-    /** 申报出清容量 kW */
-    private BigDecimal declaredClearedCapacityKw;
-    /** 实际出清 kW(电网审核通过) */
-    private BigDecimal actualClearedCapacityKw;
-    /** 预估响应容量 kW(响应完成后本平台评估) */
-    private BigDecimal estimatedResponseCapacityKw;
-    /** 实际响应容量 kW(电网评估最终结果) */
-    private BigDecimal actualResponseCapacityKw;
-    /** 响应完成率 = 实际响应容量 / 邀约规模 */
-    private BigDecimal responseCompletionRate;
-    /** 是否中标 0否 1是 */
-    private Integer isWinningBid;
-    /** 备注附件 URL */
-    private String remarkAttachmentUrl;
-    /** 备注附件名称 */
-    private String remarkAttachmentName;
+    /** 备注 */
+    private String remark;
+    /** 附件 URL */
+    private String attachmentUrl;
+    /** 附件名称 */
+    private String attachmentName;
     private BigDecimal subsidyPrice;
     private Integer eventStatus;
     private List<DrParticipationVO> participations;

+ 6 - 14
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrEventRequest.java

@@ -28,19 +28,11 @@ public class DrEventRequest {
     private BigDecimal targetCapacityKw;
     /** 出清规模 kW */
     private BigDecimal clearedCapacityKw;
-    /** 申报出清容量 kW */
-    private BigDecimal declaredClearedCapacityKw;
-    /** 实际出清 kW(电网审核通过) */
-    private BigDecimal actualClearedCapacityKw;
-    /** 预估响应容量 kW(响应完成后本平台评估) */
-    private BigDecimal estimatedResponseCapacityKw;
-    /** 实际响应容量 kW(电网评估最终结果) */
-    private BigDecimal actualResponseCapacityKw;
-    /** 是否中标 0否 1是 */
-    private Integer isWinningBid;
-    /** 备注附件 URL */
-    private String remarkAttachmentUrl;
-    /** 备注附件名称 */
-    private String remarkAttachmentName;
+    /** 备注 */
+    private String remark;
+    /** 附件 URL */
+    private String attachmentUrl;
+    /** 附件名称 */
+    private String attachmentName;
     private BigDecimal subsidyPrice;
 }

+ 11 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInvitationRequest.java

@@ -20,10 +20,21 @@ public class DrInvitationRequest {
     private BigDecimal demandCapacityKw;
     /** 申报出清容量 kW */
     private BigDecimal declaredCapacityKw;
+    /** 实际出清 kW(电网审核通过) */
+    private BigDecimal actualClearedCapacityKw;
+    /** 预估响应容量 kW(响应完成后本平台评估) */
+    private BigDecimal estimatedResponseCapacityKw;
+    /** 实际响应容量 kW(电网评估最终结果) */
+    private BigDecimal actualResponseCapacityKw;
+    /** 响应完成率 = 实际响应容量 / 邀约规模 */
+    private BigDecimal responseCompletionRate;
+    /** 是否中标 0否 1是 */
+    private Integer isWinningBid;
     /** 1削峰 2填谷 */
     private Integer transactionType;
     /** 1日前 2日内 3秒级 */
     private Integer responseType;
+    private Long siteId;
     private Long smsContactId;
     private String remark;
 }

+ 11 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInvitationVO.java

@@ -27,6 +27,16 @@ public class DrInvitationVO {
     private BigDecimal demandCapacityKw;
     /** 申报出清容量 kW */
     private BigDecimal declaredCapacityKw;
+    /** 实际出清 kW(电网审核通过) */
+    private BigDecimal actualClearedCapacityKw;
+    /** 预估响应容量 kW(响应完成后本平台评估) */
+    private BigDecimal estimatedResponseCapacityKw;
+    /** 实际响应容量 kW(电网评估最终结果) */
+    private BigDecimal actualResponseCapacityKw;
+    /** 响应完成率 = 实际响应容量 / 邀约规模 */
+    private BigDecimal responseCompletionRate;
+    /** 是否中标 0否 1是 */
+    private Integer isWinningBid;
     private Integer replyStatus;
     private Integer responseStatus;
     private Integer smsNotifyStatus;
@@ -35,6 +45,7 @@ public class DrInvitationVO {
     private String smsContactName;
     private String smsContactPhone;
     private Long participationId;
+    private Long siteId;
     private String remark;
     private Integer tenantId;
     private LocalDateTime createTime;

+ 10 - 9
service-vpp/service-vpp-biz/src/main/resources/sql/vpp_schema.sql

@@ -430,14 +430,9 @@ CREATE TABLE `vpp_dr_event` (
     `invite_scope` VARCHAR(500) NULL COMMENT '邀约范围',
     `target_capacity_kw` DECIMAL(12,4) NOT NULL COMMENT '邀约规模 kW',
     `cleared_capacity_kw` DECIMAL(12,4) NULL COMMENT '出清规模 kW',
-    `declared_cleared_capacity_kw` DECIMAL(12,4) NULL COMMENT '申报出清容量 kW',
-    `actual_cleared_capacity_kw` DECIMAL(12,4) NULL COMMENT '实际出清 kW(电网审核通过)',
-    `estimated_response_capacity_kw` DECIMAL(12,4) NULL COMMENT '预估响应容量 kW(响应完成后本平台评估)',
-    `actual_response_capacity_kw` DECIMAL(12,4) NULL COMMENT '实际响应容量 kW(电网评估最终结果)',
-    `response_completion_rate` DECIMAL(12,4) NULL COMMENT '响应完成率=实际响应容量/邀约规模',
-    `is_winning_bid` TINYINT NULL COMMENT '是否中标 0否 1是',
-    `remark_attachment_url` VARCHAR(500) NULL COMMENT '备注附件URL',
-    `remark_attachment_name` VARCHAR(200) NULL COMMENT '备注附件名称',
+    `remark` VARCHAR(500) NULL COMMENT '备注',
+    `attachment_url` VARCHAR(500) NULL COMMENT '附件URL',
+    `attachment_name` VARCHAR(200) NULL COMMENT '附件名称',
     `subsidy_price` DECIMAL(10,4) NULL COMMENT '补贴标准 元/kWh',
     `event_status` TINYINT NOT NULL COMMENT '0待参与 1已申报 2执行中 3已结束 4已取消',
     `raw_payload` JSON NULL COMMENT '原始邀约报文',
@@ -480,14 +475,20 @@ CREATE TABLE `vpp_dr_invitation` (
     `invitation_no` VARCHAR(32) NOT NULL COMMENT '邀约编号',
     `dr_event_id` BIGINT NOT NULL COMMENT '需求响应事件ID',
     `customer_id` BIGINT NOT NULL COMMENT '客户ID',
+    `site_id` BIGINT NULL COMMENT '站点ID',
     `execute_start_date` DATE NOT NULL COMMENT '执行开始日期',
     `execute_end_date` DATE NOT NULL COMMENT '执行结束日期',
     `reply_deadline` DATETIME(3) NOT NULL COMMENT '邀约回复截止日期',
     `issue_time` DATETIME(3) NOT NULL COMMENT '下发时间',
     `transaction_type` TINYINT NULL COMMENT '交易类型 1削峰 2填谷',
     `response_type` TINYINT NOT NULL COMMENT '1日前 2日内 3秒级',
-    `demand_capacity_kw` DECIMAL(12,4) NOT NULL COMMENT '需求容量 kW',
+    `demand_capacity_kw` DECIMAL(12,4) NOT NULL COMMENT '需求容量/邀约规模 kW',
     `declared_capacity_kw` DECIMAL(12,4) NULL COMMENT '申报出清容量 kW',
+    `actual_cleared_capacity_kw` DECIMAL(12,4) NULL COMMENT '实际出清 kW(电网审核通过)',
+    `estimated_response_capacity_kw` DECIMAL(12,4) NULL COMMENT '预估响应容量 kW(响应完成后本平台评估)',
+    `actual_response_capacity_kw` DECIMAL(12,4) NULL COMMENT '实际响应容量 kW(电网评估最终结果)',
+    `response_completion_rate` DECIMAL(12,4) NULL COMMENT '响应完成率=实际响应容量/邀约规模',
+    `is_winning_bid` TINYINT NULL COMMENT '是否中标 0否 1是',
     `reply_status` TINYINT NOT NULL COMMENT '0待回复 1参与 2拒绝 3超时',
     `response_status` TINYINT NOT NULL COMMENT '0待响应 1已申报 2执行中 3已结束 4已取消',
     `sms_notify_status` TINYINT NOT NULL DEFAULT 0 COMMENT '0未发送 1已发送 2发送失败',