Selaa lähdekoodia

Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701

fuyuchuan 17 tuntia sitten
vanhempi
commit
9c8360545f
15 muutettua tiedostoa jossa 253 lisäystä ja 54 poistoa
  1. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrMonitorController.java
  2. 6 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrInvitation.java
  3. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDashboardServiceImpl.java
  4. 20 10
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrInvitationServiceImpl.java
  5. 17 3
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrMonitorServiceImpl.java
  6. 107 20
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrServiceImpl.java
  7. 3 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInvitationRequest.java
  8. 3 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInvitationVO.java
  9. 5 3
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrMonitorRecordVO.java
  10. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrMonitorSummaryVO.java
  11. 0 5
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrParticipateRequest.java
  12. 44 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrParticipationVO.java
  13. 17 5
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppDrParticipationHelper.java
  14. 23 0
      service-vpp/service-vpp-biz/src/main/resources/sql/alter_vpp_dr_invitation_estimated_completion_rate.sql
  15. 2 1
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_schema.sql

+ 1 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrMonitorController.java

@@ -40,7 +40,7 @@ public class DrMonitorController {
 
     /**
      * 响应记录分页(口径与 summary 一致,按邀约展开)
-     * <p>在事件字段基础上补充客户、站点;预估响应容量/实际出清取自邀约,并计算补贴金额预测。</p>
+     * <p>在事件字段基础上补充客户、站点;预估响应容量/预估完成率/实际出清取自邀约,并计算补贴金额预测。</p>
      */
     @GetMapping("/records")
     public ApiResult<CommonPage<DrMonitorRecordVO>> records(@RequestParam("startDate") String startDate,

+ 6 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrInvitation.java

@@ -55,11 +55,16 @@ public class VppDrInvitation implements Serializable {
     /** 预估响应容量 kW(响应完成后本平台评估) */
     @TableField("estimated_response_capacity_kw")
     private BigDecimal estimatedResponseCapacityKw;
+    /**
+     * 预估完成率 = 预估响应容量 / 实际出清;展示时 ×100%
+     */
+    @TableField("estimated_completion_rate")
+    private BigDecimal estimatedCompletionRate;
     /** 实际响应容量 kW(电网评估最终结果) */
     @TableField("actual_response_capacity_kw")
     private BigDecimal actualResponseCapacityKw;
     /**
-     * 响应完成率 = 实际响应容量 / 邀约规模(需求容量);展示时 ×100%
+     * 响应完成率 = 实际响应容量 / 实际出清;展示时 ×100%
      */
     @TableField("response_completion_rate")
     private BigDecimal responseCompletionRate;

+ 4 - 2
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDashboardServiceImpl.java

@@ -32,6 +32,7 @@ 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 com.usky.vpp.util.VppDrParticipationHelper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
@@ -357,8 +358,9 @@ public class VppDashboardServiceImpl implements VppDashboardService {
             }
 
             BigDecimal completion = invitation.getResponseCompletionRate();
-            if (completion == null && inviteScale.compareTo(BigDecimal.ZERO) > 0) {
-                completion = actual.divide(inviteScale, SCALE, RoundingMode.HALF_UP);
+            if (completion == null) {
+                completion = VppDrParticipationHelper.calcResponseCompletionRate(
+                        invitation.getActualResponseCapacityKw(), invitation.getActualClearedCapacityKw());
             }
             if (completion != null) {
                 completionRates.add(completion);

+ 20 - 10
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrInvitationServiceImpl.java

@@ -129,7 +129,8 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
         invitation.setEstimatedResponseCapacityKw(request.getEstimatedResponseCapacityKw());
         invitation.setActualResponseCapacityKw(request.getActualResponseCapacityKw());
         invitation.setIsWinningBid(request.getIsWinningBid());
-        invitation.setResponseCompletionRate(resolveCompletionRate(request, invitation.getDemandCapacityKw()));
+        invitation.setResponseCompletionRate(resolveCompletionRate(request, invitation));
+        invitation.setEstimatedCompletionRate(resolveEstimatedCompletionRate(request, invitation));
         invitation.setSiteId(request.getSiteId());
         invitation.setReplyStatus(REPLY_PENDING);
         invitation.setResponseStatus(RESPONSE_PENDING);
@@ -184,7 +185,8 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
         if (request.getSiteId() != null) {
             invitation.setSiteId(request.getSiteId());
         }
-        invitation.setResponseCompletionRate(resolveCompletionRate(request, invitation.getDemandCapacityKw()));
+        invitation.setResponseCompletionRate(resolveCompletionRate(request, invitation));
+        invitation.setEstimatedCompletionRate(resolveEstimatedCompletionRate(request, invitation));
         if (request.getTransactionType() != null) {
             invitation.setTransactionType(request.getTransactionType());
         }
@@ -560,17 +562,25 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
     }
 
     /**
-     * 优先用请求显式完成率;否则按 实际响应容量 / 需求容量(邀约规模) 计算。
+     * 优先用请求显式响应完成率;否则按 实际响应容量 / 实际出清 计算。
      */
-    private BigDecimal resolveCompletionRate(DrInvitationRequest request, BigDecimal demandCapacityKw) {
-        if (request == null) {
-            return null;
-        }
-        if (request.getResponseCompletionRate() != null) {
+    private BigDecimal resolveCompletionRate(DrInvitationRequest request, VppDrInvitation invitation) {
+        if (request != null && request.getResponseCompletionRate() != null) {
             return request.getResponseCompletionRate();
         }
-        BigDecimal actual = request.getActualResponseCapacityKw();
-        return VppDrParticipationHelper.calcResponseCompletionRate(actual, demandCapacityKw);
+        return VppDrParticipationHelper.calcResponseCompletionRate(
+                invitation.getActualResponseCapacityKw(), invitation.getActualClearedCapacityKw());
+    }
+
+    /**
+     * 优先用请求显式预估完成率;否则按 预估响应容量 / 实际出清 计算。
+     */
+    private BigDecimal resolveEstimatedCompletionRate(DrInvitationRequest request, VppDrInvitation invitation) {
+        if (request != null && request.getEstimatedCompletionRate() != null) {
+            return request.getEstimatedCompletionRate();
+        }
+        return VppDrParticipationHelper.calcEstimatedCompletionRate(
+                invitation.getEstimatedResponseCapacityKw(), invitation.getActualClearedCapacityKw());
     }
 
     private String resolveTemplateCode(DrInvitationNotifyRequest request) {

+ 17 - 3
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrMonitorServiceImpl.java

@@ -29,6 +29,7 @@ import com.usky.vpp.service.vo.DrMonitorSummaryVO;
 import com.usky.vpp.service.vo.SiteBaselineVO;
 import com.usky.vpp.service.vo.SubsidyPredictionCalcResult;
 import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppDrParticipationHelper;
 import com.usky.vpp.util.VppPageHelper;
 import com.usky.vpp.util.VppResourceTypeHelper;
 import org.springframework.beans.BeanUtils;
@@ -102,7 +103,7 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
         long qualified = 0L;
         for (VppDrInvitation invitation : invitations) {
             capacity = capacity.add(nz(invitation.getActualResponseCapacityKw()));
-            if (isCompletionQualified(invitation.getResponseCompletionRate())) {
+            if (isCompletionQualified(resolveEstimatedCompletionRate(invitation))) {
                 qualified++;
             }
         }
@@ -385,10 +386,12 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
         if (site != null) {
             vo.setSiteName(site.getSiteName());
         }
+        vo.setTransactionType(invitation.getTransactionType());
         vo.setEstimatedResponseCapacityKw(invitation.getEstimatedResponseCapacityKw());
         vo.setClearedCapacityKw(invitation.getActualClearedCapacityKw());
-        vo.setResponseCompletionRate(invitation.getResponseCompletionRate());
-        vo.setQualified(isCompletionQualified(invitation.getResponseCompletionRate()));
+        BigDecimal estimatedCompletionRate = resolveEstimatedCompletionRate(invitation);
+        vo.setEstimatedCompletionRate(estimatedCompletionRate);
+        vo.setQualified(isCompletionQualified(estimatedCompletionRate));
         SubsidyPredictionCalcResult subsidy = subsidyPredictionCalculator.calculate(
                 invitation, event, adjustments, biddingConfigs);
         vo.setEstimatedSubsidyAmount(subsidy.getEstimatedSubsidyAmount());
@@ -428,6 +431,17 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
         return scaled.compareTo(ONE) >= 0;
     }
 
+    private static BigDecimal resolveEstimatedCompletionRate(VppDrInvitation invitation) {
+        if (invitation == null) {
+            return null;
+        }
+        if (invitation.getEstimatedCompletionRate() != null) {
+            return invitation.getEstimatedCompletionRate();
+        }
+        return VppDrParticipationHelper.calcEstimatedCompletionRate(
+                invitation.getEstimatedResponseCapacityKw(), invitation.getActualClearedCapacityKw());
+    }
+
     private static LocalDateTime parseStart(String date) {
         if (!StringUtils.hasText(date)) {
             throw new BusinessException("开始日期不能为空");

+ 107 - 20
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrServiceImpl.java

@@ -10,18 +10,22 @@ import com.usky.vpp.domain.VppCustomer;
 import com.usky.vpp.domain.VppDrEvaluation;
 import com.usky.vpp.domain.VppDrEvent;
 import com.usky.vpp.domain.VppDrExecution;
+import com.usky.vpp.domain.VppDrInvitation;
 import com.usky.vpp.domain.VppDrParticipation;
 import com.usky.vpp.domain.VppDrStrategy;
 import com.usky.vpp.domain.VppDrStrategyResource;
 import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.domain.VppSite;
 import com.usky.vpp.mapper.VppCustomerMapper;
 import com.usky.vpp.mapper.VppDrEvaluationMapper;
 import com.usky.vpp.mapper.VppDrEventMapper;
 import com.usky.vpp.mapper.VppDrExecutionMapper;
+import com.usky.vpp.mapper.VppDrInvitationMapper;
 import com.usky.vpp.mapper.VppDrParticipationMapper;
 import com.usky.vpp.mapper.VppDrStrategyMapper;
 import com.usky.vpp.mapper.VppDrStrategyResourceMapper;
 import com.usky.vpp.mapper.VppResourcePointMapper;
+import com.usky.vpp.mapper.VppSiteMapper;
 import com.usky.vpp.service.VppDrService;
 import com.usky.vpp.service.VppDrSubsidyPredictionService;
 import com.usky.vpp.service.VppUnIntegrationService;
@@ -40,6 +44,7 @@ import com.usky.vpp.util.VppPageHelper;
 import com.usky.vpp.util.VppSiteResourceHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -73,6 +78,8 @@ public class VppDrServiceImpl implements VppDrService {
     @Autowired
     private VppDrParticipationMapper participationMapper;
     @Autowired
+    private VppDrInvitationMapper invitationMapper;
+    @Autowired
     private VppCustomerMapper customerMapper;
     @Autowired
     private VppDrStrategyMapper strategyMapper;
@@ -85,6 +92,8 @@ public class VppDrServiceImpl implements VppDrService {
     @Autowired
     private VppResourcePointMapper resourcePointMapper;
     @Autowired
+    private VppSiteMapper siteMapper;
+    @Autowired
     private VppSiteResourceHelper siteResourceHelper;
     @Autowired
     private VppUnIntegrationService unIntegrationService;
@@ -311,28 +320,32 @@ public class VppDrServiceImpl implements VppDrService {
         }
 
         if (Boolean.TRUE.equals(request.getParticipate())) {
-            if (request.getDeclaredCapacityKw() == null || request.getDeclaredCapacityKw().compareTo(BigDecimal.ZERO) <= 0) {
-                throw new BusinessException("申报出清容量必须大于0");
-            }
-
             if (request.getResources() == null || request.getResources().isEmpty()) {
                 throw new BusinessException("必须选择参与资源");
             }
 
-            Map<Long, com.usky.vpp.domain.VppSite> siteMap = siteResourceHelper.loadSiteMap(
-                    request.getResources().stream()
-                            .map(r -> resourcePointMapper.selectById(r.getResourceId()))
-                            .filter(Objects::nonNull)
-                            .collect(Collectors.toList()));
-
-            BigDecimal resourceDeclaredTotal = BigDecimal.ZERO;
+            List<VppResourcePoint> selectedResources = request.getResources().stream()
+                    .map(r -> r.getResourceId() == null ? null : resourcePointMapper.selectById(r.getResourceId()))
+                    .filter(Objects::nonNull)
+                    .collect(Collectors.toList());
+            Map<Long, com.usky.vpp.domain.VppSite> siteMap = siteResourceHelper.loadSiteMap(selectedResources);
+
+            List<VppDrInvitation> invitations = invitationMapper.selectList(
+                    new LambdaQueryWrapper<VppDrInvitation>()
+                            .eq(VppDrInvitation::getDrEventId, eventId)
+                            .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+            Map<Long, VppDrInvitation> invitationByCustomerId = invitations.stream()
+                    .filter(inv -> inv.getCustomerId() != null)
+                    .collect(Collectors.toMap(VppDrInvitation::getCustomerId, inv -> inv, (a, b) -> a));
+            Map<Long, VppDrInvitation> invitationBySiteId = invitations.stream()
+                    .filter(inv -> inv.getSiteId() != null)
+                    .collect(Collectors.toMap(VppDrInvitation::getSiteId, inv -> inv, (a, b) -> a));
+
+            Map<Long, BigDecimal> declaredByResourceId = new HashMap<>();
             for (DrParticipateRequest.DrResourceDeclare resource : request.getResources()) {
                 if (resource.getResourceId() == null) {
                     throw new BusinessException("资源ID不能为空");
                 }
-                if (resource.getDeclaredCapacityKw() == null || resource.getDeclaredCapacityKw().compareTo(BigDecimal.ZERO) <= 0) {
-                    throw new BusinessException("资源申报出清容量必须大于0");
-                }
                 VppResourcePoint rp = resourcePointMapper.selectById(resource.getResourceId());
                 if (rp == null || VppAuditHelper.isDeleted(rp.getDeleteFlag())) {
                     throw new BusinessException("资源点不存在");
@@ -344,11 +357,16 @@ public class VppDrServiceImpl implements VppDrService {
                 if (customerId == null) {
                     throw new BusinessException("资源点未关联客户");
                 }
-                resourceDeclaredTotal = resourceDeclaredTotal.add(resource.getDeclaredCapacityKw());
-            }
-
-            if (resourceDeclaredTotal.compareTo(request.getDeclaredCapacityKw()) != 0) {
-                throw new BusinessException("各资源申报出清容量之和与总申报出清容量不一致");
+                VppDrInvitation invitation = resolveInvitationForResource(
+                        customerId, rp.getSiteId(), invitationByCustomerId, invitationBySiteId);
+                if (invitation == null) {
+                    throw new BusinessException("资源点未关联邀约,无法申报");
+                }
+                BigDecimal declaredCapacityKw = invitation.getDeclaredCapacityKw();
+                if (declaredCapacityKw == null || declaredCapacityKw.compareTo(BigDecimal.ZERO) <= 0) {
+                    throw new BusinessException("邀约申报出清容量必须大于0");
+                }
+                declaredByResourceId.put(resource.getResourceId(), declaredCapacityKw);
             }
 
             for (DrParticipateRequest.DrResourceDeclare resource : request.getResources()) {
@@ -358,7 +376,7 @@ public class VppDrServiceImpl implements VppDrService {
                 participation.setCustomerId(siteResourceHelper.resolveCustomerId(rp, siteMap));
                 participation.setResourceId(resource.getResourceId());
                 participation.setParticipateStatus(PARTICIPATE_STATUS_ACCEPT);
-                participation.setDeclaredCapacityKw(resource.getDeclaredCapacityKw());
+                participation.setDeclaredCapacityKw(declaredByResourceId.get(resource.getResourceId()));
                 participation.setDeclaredAt(LocalDateTime.now());
                 VppDrParticipationHelper.refreshCompletionRate(participation);
                 VppAuditHelper.fillCreate(participation);
@@ -385,6 +403,21 @@ public class VppDrServiceImpl implements VppDrService {
         }
     }
 
+    private VppDrInvitation resolveInvitationForResource(Long customerId, Long siteId,
+                                                         Map<Long, VppDrInvitation> invitationByCustomerId,
+                                                         Map<Long, VppDrInvitation> invitationBySiteId) {
+        if (customerId != null) {
+            VppDrInvitation invitation = invitationByCustomerId.get(customerId);
+            if (invitation != null) {
+                return invitation;
+            }
+        }
+        if (siteId != null) {
+            return invitationBySiteId.get(siteId);
+        }
+        return null;
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void clearing(Long eventId, DrClearingRequest request) {
@@ -961,6 +994,27 @@ public class VppDrServiceImpl implements VppDrService {
                 .filter(r -> !VppAuditHelper.isDeleted(r.getDeleteFlag()))
                 .collect(Collectors.toMap(VppResourcePoint::getId, r -> r, (a, b) -> a));
 
+        List<VppDrInvitation> invitations = invitationMapper.selectList(
+                new LambdaQueryWrapper<VppDrInvitation>()
+                        .eq(VppDrInvitation::getDrEventId, eventId)
+                        .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        Map<Long, VppDrInvitation> invitationByParticipationId = invitations.stream()
+                .filter(inv -> inv.getParticipationId() != null)
+                .collect(Collectors.toMap(VppDrInvitation::getParticipationId, inv -> inv, (a, b) -> a));
+        Map<Long, VppDrInvitation> invitationByCustomerId = invitations.stream()
+                .filter(inv -> inv.getCustomerId() != null)
+                .collect(Collectors.toMap(VppDrInvitation::getCustomerId, inv -> inv, (a, b) -> a));
+
+        Set<Long> siteIds = invitations.stream()
+                .map(VppDrInvitation::getSiteId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        Map<Long, String> siteNameMap = siteIds.isEmpty()
+                ? Collections.emptyMap()
+                : siteMapper.selectBatchIds(siteIds).stream()
+                .filter(s -> !VppAuditHelper.isDeleted(s.getDeleteFlag()))
+                .collect(Collectors.toMap(VppSite::getId, VppSite::getSiteName, (a, b) -> a));
+
         return participations.stream().map(p -> {
             DrParticipationVO item = new DrParticipationVO();
             item.setId(p.getId());
@@ -982,10 +1036,43 @@ public class VppDrServiceImpl implements VppDrService {
             if (resource != null) {
                 item.setResourceName(resource.getResourceName());
             }
+            fillInvitationFields(item, resolveInvitation(p, invitationByParticipationId, invitationByCustomerId));
+            if (item.getSiteId() != null) {
+                item.setSiteName(siteNameMap.get(item.getSiteId()));
+            }
             return item;
         }).collect(Collectors.toList());
     }
 
+    private VppDrInvitation resolveInvitation(VppDrParticipation participation,
+                                              Map<Long, VppDrInvitation> invitationByParticipationId,
+                                              Map<Long, VppDrInvitation> invitationByCustomerId) {
+        VppDrInvitation invitation = invitationByParticipationId.get(participation.getId());
+        if (invitation != null) {
+            return invitation;
+        }
+        return invitationByCustomerId.get(participation.getCustomerId());
+    }
+
+    /**
+     * 将邀约表字段完整映射到参与 VO,保留既有参与记录字段不被覆盖。
+     */
+    private void fillInvitationFields(DrParticipationVO item, VppDrInvitation invitation) {
+        if (invitation == null) {
+            return;
+        }
+        Long participationPk = item.getId();
+        Long customerId = item.getCustomerId();
+        BigDecimal declaredCapacityKw = item.getDeclaredCapacityKw();
+        BeanUtils.copyProperties(invitation, item);
+        item.setId(participationPk);
+        item.setInvitationId(invitation.getId());
+        item.setCustomerId(customerId);
+        if (declaredCapacityKw != null) {
+            item.setDeclaredCapacityKw(declaredCapacityKw);
+        }
+    }
+
     private DrStrategyVO toStrategyVo(VppDrStrategy strategy) {
         DrStrategyVO vo = new DrStrategyVO();
         vo.setId(strategy.getId());

+ 3 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInvitationRequest.java

@@ -24,9 +24,11 @@ public class DrInvitationRequest {
     private BigDecimal actualClearedCapacityKw;
     /** 预估响应容量 kW(响应完成后本平台评估) */
     private BigDecimal estimatedResponseCapacityKw;
+    /** 预估完成率 = 预估响应容量 / 实际出清 */
+    private BigDecimal estimatedCompletionRate;
     /** 实际响应容量 kW(电网评估最终结果) */
     private BigDecimal actualResponseCapacityKw;
-    /** 响应完成率 = 实际响应容量 / 邀约规模 */
+    /** 响应完成率 = 实际响应容量 / 实际出清 */
     private BigDecimal responseCompletionRate;
     /** 是否中标 0否 1是 */
     private Integer isWinningBid;

+ 3 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInvitationVO.java

@@ -32,9 +32,11 @@ public class DrInvitationVO {
     private BigDecimal actualClearedCapacityKw;
     /** 预估响应容量 kW(响应完成后本平台评估) */
     private BigDecimal estimatedResponseCapacityKw;
+    /** 预估完成率 = 预估响应容量 / 实际出清 */
+    private BigDecimal estimatedCompletionRate;
     /** 实际响应容量 kW(电网评估最终结果) */
     private BigDecimal actualResponseCapacityKw;
-    /** 响应完成率 = 实际响应容量 / 邀约规模 */
+    /** 响应完成率 = 实际响应容量 / 实际出清 */
     private BigDecimal responseCompletionRate;
     /** 是否中标 0否 1是 */
     private Integer isWinningBid;

+ 5 - 3
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrMonitorRecordVO.java

@@ -23,6 +23,8 @@ public class DrMonitorRecordVO {
     private Long siteId;
     private String siteName;
     private Integer responseType;
+    /** 交易类型:1削峰 2填谷(邀约 transaction_type) */
+    private Integer transactionType;
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime startTime;
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@@ -55,9 +57,9 @@ public class DrMonitorRecordVO {
     private String createdBy;
     private String updatedBy;
 
-    /** 邀约响应完成率 */
-    private BigDecimal responseCompletionRate;
-    /** 是否达标:完成率 >= 100% */
+    /** 预估完成率 = 预估响应容量 / 实际出清 */
+    private BigDecimal estimatedCompletionRate;
+    /** 是否达标:预估完成率 >= 100% */
     private Boolean qualified;
     /** 补贴金额预测(元)= 预估响应容量 × 时长(h) × 竞价价格 × 价格调整系数 */
     private BigDecimal estimatedSubsidyAmount;

+ 1 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrMonitorSummaryVO.java

@@ -14,6 +14,6 @@ public class DrMonitorSummaryVO {
     private Long responseCount = 0L;
     /** 响应容量 kW(actual_response_capacity_kw 累加) */
     private BigDecimal responseCapacityKw = BigDecimal.ZERO;
-    /** 历史达标率 0~1(完成率 >= 100% 的条数 / 总条数) */
+    /** 历史达标率 0~1(预估完成率 >= 100% 的条数 / 总条数) */
     private BigDecimal historicalQualifiedRate = BigDecimal.ZERO;
 }

+ 0 - 5
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrParticipateRequest.java

@@ -2,7 +2,6 @@ package com.usky.vpp.service.vo;
 
 import lombok.Data;
 
-import java.math.BigDecimal;
 import java.util.List;
 
 /**
@@ -13,15 +12,11 @@ public class DrParticipateRequest {
 
     private Boolean participate;
     private Long strategyId;
-    /** 申报出清容量 kW */
-    private BigDecimal declaredCapacityKw;
     private String reason;
     private List<DrResourceDeclare> resources;
 
     @Data
     public static class DrResourceDeclare {
         private Long resourceId;
-        /** 申报出清容量 kW */
-        private BigDecimal declaredCapacityKw;
     }
 }

+ 44 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrParticipationVO.java

@@ -3,10 +3,12 @@ package com.usky.vpp.service.vo;
 import lombok.Data;
 
 import java.math.BigDecimal;
+import java.time.LocalDate;
 import java.time.LocalDateTime;
 
 /**
  * 需求响应参与记录(事件详情嵌套)
+ * <p>在既有参与记录字段基础上,附带关联邀约表(vpp_dr_invitation)全部业务字段。</p>
  */
 @Data
 public class DrParticipationVO {
@@ -22,4 +24,46 @@ public class DrParticipationVO {
     /** 完成率 = 出清容量 / 申报出清容量 */
     private BigDecimal completionRate;
     private LocalDateTime declaredAt;
+
+    /** 邀约主键(vpp_dr_invitation.id) */
+    private Long invitationId;
+    private String invitationNo;
+    private Long drEventId;
+    private Long siteId;
+    /** 站点名称 */
+    private String siteName;
+    private LocalDate executeStartDate;
+    private LocalDate executeEndDate;
+    private LocalDateTime replyDeadline;
+    private LocalDateTime issueTime;
+    private Integer transactionType;
+    private Integer responseType;
+    private BigDecimal demandCapacityKw;
+    /** 实际出清 kW(电网审核通过) */
+    private BigDecimal actualClearedCapacityKw;
+    /** 预估响应容量 kW(响应完成后本平台评估) */
+    private BigDecimal estimatedResponseCapacityKw;
+    /** 预估完成率 = 预估响应容量 / 实际出清 */
+    private BigDecimal estimatedCompletionRate;
+    /** 实际响应容量 kW(电网评估最终结果) */
+    private BigDecimal actualResponseCapacityKw;
+    /** 响应完成率 = 实际响应容量 / 实际出清 */
+    private BigDecimal responseCompletionRate;
+    /** 是否中标 0否 1是 */
+    private Integer isWinningBid;
+    private Integer replyStatus;
+    /** 0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消 */
+    private Integer responseStatus;
+    private Integer smsNotifyStatus;
+    private LocalDateTime smsNotifyAt;
+    private Long smsContactId;
+    private Long participationId;
+    private String remark;
+    private Integer tenantId;
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+    private String createdBy;
+    private String updatedBy;
+    private Integer deleteFlag;
+    private LocalDateTime deletedAt;
 }

+ 17 - 5
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppDrParticipationHelper.java

@@ -27,15 +27,27 @@ public final class VppDrParticipationHelper {
     }
 
     /**
-     * 响应完成率 = 实际响应容量 / 邀约规模;邀约规模为空或 ≤0、实际响应容量为空时返回 null。
+     * 响应完成率 = 实际响应容量 / 实际出清;分母为空或 ≤0、实际响应容量为空时返回 null。
      */
     public static BigDecimal calcResponseCompletionRate(BigDecimal actualResponseCapacityKw,
-                                                        BigDecimal inviteScaleKw) {
-        if (actualResponseCapacityKw == null || inviteScaleKw == null
-                || inviteScaleKw.compareTo(BigDecimal.ZERO) <= 0) {
+                                                        BigDecimal actualClearedCapacityKw) {
+        return divideRate(actualResponseCapacityKw, actualClearedCapacityKw);
+    }
+
+    /**
+     * 预估完成率 = 预估响应容量 / 实际出清;分母为空或 ≤0、预估响应容量为空时返回 null。
+     */
+    public static BigDecimal calcEstimatedCompletionRate(BigDecimal estimatedResponseCapacityKw,
+                                                         BigDecimal actualClearedCapacityKw) {
+        return divideRate(estimatedResponseCapacityKw, actualClearedCapacityKw);
+    }
+
+    private static BigDecimal divideRate(BigDecimal numerator, BigDecimal denominator) {
+        if (numerator == null || denominator == null
+                || denominator.compareTo(BigDecimal.ZERO) <= 0) {
             return null;
         }
-        return actualResponseCapacityKw.divide(inviteScaleKw, RATE_SCALE, RoundingMode.HALF_UP);
+        return numerator.divide(denominator, RATE_SCALE, RoundingMode.HALF_UP);
     }
 
     public static void refreshCompletionRate(VppDrParticipation participation) {

+ 23 - 0
service-vpp/service-vpp-biz/src/main/resources/sql/alter_vpp_dr_invitation_estimated_completion_rate.sql

@@ -0,0 +1,23 @@
+-- 邀约表新增预估完成率,并按最新口径回填完成率(已上线库执行本脚本)
+-- 响应完成率 = 实际响应容量 / 实际出清
+-- 预估完成率 = 预估响应容量 / 实际出清
+-- 若 estimated_completion_rate 列已存在,请跳过第一条 ADD COLUMN,继续执行后续语句。
+
+ALTER TABLE `vpp_dr_invitation`
+    ADD COLUMN `estimated_completion_rate` DECIMAL(12,2) NULL COMMENT '预估完成率=预估响应容量/实际出清' AFTER `estimated_response_capacity_kw`;
+
+ALTER TABLE `vpp_dr_invitation`
+    MODIFY COLUMN `estimated_completion_rate` DECIMAL(12,2) NULL COMMENT '预估完成率=预估响应容量/实际出清',
+    MODIFY COLUMN `response_completion_rate` DECIMAL(12,2) NULL COMMENT '响应完成率=实际响应容量/实际出清';
+
+UPDATE `vpp_dr_invitation`
+SET `estimated_completion_rate` = ROUND(`estimated_response_capacity_kw` / `actual_cleared_capacity_kw`, 4)
+WHERE `estimated_response_capacity_kw` IS NOT NULL
+  AND `actual_cleared_capacity_kw` IS NOT NULL
+  AND `actual_cleared_capacity_kw` > 0;
+
+UPDATE `vpp_dr_invitation`
+SET `response_completion_rate` = ROUND(`actual_response_capacity_kw` / `actual_cleared_capacity_kw`, 4)
+WHERE `actual_response_capacity_kw` IS NOT NULL
+  AND `actual_cleared_capacity_kw` IS NOT NULL
+  AND `actual_cleared_capacity_kw` > 0;

+ 2 - 1
service-vpp/service-vpp-biz/src/main/resources/sql/vpp_schema.sql

@@ -505,8 +505,9 @@ CREATE TABLE `vpp_dr_invitation` (
     `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(响应完成后本平台评估)',
+    `estimated_completion_rate` DECIMAL(12,2) NULL COMMENT '预估完成率=预估响应容量/实际出清',
     `actual_response_capacity_kw` DECIMAL(12,4) NULL COMMENT '实际响应容量 kW(电网评估最终结果)',
-    `response_completion_rate` DECIMAL(12,4) NULL COMMENT '响应完成率=实际响应容量/邀约规模',
+    `response_completion_rate` DECIMAL(12,2) 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已结束 5已取消',