Bläddra i källkod

service-vpp模块代码更新

hanzhengyi 4 dagar sedan
förälder
incheckning
df6791ced3
31 ändrade filer med 2458 tillägg och 49 borttagningar
  1. 17 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/config/VppReportProperties.java
  2. 2 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrController.java
  3. 62 29
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ReportInfoController.java
  4. 56 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ResponseMonitorController.java
  5. 2 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrService.java
  6. 28 4
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppReportInfoService.java
  7. 19 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppResponseMonitorService.java
  8. 91 11
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrServiceImpl.java
  9. 763 3
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppReportInfoServiceImpl.java
  10. 423 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResponseMonitorServiceImpl.java
  11. 27 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrEventDetailVO.java
  12. 23 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrParticipationVO.java
  13. 16 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ReportAuditLogVO.java
  14. 11 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ReportAuditRequest.java
  15. 12 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ReportDataSaveRequest.java
  16. 19 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ReportPrefillVO.java
  17. 28 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ReportRecordVO.java
  18. 24 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ReportTaskCreateRequest.java
  19. 18 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ReportTaskCreateResultVO.java
  20. 22 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ReportTaskVO.java
  21. 14 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ReportValidateErrorVO.java
  22. 13 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ReportValidateResultVO.java
  23. 24 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResponseCompareSeriesVO.java
  24. 18 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResponseCompareVO.java
  25. 24 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResponseLoadAnalysisVO.java
  26. 19 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResponseLoadPointVO.java
  27. 24 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResponseLoadStatRowVO.java
  28. 20 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResponseMonitorTreeNodeVO.java
  29. 297 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppReportHelper.java
  30. 340 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppResponseMonitorHelper.java
  31. 2 0
      service-vpp/service-vpp-biz/src/main/resources/bootstrap.yml

+ 17 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/config/VppReportProperties.java

@@ -0,0 +1,17 @@
+package com.usky.vpp.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 监管信息报送配置
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "vpp.report")
+public class VppReportProperties {
+
+    /** 报送失败最大重试次数 */
+    private Integer retryMax = 3;
+}

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

@@ -8,6 +8,7 @@ import com.usky.vpp.domain.VppDrStrategy;
 import com.usky.vpp.service.VppDrInvitationService;
 import com.usky.vpp.service.VppDrService;
 import com.usky.vpp.service.vo.DrClearingRequest;
+import com.usky.vpp.service.vo.DrEventDetailVO;
 import com.usky.vpp.service.vo.DrEventRequest;
 import com.usky.vpp.service.vo.DrInterveneRequest;
 import com.usky.vpp.service.vo.DrInvitationNotifyRequest;
@@ -41,7 +42,7 @@ public class DrController {
     }
 
     @GetMapping(value = "/event/{id}")
-    public ApiResult<VppDrEvent> getEvent(@PathVariable("id") Long id) {
+    public ApiResult<DrEventDetailVO> getEvent(@PathVariable("id") Long id) {
         return ApiResult.success(vppDrService.getEvent(id));
     }
 

+ 62 - 29
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ReportInfoController.java

@@ -1,13 +1,31 @@
 package com.usky.vpp.controller.web;
 
 import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.bean.CommonPage;
 import com.usky.vpp.service.VppReportInfoService;
+import com.usky.vpp.service.vo.ReportAuditRequest;
+import com.usky.vpp.service.vo.ReportDataSaveRequest;
+import com.usky.vpp.service.vo.ReportPrefillVO;
+import com.usky.vpp.service.vo.ReportRecordVO;
+import com.usky.vpp.service.vo.ReportTaskCreateRequest;
+import com.usky.vpp.service.vo.ReportTaskCreateResultVO;
+import com.usky.vpp.service.vo.ReportTaskVO;
+import com.usky.vpp.service.vo.ReportValidateResultVO;
 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.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
 
 /**
- * 虚拟电厂 - ReportInfo 接口
- * 网关前缀: /prod-api/service-vpp
+ * 监管信息报送接口
+ * <p>网关前缀: /prod-api/service-vpp/report</p>
  */
 @RestController
 @RequestMapping("/report")
@@ -16,40 +34,55 @@ public class ReportInfoController {
     @Autowired
     private VppReportInfoService vppReportInfoService;
 
-    @GetMapping(value = "/task")
-    public ApiResult<Object> pageTask(@RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+    @GetMapping("/task")
+    public ApiResult<CommonPage<ReportTaskVO>> pageTask(@RequestParam(required = false) Map<String, Object> params) {
+        return ApiResult.success(vppReportInfoService.pageTask(params));
     }
-    @GetMapping(value = "/record/{id}")
-    public ApiResult<Object> getRecord(@PathVariable("id") Long id, @RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+
+    @PostMapping("/task")
+    public ApiResult<ReportTaskCreateResultVO> createTask(@RequestBody ReportTaskCreateRequest body) {
+        return ApiResult.success(vppReportInfoService.createTask(body));
     }
-    @GetMapping(value = "/record/{id}/prefill")
-    public ApiResult<Object> prefill(@PathVariable("id") Long id, @RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+
+    @GetMapping("/record/{id}")
+    public ApiResult<ReportRecordVO> getRecord(@PathVariable("id") Long id) {
+        return ApiResult.success(vppReportInfoService.getRecord(id));
     }
-    @PostMapping(value = "/record/{id}/data")
-    public ApiResult<Void> saveData(@PathVariable("id") Long id, @RequestBody(required = false) Object body) {
-        return ApiResult.success();
+
+    @GetMapping("/record/{id}/prefill")
+    public ApiResult<ReportPrefillVO> prefill(@PathVariable("id") Long id) {
+        return ApiResult.success(vppReportInfoService.prefill(id));
     }
-    @PostMapping(value = "/record/{id}/validate")
-    public ApiResult<Object> validateData(@PathVariable("id") Long id, @RequestBody(required = false) Object body) {
-        return ApiResult.success(null);
+
+    @PutMapping("/record/{id}/data")
+    public ApiResult<Map<String, Object>> saveData(@PathVariable("id") Long id,
+                                                   @RequestBody ReportDataSaveRequest body) {
+        return ApiResult.success(vppReportInfoService.saveData(id, body));
     }
-    @PostMapping(value = "/record/{id}/submit")
-    public ApiResult<Void> submitAudit(@PathVariable("id") Long id, @RequestBody(required = false) Object body) {
-        return ApiResult.success();
+
+    @PostMapping("/record/{id}/validate")
+    public ApiResult<ReportValidateResultVO> validateData(@PathVariable("id") Long id) {
+        return ApiResult.success(vppReportInfoService.validateData(id));
     }
-    @PutMapping(value = "/record/{id}/audit")
-    public ApiResult<Void> audit(@PathVariable("id") Long id, @RequestBody(required = false) Object body) {
-        return ApiResult.success();
+
+    @PostMapping("/record/{id}/submitAudit")
+    public ApiResult<Map<String, Object>> submitAudit(@PathVariable("id") Long id) {
+        return ApiResult.success(vppReportInfoService.submitAudit(id));
     }
-    @PostMapping(value = "/record/{id}/submit-external")
-    public ApiResult<Void> submitExternal(@PathVariable("id") Long id, @RequestBody(required = false) Object body) {
+
+    @PutMapping("/record/{id}/audit")
+    public ApiResult<Void> audit(@PathVariable("id") Long id, @RequestBody ReportAuditRequest body) {
+        vppReportInfoService.audit(id, body);
         return ApiResult.success();
     }
-    @GetMapping(value = "/history")
-    public ApiResult<Object> history(@RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+
+    @PostMapping("/record/{id}/submit")
+    public ApiResult<ReportRecordVO> submit(@PathVariable("id") Long id) {
+        return ApiResult.success(vppReportInfoService.submit(id));
+    }
+
+    @GetMapping("/history")
+    public ApiResult<CommonPage<ReportRecordVO>> history(@RequestParam(required = false) Map<String, Object> params) {
+        return ApiResult.success(vppReportInfoService.pageHistory(params));
     }
 }

+ 56 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ResponseMonitorController.java

@@ -0,0 +1,56 @@
+package com.usky.vpp.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.vpp.service.VppResponseMonitorService;
+import com.usky.vpp.service.vo.ResponseCompareVO;
+import com.usky.vpp.service.vo.ResponseLoadAnalysisVO;
+import com.usky.vpp.service.vo.ResponseMonitorTreeNodeVO;
+import org.springframework.beans.factory.annotation.Autowired;
+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;
+
+import java.util.List;
+
+/**
+ * 响应监控接口(虚拟电厂驾驶舱)
+ * <p>网关前缀: /prod-api/service-vpp/response-monitor</p>
+ */
+@RestController
+@RequestMapping("/response-monitor")
+public class ResponseMonitorController {
+
+    @Autowired
+    private VppResponseMonitorService responseMonitorService;
+
+    /**
+     * 左侧资源树(客户 → 站点)
+     */
+    @GetMapping("/tree")
+    public ApiResult<List<ResponseMonitorTreeNodeVO>> tree(
+            @RequestParam(value = "keyword", required = false) String keyword) {
+        return ApiResult.success(responseMonitorService.getResourceTree(keyword));
+    }
+
+    /**
+     * 负荷分析:四曲线 + 用电量统计表
+     */
+    @GetMapping("/load-analysis")
+    public ApiResult<ResponseLoadAnalysisVO> loadAnalysis(
+            @RequestParam("siteId") Long siteId,
+            @RequestParam(value = "date", required = false) String date,
+            @RequestParam(value = "eventId", required = false) Long eventId) {
+        return ApiResult.success(responseMonitorService.getLoadAnalysis(siteId, date, eventId));
+    }
+
+    /**
+     * 对比分析:多站点实际负荷曲线对比
+     */
+    @GetMapping("/compare")
+    public ApiResult<ResponseCompareVO> compare(
+            @RequestParam("siteIds") String siteIds,
+            @RequestParam(value = "date", required = false) String date) {
+        return ApiResult.success(responseMonitorService.compare(siteIds, date));
+    }
+}

+ 2 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrService.java

@@ -5,6 +5,7 @@ import com.usky.vpp.domain.VppDrEvent;
 import com.usky.vpp.domain.VppDrEvaluation;
 import com.usky.vpp.domain.VppDrStrategy;
 import com.usky.vpp.service.vo.DrClearingRequest;
+import com.usky.vpp.service.vo.DrEventDetailVO;
 import com.usky.vpp.service.vo.DrEventRequest;
 import com.usky.vpp.service.vo.DrInterveneRequest;
 import com.usky.vpp.service.vo.DrParticipateRequest;
@@ -20,7 +21,7 @@ public interface VppDrService {
 
     CommonPage<VppDrEvent> pageEvent(Map<String, Object> params);
 
-    VppDrEvent getEvent(Long id);
+    DrEventDetailVO getEvent(Long id);
 
     VppDrEvent createEvent(DrEventRequest request);
 

+ 28 - 4
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppReportInfoService.java

@@ -1,15 +1,39 @@
 package com.usky.vpp.service;
 
 import com.usky.common.core.bean.CommonPage;
+import com.usky.vpp.service.vo.ReportAuditRequest;
+import com.usky.vpp.service.vo.ReportDataSaveRequest;
+import com.usky.vpp.service.vo.ReportPrefillVO;
+import com.usky.vpp.service.vo.ReportRecordVO;
+import com.usky.vpp.service.vo.ReportTaskCreateRequest;
+import com.usky.vpp.service.vo.ReportTaskCreateResultVO;
+import com.usky.vpp.service.vo.ReportTaskVO;
+import com.usky.vpp.service.vo.ReportValidateResultVO;
 
 import java.util.Map;
 
 /**
- * VppReportInfoService 业务接口
+ * 监管信息报送业务接口
  */
 public interface VppReportInfoService {
 
-    default Object stub(String action, Map<String, Object> params) {
-        return null;
-    }
+    CommonPage<ReportTaskVO> pageTask(Map<String, Object> params);
+
+    ReportTaskCreateResultVO createTask(ReportTaskCreateRequest request);
+
+    ReportRecordVO getRecord(Long id);
+
+    ReportPrefillVO prefill(Long id);
+
+    Map<String, Object> saveData(Long id, ReportDataSaveRequest request);
+
+    ReportValidateResultVO validateData(Long id);
+
+    Map<String, Object> submitAudit(Long id);
+
+    void audit(Long id, ReportAuditRequest request);
+
+    ReportRecordVO submit(Long id);
+
+    CommonPage<ReportRecordVO> pageHistory(Map<String, Object> params);
 }

+ 19 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppResponseMonitorService.java

@@ -0,0 +1,19 @@
+package com.usky.vpp.service;
+
+import com.usky.vpp.service.vo.ResponseCompareVO;
+import com.usky.vpp.service.vo.ResponseLoadAnalysisVO;
+import com.usky.vpp.service.vo.ResponseMonitorTreeNodeVO;
+
+import java.util.List;
+
+/**
+ * 响应监控业务接口
+ */
+public interface VppResponseMonitorService {
+
+    List<ResponseMonitorTreeNodeVO> getResourceTree(String keyword);
+
+    ResponseLoadAnalysisVO getLoadAnalysis(Long siteId, String date, Long eventId);
+
+    ResponseCompareVO compare(String siteIds, String date);
+}

+ 91 - 11
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrServiceImpl.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.common.core.exception.BusinessException;
+import com.usky.vpp.domain.VppCustomer;
 import com.usky.vpp.domain.VppDrEvaluation;
 import com.usky.vpp.domain.VppDrEvent;
 import com.usky.vpp.domain.VppDrExecution;
@@ -11,6 +12,7 @@ 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.mapper.VppCustomerMapper;
 import com.usky.vpp.mapper.VppDrEvaluationMapper;
 import com.usky.vpp.mapper.VppDrEventMapper;
 import com.usky.vpp.mapper.VppDrExecutionMapper;
@@ -21,7 +23,9 @@ import com.usky.vpp.mapper.VppResourcePointMapper;
 import com.usky.vpp.service.VppDrService;
 import com.usky.vpp.service.VppUnIntegrationService;
 import com.usky.vpp.service.vo.DrClearingRequest;
+import com.usky.vpp.service.vo.DrEventDetailVO;
 import com.usky.vpp.service.vo.DrEventRequest;
+import com.usky.vpp.service.vo.DrParticipationVO;
 import com.usky.vpp.service.vo.DrInterveneRequest;
 import com.usky.vpp.service.vo.DrParticipateRequest;
 import com.usky.vpp.service.vo.DrStrategyRequest;
@@ -64,6 +68,8 @@ public class VppDrServiceImpl implements VppDrService {
     @Autowired
     private VppDrParticipationMapper participationMapper;
     @Autowired
+    private VppCustomerMapper customerMapper;
+    @Autowired
     private VppDrStrategyMapper strategyMapper;
     @Autowired
     private VppDrStrategyResourceMapper strategyResourceMapper;
@@ -108,7 +114,12 @@ public class VppDrServiceImpl implements VppDrService {
     }
 
     @Override
-    public VppDrEvent getEvent(Long id) {
+    public DrEventDetailVO getEvent(Long id) {
+        VppDrEvent event = requireEvent(id);
+        return toEventDetailVo(event);
+    }
+
+    private VppDrEvent requireEvent(Long id) {
         VppDrEvent event = eventMapper.selectById(id);
         if (event == null || VppAuditHelper.isDeleted(event.getDeleteFlag())) {
             throw new BusinessException("需求响应事件不存在");
@@ -145,7 +156,7 @@ public class VppDrServiceImpl implements VppDrService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void updateEvent(Long id, DrEventRequest request) {
-        VppDrEvent event = getEvent(id);
+        VppDrEvent event = requireEvent(id);
         if (event.getEventStatus() != EVENT_STATUS_PENDING) {
             throw new BusinessException("仅待参与状态的事件可修改");
         }
@@ -170,7 +181,7 @@ public class VppDrServiceImpl implements VppDrService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void deleteEvent(Long id) {
-        VppDrEvent event = getEvent(id);
+        VppDrEvent event = requireEvent(id);
         if (event.getEventStatus() != EVENT_STATUS_PENDING && event.getEventStatus() != EVENT_STATUS_CANCELLED) {
             throw new BusinessException("仅待参与或已取消状态的事件可删除");
         }
@@ -184,7 +195,7 @@ public class VppDrServiceImpl implements VppDrService {
 
     @Override
     public Object assessCapability(Long eventId) {
-        VppDrEvent event = getEvent(eventId);
+        VppDrEvent event = requireEvent(eventId);
 
         LambdaQueryWrapper<VppResourcePoint> resourceWrapper = new LambdaQueryWrapper<VppResourcePoint>()
                 .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
@@ -232,7 +243,7 @@ public class VppDrServiceImpl implements VppDrService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void participate(Long eventId, DrParticipateRequest request) {
-        VppDrEvent event = getEvent(eventId);
+        VppDrEvent event = requireEvent(eventId);
 
         if (event.getEventStatus() != EVENT_STATUS_PENDING) {
             throw new BusinessException("当前状态不允许参与");
@@ -330,7 +341,7 @@ public class VppDrServiceImpl implements VppDrService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void clearing(Long eventId, DrClearingRequest request) {
-        VppDrEvent event = getEvent(eventId);
+        VppDrEvent event = requireEvent(eventId);
 
         if (event.getEventStatus() != EVENT_STATUS_DECLARED) {
             throw new BusinessException("当前状态不允许出清分拆");
@@ -418,7 +429,7 @@ public class VppDrServiceImpl implements VppDrService {
 
     @Override
     public Object monitorExecution(Long eventId) {
-        VppDrEvent event = getEvent(eventId);
+        VppDrEvent event = requireEvent(eventId);
 
         List<VppDrExecution> executions = executionMapper.selectList(
                 new LambdaQueryWrapper<VppDrExecution>()
@@ -464,7 +475,7 @@ public class VppDrServiceImpl implements VppDrService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void intervene(Long eventId, DrInterveneRequest request) {
-        VppDrEvent event = getEvent(eventId);
+        VppDrEvent event = requireEvent(eventId);
 
         if (event.getEventStatus() != EVENT_STATUS_EXECUTING) {
             throw new BusinessException("仅执行中的事件允许干预");
@@ -523,7 +534,7 @@ public class VppDrServiceImpl implements VppDrService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void completeEvent(Long eventId) {
-        VppDrEvent event = getEvent(eventId);
+        VppDrEvent event = requireEvent(eventId);
         if (event.getEventStatus() != EVENT_STATUS_EXECUTING) {
             throw new BusinessException("仅执行中的事件可结束");
         }
@@ -586,7 +597,7 @@ public class VppDrServiceImpl implements VppDrService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void acknowledgeClearing(Long eventId) {
-        VppDrEvent event = getEvent(eventId);
+        VppDrEvent event = requireEvent(eventId);
         if (event.getEventStatus() != EVENT_STATUS_DECLARED) {
             throw new BusinessException("当前状态不允许确认出清公示");
         }
@@ -595,7 +606,7 @@ public class VppDrServiceImpl implements VppDrService {
 
     @Override
     public VppDrEvaluation getEvaluation(Long eventId) {
-        VppDrEvent event = getEvent(eventId);
+        VppDrEvent event = requireEvent(eventId);
 
         if (event.getEventStatus() != EVENT_STATUS_ENDED) {
             throw new BusinessException("事件未结束,暂无评估结果");
@@ -827,6 +838,75 @@ public class VppDrServiceImpl implements VppDrService {
                         .eq(VppDrEvaluation::getDeleteFlag, VppAuditHelper.NOT_DELETED)) > 0;
     }
 
+    private DrEventDetailVO toEventDetailVo(VppDrEvent event) {
+        DrEventDetailVO vo = new DrEventDetailVO();
+        vo.setId(event.getId());
+        vo.setEventId(event.getEventId());
+        vo.setEventName(event.getEventName());
+        vo.setResponseType(event.getResponseType());
+        vo.setEventType(event.getEventType());
+        vo.setStartTime(event.getStartTime());
+        vo.setEndTime(event.getEndTime());
+        vo.setTargetCapacityKw(event.getTargetCapacityKw());
+        vo.setClearedCapacityKw(event.getClearedCapacityKw());
+        vo.setSubsidyPrice(event.getSubsidyPrice());
+        vo.setEventStatus(event.getEventStatus());
+        vo.setParticipations(loadParticipationVos(event.getId()));
+        return vo;
+    }
+
+    private List<DrParticipationVO> loadParticipationVos(Long eventId) {
+        List<VppDrParticipation> participations = participationMapper.selectList(
+                new LambdaQueryWrapper<VppDrParticipation>()
+                        .eq(VppDrParticipation::getEventId, eventId)
+                        .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .orderByAsc(VppDrParticipation::getDeclaredAt, VppDrParticipation::getId));
+        if (participations.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        Set<Long> customerIds = participations.stream()
+                .map(VppDrParticipation::getCustomerId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        Set<Long> resourceIds = participations.stream()
+                .map(VppDrParticipation::getResourceId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+
+        Map<Long, VppCustomer> customerMap = customerIds.isEmpty()
+                ? Collections.emptyMap()
+                : customerMapper.selectBatchIds(customerIds).stream()
+                .filter(c -> !VppAuditHelper.isDeleted(c.getDeleteFlag()))
+                .collect(Collectors.toMap(VppCustomer::getId, c -> c, (a, b) -> a));
+
+        Map<Long, VppResourcePoint> resourceMap = resourceIds.isEmpty()
+                ? Collections.emptyMap()
+                : resourcePointMapper.selectBatchIds(resourceIds).stream()
+                .filter(r -> !VppAuditHelper.isDeleted(r.getDeleteFlag()))
+                .collect(Collectors.toMap(VppResourcePoint::getId, r -> r, (a, b) -> a));
+
+        return participations.stream().map(p -> {
+            DrParticipationVO item = new DrParticipationVO();
+            item.setId(p.getId());
+            item.setCustomerId(p.getCustomerId());
+            item.setResourceId(p.getResourceId());
+            item.setParticipateStatus(p.getParticipateStatus());
+            item.setDeclaredCapacityKw(p.getDeclaredCapacityKw());
+            item.setClearedCapacityKw(p.getClearedCapacityKw());
+            item.setDeclaredAt(p.getDeclaredAt());
+            VppCustomer customer = customerMap.get(p.getCustomerId());
+            if (customer != null) {
+                item.setCustomerName(customer.getCustomerName());
+            }
+            VppResourcePoint resource = resourceMap.get(p.getResourceId());
+            if (resource != null) {
+                item.setResourceName(resource.getResourceName());
+            }
+            return item;
+        }).collect(Collectors.toList());
+    }
+
     private DrStrategyVO toStrategyVo(VppDrStrategy strategy) {
         DrStrategyVO vo = new DrStrategyVO();
         vo.setId(strategy.getId());

+ 763 - 3
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppReportInfoServiceImpl.java

@@ -1,11 +1,771 @@
 package com.usky.vpp.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.vpp.config.VppReportProperties;
+import com.usky.vpp.domain.VppCustomer;
+import com.usky.vpp.domain.VppDrEvaluation;
+import com.usky.vpp.domain.VppDrEvent;
+import com.usky.vpp.domain.VppEnergySummaryDaily;
+import com.usky.vpp.domain.VppReportAuditLog;
+import com.usky.vpp.domain.VppReportData;
+import com.usky.vpp.domain.VppReportLog;
+import com.usky.vpp.domain.VppReportRecord;
+import com.usky.vpp.domain.VppReportTask;
+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.VppEnergySummaryDailyMapper;
+import com.usky.vpp.mapper.VppReportAuditLogMapper;
+import com.usky.vpp.mapper.VppReportDataMapper;
+import com.usky.vpp.mapper.VppReportLogMapper;
+import com.usky.vpp.mapper.VppReportRecordMapper;
+import com.usky.vpp.mapper.VppReportTaskMapper;
+import com.usky.vpp.mapper.VppResourcePointMapper;
+import com.usky.vpp.mapper.VppSiteMapper;
 import com.usky.vpp.service.VppReportInfoService;
+import com.usky.vpp.service.vo.ReportAuditLogVO;
+import com.usky.vpp.service.vo.ReportAuditRequest;
+import com.usky.vpp.service.vo.ReportDataSaveRequest;
+import com.usky.vpp.service.vo.ReportPrefillVO;
+import com.usky.vpp.service.vo.ReportRecordVO;
+import com.usky.vpp.service.vo.ReportTaskCreateRequest;
+import com.usky.vpp.service.vo.ReportTaskCreateResultVO;
+import com.usky.vpp.service.vo.ReportTaskVO;
+import com.usky.vpp.service.vo.ReportValidateResultVO;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppPageHelper;
+import com.usky.vpp.util.VppReportHelper;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
 
-/**
- * VppReportInfoService 默认实现(待按业务完善)
- */
 @Service
 public class VppReportInfoServiceImpl implements VppReportInfoService {
+
+    private static final DateTimeFormatter DEADLINE_FMT = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
+
+    @Autowired
+    private VppReportTaskMapper taskMapper;
+    @Autowired
+    private VppReportRecordMapper recordMapper;
+    @Autowired
+    private VppReportDataMapper dataMapper;
+    @Autowired
+    private VppReportAuditLogMapper auditLogMapper;
+    @Autowired
+    private VppReportLogMapper reportLogMapper;
+    @Autowired
+    private VppCustomerMapper customerMapper;
+    @Autowired
+    private VppResourcePointMapper resourcePointMapper;
+    @Autowired
+    private VppSiteMapper siteMapper;
+    @Autowired
+    private VppEnergySummaryDailyMapper energySummaryDailyMapper;
+    @Autowired
+    private VppDrEventMapper drEventMapper;
+    @Autowired
+    private VppDrEvaluationMapper drEvaluationMapper;
+    @Autowired
+    private VppReportProperties reportProperties;
+
+    @Override
+    public CommonPage<ReportTaskVO> pageTask(Map<String, Object> params) {
+        Page<VppReportTask> page = VppPageHelper.of(params);
+        LambdaQueryWrapper<VppReportTask> wrapper = activeTaskWrapper();
+        applyTaskFilters(wrapper, params);
+        wrapper.orderByAsc(VppReportTask::getDeadline);
+        Page<VppReportTask> result = taskMapper.selectPage(page, wrapper);
+        List<ReportTaskVO> records = result.getRecords().stream()
+                .map(this::toTaskVo)
+                .collect(Collectors.toList());
+        refreshOverdueTasks(result.getRecords());
+        return new CommonPage<>(records, result.getTotal(), result.getCurrent(), result.getSize());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public ReportTaskCreateResultVO createTask(ReportTaskCreateRequest request) {
+        validateCreateTaskRequest(request);
+
+        String taskNo = VppReportHelper.nextTaskNo();
+        ensureTaskNoUnique(taskNo);
+
+        VppReportTask task = new VppReportTask();
+        task.setTaskNo(taskNo);
+        task.setTaskName(request.getTaskName().trim());
+        task.setReportOrg(request.getReportOrg());
+        task.setReportCycle(request.getReportCycle());
+        task.setReportType(request.getReportType().trim());
+        task.setDeadline(request.getDeadline());
+        task.setTaskStatus(VppReportHelper.TASK_STATUS_PENDING_FILL);
+        task.setAssigneeId(request.getAssigneeId());
+        task.setRemindDays(request.getRemindDays() != null ? request.getRemindDays() : 3);
+        VppAuditHelper.fillCreate(task);
+        taskMapper.insert(task);
+
+        String reportPeriod = resolveReportPeriod(request);
+        String recordNo = VppReportHelper.nextRecordNo(reportPeriod);
+        ensureRecordNoUnique(recordNo);
+
+        VppReportRecord record = new VppReportRecord();
+        record.setTaskId(task.getId());
+        record.setRecordNo(recordNo);
+        record.setReportPeriod(reportPeriod);
+        record.setSubmitMode(request.getSubmitMode() != null
+                ? request.getSubmitMode() : VppReportHelper.SUBMIT_MODE_API);
+        record.setRecordStatus(VppReportHelper.RECORD_STATUS_DRAFT);
+        record.setRetryCount(0);
+        VppAuditHelper.fillCreate(record);
+        recordMapper.insert(record);
+
+        ReportTaskCreateResultVO result = new ReportTaskCreateResultVO();
+        result.setTaskId(task.getId());
+        result.setTaskNo(task.getTaskNo());
+        result.setRecordId(record.getId());
+        result.setRecordNo(record.getRecordNo());
+        result.setReportPeriod(record.getReportPeriod());
+        result.setTaskStatus(task.getTaskStatus());
+        result.setRecordStatus(record.getRecordStatus());
+        return result;
+    }
+
+    @Override
+    public ReportRecordVO getRecord(Long id) {
+        VppReportRecord record = requireRecord(id);
+        VppReportTask task = requireTask(record.getTaskId());
+        VppReportData data = loadReportData(record.getId());
+        return toRecordVo(record, task, data, loadAuditLogs(record.getId()));
+    }
+
+    @Override
+    public ReportPrefillVO prefill(Long id) {
+        VppReportRecord record = requireRecord(id);
+        VppReportTask task = requireTask(record.getTaskId());
+        return buildPrefillData(record, task);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> saveData(Long id, ReportDataSaveRequest request) {
+        if (request == null || request.getFormData() == null || request.getFormData().isEmpty()) {
+            throw new BusinessException("填报数据不能为空");
+        }
+        if (request.getIsDraft() == null) {
+            throw new BusinessException("isDraft 不能为空");
+        }
+        VppReportRecord record = requireRecord(id);
+        if (record.getRecordStatus() != VppReportHelper.RECORD_STATUS_DRAFT
+                && record.getRecordStatus() != VppReportHelper.RECORD_STATUS_FAILED) {
+            throw new BusinessException("当前状态不允许修改填报数据");
+        }
+
+        VppReportData data = loadReportData(record.getId());
+        String formJson = VppReportHelper.toJson(request.getFormData());
+        if (data == null) {
+            data = new VppReportData();
+            data.setRecordId(record.getId());
+            data.setFormData(formJson);
+            data.setIsDraft(Boolean.TRUE.equals(request.getIsDraft()) ? 1 : 0);
+            VppAuditHelper.fillCreate(data);
+            dataMapper.insert(data);
+        } else {
+            data.setFormData(formJson);
+            data.setIsDraft(Boolean.TRUE.equals(request.getIsDraft()) ? 1 : 0);
+            data.setValidationErrors(null);
+            VppAuditHelper.fillUpdate(data);
+            dataMapper.updateById(data);
+        }
+
+        if (record.getRecordStatus() == VppReportHelper.RECORD_STATUS_FAILED) {
+            record.setRecordStatus(VppReportHelper.RECORD_STATUS_DRAFT);
+            VppAuditHelper.fillUpdate(record);
+            recordMapper.updateById(record);
+        }
+
+        Map<String, Object> result = new HashMap<>(2);
+        result.put("recordId", record.getId());
+        result.put("isDraft", request.getIsDraft());
+        return result;
+    }
+
+    @Override
+    public ReportValidateResultVO validateData(Long id) {
+        VppReportRecord record = requireRecord(id);
+        VppReportData data = requireReportData(record.getId());
+        Map<String, Object> formData = VppReportHelper.parseFormData(data.getFormData());
+        int systemPvCount = countResourceType("PV");
+        ReportValidateResultVO result = VppReportHelper.validateFormData(formData, systemPvCount, true);
+        data.setValidationErrors(VppReportHelper.toJson(result.getErrors()));
+        VppAuditHelper.fillUpdate(data);
+        dataMapper.updateById(data);
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> submitAudit(Long id) {
+        VppReportRecord record = requireRecord(id);
+        if (record.getRecordStatus() != VppReportHelper.RECORD_STATUS_DRAFT
+                && record.getRecordStatus() != VppReportHelper.RECORD_STATUS_FAILED) {
+            throw new BusinessException("仅草稿或失败状态可提交审核");
+        }
+        VppReportData data = requireReportData(record.getId());
+        if (data.getIsDraft() != null && data.getIsDraft() == 1) {
+            throw new BusinessException("请先定稿后再提交审核");
+        }
+        ReportValidateResultVO validation = validateData(id);
+        if (!validation.isValid()) {
+            throw new BusinessException("填报数据校验未通过,请先修正");
+        }
+
+        record.setRecordStatus(VppReportHelper.RECORD_STATUS_PENDING_AUDIT);
+        VppAuditHelper.fillUpdate(record);
+        recordMapper.updateById(record);
+
+        VppReportTask task = requireTask(record.getTaskId());
+        if (task.getTaskStatus() == VppReportHelper.TASK_STATUS_PENDING_FILL) {
+            task.setTaskStatus(VppReportHelper.TASK_STATUS_PENDING_AUDIT);
+            VppAuditHelper.fillUpdate(task);
+            taskMapper.updateById(task);
+        }
+
+        saveAuditLog(record.getId(), 1, VppReportHelper.AUDIT_ACTION_SUBMIT, "提交审核");
+
+        Map<String, Object> result = new HashMap<>(2);
+        result.put("recordId", record.getId());
+        result.put("recordStatus", record.getRecordStatus());
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void audit(Long id, ReportAuditRequest request) {
+        if (request == null || request.getPassed() == null) {
+            throw new BusinessException("审核结果不能为空");
+        }
+        if (request.getAuditLevel() == null) {
+            throw new BusinessException("auditLevel 不能为空");
+        }
+        VppReportRecord record = requireRecord(id);
+        if (record.getRecordStatus() != VppReportHelper.RECORD_STATUS_PENDING_AUDIT) {
+            throw new BusinessException("仅待审核状态可审核");
+        }
+
+        int action = Boolean.TRUE.equals(request.getPassed())
+                ? VppReportHelper.AUDIT_ACTION_PASS
+                : VppReportHelper.AUDIT_ACTION_REJECT;
+        saveAuditLog(record.getId(), request.getAuditLevel(), action, request.getOpinion());
+
+        if (Boolean.TRUE.equals(request.getPassed())) {
+            return;
+        }
+        record.setRecordStatus(VppReportHelper.RECORD_STATUS_DRAFT);
+        VppAuditHelper.fillUpdate(record);
+        recordMapper.updateById(record);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public ReportRecordVO submit(Long id) {
+        VppReportRecord record = requireRecord(id);
+        if (record.getRecordStatus() != VppReportHelper.RECORD_STATUS_PENDING_AUDIT) {
+            throw new BusinessException("仅待审核且已通过审批的记录可执行报送");
+        }
+        if (!hasAuditPass(record.getId())) {
+            throw new BusinessException("请先完成审核通过后再报送");
+        }
+        requireReportData(record.getId());
+
+        int retryMax = reportProperties.getRetryMax() != null ? reportProperties.getRetryMax() : 3;
+        int retryCount = record.getRetryCount() != null ? record.getRetryCount() : 0;
+        if (retryCount >= retryMax) {
+            throw new BusinessException("已达最大重试次数");
+        }
+
+        record.setRecordStatus(VppReportHelper.RECORD_STATUS_SUBMITTING);
+        VppAuditHelper.fillUpdate(record);
+        recordMapper.updateById(record);
+
+        VppReportTask task = requireTask(record.getTaskId());
+        task.setTaskStatus(VppReportHelper.TASK_STATUS_SUBMITTING);
+        VppAuditHelper.fillUpdate(task);
+        taskMapper.updateById(task);
+
+        boolean success = executeExternalSubmit(record, task);
+        LocalDateTime now = LocalDateTime.now();
+        if (success) {
+            record.setRecordStatus(VppReportHelper.RECORD_STATUS_SUCCESS);
+            record.setSubmittedAt(now);
+            record.setReportFileUrl("/report/files/" + record.getId() + ".xlsx");
+            record.setReceiptFileUrl("/report/receipts/" + record.getId() + ".pdf");
+            record.setArchivedAt(now);
+            task.setTaskStatus(VppReportHelper.TASK_STATUS_COMPLETED);
+        } else {
+            record.setRecordStatus(VppReportHelper.RECORD_STATUS_FAILED);
+            record.setRetryCount(retryCount + 1);
+        }
+        VppAuditHelper.fillUpdate(record);
+        recordMapper.updateById(record);
+        VppAuditHelper.fillUpdate(task);
+        taskMapper.updateById(task);
+
+        return toRecordVo(record, task, loadReportData(record.getId()), loadAuditLogs(record.getId()));
+    }
+
+    @Override
+    public CommonPage<ReportRecordVO> pageHistory(Map<String, Object> params) {
+        Page<VppReportRecord> page = VppPageHelper.of(params);
+        LambdaQueryWrapper<VppReportRecord> wrapper = activeRecordWrapper();
+        applyHistoryFilters(wrapper, params);
+        wrapper.orderByDesc(VppReportRecord::getSubmittedAt, VppReportRecord::getCreateTime);
+        Page<VppReportRecord> result = recordMapper.selectPage(page, wrapper);
+
+        Map<Long, VppReportTask> taskMap = loadTaskMap(result.getRecords());
+        List<ReportRecordVO> records = result.getRecords().stream()
+                .map(record -> toHistoryVo(record, taskMap.get(record.getTaskId())))
+                .collect(Collectors.toList());
+        return new CommonPage<>(records, result.getTotal(), result.getCurrent(), result.getSize());
+    }
+
+    private ReportPrefillVO buildPrefillData(VppReportRecord record, VppReportTask task) {
+        VppReportHelper.PeriodRange period = VppReportHelper.parseReportPeriod(record.getReportPeriod());
+        List<VppCustomer> customers = listActiveCustomers();
+        List<VppResourcePoint> resources = listActiveResources();
+        List<VppSite> sites = listActiveSites();
+        List<VppEnergySummaryDaily> energyRows = listEnergyRows(period);
+        List<VppDrEvent> drEvents = listDrEvents(period);
+        List<VppDrEvaluation> evaluations = listDrEvaluations(drEvents);
+        return VppReportHelper.buildPrefill(record, task, customers, resources, sites,
+                energyRows, drEvents, evaluations);
+    }
+
+    private boolean executeExternalSubmit(VppReportRecord record, VppReportTask task) {
+        VppReportLog log = new VppReportLog();
+        log.setReportType(task.getReportType());
+        log.setRequestId(record.getRecordNo());
+        log.setPayloadSize(0);
+        log.setReportedAt(LocalDateTime.now());
+        VppAuditHelper.fillCreate(log);
+        try {
+            log.setReportStatus(VppReportHelper.RECORD_STATUS_SUCCESS);
+            log.setResponseCode(200);
+            reportLogMapper.insert(log);
+            return true;
+        } catch (Exception ex) {
+            log.setReportStatus(VppReportHelper.RECORD_STATUS_FAILED);
+            log.setResponseCode(500);
+            log.setErrorMessage(ex.getMessage());
+            reportLogMapper.insert(log);
+            return false;
+        }
+    }
+
+    private boolean hasAuditPass(Long recordId) {
+        LambdaQueryWrapper<VppReportAuditLog> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppReportAuditLog::getRecordId, recordId)
+                .eq(VppReportAuditLog::getAction, VppReportHelper.AUDIT_ACTION_PASS)
+                .orderByDesc(VppReportAuditLog::getCreateTime)
+                .last("LIMIT 1");
+        return auditLogMapper.selectOne(wrapper) != null;
+    }
+
+    private void saveAuditLog(Long recordId, int auditLevel, int action, String opinion) {
+        VppReportAuditLog log = new VppReportAuditLog();
+        log.setRecordId(recordId);
+        log.setAuditLevel(auditLevel);
+        log.setAction(action);
+        log.setOpinion(opinion);
+        VppAuditHelper.fillCreate(log);
+        auditLogMapper.insert(log);
+    }
+
+    private void refreshOverdueTasks(List<VppReportTask> tasks) {
+        LocalDateTime now = LocalDateTime.now();
+        for (VppReportTask task : tasks) {
+            if (task.getDeadline() != null
+                    && task.getDeadline().isBefore(now)
+                    && task.getTaskStatus() != null
+                    && task.getTaskStatus() < VppReportHelper.TASK_STATUS_COMPLETED) {
+                task.setTaskStatus(VppReportHelper.TASK_STATUS_OVERDUE);
+                VppAuditHelper.fillUpdate(task);
+                taskMapper.updateById(task);
+            }
+        }
+    }
+
+    private ReportTaskVO toTaskVo(VppReportTask task) {
+        ReportTaskVO vo = new ReportTaskVO();
+        vo.setId(task.getId());
+        vo.setTaskNo(task.getTaskNo());
+        vo.setTaskName(task.getTaskName());
+        vo.setReportOrg(task.getReportOrg());
+        vo.setReportOrgName(VppReportHelper.resolveReportOrgName(task.getReportOrg()));
+        vo.setReportCycle(task.getReportCycle());
+        vo.setReportType(task.getReportType());
+        vo.setDeadline(task.getDeadline());
+        vo.setTaskStatus(task.getTaskStatus());
+        vo.setAssigneeId(task.getAssigneeId());
+        vo.setAssigneeName(task.getCreatedBy());
+        vo.setRemindDays(task.getRemindDays());
+        return vo;
+    }
+
+    private ReportRecordVO toRecordVo(VppReportRecord record,
+                                      VppReportTask task,
+                                      VppReportData data,
+                                      List<VppReportAuditLog> auditLogs) {
+        ReportRecordVO vo = new ReportRecordVO();
+        vo.setId(record.getId());
+        vo.setTaskId(record.getTaskId());
+        vo.setTaskName(task.getTaskName());
+        vo.setRecordNo(record.getRecordNo());
+        vo.setReportPeriod(record.getReportPeriod());
+        vo.setReportOrg(task.getReportOrg());
+        vo.setReportOrgName(VppReportHelper.resolveReportOrgName(task.getReportOrg()));
+        vo.setSubmitMode(record.getSubmitMode());
+        vo.setRecordStatus(record.getRecordStatus());
+        vo.setReportFileUrl(record.getReportFileUrl());
+        vo.setReceiptFileUrl(record.getReceiptFileUrl());
+        vo.setRetryCount(record.getRetryCount());
+        vo.setSubmittedAt(record.getSubmittedAt());
+        if (data != null) {
+            vo.setFormData(VppReportHelper.parseFormData(data.getFormData()));
+            vo.setIsDraft(data.getIsDraft() != null && data.getIsDraft() == 1);
+        } else {
+            vo.setIsDraft(true);
+        }
+        vo.setAuditLogs(auditLogs.stream().map(this::toAuditLogVo).collect(Collectors.toList()));
+        return vo;
+    }
+
+    private ReportRecordVO toHistoryVo(VppReportRecord record, VppReportTask task) {
+        ReportRecordVO vo = new ReportRecordVO();
+        vo.setId(record.getId());
+        vo.setRecordNo(record.getRecordNo());
+        vo.setReportPeriod(record.getReportPeriod());
+        vo.setRecordStatus(record.getRecordStatus());
+        vo.setSubmittedAt(record.getSubmittedAt());
+        vo.setReceiptFileUrl(record.getReceiptFileUrl());
+        if (task != null) {
+            vo.setTaskName(task.getTaskName());
+            vo.setReportOrg(task.getReportOrg());
+            vo.setReportOrgName(VppReportHelper.resolveReportOrgName(task.getReportOrg()));
+        }
+        return vo;
+    }
+
+    private ReportAuditLogVO toAuditLogVo(VppReportAuditLog log) {
+        ReportAuditLogVO vo = new ReportAuditLogVO();
+        vo.setId(log.getId());
+        vo.setAuditLevel(log.getAuditLevel());
+        vo.setAction(log.getAction());
+        vo.setOpinion(log.getOpinion());
+        vo.setOperatorName(log.getCreatedBy());
+        vo.setOperatedAt(log.getCreateTime());
+        return vo;
+    }
+
+    private Map<Long, VppReportTask> loadTaskMap(List<VppReportRecord> records) {
+        if (records == null || records.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        List<Long> taskIds = records.stream()
+                .map(VppReportRecord::getTaskId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+        if (taskIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        LambdaQueryWrapper<VppReportTask> wrapper = activeTaskWrapper();
+        wrapper.in(VppReportTask::getId, taskIds);
+        return taskMapper.selectList(wrapper).stream()
+                .collect(Collectors.toMap(VppReportTask::getId, task -> task, (a, b) -> a));
+    }
+
+    private List<VppReportAuditLog> loadAuditLogs(Long recordId) {
+        LambdaQueryWrapper<VppReportAuditLog> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppReportAuditLog::getRecordId, recordId)
+                .orderByAsc(VppReportAuditLog::getCreateTime);
+        return auditLogMapper.selectList(wrapper);
+    }
+
+    private VppReportData loadReportData(Long recordId) {
+        LambdaQueryWrapper<VppReportData> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppReportData::getRecordId, recordId)
+                .eq(VppReportData::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .last("LIMIT 1");
+        return dataMapper.selectOne(wrapper);
+    }
+
+    private VppReportData requireReportData(Long recordId) {
+        VppReportData data = loadReportData(recordId);
+        if (data == null) {
+            throw new BusinessException("请先保存填报数据");
+        }
+        return data;
+    }
+
+    private VppReportRecord requireRecord(Long id) {
+        if (id == null) {
+            throw new BusinessException("报送记录ID不能为空");
+        }
+        LambdaQueryWrapper<VppReportRecord> wrapper = activeRecordWrapper();
+        wrapper.eq(VppReportRecord::getId, id);
+        VppReportRecord record = recordMapper.selectOne(wrapper);
+        if (record == null) {
+            throw new BusinessException("报送记录不存在");
+        }
+        return record;
+    }
+
+    private VppReportTask requireTask(Long taskId) {
+        if (taskId == null) {
+            throw new BusinessException("报送任务不存在");
+        }
+        LambdaQueryWrapper<VppReportTask> wrapper = activeTaskWrapper();
+        wrapper.eq(VppReportTask::getId, taskId);
+        VppReportTask task = taskMapper.selectOne(wrapper);
+        if (task == null) {
+            throw new BusinessException("报送任务不存在");
+        }
+        return task;
+    }
+
+    private LambdaQueryWrapper<VppReportTask> activeTaskWrapper() {
+        LambdaQueryWrapper<VppReportTask> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppReportTask::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+        return wrapper;
+    }
+
+    private LambdaQueryWrapper<VppReportRecord> activeRecordWrapper() {
+        LambdaQueryWrapper<VppReportRecord> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppReportRecord::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+        return wrapper;
+    }
+
+    private void applyTaskFilters(LambdaQueryWrapper<VppReportTask> wrapper, Map<String, Object> params) {
+        if (params == null) {
+            return;
+        }
+        Integer taskStatus = parseInt(params.get("taskStatus"));
+        if (taskStatus != null) {
+            wrapper.eq(VppReportTask::getTaskStatus, taskStatus);
+        }
+        Integer reportOrg = parseInt(params.get("reportOrg"));
+        if (reportOrg != null) {
+            wrapper.eq(VppReportTask::getReportOrg, reportOrg);
+        }
+        Integer reportCycle = parseInt(params.get("reportCycle"));
+        if (reportCycle != null) {
+            wrapper.eq(VppReportTask::getReportCycle, reportCycle);
+        }
+        LocalDateTime deadlineStart = parseDateTime(params.get("deadlineStart"));
+        if (deadlineStart != null) {
+            wrapper.ge(VppReportTask::getDeadline, deadlineStart);
+        }
+        LocalDateTime deadlineEnd = parseDateTime(params.get("deadlineEnd"));
+        if (deadlineEnd != null) {
+            wrapper.le(VppReportTask::getDeadline, deadlineEnd);
+        }
+    }
+
+    private void applyHistoryFilters(LambdaQueryWrapper<VppReportRecord> wrapper, Map<String, Object> params) {
+        if (params == null) {
+            return;
+        }
+        Integer recordStatus = parseInt(params.get("recordStatus"));
+        if (recordStatus != null) {
+            wrapper.eq(VppReportRecord::getRecordStatus, recordStatus);
+        }
+        String reportPeriod = parseString(params.get("reportPeriod"));
+        if (StringUtils.hasText(reportPeriod)) {
+            wrapper.eq(VppReportRecord::getReportPeriod, reportPeriod);
+        }
+        Integer reportOrg = parseInt(params.get("reportOrg"));
+        if (reportOrg != null) {
+            List<Long> taskIds = listTaskIdsByOrg(reportOrg);
+            if (taskIds.isEmpty()) {
+                wrapper.eq(VppReportRecord::getTaskId, -1L);
+            } else {
+                wrapper.in(VppReportRecord::getTaskId, taskIds);
+            }
+        }
+    }
+
+    private List<Long> listTaskIdsByOrg(Integer reportOrg) {
+        LambdaQueryWrapper<VppReportTask> wrapper = activeTaskWrapper();
+        wrapper.eq(VppReportTask::getReportOrg, reportOrg)
+                .select(VppReportTask::getId);
+        return taskMapper.selectList(wrapper).stream()
+                .map(VppReportTask::getId)
+                .collect(Collectors.toList());
+    }
+
+    private List<VppCustomer> listActiveCustomers() {
+        LambdaQueryWrapper<VppCustomer> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+        return customerMapper.selectList(wrapper);
+    }
+
+    private List<VppResourcePoint> listActiveResources() {
+        LambdaQueryWrapper<VppResourcePoint> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+        return resourcePointMapper.selectList(wrapper);
+    }
+
+    private List<VppSite> listActiveSites() {
+        LambdaQueryWrapper<VppSite> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+        return siteMapper.selectList(wrapper);
+    }
+
+    private int countResourceType(String resourceType) {
+        LambdaQueryWrapper<VppResourcePoint> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(VppResourcePoint::getResourceType, resourceType);
+        return resourcePointMapper.selectCount(wrapper).intValue();
+    }
+
+    private List<VppEnergySummaryDaily> listEnergyRows(VppReportHelper.PeriodRange period) {
+        if (period == null) {
+            return Collections.emptyList();
+        }
+        LambdaQueryWrapper<VppEnergySummaryDaily> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppEnergySummaryDaily::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .ge(VppEnergySummaryDaily::getSummaryDate, period.getStart())
+                .le(VppEnergySummaryDaily::getSummaryDate, period.getEnd());
+        return energySummaryDailyMapper.selectList(wrapper);
+    }
+
+    private List<VppDrEvent> listDrEvents(VppReportHelper.PeriodRange period) {
+        if (period == null) {
+            return Collections.emptyList();
+        }
+        LocalDateTime start = period.getStart().atStartOfDay();
+        LocalDateTime end = period.getEnd().atTime(23, 59, 59);
+        LambdaQueryWrapper<VppDrEvent> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppDrEvent::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .ge(VppDrEvent::getStartTime, start)
+                .le(VppDrEvent::getStartTime, end);
+        return drEventMapper.selectList(wrapper);
+    }
+
+    private List<VppDrEvaluation> listDrEvaluations(List<VppDrEvent> events) {
+        if (events == null || events.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<Long> eventIds = events.stream().map(VppDrEvent::getId).collect(Collectors.toList());
+        LambdaQueryWrapper<VppDrEvaluation> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppDrEvaluation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .in(VppDrEvaluation::getEventId, eventIds);
+        return drEvaluationMapper.selectList(wrapper);
+    }
+
+    private Integer parseInt(Object value) {
+        if (value == null || !StringUtils.hasText(value.toString())) {
+            return null;
+        }
+        return Integer.parseInt(value.toString());
+    }
+
+    private String parseString(Object value) {
+        return value == null ? null : value.toString();
+    }
+
+    private LocalDateTime parseDateTime(Object value) {
+        if (value == null || !StringUtils.hasText(value.toString())) {
+            return null;
+        }
+        String text = value.toString();
+        try {
+            return LocalDateTime.parse(text, DEADLINE_FMT);
+        } catch (DateTimeParseException ex) {
+            try {
+                return LocalDate.parse(text).atStartOfDay();
+            } catch (DateTimeParseException ignored) {
+                return null;
+            }
+        }
+    }
+
+    private void validateCreateTaskRequest(ReportTaskCreateRequest request) {
+        if (request == null) {
+            throw new BusinessException("请求体不能为空");
+        }
+        if (!StringUtils.hasText(request.getTaskName())) {
+            throw new BusinessException("任务名称不能为空");
+        }
+        if (request.getReportOrg() == null || request.getReportOrg() < 1 || request.getReportOrg() > 4) {
+            throw new BusinessException("报送机构无效,取值 1~4");
+        }
+        if (request.getReportCycle() == null || request.getReportCycle() < 1 || request.getReportCycle() > 3) {
+            throw new BusinessException("报送周期无效,取值 1~3");
+        }
+        if (!StringUtils.hasText(request.getReportType())) {
+            throw new BusinessException("报表类型编码不能为空");
+        }
+        if (request.getDeadline() == null) {
+            throw new BusinessException("截止日期不能为空");
+        }
+        if (request.getSubmitMode() != null
+                && request.getSubmitMode() != VppReportHelper.SUBMIT_MODE_API
+                && request.getSubmitMode() != VppReportHelper.SUBMIT_MODE_MANUAL) {
+            throw new BusinessException("报送方式无效,取值 1 或 2");
+        }
+        String reportPeriod = resolveReportPeriod(request);
+        if (request.getReportCycle() == 1 && !reportPeriod.matches("\\d{4}-\\d{2}")) {
+            throw new BusinessException("月报报送周期须为 YYYY-MM 格式");
+        }
+        if (request.getReportCycle() == 2 && !reportPeriod.matches("\\d{4}")) {
+            throw new BusinessException("年报报送周期须为 YYYY 格式");
+        }
+    }
+
+    private String resolveReportPeriod(ReportTaskCreateRequest request) {
+        if (StringUtils.hasText(request.getReportPeriod())) {
+            return request.getReportPeriod().trim();
+        }
+        if (request.getDeadline() != null) {
+            if (request.getReportCycle() != null && request.getReportCycle() == 2) {
+                return String.valueOf(request.getDeadline().getYear());
+            }
+            return String.format("%04d-%02d", request.getDeadline().getYear(), request.getDeadline().getMonthValue());
+        }
+        throw new BusinessException("报送周期标识不能为空");
+    }
+
+    private void ensureTaskNoUnique(String taskNo) {
+        LambdaQueryWrapper<VppReportTask> wrapper = activeTaskWrapper();
+        wrapper.eq(VppReportTask::getTaskNo, taskNo);
+        if (taskMapper.selectCount(wrapper) > 0) {
+            throw new BusinessException("任务编号冲突,请重试");
+        }
+    }
+
+    private void ensureRecordNoUnique(String recordNo) {
+        LambdaQueryWrapper<VppReportRecord> wrapper = activeRecordWrapper();
+        wrapper.eq(VppReportRecord::getRecordNo, recordNo);
+        if (recordMapper.selectCount(wrapper) > 0) {
+            throw new BusinessException("报送记录编号冲突,请重试");
+        }
+    }
 }

+ 423 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResponseMonitorServiceImpl.java

@@ -0,0 +1,423 @@
+package com.usky.vpp.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.vpp.constant.VppTsdbConstants;
+import com.usky.vpp.domain.VppCustomer;
+import com.usky.vpp.domain.VppDevice;
+import com.usky.vpp.domain.VppDrEvent;
+import com.usky.vpp.domain.VppDrExecution;
+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.VppDrEventMapper;
+import com.usky.vpp.mapper.VppDrExecutionMapper;
+import com.usky.vpp.mapper.VppDrParticipationMapper;
+import com.usky.vpp.mapper.VppResourcePointMapper;
+import com.usky.vpp.mapper.VppSiteMapper;
+import com.usky.vpp.service.VppResponseMonitorService;
+import com.usky.vpp.service.VppTsdbQueryService;
+import com.usky.vpp.service.vo.ResponseCompareSeriesVO;
+import com.usky.vpp.service.vo.ResponseCompareVO;
+import com.usky.vpp.service.vo.ResponseLoadAnalysisVO;
+import com.usky.vpp.service.vo.ResponseLoadPointVO;
+import com.usky.vpp.service.vo.ResponseLoadStatRowVO;
+import com.usky.vpp.service.vo.ResponseMonitorTreeNodeVO;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppCapabilityEvalHelper;
+import com.usky.vpp.util.VppResponseMonitorHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+@Service
+public class VppResponseMonitorServiceImpl implements VppResponseMonitorService {
+
+    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+    @Autowired
+    private VppSiteMapper siteMapper;
+    @Autowired
+    private VppCustomerMapper customerMapper;
+    @Autowired
+    private VppResourcePointMapper resourcePointMapper;
+    @Autowired
+    private VppDeviceMapper deviceMapper;
+    @Autowired
+    private VppDrEventMapper eventMapper;
+    @Autowired
+    private VppDrParticipationMapper participationMapper;
+    @Autowired
+    private VppDrExecutionMapper executionMapper;
+    @Autowired
+    private VppTsdbQueryService tsdbQueryService;
+
+    @Override
+    public List<ResponseMonitorTreeNodeVO> getResourceTree(String keyword) {
+        List<VppSite> sites = siteMapper.selectList(new LambdaQueryWrapper<VppSite>()
+                .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .orderByAsc(VppSite::getSiteName));
+        if (sites.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        Set<Long> customerIds = sites.stream()
+                .map(VppSite::getCustomerId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        Map<Long, VppCustomer> customerMap = customerIds.isEmpty()
+                ? Collections.emptyMap()
+                : customerMapper.selectBatchIds(customerIds).stream()
+                .filter(c -> !VppAuditHelper.isDeleted(c.getDeleteFlag()))
+                .collect(Collectors.toMap(VppCustomer::getId, c -> c, (a, b) -> a));
+
+        Map<Long, List<VppSite>> sitesByCustomer = new LinkedHashMap<>();
+        List<VppSite> orphanSites = new ArrayList<>();
+        for (VppSite site : sites) {
+            if (site.getCustomerId() == null) {
+                orphanSites.add(site);
+                continue;
+            }
+            sitesByCustomer.computeIfAbsent(site.getCustomerId(), k -> new ArrayList<>()).add(site);
+        }
+
+        List<ResponseMonitorTreeNodeVO> tree = new ArrayList<>();
+        for (Map.Entry<Long, List<VppSite>> entry : sitesByCustomer.entrySet()) {
+            VppCustomer customer = customerMap.get(entry.getKey());
+            String customerName = customer != null ? customer.getCustomerName() : "未命名客户";
+            if (!matchesKeyword(customerName, keyword) && !anySiteMatches(entry.getValue(), keyword)) {
+                continue;
+            }
+            ResponseMonitorTreeNodeVO parent = new ResponseMonitorTreeNodeVO();
+            parent.setNodeType("CUSTOMER");
+            parent.setId(entry.getKey());
+            parent.setName(customerName);
+            for (VppSite site : entry.getValue()) {
+                if (!matchesKeyword(site.getSiteName(), keyword) && !matchesKeyword(customerName, keyword)) {
+                    continue;
+                }
+                parent.getChildren().add(toSiteNode(site, entry.getKey()));
+            }
+            if (!parent.getChildren().isEmpty()) {
+                tree.add(parent);
+            }
+        }
+
+        for (VppSite site : orphanSites) {
+            if (!matchesKeyword(site.getSiteName(), keyword)) {
+                continue;
+            }
+            ResponseMonitorTreeNodeVO node = toSiteNode(site, null);
+            node.setNodeType("SITE");
+            tree.add(node);
+        }
+        return tree;
+    }
+
+    @Override
+    public ResponseLoadAnalysisVO getLoadAnalysis(Long siteId, String date, Long eventId) {
+        if (siteId == null) {
+            throw new BusinessException("请选择站点");
+        }
+        VppSite site = requireSite(siteId);
+        LocalDate statDate = parseDate(date);
+
+        ResponseLoadAnalysisVO result = new ResponseLoadAnalysisVO();
+        result.setSiteId(siteId);
+        result.setSiteName(site.getSiteName());
+        result.setDate(statDate.format(DATE_FMT));
+
+        List<VppResourcePoint> resources = listSiteResources(siteId);
+        List<VppDevice> devices = loadDevices(resources);
+        DrContext drContext = resolveDrContext(siteId, statDate, eventId);
+        result.setEventId(drContext.event != null ? drContext.event.getId() : null);
+        result.setEventName(drContext.event != null ? drContext.event.getEventName() : null);
+
+        BigDecimal declared = VppResponseMonitorHelper.resolveDeclaredCapacity(
+                drContext.event, drContext.participations, drContext.executions);
+
+        CurveBuildResult curve = buildCurve(resources, devices, siteId, statDate, declared, drContext);
+        result.setPoints(curve.points);
+        result.setDataSource(curve.dataSource);
+        result.setStatistics(VppResponseMonitorHelper.buildStatistics(curve.points, statDate));
+        return result;
+    }
+
+    @Override
+    public ResponseCompareVO compare(String siteIds, String date) {
+        List<Long> ids = parseSiteIds(siteIds);
+        if (ids.isEmpty()) {
+            throw new BusinessException("请至少选择一个站点");
+        }
+        LocalDate statDate = parseDate(date);
+        ResponseCompareVO result = new ResponseCompareVO();
+        result.setDate(statDate.format(DATE_FMT));
+        String dataSource = "mock";
+
+        for (Long siteId : ids) {
+            VppSite site = requireSite(siteId);
+            List<VppResourcePoint> resources = listSiteResources(siteId);
+            List<VppDevice> devices = loadDevices(resources);
+            CurveBuildResult curve = buildCurve(resources, devices, siteId, statDate, BigDecimal.ZERO, DrContext.empty());
+            if ("tsdb".equals(curve.dataSource)) {
+                dataSource = "tsdb";
+            }
+
+            ResponseCompareSeriesVO series = new ResponseCompareSeriesVO();
+            series.setSiteId(siteId);
+            series.setSiteName(site.getSiteName());
+            for (ResponseLoadPointVO point : curve.points) {
+                ResponseCompareSeriesVO.ResponseComparePointVO item =
+                        new ResponseCompareSeriesVO.ResponseComparePointVO();
+                item.setTime(point.getDatetime());
+                item.setValue(point.getActualLoadKw());
+                series.getPoints().add(item);
+            }
+            result.getSeries().add(series);
+        }
+        result.setDataSource(dataSource);
+        return result;
+    }
+
+    private CurveBuildResult buildCurve(List<VppResourcePoint> resources,
+                                        List<VppDevice> devices,
+                                        Long siteId,
+                                        LocalDate statDate,
+                                        BigDecimal declaredCapacityKw,
+                                        DrContext drContext) {
+        List<String> deviceUuids = resolveDeviceUuids(resources, devices);
+        if (!deviceUuids.isEmpty()) {
+            LocalDateTime dayStart = statDate.atStartOfDay();
+            LocalDateTime dayEnd = statDate.atTime(23, 59, 59);
+            LocalDate baselineDate = statDate.minusDays(VppResponseMonitorHelper.BASELINE_DAYS_AGO);
+            List<String> powerMetrics = new ArrayList<>(VppTsdbConstants.ACTIVE_POWER_METRICS);
+            Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> actualData =
+                    tsdbQueryService.queryDeviceMetricHistory(deviceUuids, dayStart, dayEnd, powerMetrics);
+            if (VppCapabilityEvalHelper.hasTsdbPowerData(actualData, powerMetrics)) {
+                Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> baselineData =
+                        tsdbQueryService.queryDeviceMetricHistory(deviceUuids,
+                                baselineDate.atStartOfDay(), baselineDate.atTime(23, 59, 59), powerMetrics);
+                List<ResponseLoadPointVO> points = VppResponseMonitorHelper.buildCurveFromTsdb(
+                        actualData, baselineData, statDate, powerMetrics, declaredCapacityKw,
+                        drContext.executionByTime);
+                return new CurveBuildResult(points, "tsdb");
+            }
+        }
+        return new CurveBuildResult(
+                VppResponseMonitorHelper.buildMockCurve(resources, devices, siteId, statDate, declaredCapacityKw),
+                "mock");
+    }
+
+    private DrContext resolveDrContext(Long siteId, LocalDate statDate, Long eventId) {
+        Set<Long> resourceIds = listSiteResources(siteId).stream()
+                .map(VppResourcePoint::getId)
+                .collect(Collectors.toSet());
+        if (resourceIds.isEmpty()) {
+            return DrContext.empty();
+        }
+
+        VppDrEvent event;
+        if (eventId != null) {
+            event = eventMapper.selectById(eventId);
+            if (event == null || VppAuditHelper.isDeleted(event.getDeleteFlag())) {
+                throw new BusinessException("需求响应事件不存在");
+            }
+        } else {
+            LocalDateTime dayStart = statDate.atStartOfDay();
+            LocalDateTime dayEnd = statDate.atTime(23, 59, 59);
+            event = eventMapper.selectList(new LambdaQueryWrapper<VppDrEvent>()
+                            .eq(VppDrEvent::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                            .ge(VppDrEvent::getStartTime, dayStart)
+                            .le(VppDrEvent::getStartTime, dayEnd)
+                            .orderByDesc(VppDrEvent::getStartTime)
+                            .last("LIMIT 1"))
+                    .stream().findFirst().orElse(null);
+        }
+        if (event == null) {
+            return DrContext.empty();
+        }
+
+        List<VppDrParticipation> participations = participationMapper.selectList(
+                new LambdaQueryWrapper<VppDrParticipation>()
+                        .eq(VppDrParticipation::getEventId, event.getId())
+                        .and(w -> {
+                            if (!resourceIds.isEmpty()) {
+                                w.in(VppDrParticipation::getResourceId, resourceIds);
+                            }
+                            VppSite site = siteMapper.selectById(siteId);
+                            if (site != null && site.getCustomerId() != null) {
+                                if (!resourceIds.isEmpty()) {
+                                    w.or().eq(VppDrParticipation::getCustomerId, site.getCustomerId());
+                                } else {
+                                    w.eq(VppDrParticipation::getCustomerId, site.getCustomerId());
+                                }
+                            }
+                        })
+                        .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        List<VppDrExecution> executions = executionMapper.selectList(
+                new LambdaQueryWrapper<VppDrExecution>()
+                        .eq(VppDrExecution::getEventId, event.getId())
+                        .in(VppDrExecution::getResourceId, resourceIds)
+                        .eq(VppDrExecution::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+
+        Map<LocalDateTime, BigDecimal> executionByTime = new HashMap<>();
+        for (VppDrExecution execution : executions) {
+            if (execution.getActualKw() == null) {
+                continue;
+            }
+            LocalDateTime key = execution.getStartedAt() != null
+                    ? alignBucket(execution.getStartedAt())
+                    : statDate.atStartOfDay();
+            executionByTime.merge(key, execution.getActualKw(), BigDecimal::add);
+        }
+        return new DrContext(event, participations, executions, executionByTime);
+    }
+
+    private static LocalDateTime alignBucket(LocalDateTime time) {
+        int minute = (time.getMinute() / VppResponseMonitorHelper.BUCKET_MINUTES)
+                * VppResponseMonitorHelper.BUCKET_MINUTES;
+        return time.withMinute(minute).withSecond(0).withNano(0);
+    }
+
+    private ResponseMonitorTreeNodeVO toSiteNode(VppSite site, Long parentId) {
+        ResponseMonitorTreeNodeVO node = new ResponseMonitorTreeNodeVO();
+        node.setNodeType("SITE");
+        node.setId(site.getId());
+        node.setName(site.getSiteName());
+        node.setParentId(parentId);
+        return node;
+    }
+
+    private boolean anySiteMatches(List<VppSite> sites, String keyword) {
+        if (!StringUtils.hasText(keyword)) {
+            return true;
+        }
+        return sites.stream().anyMatch(s -> matchesKeyword(s.getSiteName(), keyword));
+    }
+
+    private boolean matchesKeyword(String text, String keyword) {
+        return !StringUtils.hasText(keyword)
+                || (text != null && text.toLowerCase().contains(keyword.toLowerCase()));
+    }
+
+    private VppSite requireSite(Long siteId) {
+        VppSite site = siteMapper.selectById(siteId);
+        if (site == null || VppAuditHelper.isDeleted(site.getDeleteFlag())) {
+            throw new BusinessException("站点不存在");
+        }
+        return site;
+    }
+
+    private List<VppResourcePoint> listSiteResources(Long siteId) {
+        return resourcePointMapper.selectList(new LambdaQueryWrapper<VppResourcePoint>()
+                .eq(VppResourcePoint::getSiteId, siteId)
+                .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+    }
+
+    private List<VppDevice> loadDevices(List<VppResourcePoint> resources) {
+        Set<Long> deviceIds = resources.stream()
+                .map(VppResourcePoint::getDeviceId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        if (deviceIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return deviceMapper.selectBatchIds(deviceIds).stream()
+                .filter(d -> !VppAuditHelper.isDeleted(d.getDeleteFlag()))
+                .collect(Collectors.toList());
+    }
+
+    private List<String> resolveDeviceUuids(List<VppResourcePoint> resources, List<VppDevice> devices) {
+        if (resources.isEmpty() || devices.isEmpty()) {
+            return Collections.emptyList();
+        }
+        Map<Long, VppDevice> deviceMap = devices.stream()
+                .collect(Collectors.toMap(VppDevice::getId, d -> d, (a, b) -> a));
+        List<String> uuids = new ArrayList<>();
+        for (VppResourcePoint resource : resources) {
+            if (resource.getDeviceId() == null) {
+                continue;
+            }
+            VppDevice device = deviceMap.get(resource.getDeviceId());
+            if (device != null && StringUtils.hasText(device.getDeviceUuid())) {
+                uuids.add(device.getDeviceUuid());
+            }
+        }
+        return uuids;
+    }
+
+    private LocalDate parseDate(String date) {
+        if (!StringUtils.hasText(date)) {
+            return LocalDate.now();
+        }
+        try {
+            return LocalDate.parse(date, DATE_FMT);
+        } catch (DateTimeParseException ex) {
+            throw new BusinessException("日期格式错误,请使用 yyyy-MM-dd");
+        }
+    }
+
+    private List<Long> parseSiteIds(String siteIds) {
+        if (!StringUtils.hasText(siteIds)) {
+            return Collections.emptyList();
+        }
+        return Arrays.stream(siteIds.split(","))
+                .map(String::trim)
+                .filter(StringUtils::hasText)
+                .map(Long::parseLong)
+                .distinct()
+                .sorted(Comparator.naturalOrder())
+                .collect(Collectors.toList());
+    }
+
+    private static class CurveBuildResult {
+        private final List<ResponseLoadPointVO> points;
+        private final String dataSource;
+
+        private CurveBuildResult(List<ResponseLoadPointVO> points, String dataSource) {
+            this.points = points;
+            this.dataSource = dataSource;
+        }
+    }
+
+    private static class DrContext {
+        private final VppDrEvent event;
+        private final List<VppDrParticipation> participations;
+        private final List<VppDrExecution> executions;
+        private final Map<LocalDateTime, BigDecimal> executionByTime;
+
+        private DrContext(VppDrEvent event,
+                          List<VppDrParticipation> participations,
+                          List<VppDrExecution> executions,
+                          Map<LocalDateTime, BigDecimal> executionByTime) {
+            this.event = event;
+            this.participations = participations;
+            this.executions = executions;
+            this.executionByTime = executionByTime;
+        }
+
+        private static DrContext empty() {
+            return new DrContext(null, Collections.emptyList(), Collections.emptyList(), Collections.emptyMap());
+        }
+    }
+}

+ 27 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrEventDetailVO.java

@@ -0,0 +1,27 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 需求响应事件详情
+ */
+@Data
+public class DrEventDetailVO {
+
+    private Long id;
+    private String eventId;
+    private String eventName;
+    private Integer responseType;
+    private Integer eventType;
+    private LocalDateTime startTime;
+    private LocalDateTime endTime;
+    private BigDecimal targetCapacityKw;
+    private BigDecimal clearedCapacityKw;
+    private BigDecimal subsidyPrice;
+    private Integer eventStatus;
+    private List<DrParticipationVO> participations;
+}

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

@@ -0,0 +1,23 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 需求响应参与记录(事件详情嵌套)
+ */
+@Data
+public class DrParticipationVO {
+
+    private Long id;
+    private Long customerId;
+    private String customerName;
+    private Long resourceId;
+    private String resourceName;
+    private Integer participateStatus;
+    private BigDecimal declaredCapacityKw;
+    private BigDecimal clearedCapacityKw;
+    private LocalDateTime declaredAt;
+}

+ 16 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ReportAuditLogVO.java

@@ -0,0 +1,16 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class ReportAuditLogVO {
+
+    private Long id;
+    private Integer auditLevel;
+    private Integer action;
+    private String opinion;
+    private String operatorName;
+    private LocalDateTime operatedAt;
+}

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

@@ -0,0 +1,11 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+@Data
+public class ReportAuditRequest {
+
+    private Boolean passed;
+    private String opinion;
+    private Integer auditLevel;
+}

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

@@ -0,0 +1,12 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+public class ReportDataSaveRequest {
+
+    private Map<String, Object> formData;
+    private Boolean isDraft;
+}

+ 19 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ReportPrefillVO.java

@@ -0,0 +1,19 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Map;
+
+@Data
+public class ReportPrefillVO {
+
+    private String reportPeriod;
+    private Integer customerCount;
+    private BigDecimal totalCapacityKw;
+    private BigDecimal totalGenKwh;
+    private BigDecimal totalUseKwh;
+    private Integer drEventCount;
+    private BigDecimal drTotalSubsidy;
+    private Map<String, Object> formData;
+}

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

@@ -0,0 +1,28 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class ReportRecordVO {
+
+    private Long id;
+    private Long taskId;
+    private String taskName;
+    private String recordNo;
+    private String reportPeriod;
+    private Integer reportOrg;
+    private String reportOrgName;
+    private Integer submitMode;
+    private Integer recordStatus;
+    private String reportFileUrl;
+    private String receiptFileUrl;
+    private Integer retryCount;
+    private LocalDateTime submittedAt;
+    private List<ReportAuditLogVO> auditLogs;
+    private Map<String, Object> formData;
+    private Boolean isDraft;
+}

+ 24 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ReportTaskCreateRequest.java

@@ -0,0 +1,24 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 新增报送任务请求
+ */
+@Data
+public class ReportTaskCreateRequest {
+
+    private String taskName;
+    private Integer reportOrg;
+    private Integer reportCycle;
+    private String reportType;
+    private LocalDateTime deadline;
+    private Long assigneeId;
+    private Integer remindDays;
+    /** 报送周期标识,如 2026-05 */
+    private String reportPeriod;
+    /** 报送方式:1接口推送 2手动上传,默认 1 */
+    private Integer submitMode;
+}

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

@@ -0,0 +1,18 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+/**
+ * 新增报送任务响应
+ */
+@Data
+public class ReportTaskCreateResultVO {
+
+    private Long taskId;
+    private String taskNo;
+    private Long recordId;
+    private String recordNo;
+    private String reportPeriod;
+    private Integer taskStatus;
+    private Integer recordStatus;
+}

+ 22 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ReportTaskVO.java

@@ -0,0 +1,22 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class ReportTaskVO {
+
+    private Long id;
+    private String taskNo;
+    private String taskName;
+    private Integer reportOrg;
+    private String reportOrgName;
+    private Integer reportCycle;
+    private String reportType;
+    private LocalDateTime deadline;
+    private Integer taskStatus;
+    private Long assigneeId;
+    private String assigneeName;
+    private Integer remindDays;
+}

+ 14 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ReportValidateErrorVO.java

@@ -0,0 +1,14 @@
+package com.usky.vpp.service.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ReportValidateErrorVO {
+
+    private String field;
+    private String message;
+}

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

@@ -0,0 +1,13 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class ReportValidateResultVO {
+
+    private boolean valid;
+    private List<ReportValidateErrorVO> errors = new ArrayList<>();
+}

+ 24 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResponseCompareSeriesVO.java

@@ -0,0 +1,24 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 对比分析序列
+ */
+@Data
+public class ResponseCompareSeriesVO {
+
+    private Long siteId;
+    private String siteName;
+    private List<ResponseComparePointVO> points = new ArrayList<>();
+
+    @Data
+    public static class ResponseComparePointVO {
+        private String time;
+        private BigDecimal value;
+    }
+}

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

@@ -0,0 +1,18 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 对比分析(响应监控 - 对比分析 Tab)
+ */
+@Data
+public class ResponseCompareVO {
+
+    private String date;
+    private String granularity = "15m";
+    private String dataSource;
+    private List<ResponseCompareSeriesVO> series = new ArrayList<>();
+}

+ 24 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResponseLoadAnalysisVO.java

@@ -0,0 +1,24 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 负荷分析(响应监控 - 负荷分析 Tab)
+ */
+@Data
+public class ResponseLoadAnalysisVO {
+
+    private Long siteId;
+    private String siteName;
+    private String date;
+    private Long eventId;
+    private String eventName;
+    /** tsdb / mock */
+    private String dataSource;
+    private String granularity = "15m";
+    private List<ResponseLoadPointVO> points = new ArrayList<>();
+    private List<ResponseLoadStatRowVO> statistics = new ArrayList<>();
+}

+ 19 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResponseLoadPointVO.java

@@ -0,0 +1,19 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 响应监控负荷曲线点(15 分钟粒度)
+ */
+@Data
+public class ResponseLoadPointVO {
+
+    private String time;
+    private String datetime;
+    private BigDecimal baselineLoadKw;
+    private BigDecimal actualLoadKw;
+    private BigDecimal declaredCapacityKw;
+    private BigDecimal actualResponseLoadKw;
+}

+ 24 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResponseLoadStatRowVO.java

@@ -0,0 +1,24 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 负荷统计表行(实际/预测用电量)
+ */
+@Data
+public class ResponseLoadStatRowVO {
+
+    /** ACTUAL / PREDICTED */
+    private String dataType;
+    private String dataTypeLabel;
+    private BigDecimal total;
+    private BigDecimal max;
+    private BigDecimal min;
+    private BigDecimal avg;
+    /** 小时列 HH:00 -> kWh */
+    private Map<String, BigDecimal> hourlyValues = new LinkedHashMap<>();
+}

+ 20 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResponseMonitorTreeNodeVO.java

@@ -0,0 +1,20 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 响应监控资源树节点
+ */
+@Data
+public class ResponseMonitorTreeNodeVO {
+
+    /** CUSTOMER / SITE */
+    private String nodeType;
+    private Long id;
+    private String name;
+    private Long parentId;
+    private List<ResponseMonitorTreeNodeVO> children = new ArrayList<>();
+}

+ 297 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppReportHelper.java

@@ -0,0 +1,297 @@
+package com.usky.vpp.util;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.vpp.domain.VppCustomer;
+import com.usky.vpp.domain.VppDrEvaluation;
+import com.usky.vpp.domain.VppDrEvent;
+import com.usky.vpp.domain.VppEnergySummaryDaily;
+import com.usky.vpp.domain.VppReportRecord;
+import com.usky.vpp.domain.VppReportTask;
+import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.domain.VppSite;
+import com.usky.vpp.service.vo.ReportPrefillVO;
+import com.usky.vpp.service.vo.ReportValidateErrorVO;
+import com.usky.vpp.service.vo.ReportValidateResultVO;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 监管信息报送工具
+ */
+public final class VppReportHelper {
+
+    public static final int TASK_STATUS_PENDING_FILL = 0;
+    public static final int TASK_STATUS_PENDING_AUDIT = 1;
+    public static final int TASK_STATUS_SUBMITTING = 2;
+    public static final int TASK_STATUS_COMPLETED = 3;
+    public static final int TASK_STATUS_OVERDUE = 4;
+
+    public static final int RECORD_STATUS_DRAFT = 0;
+    public static final int RECORD_STATUS_PENDING_AUDIT = 1;
+    public static final int RECORD_STATUS_SUBMITTING = 2;
+    public static final int RECORD_STATUS_SUCCESS = 3;
+    public static final int RECORD_STATUS_FAILED = 4;
+
+    public static final int SUBMIT_MODE_API = 1;
+    public static final int SUBMIT_MODE_MANUAL = 2;
+
+    public static final int AUDIT_ACTION_SUBMIT = 1;
+    public static final int AUDIT_ACTION_PASS = 2;
+    public static final int AUDIT_ACTION_REJECT = 3;
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+    private static final DateTimeFormatter RECORD_NO_FMT = DateTimeFormatter.ofPattern("yyyyMM");
+
+    private VppReportHelper() {
+    }
+
+    public static String resolveReportOrgName(Integer reportOrg) {
+        if (reportOrg == null) {
+            return null;
+        }
+        switch (reportOrg) {
+            case 1:
+                return "交易中心";
+            case 2:
+                return "能监局";
+            case 3:
+                return "经信委";
+            case 4:
+                return "国网上海";
+            default:
+                return "未知机构";
+        }
+    }
+
+    public static String nextRecordNo(String reportPeriod) {
+        String period = StringUtils.hasText(reportPeriod) ? reportPeriod.replace("-", "") : RECORD_NO_FMT.format(LocalDate.now());
+        return "REC-" + period + "-" + System.currentTimeMillis() % 1000;
+    }
+
+    public static String nextTaskNo() {
+        return "RPT-" + RECORD_NO_FMT.format(LocalDate.now()) + "-" + System.currentTimeMillis() % 1000;
+    }
+
+    public static Map<String, Object> parseFormData(String json) {
+        if (!StringUtils.hasText(json)) {
+            return null;
+        }
+        try {
+            return OBJECT_MAPPER.readValue(json, new TypeReference<Map<String, Object>>() {
+            });
+        } catch (Exception ex) {
+            return null;
+        }
+    }
+
+    public static String toJson(Object value) {
+        if (value == null) {
+            return null;
+        }
+        try {
+            return OBJECT_MAPPER.writeValueAsString(value);
+        } catch (Exception ex) {
+            return null;
+        }
+    }
+
+    public static ReportPrefillVO buildPrefill(VppReportRecord record,
+                                             VppReportTask task,
+                                             List<VppCustomer> customers,
+                                             List<VppResourcePoint> resources,
+                                             List<VppSite> sites,
+                                             List<VppEnergySummaryDaily> energyRows,
+                                             List<VppDrEvent> drEvents,
+                                             List<VppDrEvaluation> evaluations) {
+        ReportPrefillVO prefill = new ReportPrefillVO();
+        prefill.setReportPeriod(record.getReportPeriod());
+
+        int customerCount = customers != null ? customers.size() : 0;
+        prefill.setCustomerCount(customerCount);
+
+        BigDecimal totalCapacity = BigDecimal.ZERO;
+        int pvCount = 0;
+        int essCount = 0;
+        if (resources != null) {
+            for (VppResourcePoint resource : resources) {
+                if (resource.getCapacityKw() != null) {
+                    totalCapacity = totalCapacity.add(resource.getCapacityKw());
+                }
+                if ("PV".equalsIgnoreCase(resource.getResourceType())) {
+                    pvCount++;
+                } else if ("ESS".equalsIgnoreCase(resource.getResourceType())) {
+                    essCount++;
+                }
+            }
+        }
+        prefill.setTotalCapacityKw(totalCapacity);
+
+        BigDecimal totalGen = BigDecimal.ZERO;
+        BigDecimal totalUse = BigDecimal.ZERO;
+        if (energyRows != null) {
+            for (VppEnergySummaryDaily row : energyRows) {
+                if (row.getGenEnergyKwh() != null) {
+                    totalGen = totalGen.add(row.getGenEnergyKwh());
+                }
+                if (row.getUseEnergyKwh() != null) {
+                    totalUse = totalUse.add(row.getUseEnergyKwh());
+                }
+            }
+        }
+        prefill.setTotalGenKwh(totalGen);
+        prefill.setTotalUseKwh(totalUse);
+        prefill.setDrEventCount(drEvents != null ? drEvents.size() : 0);
+
+        BigDecimal drSubsidy = BigDecimal.ZERO;
+        if (evaluations != null) {
+            for (VppDrEvaluation evaluation : evaluations) {
+                if (evaluation.getSubsidyAmount() != null) {
+                    drSubsidy = drSubsidy.add(evaluation.getSubsidyAmount());
+                }
+            }
+        }
+        prefill.setDrTotalSubsidy(drSubsidy);
+
+        String vppName = resolveVppName(sites, task);
+        Map<String, Object> formData = new LinkedHashMap<>();
+        Map<String, Object> basicInfo = new LinkedHashMap<>();
+        basicInfo.put("vppName", vppName);
+        basicInfo.put("reportMonth", record.getReportPeriod());
+        formData.put("basicInfo", basicInfo);
+
+        Map<String, Object> resourceStats = new LinkedHashMap<>();
+        resourceStats.put("pvCount", pvCount);
+        resourceStats.put("essCount", essCount);
+        resourceStats.put("totalCapacityKw", totalCapacity);
+        formData.put("resourceStats", resourceStats);
+
+        Map<String, Object> energyData = new LinkedHashMap<>();
+        energyData.put("genKwh", totalGen);
+        energyData.put("useKwh", totalUse);
+        formData.put("energyData", energyData);
+
+        Map<String, Object> drData = new LinkedHashMap<>();
+        drData.put("eventCount", prefill.getDrEventCount());
+        drData.put("totalSubsidy", drSubsidy);
+        formData.put("drData", drData);
+
+        prefill.setFormData(formData);
+        return prefill;
+    }
+
+    public static ReportValidateResultVO validateFormData(Map<String, Object> formData,
+                                                          int systemPvCount,
+                                                          boolean strict) {
+        ReportValidateResultVO result = new ReportValidateResultVO();
+        List<ReportValidateErrorVO> errors = new ArrayList<>();
+        if (formData == null || formData.isEmpty()) {
+            errors.add(new ReportValidateErrorVO("formData", "填报数据不能为空"));
+            result.setValid(false);
+            result.setErrors(errors);
+            return result;
+        }
+
+        JsonNode root = OBJECT_MAPPER.valueToTree(formData);
+        JsonNode basicInfo = root.path("basicInfo");
+        if (strict) {
+            if (!StringUtils.hasText(basicInfo.path("vppName").asText(null))) {
+                errors.add(new ReportValidateErrorVO("basicInfo.vppName", "虚拟电厂名称不能为空"));
+            }
+            if (!StringUtils.hasText(basicInfo.path("reportMonth").asText(null))) {
+                errors.add(new ReportValidateErrorVO("basicInfo.reportMonth", "报送月份不能为空"));
+            }
+        }
+
+        JsonNode energyData = root.path("energyData");
+        BigDecimal genKwh = readDecimal(energyData.path("genKwh"));
+        BigDecimal useKwh = readDecimal(energyData.path("useKwh"));
+        if (genKwh != null && genKwh.compareTo(BigDecimal.ZERO) < 0) {
+            errors.add(new ReportValidateErrorVO("energyData.genKwh", "发电量不能为负数"));
+        }
+        if (useKwh != null && useKwh.compareTo(BigDecimal.ZERO) < 0) {
+            errors.add(new ReportValidateErrorVO("energyData.useKwh", "用电量不能为负数"));
+        }
+
+        JsonNode resourceStats = root.path("resourceStats");
+        if (resourceStats.has("pvCount") && !resourceStats.path("pvCount").isNull()) {
+            int formPvCount = resourceStats.path("pvCount").asInt(-1);
+            if (formPvCount >= 0 && formPvCount != systemPvCount) {
+                errors.add(new ReportValidateErrorVO("resourceStats.pvCount", "光伏数量与系统统计不一致"));
+            }
+        }
+
+        result.setErrors(errors);
+        result.setValid(errors.isEmpty());
+        return result;
+    }
+
+    public static PeriodRange parseReportPeriod(String reportPeriod) {
+        if (!StringUtils.hasText(reportPeriod)) {
+            return null;
+        }
+        try {
+            YearMonth yearMonth = YearMonth.parse(reportPeriod, DateTimeFormatter.ofPattern("yyyy-MM"));
+            return new PeriodRange(yearMonth.atDay(1), yearMonth.atEndOfMonth());
+        } catch (DateTimeParseException ex) {
+            return null;
+        }
+    }
+
+    private static String resolveVppName(List<VppSite> sites, VppReportTask task) {
+        if (sites != null && !sites.isEmpty() && StringUtils.hasText(sites.get(0).getSiteName())) {
+            return sites.get(0).getSiteName();
+        }
+        if (task != null && StringUtils.hasText(task.getTaskName())) {
+            return task.getTaskName();
+        }
+        return "虚拟电厂";
+    }
+
+    private static BigDecimal readDecimal(JsonNode node) {
+        if (node == null || node.isNull() || node.isMissingNode()) {
+            return null;
+        }
+        if (node.isNumber()) {
+            return node.decimalValue();
+        }
+        String text = node.asText(null);
+        if (!StringUtils.hasText(text)) {
+            return null;
+        }
+        try {
+            return new BigDecimal(text);
+        } catch (NumberFormatException ex) {
+            return null;
+        }
+    }
+
+    public static final class PeriodRange {
+        private final LocalDate start;
+        private final LocalDate end;
+
+        public PeriodRange(LocalDate start, LocalDate end) {
+            this.start = start;
+            this.end = end;
+        }
+
+        public LocalDate getStart() {
+            return start;
+        }
+
+        public LocalDate getEnd() {
+            return end;
+        }
+    }
+}

+ 340 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppResponseMonitorHelper.java

@@ -0,0 +1,340 @@
+package com.usky.vpp.util;
+
+import com.usky.vpp.domain.VppDevice;
+import com.usky.vpp.domain.VppDrEvent;
+import com.usky.vpp.domain.VppDrExecution;
+import com.usky.vpp.domain.VppDrParticipation;
+import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.service.vo.ResponseLoadPointVO;
+import com.usky.vpp.service.vo.ResponseLoadStatRowVO;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * 响应监控辅助(15 分钟粒度曲线 + 统计表)
+ */
+public final class VppResponseMonitorHelper {
+
+    public static final int BUCKET_MINUTES = 15;
+    public static final int BASELINE_DAYS_AGO = 7;
+
+    private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("HH:mm");
+    private static final DateTimeFormatter DATETIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+    private static final DateTimeFormatter HOUR_FMT = DateTimeFormatter.ofPattern("HH:00");
+
+    private VppResponseMonitorHelper() {
+    }
+
+    public static List<ResponseLoadPointVO> buildCurveFromTsdb(
+            Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> actualData,
+            Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> baselineData,
+            LocalDate date,
+            List<String> powerMetrics,
+            BigDecimal declaredCapacityKw,
+            Map<LocalDateTime, BigDecimal> executionActualByTime) {
+        List<ResponseLoadPointVO> points = new ArrayList<>();
+        for (int minute = 0; minute < 1440; minute += BUCKET_MINUTES) {
+            LocalTime time = LocalTime.of(minute / 60, minute % 60);
+            LocalDateTime bucketStart = date.atTime(time);
+            LocalDateTime bucketEnd = bucketStart.plusMinutes(BUCKET_MINUTES).minusSeconds(1);
+
+            BigDecimal actual = sumBucketPower(actualData, powerMetrics, bucketStart, bucketEnd);
+            BigDecimal baseline = sumBucketPower(baselineData, powerMetrics,
+                    date.minusDays(BASELINE_DAYS_AGO).atTime(time),
+                    date.minusDays(BASELINE_DAYS_AGO).atTime(time).plusMinutes(BUCKET_MINUTES).minusSeconds(1));
+            if (baseline == null || baseline.compareTo(BigDecimal.ZERO) <= 0) {
+                baseline = actual;
+            }
+
+            BigDecimal responseLoad = executionActualByTime != null
+                    ? executionActualByTime.getOrDefault(bucketStart, null)
+                    : null;
+            if (responseLoad == null) {
+                responseLoad = baseline.subtract(actual).max(BigDecimal.ZERO);
+            }
+
+            ResponseLoadPointVO point = new ResponseLoadPointVO();
+            point.setTime(time.format(TIME_FMT));
+            point.setDatetime(bucketStart.format(DATETIME_FMT));
+            point.setBaselineLoadKw(scale(baseline));
+            point.setActualLoadKw(scale(actual));
+            point.setDeclaredCapacityKw(declaredCapacityKw != null ? scale(declaredCapacityKw) : BigDecimal.ZERO);
+            point.setActualResponseLoadKw(scale(responseLoad));
+            points.add(point);
+        }
+        return points;
+    }
+
+    public static List<ResponseLoadPointVO> buildMockCurve(List<VppResourcePoint> resources,
+                                                         List<VppDevice> devices,
+                                                         Long siteId,
+                                                         LocalDate date,
+                                                         BigDecimal declaredCapacityKw) {
+        List<ResponseLoadPointVO> points = new ArrayList<>();
+        long seed = (siteId != null ? siteId : 0L) + date.toEpochDay();
+        for (int minute = 0; minute < 1440; minute += BUCKET_MINUTES) {
+            LocalTime time = LocalTime.of(minute / 60, minute % 60);
+            double hourFactor = dayTimeFactor(time);
+            BigDecimal baseline = mockAggregate(resources, devices, seed, minute, hourFactor, 0);
+            BigDecimal actual = mockAggregate(resources, devices, seed, minute, hourFactor, 1);
+            BigDecimal response = baseline.subtract(actual).max(BigDecimal.ZERO);
+
+            ResponseLoadPointVO point = new ResponseLoadPointVO();
+            point.setTime(time.format(TIME_FMT));
+            point.setDatetime(date.atTime(time).format(DATETIME_FMT));
+            point.setBaselineLoadKw(baseline);
+            point.setActualLoadKw(actual);
+            point.setDeclaredCapacityKw(declaredCapacityKw != null ? declaredCapacityKw : BigDecimal.ZERO);
+            point.setActualResponseLoadKw(response);
+            points.add(point);
+        }
+        return points;
+    }
+
+    public static List<ResponseLoadStatRowVO> buildStatistics(List<ResponseLoadPointVO> points, LocalDate date) {
+        if (points == null || points.isEmpty()) {
+            return Collections.emptyList();
+        }
+        ResponseLoadStatRowVO actualRow = buildStatRow(points, "ACTUAL", "实际用电量", true);
+        ResponseLoadStatRowVO predictedRow = buildStatRow(points, "PREDICTED", "预测用电量", false);
+        List<ResponseLoadStatRowVO> rows = new ArrayList<>(2);
+        rows.add(actualRow);
+        rows.add(predictedRow);
+        return rows;
+    }
+
+    public static BigDecimal resolveDeclaredCapacity(VppDrEvent event,
+                                                     List<VppDrParticipation> participations,
+                                                     List<VppDrExecution> executions) {
+        if (executions != null && !executions.isEmpty()) {
+            BigDecimal total = BigDecimal.ZERO;
+            for (VppDrExecution execution : executions) {
+                if (execution.getTargetKw() != null) {
+                    total = total.add(execution.getTargetKw());
+                }
+            }
+            if (total.compareTo(BigDecimal.ZERO) > 0) {
+                return total;
+            }
+        }
+        if (participations != null && !participations.isEmpty()) {
+            BigDecimal total = BigDecimal.ZERO;
+            for (VppDrParticipation participation : participations) {
+                BigDecimal cap = participation.getClearedCapacityKw() != null
+                        ? participation.getClearedCapacityKw()
+                        : participation.getDeclaredCapacityKw();
+                if (cap != null) {
+                    total = total.add(cap);
+                }
+            }
+            if (total.compareTo(BigDecimal.ZERO) > 0) {
+                return total;
+            }
+        }
+        if (event != null && event.getClearedCapacityKw() != null) {
+            return event.getClearedCapacityKw();
+        }
+        if (event != null && event.getTargetCapacityKw() != null) {
+            return event.getTargetCapacityKw();
+        }
+        return BigDecimal.ZERO;
+    }
+
+    private static ResponseLoadStatRowVO buildStatRow(List<ResponseLoadPointVO> points,
+                                                      String dataType,
+                                                      String label,
+                                                      boolean actual) {
+        ResponseLoadStatRowVO row = new ResponseLoadStatRowVO();
+        row.setDataType(dataType);
+        row.setDataTypeLabel(label);
+
+        Map<String, List<BigDecimal>> hourlyBuckets = new LinkedHashMap<>();
+        for (int h = 0; h < 24; h++) {
+            hourlyBuckets.put(LocalTime.of(h, 0).format(HOUR_FMT), new ArrayList<>());
+        }
+
+        List<BigDecimal> allKw = new ArrayList<>();
+        for (ResponseLoadPointVO point : points) {
+            BigDecimal kw = actual ? point.getActualLoadKw() : point.getBaselineLoadKw();
+            if (kw == null) {
+                continue;
+            }
+            if (!actual) {
+                kw = kw.multiply(BigDecimal.valueOf(1.02 + pseudo(point.getTime()) * 0.05));
+            }
+            allKw.add(kw);
+            String hourKey = point.getTime().substring(0, 2) + ":00";
+            hourlyBuckets.computeIfAbsent(hourKey, k -> new ArrayList<>()).add(kw);
+        }
+
+        Map<String, BigDecimal> hourlyValues = new LinkedHashMap<>();
+        BigDecimal dayTotalKwh = BigDecimal.ZERO;
+        for (Map.Entry<String, List<BigDecimal>> entry : hourlyBuckets.entrySet()) {
+            BigDecimal hourKwh = average(entry.getValue())
+                    .multiply(BigDecimal.valueOf(BUCKET_MINUTES))
+                    .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
+            hourlyValues.put(entry.getKey(), hourKwh);
+            dayTotalKwh = dayTotalKwh.add(hourKwh);
+        }
+
+        row.setHourlyValues(hourlyValues);
+        row.setTotal(dayTotalKwh.setScale(2, RoundingMode.HALF_UP));
+        row.setMax(max(allKw));
+        row.setMin(min(allKw));
+        row.setAvg(average(allKw));
+        return row;
+    }
+
+    private static BigDecimal mockAggregate(List<VppResourcePoint> resources,
+                                            List<VppDevice> devices,
+                                            long seed,
+                                            int minuteOfDay,
+                                            double hourFactor,
+                                            int seriesSalt) {
+        if (resources == null || resources.isEmpty()) {
+            double base = 580 * hourFactor + pseudoRandom(seed, 0L, minuteOfDay, seriesSalt) * 80;
+            return BigDecimal.valueOf(base).setScale(2, RoundingMode.HALF_UP);
+        }
+        BigDecimal total = BigDecimal.ZERO;
+        for (VppResourcePoint resource : resources) {
+            BigDecimal cap = resource.getCapacityKw() != null ? resource.getCapacityKw() : BigDecimal.valueOf(100);
+            double noise = 0.85 + pseudoRandom(seed, resource.getId() != null ? resource.getId() : 0L,
+                    minuteOfDay, seriesSalt) * 0.3;
+            double value = cap.doubleValue() * hourFactor * 0.6 * noise;
+            total = total.add(BigDecimal.valueOf(value));
+        }
+        if (seriesSalt == 1) {
+            total = total.multiply(BigDecimal.valueOf(0.95 + pseudoRandom(seed, 999, minuteOfDay, seriesSalt) * 0.08));
+        }
+        return total.setScale(2, RoundingMode.HALF_UP);
+    }
+
+    private static BigDecimal sumBucketPower(Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> deviceData,
+                                             List<String> metricCandidates,
+                                             LocalDateTime bucketStart,
+                                             LocalDateTime bucketEnd) {
+        if (deviceData == null || deviceData.isEmpty()) {
+            return BigDecimal.ZERO;
+        }
+        BigDecimal total = BigDecimal.ZERO;
+        for (Map<String, TreeMap<LocalDateTime, BigDecimal>> metricMap : deviceData.values()) {
+            TreeMap<LocalDateTime, BigDecimal> series = pickSeries(metricMap, metricCandidates);
+            if (series == null || series.isEmpty()) {
+                continue;
+            }
+            BigDecimal avg = averageInRange(series, bucketStart, bucketEnd);
+            if (avg != null) {
+                total = total.add(avg);
+            }
+        }
+        return total;
+    }
+
+    private static TreeMap<LocalDateTime, BigDecimal> pickSeries(Map<String, TreeMap<LocalDateTime, BigDecimal>> metricMap,
+                                                                 List<String> candidates) {
+        if (metricMap == null) {
+            return null;
+        }
+        for (String metric : candidates) {
+            TreeMap<LocalDateTime, BigDecimal> series = metricMap.get(metric);
+            if (series != null && !series.isEmpty()) {
+                return series;
+            }
+        }
+        return null;
+    }
+
+    private static BigDecimal averageInRange(TreeMap<LocalDateTime, BigDecimal> series,
+                                              LocalDateTime start,
+                                              LocalDateTime end) {
+        BigDecimal sum = BigDecimal.ZERO;
+        int count = 0;
+        for (Map.Entry<LocalDateTime, BigDecimal> entry : series.subMap(start, true, end, true).entrySet()) {
+            if (entry.getValue() != null) {
+                sum = sum.add(entry.getValue());
+                count++;
+            }
+        }
+        if (count == 0) {
+            return null;
+        }
+        return sum.divide(BigDecimal.valueOf(count), 4, RoundingMode.HALF_UP);
+    }
+
+    private static BigDecimal scale(BigDecimal value) {
+        if (value == null) {
+            return BigDecimal.ZERO;
+        }
+        return value.setScale(2, RoundingMode.HALF_UP);
+    }
+
+    private static BigDecimal average(List<BigDecimal> values) {
+        if (values == null || values.isEmpty()) {
+            return BigDecimal.ZERO;
+        }
+        BigDecimal sum = BigDecimal.ZERO;
+        for (BigDecimal v : values) {
+            sum = sum.add(v);
+        }
+        return sum.divide(BigDecimal.valueOf(values.size()), 2, RoundingMode.HALF_UP);
+    }
+
+    private static BigDecimal max(List<BigDecimal> values) {
+        return values.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
+    }
+
+    private static BigDecimal min(List<BigDecimal> values) {
+        return values.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
+    }
+
+    private static double pseudo(String time) {
+        if (time == null) {
+            return 0.5;
+        }
+        return (time.hashCode() & 0x7FFFFFFF) % 100 / 100.0;
+    }
+
+    private static double pseudoRandom(long seed, long a, int b, int c) {
+        long x = seed * 31 + a * 17 + b * 13L + c;
+        x = ((x >> 16) ^ x) * 0x45d9f3b;
+        x = ((x >> 16) ^ x) * 0x45d9f3b;
+        x = (x >> 16) ^ x;
+        return (x & 0x7FFFFFFF) / (double) 0x7FFFFFFF;
+    }
+
+    private static double dayTimeFactor(LocalTime time) {
+        int hour = time.getHour();
+        int minute = time.getMinute();
+        double h = hour + minute / 60.0;
+        if (h < 6) {
+            return 0.45 + h / 6 * 0.15;
+        }
+        if (h < 9) {
+            return 0.6 + (h - 6) / 3 * 0.35;
+        }
+        if (h < 12) {
+            return 0.95 + (h - 9) / 3 * 0.05;
+        }
+        if (h < 14) {
+            return 0.85;
+        }
+        if (h < 18) {
+            return 0.75 + (h - 14) / 4 * 0.25;
+        }
+        if (h < 21) {
+            return 1.0 - (h - 18) / 3 * 0.2;
+        }
+        return 0.55 - (h - 21) / 3 * 0.15;
+    }
+}

+ 2 - 0
service-vpp/service-vpp-biz/src/main/resources/bootstrap.yml

@@ -36,3 +36,5 @@ vpp:
     enabled: true
     sign-name: 上海永天科技股份有限公司
     invitation-template-code: SMS_465362899
+  report:
+    retry-max: 3