Explorar el Código

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

hanzhengyi hace 6 días
padre
commit
0caf41707b
Se han modificado 69 ficheros con 2278 adiciones y 422 borrados
  1. 2 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppTsdbConstants.java
  2. 9 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ContractController.java
  3. 99 7
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrController.java
  4. 7 29
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrMonitorController.java
  5. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ResourceController.java
  6. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/SiteController.java
  7. 16 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContract.java
  8. 7 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContractTemplate.java
  9. 24 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppCustomer.java
  10. 5 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDevice.java
  11. 3 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrEvent.java
  12. 32 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrSubsidyPrediction.java
  13. 64 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppResponseDeviationPrice.java
  14. 12 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/mapper/VppResponseDeviationPriceMapper.java
  15. 11 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppBaselineService.java
  16. 6 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppContractService.java
  17. 6 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrMonitorService.java
  18. 25 9
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrSubsidyPredictionService.java
  19. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppResourceBoardService.java
  20. 2 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppResourceOverviewService.java
  21. 26 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppResponseDeviationPriceService.java
  22. 33 8
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppBaselineServiceImpl.java
  23. 6 5
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppCapabilityEvalServiceImpl.java
  24. 212 32
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppContractServiceImpl.java
  25. 12 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppContractTemplateServiceImpl.java
  26. 72 8
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppCustomerServiceImpl.java
  27. 7 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDeviceServiceImpl.java
  28. 3 10
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrInvitationServiceImpl.java
  29. 116 89
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrMonitorServiceImpl.java
  30. 6 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrServiceImpl.java
  31. 441 50
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrSubsidyPredictionServiceImpl.java
  32. 13 7
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResourceBoardServiceImpl.java
  33. 15 4
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResourceOverviewServiceImpl.java
  34. 2 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResourcePointServiceImpl.java
  35. 205 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResponseDeviationPriceServiceImpl.java
  36. 12 11
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResponseMonitorServiceImpl.java
  37. 2 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppSiteServiceImpl.java
  38. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/AlarmLevelStatVO.java
  39. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CapabilityLoadCurveVO.java
  40. 20 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CustomerRequestVO.java
  41. 12 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CustomerResponseVO.java
  42. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DashboardCurveVO.java
  43. 4 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DeviceRequest.java
  44. 2 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrEventDetailVO.java
  45. 2 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrEventRequest.java
  46. 0 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInvitationVO.java
  47. 4 10
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrMonitorCurveVO.java
  48. 17 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrMonitorRecordVO.java
  49. 19 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrSubsidyPredictionCalculateRequest.java
  50. 70 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrSubsidyPredictionRequest.java
  51. 31 45
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrSubsidyPredictionVO.java
  52. 29 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResponseDeviationPriceAdjustmentRequest.java
  53. 36 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResponseDeviationPriceAdjustmentVO.java
  54. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResponseLoadAnalysisVO.java
  55. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/SiteBaselineVO.java
  56. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/SiteDeclaredCapacityVO.java
  57. 31 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractOverviewVO.java
  58. 25 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractRequestVO.java
  59. 31 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractResponseVO.java
  60. 5 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractTemplateRequestVO.java
  61. 5 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractTemplateResponseVO.java
  62. 21 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppBaselineHelper.java
  63. 5 9
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppCapabilityEvalHelper.java
  64. 11 9
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppResourceMockHelper.java
  65. 30 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppResourceTypeHelper.java
  66. 167 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppResponseCoefficientHelper.java
  67. 6 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppResponseMonitorHelper.java
  68. 24 23
      service-vpp/service-vpp-biz/src/main/resources/bootstrap.yml
  69. 149 34
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_schema.sql

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

@@ -36,7 +36,8 @@ public final class VppTsdbConstants {
             "device_status",
             "p",
             "ua",
-            "ia"
+            "ia",
+            "soc"
     );
 
     /** 日发电量(光伏) */

+ 9 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ContractController.java

@@ -5,6 +5,7 @@ import com.usky.common.core.bean.CommonPage;
 import com.usky.vpp.service.VppContractService;
 import com.usky.vpp.service.VppContractTemplateService;
 import com.usky.vpp.service.vo.VppContractExpireStatVO;
+import com.usky.vpp.service.vo.VppContractOverviewVO;
 import com.usky.vpp.service.vo.VppContractRequestVO;
 import com.usky.vpp.service.vo.VppContractResponseVO;
 import com.usky.vpp.service.vo.VppContractTemplateRequestVO;
@@ -147,6 +148,14 @@ public class ContractController {
         return ApiResult.success(vppContractService.expireStat());
     }
 
+    /**
+     * 合同概况(甲方、乙方、签订日期、合同金额、起止时间、生效状态)
+     */
+    @GetMapping("/{id}/overview")
+    public ApiResult<VppContractOverviewVO> overview(@PathVariable("id") Long id) {
+        return ApiResult.success(vppContractService.overview(id));
+    }
+
     /**
      * 删除合同(软删除)
      */

+ 99 - 7
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.VppDrSubsidyPredictionService;
+import com.usky.vpp.service.VppResponseDeviationPriceService;
 import com.usky.vpp.service.vo.DrClearingRequest;
 import com.usky.vpp.service.vo.DrEventDetailVO;
 import com.usky.vpp.service.vo.DrEventRequest;
@@ -19,10 +20,15 @@ import com.usky.vpp.service.vo.DrInvitationVO;
 import com.usky.vpp.service.vo.DrParticipateRequest;
 import com.usky.vpp.service.vo.DrStrategyRequest;
 import com.usky.vpp.service.vo.DrStrategyVO;
+import com.usky.vpp.service.vo.DrSubsidyPredictionCalculateRequest;
+import com.usky.vpp.service.vo.DrSubsidyPredictionRequest;
 import com.usky.vpp.service.vo.DrSubsidyPredictionVO;
+import com.usky.vpp.service.vo.ResponseDeviationPriceAdjustmentRequest;
+import com.usky.vpp.service.vo.ResponseDeviationPriceAdjustmentVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
+import java.math.BigDecimal;
 import java.util.Map;
 
 /**
@@ -39,6 +45,8 @@ public class DrController {
     private VppDrInvitationService vppDrInvitationService;
     @Autowired
     private VppDrSubsidyPredictionService vppDrSubsidyPredictionService;
+    @Autowired
+    private VppResponseDeviationPriceService responseDeviationPriceService;
 
     @GetMapping(value = "/event")
     public ApiResult<CommonPage<VppDrEvent>> pageEvent(@RequestParam(required = false) Map<String, Object> params) {
@@ -182,19 +190,103 @@ public class DrController {
 
     /**
      * 分页查询补贴金额预测
-     * siteName  站点名称(模糊匹配)
-     * startTime 响应开始时间起始
-     * endTime   响应开始时间截止
-     * current   页码
-     * size      页大小
+     * siteName     站点名称(模糊匹配)
+     * customerName 企业名称(模糊匹配)
+     * startTime    响应开始时间起始
+     * endTime      响应开始时间截止
+     * current      页码
+     * size         页大小
      */
     @GetMapping(value = "/subsidyPrediction")
     public ApiResult<CommonPage<DrSubsidyPredictionVO>> pageSubsidyPrediction(
             @RequestParam(value = "siteName", required = false) String siteName,
+            @RequestParam(value = "customerName", required = false) String customerName,
             @RequestParam(value = "startTime", required = false) String startTime,
             @RequestParam(value = "endTime", required = false) String endTime,
             @RequestParam(value = "current", required = false, defaultValue = "1") Integer current,
             @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
-        return ApiResult.success(vppDrSubsidyPredictionService.page(siteName, startTime, endTime, current, size));
+        return ApiResult.success(vppDrSubsidyPredictionService.page(
+                siteName, customerName, startTime, endTime, current, size));
+    }
+
+    /**
+     * 试算补贴金额预测(不落库)。
+     * <p>入参:邀约编号、实际响应容量(电网);其余计算字段由服务端按邀约/事件查询。
+     * 前端拿到结果后再调用新增接口入库。</p>
+     */
+    @PostMapping(value = "/subsidyPrediction/calculate")
+    public ApiResult<DrSubsidyPredictionVO> calculateSubsidyPrediction(
+            @RequestBody DrSubsidyPredictionCalculateRequest request) {
+        return ApiResult.success(vppDrSubsidyPredictionService.calculate(request));
+    }
+
+    /**
+     * 新增补贴金额预测。
+     * <p>按前端传参直接入库(通常携带试算结果),服务端不再重算。</p>
+     */
+    @PostMapping(value = "/subsidyPrediction")
+    public ApiResult<Boolean> createSubsidyPrediction(@RequestBody DrSubsidyPredictionRequest request) {
+        return vppDrSubsidyPredictionService.create(request)
+                ? ApiResult.success(true) : ApiResult.error("新增补贴预测失败!请重试!");
+    }
+
+    /**
+     * 修改补贴金额预测。
+     * <p>按前端传参直接入库,服务端不再重算。</p>
+     */
+    @PutMapping(value = "/subsidyPrediction")
+    public ApiResult<Boolean> updateSubsidyPrediction(@RequestBody DrSubsidyPredictionRequest request) {
+        return vppDrSubsidyPredictionService.update(request)
+                ? ApiResult.success(true) : ApiResult.error("修改补贴预测失败!请重试!");
+    }
+
+    /**
+     * 删除补贴金额预测
+     */
+    @DeleteMapping(value = "/subsidyPrediction/{id}")
+    public ApiResult<Boolean> deleteSubsidyPrediction(@PathVariable("id") Long id) {
+        return vppDrSubsidyPredictionService.delete(id)
+                ? ApiResult.success(true) : ApiResult.error("删除补贴预测失败!请重试!");
+    }
+
+    // ==================== 响应量偏差价格调整 ====================
+    /**
+     * 分页查询响应量偏差价格调整。
+     * completionRate 可选,传入时按结构化区间匹配命中配置。
+     * current   页码
+     * size      页大小
+     */
+    @GetMapping(value = "/responseDeviationPrice")
+    public ApiResult<CommonPage<ResponseDeviationPriceAdjustmentVO>> pageResponseDeviationPriceAdjustment(
+            @RequestParam(value = "completionRate", required = false) BigDecimal completionRate,
+            @RequestParam(value = "current", required = false, defaultValue = "1") Integer current,
+            @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
+        return ApiResult.success(responseDeviationPriceService.page(completionRate, current, size));
+    }
+
+    @GetMapping(value = "/responseDeviationPrice/{id}")
+    public ApiResult<ResponseDeviationPriceAdjustmentVO> getResponseDeviationPriceAdjustment(
+            @PathVariable("id") Long id) {
+        return ApiResult.success(responseDeviationPriceService.get(id));
+    }
+
+    @PostMapping(value = "/responseDeviationPrice")
+    public ApiResult<Long> createResponseDeviationPriceAdjustment(
+            @RequestBody ResponseDeviationPriceAdjustmentRequest request) {
+        return ApiResult.success(responseDeviationPriceService.create(request));
+    }
+
+    @PutMapping(value = "/responseDeviationPrice/{id}")
+    public ApiResult<Void> updateResponseDeviationPriceAdjustment(
+            @PathVariable("id") Long id,
+            @RequestBody ResponseDeviationPriceAdjustmentRequest request) {
+        responseDeviationPriceService.update(id, request);
+        return ApiResult.success();
+    }
+
+    @DeleteMapping(value = "/responseDeviationPrice/{id}")
+    public ApiResult<Void> deleteResponseDeviationPriceAdjustment(@PathVariable("id") Long id) {
+        responseDeviationPriceService.delete(id);
+        return ApiResult.success();
     }
-}
+}

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

@@ -7,19 +7,13 @@ import com.usky.vpp.service.vo.DrMonitorCurveVO;
 import com.usky.vpp.service.vo.DrMonitorRecordVO;
 import com.usky.vpp.service.vo.DrMonitorSummaryVO;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.util.StringUtils;
 import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 
 /**
  * 需求响应监测(响应记录)
@@ -56,29 +50,13 @@ public class DrMonitorController {
     }
 
     /**
-     * 折线图:基线/实际负荷 + 申报/实际响应容量
-     * <p>必传站点;仅 is_support_peak=1 资源点;曲线按资源点加权平均。</p>
+     * 折线图:基线负荷 + 实际响应容量;查询范围为当天
+     * <p>基线负荷复用 getSiteBaseline,实际响应容量复用 getSiteDeclaredCapacity。</p>
+     * <p>baselineDays 传 3 或 5 时按对应周期计算基线负荷;为空则按 5 日基线。</p>
      */
-    @GetMapping("/records/{id}/curve")
-    public ApiResult<DrMonitorCurveVO> curve(@PathVariable("id") Long id,
-                                             @RequestParam("siteIds") List<Long> siteIds) {
-        return ApiResult.success(drMonitorService.getCurve(id, siteIds));
+    @GetMapping("/records/curve")
+    public ApiResult<DrMonitorCurveVO> curve(@RequestParam("siteId") Long siteId,
+                                             @RequestParam(value = "baselineDays", required = false) Integer baselineDays) {
+        return ApiResult.success(drMonitorService.getCurve(siteId, baselineDays));
     }
-
-    // private List<Long> parseSiteIds(List<String> raw) {
-    //     if (raw == null || raw.isEmpty()) {
-    //         return Collections.emptyList();
-    //     }
-    //     List<Long> ids = new ArrayList<>();
-    //     for (String item : raw) {
-    //         if (!StringUtils.hasText(item)) {
-    //             continue;
-    //         }
-    //         Arrays.stream(item.split(","))
-    //                 .map(String::trim)
-    //                 .filter(StringUtils::hasText)
-    //                 .forEach(token -> ids.add(Long.parseLong(token)));
-    //     }
-    //     return ids.stream().distinct().collect(Collectors.toList());
-    // }
 }

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

@@ -90,7 +90,7 @@ public class ResourceController {
 
 //    /**
 //     * 资源概览 - 实时告警统计
-//     * <p>按紧急/重要/一般统计告警数量,含跳转告警列表路径(模拟数据)。</p>
+//     * <p>按紧急/重要/一般统计告警数量,含跳转告警列表路径。</p>
 //     */
 //    @GetMapping("/overview/alarm-stats")
 //    public ApiResult<List<AlarmLevelStatVO>> resourceOverviewAlarmStats() {

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

@@ -160,7 +160,7 @@ public class SiteController {
      *   <li>responseDayTypeLabel — 工作日 / 非工作日</li>
      *   <li>referenceDates — 参与计算的典型历史日(yyyy-MM-dd)</li>
      *   <li>correctionFactorK — 日内修正系数 K</li>
-     *   <li>dataSource — tsdb(时序库)/ mock(联调模拟)</li>
+     *   <li>dataSource — tsdb(时序库)</li>
      *   <li>points — 曲线点列表(BaselinePointVO:time、predictedBaselineKw、todayActualKw、yesterdayActualKw)</li>
      * </ul>
      *

+ 16 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContract.java

@@ -40,6 +40,10 @@ public class VppContract implements Serializable {
     @NotNull(message = "合同模板ID不能为空!")
     private Long templateId;
 
+    /** 关联合同ID(居间/普通一对一) */
+    @TableField("related_contract_id")
+    private Long relatedContractId;
+
     /**
      * 1购售电 2需求响应合作 3聚合代理 4服务代理 5居民充电桩 6自有资产
      */
@@ -51,6 +55,18 @@ public class VppContract implements Serializable {
     @NotNull(message = "合同名称不能为空!")
     private String contractName;
 
+    /** 甲方 */
+    @TableField("party_a")
+    private String partyA;
+
+    /** 乙方 */
+    @TableField("party_b")
+    private String partyB;
+
+    /** 合同金额 */
+    @TableField("contract_amount")
+    private BigDecimal contractAmount;
+
     /**
      * 0草稿 1审核中 2待生效 3已生效 4已驳回 5已到期 6已终止
      */

+ 7 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContractTemplate.java

@@ -38,6 +38,13 @@ public class VppContractTemplate implements Serializable {
     @TableField("contract_type")
     @NotNull(message = "合同类型不能为空!")
     private Integer contractType;
+
+    /**
+     * 模板分类:1居间/层间 2普通/直网(用于一对一关联校验)
+     */
+    @TableField("template_kind")
+    private Integer templateKind;
+
     private String version;
     @TableField("file_url")
     @NotBlank(message = "文件路径不能为空!")

+ 24 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppCustomer.java

@@ -78,6 +78,30 @@ public class VppCustomer implements Serializable {
      */
     private String province;
 
+    /**
+     * 是否集团内用户 0否 1是
+     */
+    @TableField("is_group_user")
+    private Integer isGroupUser;
+
+    /**
+     * 是否绿电 0否 1是
+     */
+    @TableField("is_green_power")
+    private Integer isGreenPower;
+
+    /**
+     * 是否合作开发用户 0否 1是
+     */
+    @TableField("is_coop_dev_user")
+    private Integer isCoopDevUser;
+
+    /**
+     * 合作开发单位名称
+     */
+    @TableField("coop_partner_name")
+    private String coopPartnerName;
+
     /**
      * 市
      */

+ 5 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDevice.java

@@ -42,6 +42,11 @@ public class VppDevice implements Serializable {
     private LocalDateTime lastOnlineAt;
     @TableField("gateway_id")
     private String gatewayId;
+    /**
+     * 运管资源ID,全局唯一
+     */
+    @TableField("un_resource_id")
+    private String unResourceId;
     private String remark;
     @TableField("tenant_id")
     private Integer tenantId;

+ 3 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrEvent.java

@@ -57,6 +57,9 @@ public class VppDrEvent implements Serializable {
     private String attachmentName;
     @TableField("subsidy_price")
     private BigDecimal subsidyPrice;
+    /** 下浮系数(必填) */
+    @TableField("floating_coefficient")
+    private BigDecimal floatingCoefficient;
     @TableField("event_status")
     private Integer eventStatus;
     @TableField("raw_payload")

+ 32 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrSubsidyPrediction.java

@@ -37,6 +37,14 @@ public class VppDrSubsidyPrediction implements Serializable {
     @TableField("site_name")
     private String siteName;
 
+    /** 企业/客户ID */
+    @TableField("customer_id")
+    private Long customerId;
+
+    /** 企业名称 */
+    @TableField("customer_name")
+    private String customerName;
+
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     @TableField("event_start_time")
     private LocalDateTime eventStartTime;
@@ -51,15 +59,39 @@ public class VppDrSubsidyPrediction implements Serializable {
     @TableField("subsidy_price")
     private BigDecimal subsidyPrice;
 
+    /** 预估响应容量(兼容旧字段,等同平台实际响应量) */
     @TableField("estimated_capacity_kw")
     private BigDecimal estimatedCapacityKw;
 
+    /** 实际出清(兼容旧字段,等同申报出清量) */
     @TableField("cleared_capacity_kw")
     private BigDecimal clearedCapacityKw;
 
+    /** 实际响应量-电网 kW */
+    @TableField("actual_response_grid_kw")
+    private BigDecimal actualResponseGridKw;
+
+    /** 实际响应量-平台 kW */
+    @TableField("actual_response_platform_kw")
+    private BigDecimal actualResponsePlatformKw;
+
+    /** 申报出清量 kW */
+    @TableField("declared_cleared_kw")
+    private BigDecimal declaredClearedKw;
+
+    /** 分成比例 0~1 */
+    @TableField("share_ratio")
+    private BigDecimal shareRatio;
+
+    /** 按完成率(比例)命中响应量偏差价格调整表得到的价格调整系数。 */
+    @TableField("price_adjustment_coefficient")
+    private BigDecimal priceAdjustmentCoefficient;
+
+    /** 完成率(百分比)= 实际响应容量(电网) / 实际出清 × 100 */
     @TableField("completion_rate")
     private BigDecimal completionRate;
 
+    /** 偏差率(百分比)= (1 − 完成率) × 100 */
     @TableField("deviation_rate")
     private BigDecimal deviationRate;
 

+ 64 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppResponseDeviationPrice.java

@@ -0,0 +1,64 @@
+package com.usky.vpp.domain;
+
+import com.baomidou.mybatisplus.annotation.FieldStrategy;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 响应量偏差价格调整表。
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("vpp_response_deviation_price")
+public class VppResponseDeviationPrice implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /** 响应量系数下限,null 表示无下界。 */
+    @TableField(value = "coeff_lower", updateStrategy = FieldStrategy.IGNORED)
+    private BigDecimal coeffLower;
+
+    /** 响应量系数上限,null 表示无上界。 */
+    @TableField(value = "coeff_upper", updateStrategy = FieldStrategy.IGNORED)
+    private BigDecimal coeffUpper;
+
+    /** 下限是否闭区间。 */
+    @TableField("lower_inclusive")
+    private Boolean lowerInclusive;
+
+    /** 上限是否闭区间。 */
+    @TableField("upper_inclusive")
+    private Boolean upperInclusive;
+
+    /** 命中区间后用于补贴计算的价格调整系数。 */
+    @TableField("price_adjustment_coefficient")
+    private BigDecimal priceAdjustmentCoefficient;
+
+    private String remark;
+
+    @TableField("tenant_id")
+    private Integer tenantId;
+    @TableField("create_time")
+    private LocalDateTime createTime;
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+    @TableField("created_by")
+    private String createdBy;
+    @TableField("updated_by")
+    private String updatedBy;
+    @TableField("delete_flag")
+    private Integer deleteFlag;
+    @TableField("deleted_at")
+    private LocalDateTime deletedAt;
+}

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

@@ -0,0 +1,12 @@
+package com.usky.vpp.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.vpp.domain.VppResponseDeviationPrice;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 响应量偏差价格调整表 Mapper。
+ */
+@Repository
+public interface VppResponseDeviationPriceMapper extends CrudMapper<VppResponseDeviationPrice> {
+}

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

@@ -19,6 +19,17 @@ public interface VppBaselineService {
      */
     SiteBaselineVO getSiteBaseline(Long siteId, String responseStartTime);
 
+    /**
+     * 查询站点基线负荷曲线(可指定参考天数、实测截止时间)。
+     *
+     * @param siteId            站点 ID
+     * @param responseStartTime 响应开始时间
+     * @param baselineDays      参考天数,仅 3 或 5;为空则按工作日/非工作日自动选择
+     * @param actualEndTime     当日实测查询截止时间;为空则截止到响应开始
+     */
+    SiteBaselineVO getSiteBaseline(Long siteId, String responseStartTime,
+                                   Integer baselineDays, String actualEndTime);
+
     /**
      * 查询站点响应申报容量。
      *

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

@@ -2,6 +2,7 @@ package com.usky.vpp.service;
 
 import com.usky.common.core.bean.CommonPage;
 import com.usky.vpp.service.vo.VppContractExpireStatVO;
+import com.usky.vpp.service.vo.VppContractOverviewVO;
 import com.usky.vpp.service.vo.VppContractRequestVO;
 import com.usky.vpp.service.vo.VppContractResponseVO;
 
@@ -64,4 +65,9 @@ public interface VppContractService {
      * 列表请走分页接口,按 contractStatus=5(已到期)/ 7(即将到期)查询。</p>
      */
     VppContractExpireStatVO expireStat();
+
+    /**
+     * 合同概况:甲方、乙方、签订日期、合同金额、起止时间、生效状态
+     */
+    VppContractOverviewVO overview(Long id);
 }

+ 6 - 2
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrMonitorService.java

@@ -29,7 +29,11 @@ public interface VppDrMonitorService {
                                               List<Long> siteIds, Map<String, Object> params);
 
     /**
-     * 折线图:基线/实际负荷按可调峰资源点加权平均;申报/实际响应按资源点平均
+     * 折线图:以当天为查询范围,基线负荷复用 getSiteBaseline,实际响应容量复用
+     * getSiteDeclaredCapacity,再按可调峰资源点平均
+     *
+     * @param siteId       站点 ID(必填)
+     * @param baselineDays 基线参考天数,仅支持 3 或 5;为空时按 5 日基线
      */
-    DrMonitorCurveVO getCurve(Long eventId, List<Long> siteIds);
+    DrMonitorCurveVO getCurve(Long siteId, Integer baselineDays);
 }

+ 25 - 9
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrSubsidyPredictionService.java

@@ -1,6 +1,8 @@
 package com.usky.vpp.service;
 
 import com.usky.common.core.bean.CommonPage;
+import com.usky.vpp.service.vo.DrSubsidyPredictionCalculateRequest;
+import com.usky.vpp.service.vo.DrSubsidyPredictionRequest;
 import com.usky.vpp.service.vo.DrSubsidyPredictionVO;
 
 /**
@@ -10,20 +12,34 @@ public interface VppDrSubsidyPredictionService {
 
     /**
      * 分页查询补贴金额预测
-     *
-     * @param siteName   站点名称(模糊匹配)
-     * @param startTime  响应开始时间起始
-     * @param endTime    响应开始时间截止
-     * @param current    页码
-     * @param size       页大小
      */
-    CommonPage<DrSubsidyPredictionVO> page(String siteName, String startTime, String endTime,
+    CommonPage<DrSubsidyPredictionVO> page(String siteName, String customerName,
+                                           String startTime, String endTime,
                                            Integer current, Integer size);
 
+    /**
+     * 试算补贴预测(不落库)。
+     * <p>入参为邀约编号 + 电网实际响应容量;时长/平台响应量/申报出清量/补贴标准等从邀约与事件查询后计算。</p>
+     */
+    DrSubsidyPredictionVO calculate(DrSubsidyPredictionCalculateRequest request);
+
+    /**
+     * 新增补贴预测:按前端传参直接入库,服务端不再重算。
+     */
+    Boolean create(DrSubsidyPredictionRequest request);
+
+    /**
+     * 修改补贴预测:按前端传参直接入库,服务端不再重算。
+     */
+    Boolean update(DrSubsidyPredictionRequest request);
+
+    /**
+     * 删除补贴预测
+     */
+    Boolean delete(Long id);
+
     /**
      * 根据事件ID生成/更新补贴预测数据
-     *
-     * @param drEventId 需求响应事件ID
      */
     void generateByEventId(Long drEventId);
 }

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

@@ -12,7 +12,7 @@ import java.util.Map;
 public interface VppResourceBoardService {
 
     /**
-     * 资源看板列表(模拟实时运行数据)
+     * 资源看板列表(TSDB 实时运行数据)
      */
     CommonPage<ResourceBoardItemVO> pageBoard(Map<String, Object> params);
 

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

@@ -18,7 +18,7 @@ public interface VppResourceOverviewService {
     List<ResourceTypeStatVO> getTypeStats();
 
     /**
-     * 今日电能概况(总发电量、总用电量、绿电消纳比例,模拟数据
+     * 今日电能概况(总发电量、总用电量、绿电消纳比例,TSDB 今日 eday/epp 汇总
      */
     EnergyTodayVO getEnergyToday();
 
@@ -28,7 +28,7 @@ public interface VppResourceOverviewService {
     ResourceOverviewRealtimeCurveVO getRealtimeCurves(String startTime, String endTime);
 
     /**
-     * 实时告警统计(紧急/重要/一般,含跳转路径,模拟数据
+     * 实时告警统计(紧急/重要/一般,含跳转告警列表路径)
      */
     List<AlarmLevelStatVO> getAlarmStats();
 }

+ 26 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppResponseDeviationPriceService.java

@@ -0,0 +1,26 @@
+package com.usky.vpp.service;
+
+import com.usky.common.core.bean.CommonPage;
+import com.usky.vpp.service.vo.ResponseDeviationPriceAdjustmentRequest;
+import com.usky.vpp.service.vo.ResponseDeviationPriceAdjustmentVO;
+
+import java.math.BigDecimal;
+
+/**
+ * 响应量偏差价格调整配置服务。
+ */
+public interface VppResponseDeviationPriceService {
+
+    /**
+     * 分页查询。completionRate 非空时按结构化区间匹配命中配置。
+     */
+    CommonPage<ResponseDeviationPriceAdjustmentVO> page(BigDecimal completionRate, Integer current, Integer size);
+
+    ResponseDeviationPriceAdjustmentVO get(Long id);
+
+    Long create(ResponseDeviationPriceAdjustmentRequest request);
+
+    void update(Long id, ResponseDeviationPriceAdjustmentRequest request);
+
+    void delete(Long id);
+}

+ 33 - 8
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppBaselineServiceImpl.java

@@ -67,13 +67,13 @@ public class VppBaselineServiceImpl implements VppBaselineService {
      *
      * <p><b>处理流程</b></p>
      * <ol>
-     *   <li>查询站点下 is_control=1 的资源点关联设备 UUID;无设备则返回 mock 数据</li>
+     *   <li>查询站点下 is_support_peak=1 的资源点关联设备 UUID;无设备则返回空曲线</li>
      *   <li>按 Hutool 法定节假日规则判定响应日类型(工作日 / 非工作日),选取典型历史日:
      *       工作日 5 天 / 非工作日 3 天,剔除历史响应日</li>
      *   <li>本模块按典型历史日逐日调用 TSDB {@code queryHistoryDeviceData},聚合计算全天原始基线(10 分钟粒度)</li>
      *   <li>取响应开始时间前连续 2 小时的今日实时负荷(绿色曲线)与同时段原始基线比值,计算修正系数 K(0.7 ~ 1.2)</li>
      *   <li>原始基线 × K 得到预测基线;并查询今日 / 昨日实测曲线</li>
-     *   <li>组装 {@link SiteBaselineVO},dataSource 为 tsdb;TSDB 无数据时降级为 mock</li>
+     *   <li>组装 {@link SiteBaselineVO},dataSource 为 tsdb;无 TSDB 数据时返回空曲线</li>
      * </ol>
      *
      * @param siteId            站点 ID
@@ -83,9 +83,18 @@ public class VppBaselineServiceImpl implements VppBaselineService {
      */
     @Override
     public SiteBaselineVO getSiteBaseline(Long siteId, String responseStartTime) {
+        return getSiteBaseline(siteId, responseStartTime, null, null);
+    }
+
+    @Override
+    public SiteBaselineVO getSiteBaseline(Long siteId, String responseStartTime,
+                                          Integer baselineDays, String actualEndTime) {
         VppSite site = resolveSite(siteId);
         LocalDateTime startTime = parseResponseStartTime(responseStartTime);
         LocalDate responseDate = startTime.toLocalDate();
+        LocalDateTime actualUntil = StringUtils.hasText(actualEndTime)
+                ? parseResponseStartTime(actualEndTime)
+                : startTime;
 
         List<String> deviceUuids = listControllableDeviceUuids(siteId);
         SiteBaselineVO result = new SiteBaselineVO();
@@ -94,6 +103,7 @@ public class VppBaselineServiceImpl implements VppBaselineService {
         result.setResponseStartTime(startTime);
 
         if (deviceUuids.isEmpty()) {
+            // return buildMockResult(result, siteId, responseDate, startTime);
             return result;
         }
 
@@ -101,9 +111,14 @@ public class VppBaselineServiceImpl implements VppBaselineService {
         boolean workday = VppBaselineHelper.isWorkdayResponse(responseDate);
         result.setResponseDayType(VppWorkdayHelper.resolveDayType(responseDate));
         result.setResponseDayTypeLabel(VppWorkdayHelper.resolveDayTypeLabel(responseDate));
-        int requiredCount = workday
-                ? VppBaselineHelper.WEEKDAY_REFERENCE_COUNT
-                : VppBaselineHelper.NON_WEEKDAY_REFERENCE_COUNT;
+        int requiredCount;
+        if (baselineDays != null && (baselineDays == 3 || baselineDays == 5)) {
+            requiredCount = baselineDays;
+        } else {
+            requiredCount = workday
+                    ? VppBaselineHelper.WEEKDAY_REFERENCE_COUNT
+                    : VppBaselineHelper.NON_WEEKDAY_REFERENCE_COUNT;
+        }
         List<LocalDate> referenceDates = VppBaselineHelper.selectReferenceDates(
                 responseDate, workday, excludeDates, requiredCount);
         result.setReferenceDates(referenceDates.stream()
@@ -120,6 +135,7 @@ public class VppBaselineServiceImpl implements VppBaselineService {
                 VppBaselineHelper.INTERVAL_MINUTES,
                 null);
         if (originalBaselineByTime.isEmpty()) {
+            // return buildMockResult(result, siteId, responseDate, startTime);
             return result;
         }
 
@@ -135,10 +151,11 @@ public class VppBaselineServiceImpl implements VppBaselineService {
                 null);
 
         LocalDate yesterday = responseDate.minusDays(1);
-        //响应开始时间日数据线
+        //响应日实测(可延至响应后窗口)
+        LocalDateTime todayQueryEnd = actualUntil.isAfter(startTime) ? actualUntil : startTime;
         Map<String, BigDecimal> todayActualByTime = VppBaselineHelper.buildActualLoadByTimePoint(
                 vppTsdbQueryService.queryDeviceMetricHistory(
-                        deviceUuids, responseDate.atStartOfDay(), startTime, VppBaselineHelper.POWER_METRICS),
+                        deviceUuids, responseDate.atStartOfDay(), todayQueryEnd, VppBaselineHelper.POWER_METRICS),
                 VppBaselineHelper.INTERVAL_MINUTES);
         //响应开始时间日前一天数据线
         Map<String, BigDecimal> yesterdayActualByTime = VppBaselineHelper.buildActualLoadByTimePoint(
@@ -164,6 +181,7 @@ public class VppBaselineServiceImpl implements VppBaselineService {
         List<BaselinePointVO> points = VppBaselineHelper.buildBaselinePoints(
                 responseDate,
                 startTime,
+                todayQueryEnd,
                 predictedBaselineByTime.isEmpty() ? applyCorrection(originalBaselineByTime, correctionFactorK)
                         : predictedBaselineByTime,
                 todayActualByTime,
@@ -198,6 +216,7 @@ public class VppBaselineServiceImpl implements VppBaselineService {
 
         List<String> deviceUuids = listControllableDeviceUuids(siteId);
         if (deviceUuids.isEmpty()) {
+            // return buildMockDeclaredCapacityResult(result, siteId, responseDate, startTime, endTime);
             return result;
         }
 
@@ -223,9 +242,10 @@ public class VppBaselineServiceImpl implements VppBaselineService {
                 VppBaselineHelper.POWER_METRICS,
                 VppBaselineHelper.INTERVAL_MINUTES,
                 null);
+        // 查询至响应结束,供修正系数 K(响应前)与预估响应容量(响应时段内)共用
         Map<String, BigDecimal> todayActualByTime = VppBaselineHelper.buildActualLoadByTimePoint(
                 vppTsdbQueryService.queryDeviceMetricHistory(
-                        deviceUuids, responseDate.atStartOfDay(), startTime, VppBaselineHelper.POWER_METRICS),
+                        deviceUuids, responseDate.atStartOfDay(), endTime, VppBaselineHelper.POWER_METRICS),
                 VppBaselineHelper.INTERVAL_MINUTES);
         BigDecimal correctionFactorK = VppBaselineHelper.calculateCorrectionFactor(
                 startTime, todayActualByTime, correctionBaselineByTime);
@@ -239,6 +259,7 @@ public class VppBaselineServiceImpl implements VppBaselineService {
                 VppBaselineHelper.INTERVAL_MINUTES,
                 null);
         if (historicalAvgByTime.isEmpty()) {
+            // return buildMockDeclaredCapacityResult(result, siteId, responseDate, startTime, endTime);
             return result;
         }
 
@@ -252,6 +273,7 @@ public class VppBaselineServiceImpl implements VppBaselineService {
         return result;
     }
 
+    /*
     private SiteDeclaredCapacityVO buildMockDeclaredCapacityResult(SiteDeclaredCapacityVO result,
                                                                  Long siteId,
                                                                  LocalDate responseDate,
@@ -269,6 +291,7 @@ public class VppBaselineServiceImpl implements VppBaselineService {
         result.setDataSource("mock");
         return result;
     }
+    */
 
     private Map<String, BigDecimal> applyCorrection(Map<String, BigDecimal> originalBaselineByTime,
                                                     BigDecimal correctionFactorK) {
@@ -280,6 +303,7 @@ public class VppBaselineServiceImpl implements VppBaselineService {
         return corrected;
     }
 
+    /*
     private SiteBaselineVO buildMockResult(SiteBaselineVO result,
                                            Long siteId,
                                            LocalDate responseDate,
@@ -292,6 +316,7 @@ public class VppBaselineServiceImpl implements VppBaselineService {
         result.setDataSource("mock");
         return result;
     }
+    */
 
     private List<String> listControllableDeviceUuids(Long siteId) {
         List<VppResourcePoint> resources = resourcePointMapper.selectList(

+ 6 - 5
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppCapabilityEvalServiceImpl.java

@@ -134,7 +134,7 @@ public class VppCapabilityEvalServiceImpl implements VppCapabilityEvalService {
         curve.setSiteId(siteId);
         curve.setSiteName(site != null ? site.getSiteName() : "全部站点");
         curve.setDate(statDate.format(DATE_FMT));
-        LoadCurveBuildResult buildResult = buildLoadCurvePoints(resources, devices, siteId, statDate);
+        LoadCurveBuildResult buildResult = buildLoadCurvePoints(resources, devices, statDate);
         curve.setPoints(buildResult.points);
         curve.setDataSource(buildResult.dataSource);
         return curve;
@@ -152,7 +152,6 @@ public class VppCapabilityEvalServiceImpl implements VppCapabilityEvalService {
 
     private LoadCurveBuildResult buildLoadCurvePoints(List<VppResourcePoint> resources,
                                                       List<VppDevice> devices,
-                                                      Long siteId,
                                                       LocalDate statDate) {
         List<String> deviceUuids = resolveDeviceUuids(resources, devices);
         if (!deviceUuids.isEmpty()) {
@@ -172,10 +171,12 @@ public class VppCapabilityEvalServiceImpl implements VppCapabilityEvalService {
                         VppCapabilityEvalHelper.buildLoadCurveFromTsdb(actualData, baselineData, statDate, powerMetrics),
                         "tsdb");
             }
+            return new LoadCurveBuildResult(Collections.emptyList(), "tsdb");
         }
-        return new LoadCurveBuildResult(
-                VppCapabilityEvalHelper.buildLoadCurve(resources, devices, siteId, statDate),
-                "mock");
+        // return new LoadCurveBuildResult(
+        //         VppCapabilityEvalHelper.buildLoadCurve(resources, devices, siteId, statDate),
+        //         "mock");
+        return new LoadCurveBuildResult(Collections.emptyList(), null);
     }
 
     @Override

+ 212 - 32
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppContractServiceImpl.java

@@ -14,6 +14,7 @@ import com.usky.vpp.mapper.VppContractTemplateMapper;
 import com.usky.vpp.mapper.VppCustomerMapper;
 import com.usky.vpp.service.VppContractService;
 import com.usky.vpp.service.vo.VppContractExpireStatVO;
+import com.usky.vpp.service.vo.VppContractOverviewVO;
 import com.usky.vpp.service.vo.VppContractRequestVO;
 import com.usky.vpp.service.vo.VppContractResponseVO;
 import com.usky.vpp.util.VppAuditHelper;
@@ -25,6 +26,7 @@ import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.client.RestTemplate;
 
 import javax.servlet.ServletOutputStream;
@@ -61,20 +63,27 @@ public class VppContractServiceImpl implements VppContractService {
     private final RestTemplate restTemplate = new RestTemplate();
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public Boolean create(VppContractRequestVO vo) {
-        // ========== 根据 templateId 查询模板编码并生成合同编号 ==========
+        if (StringUtils.isBlank(vo.getContractNo())) {
+            throw new BusinessException("合同编号不能为空!");
+        }
         VppContractTemplate template = getTemplateById(vo.getTemplateId());
-        String contractNo = VppFieldValidator.generateContractNo(template.getTemplateCode());
-
-        // ========== 合同编号全租户唯一性校验(极小概率冲突) ==========
+        String contractNo = vo.getContractNo().trim();
         checkContractNoUnique(contractNo, null);
+        // 关联校验必须在入库前完成,避免失败后留下脏数据导致“合同编号已存在”
+        validateRelatedContract(null, vo.getRelatedContractId(), template);
 
         VppContract contract = new VppContract();
         contract.setContractNo(contractNo);
         contract.setCustomerId(vo.getCustomerId());
         contract.setTemplateId(vo.getTemplateId());
+        contract.setRelatedContractId(vo.getRelatedContractId());
         contract.setContractType(vo.getContractType());
         contract.setContractName(vo.getContractName());
+        contract.setPartyA(vo.getPartyA());
+        contract.setPartyB(vo.getPartyB());
+        contract.setContractAmount(vo.getContractAmount());
         contract.setContractStatus(VppContractStatusHelper.resolveForPersist(
                 vo.getContractStatus(), vo.getEffectiveDate(), vo.getExpireDate()));
         contract.setSignDate(vo.getSignDate());
@@ -88,12 +97,15 @@ public class VppContractServiceImpl implements VppContractService {
         contract.setRemark(vo.getRemark());
 
         VppAuditHelper.fillCreate(contract);
-
         int insert = vppContractMapper.insert(contract);
+        if (insert > 0 && vo.getRelatedContractId() != null) {
+            syncPeerRelated(contract.getId(), vo.getRelatedContractId(), null);
+        }
         return insert > 0;
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public Boolean update(VppContractRequestVO vo) {
         if (vo.getId() == null) {
             throw new BusinessException("主键ID不能为空!");
@@ -108,7 +120,15 @@ public class VppContractServiceImpl implements VppContractService {
             throw new BusinessException("合同不存在或已被删除!");
         }
 
-        // 仅更新非空字段
+        Long previousRelatedId = existingRecord.getRelatedContractId();
+
+        if (StringUtils.isNotBlank(vo.getContractNo())) {
+            String contractNo = vo.getContractNo().trim();
+            if (!contractNo.equals(existingRecord.getContractNo())) {
+                checkContractNoUnique(contractNo, vo.getId());
+            }
+            existingRecord.setContractNo(contractNo);
+        }
         if (vo.getCustomerId() != null) {
             existingRecord.setCustomerId(vo.getCustomerId());
         }
@@ -121,6 +141,15 @@ public class VppContractServiceImpl implements VppContractService {
         if (StringUtils.isNotBlank(vo.getContractName())) {
             existingRecord.setContractName(vo.getContractName());
         }
+        if (vo.getPartyA() != null) {
+            existingRecord.setPartyA(vo.getPartyA());
+        }
+        if (vo.getPartyB() != null) {
+            existingRecord.setPartyB(vo.getPartyB());
+        }
+        if (vo.getContractAmount() != null) {
+            existingRecord.setContractAmount(vo.getContractAmount());
+        }
         if (vo.getSignDate() != null) {
             existingRecord.setSignDate(vo.getSignDate());
         }
@@ -154,9 +183,20 @@ public class VppContractServiceImpl implements VppContractService {
         existingRecord.setContractStatus(VppContractStatusHelper.resolveForPersist(
                 requestedStatus, existingRecord.getEffectiveDate(), existingRecord.getExpireDate()));
 
-        VppAuditHelper.fillUpdate(existingRecord);
+        // 关联变更:入库前先校验;写入 relatedContractId 后同步对端
+        boolean relatedChanged = vo.getRelatedContractId() != null
+                && !Objects.equals(vo.getRelatedContractId(), previousRelatedId);
+        if (relatedChanged) {
+            VppContractTemplate template = getTemplateById(existingRecord.getTemplateId());
+            validateRelatedContract(existingRecord.getId(), vo.getRelatedContractId(), template);
+            existingRecord.setRelatedContractId(vo.getRelatedContractId());
+        }
 
+        VppAuditHelper.fillUpdate(existingRecord);
         int result = vppContractMapper.updateById(existingRecord);
+        if (result > 0 && relatedChanged) {
+            syncPeerRelated(existingRecord.getId(), vo.getRelatedContractId(), previousRelatedId);
+        }
         return result > 0;
     }
 
@@ -204,6 +244,7 @@ public class VppContractServiceImpl implements VppContractService {
         // ========== 批量反查客户名称和模板名称 ==========
         Set<Long> customerIds = records.stream().map(VppContract::getCustomerId).filter(Objects::nonNull).collect(Collectors.toSet());
         Set<Long> templateIds = records.stream().map(VppContract::getTemplateId).filter(Objects::nonNull).collect(Collectors.toSet());
+        Set<Long> relatedIds = records.stream().map(VppContract::getRelatedContractId).filter(Objects::nonNull).collect(Collectors.toSet());
 
         final Map<Long, String> customerNameMap = customerIds.isEmpty() ? Collections.emptyMap()
                 : vppCustomerMapper.selectBatchIds(customerIds).stream()
@@ -213,39 +254,46 @@ public class VppContractServiceImpl implements VppContractService {
                 : vppContractTemplateMapper.selectBatchIds(templateIds).stream()
                         .collect(Collectors.toMap(VppContractTemplate::getId, VppContractTemplate::getTemplateName, (a, b) -> a));
 
+        final Map<Long, VppContract> relatedMap = relatedIds.isEmpty() ? Collections.emptyMap()
+                : vppContractMapper.selectBatchIds(relatedIds).stream()
+                        .collect(Collectors.toMap(VppContract::getId, c -> c, (a, b) -> a));
+
         // ========== 组装响应VO ==========
         List<VppContractResponseVO> list = records.stream().map(entity -> {
-            VppContractResponseVO vo = new VppContractResponseVO();
-            vo.setId(entity.getId());
-            vo.setContractNo(entity.getContractNo());
-            vo.setCustomerId(entity.getCustomerId());
-            vo.setCustomerName(customerNameMap.get(entity.getCustomerId()));
-            vo.setTemplateId(entity.getTemplateId());
-            vo.setTemplateName(templateNameMap.get(entity.getTemplateId()));
-            vo.setContractType(entity.getContractType());
-            vo.setContractName(entity.getContractName());
-            vo.setContractStatus(VppContractStatusHelper.resolve(
-                    entity.getContractStatus(), entity.getEffectiveDate(), entity.getExpireDate()));
-            vo.setSignDate(entity.getSignDate());
-            vo.setEffectiveDate(entity.getEffectiveDate());
-            vo.setExpireDate(entity.getExpireDate());
-            vo.setFileUrl(entity.getFileUrl());
-            vo.setCustomerRatio(entity.getCustomerRatio());
-            vo.setOperatorRatio(entity.getOperatorRatio());
-            vo.setPriceJson(entity.getPriceJson());
-            vo.setAccountInfoJson(entity.getAccountInfoJson());
-            vo.setRemark(entity.getRemark());
-            vo.setTenantId(entity.getTenantId());
-            vo.setCreateTime(entity.getCreateTime());
-            vo.setUpdateTime(entity.getUpdateTime());
-            vo.setCreatedBy(entity.getCreatedBy());
-            vo.setUpdatedBy(entity.getUpdatedBy());
+            VppContractResponseVO vo = toResponseVO(entity, customerNameMap, templateNameMap, relatedMap);
             return vo;
         }).collect(Collectors.toList());
 
         return new CommonPage<>(list, page.getTotal(), pageSize, pageNum);
     }
 
+    @Override
+    public VppContractOverviewVO overview(Long id) {
+        if (id == null) {
+            throw new BusinessException("主键ID不能为空!");
+        }
+        LambdaQueryWrapper<VppContract> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(VppContract::getId, id)
+                .eq(VppContract::getDeleteFlag, 0);
+        VppContract contract = vppContractMapper.selectOne(queryWrapper);
+        if (contract == null) {
+            throw new BusinessException("合同不存在或已被删除!");
+        }
+        VppContractOverviewVO vo = new VppContractOverviewVO();
+        vo.setId(contract.getId());
+        vo.setContractNo(contract.getContractNo());
+        vo.setContractName(contract.getContractName());
+        vo.setPartyA(contract.getPartyA());
+        vo.setPartyB(contract.getPartyB());
+        vo.setSignDate(contract.getSignDate());
+        vo.setContractAmount(contract.getContractAmount());
+        vo.setEffectiveDate(contract.getEffectiveDate());
+        vo.setExpireDate(contract.getExpireDate());
+        vo.setContractStatus(VppContractStatusHelper.resolve(
+                contract.getContractStatus(), contract.getEffectiveDate(), contract.getExpireDate()));
+        return vo;
+    }
+
     @Override
     public Boolean delete(Long id) {
         LambdaQueryWrapper<VppContract> queryWrapper = new LambdaQueryWrapper<>();
@@ -257,6 +305,20 @@ public class VppContractServiceImpl implements VppContractService {
             throw new BusinessException("合同不存在或已被删除!");
         }
 
+        // 解除一对一关联
+        if (contract.getRelatedContractId() != null) {
+            clearRelatedLink(contract.getId(), contract.getRelatedContractId());
+        }
+        // 若有对方指向自己,一并解除
+        List<VppContract> reverseLinks = vppContractMapper.selectList(new LambdaQueryWrapper<VppContract>()
+                .eq(VppContract::getRelatedContractId, id)
+                .eq(VppContract::getDeleteFlag, 0));
+        for (VppContract reverse : reverseLinks) {
+            reverse.setRelatedContractId(null);
+            VppAuditHelper.fillUpdate(reverse);
+            vppContractMapper.updateById(reverse);
+        }
+
         VppAuditHelper.fillSoftDelete(contract);
 
         int i = vppContractMapper.updateById(contract);
@@ -355,6 +417,124 @@ public class VppContractServiceImpl implements VppContractService {
 
     // ========== 内部辅助方法 ==========
 
+    private VppContractResponseVO toResponseVO(VppContract entity,
+                                               Map<Long, String> customerNameMap,
+                                               Map<Long, String> templateNameMap,
+                                               Map<Long, VppContract> relatedMap) {
+        VppContractResponseVO vo = new VppContractResponseVO();
+        vo.setId(entity.getId());
+        vo.setContractNo(entity.getContractNo());
+        vo.setCustomerId(entity.getCustomerId());
+        vo.setCustomerName(customerNameMap.get(entity.getCustomerId()));
+        vo.setTemplateId(entity.getTemplateId());
+        vo.setTemplateName(templateNameMap.get(entity.getTemplateId()));
+        vo.setRelatedContractId(entity.getRelatedContractId());
+        VppContract related = relatedMap.get(entity.getRelatedContractId());
+        if (related != null) {
+            vo.setRelatedContractNo(related.getContractNo());
+            vo.setRelatedContractName(related.getContractName());
+        }
+        vo.setContractType(entity.getContractType());
+        vo.setContractName(entity.getContractName());
+        vo.setPartyA(entity.getPartyA());
+        vo.setPartyB(entity.getPartyB());
+        vo.setContractAmount(entity.getContractAmount());
+        vo.setContractStatus(VppContractStatusHelper.resolve(
+                entity.getContractStatus(), entity.getEffectiveDate(), entity.getExpireDate()));
+        vo.setSignDate(entity.getSignDate());
+        vo.setEffectiveDate(entity.getEffectiveDate());
+        vo.setExpireDate(entity.getExpireDate());
+        vo.setFileUrl(entity.getFileUrl());
+        vo.setCustomerRatio(entity.getCustomerRatio());
+        vo.setOperatorRatio(entity.getOperatorRatio());
+        vo.setPriceJson(entity.getPriceJson());
+        vo.setAccountInfoJson(entity.getAccountInfoJson());
+        vo.setRemark(entity.getRemark());
+        vo.setTenantId(entity.getTenantId());
+        vo.setCreateTime(entity.getCreateTime());
+        vo.setUpdateTime(entity.getUpdateTime());
+        vo.setCreatedBy(entity.getCreatedBy());
+        vo.setUpdatedBy(entity.getUpdatedBy());
+        return vo;
+    }
+
+    /**
+     * 关联校验(入库前调用):跨类、一对一占用、不能关联自身
+     */
+    private void validateRelatedContract(Long selfId, Long relatedId, VppContractTemplate selfTemplate) {
+        if (relatedId == null) {
+            return;
+        }
+        if (selfId != null && relatedId.equals(selfId)) {
+            throw new BusinessException("不能关联合同自身!");
+        }
+        if (selfTemplate == null || selfTemplate.getTemplateKind() == null) {
+            throw new BusinessException("当前合同模板未配置分类(居间/普通),无法关联!");
+        }
+        if (selfTemplate.getTemplateKind() != 1 && selfTemplate.getTemplateKind() != 2) {
+            throw new BusinessException("模板分类无效,仅支持 1居间 2普通!");
+        }
+
+        VppContract related = vppContractMapper.selectOne(new LambdaQueryWrapper<VppContract>()
+                .eq(VppContract::getId, relatedId)
+                .eq(VppContract::getDeleteFlag, 0));
+        if (related == null) {
+            throw new BusinessException("关联合同不存在或已被删除!");
+        }
+        VppContractTemplate relatedTemplate = getTemplateById(related.getTemplateId());
+        if (relatedTemplate.getTemplateKind() == null) {
+            throw new BusinessException("关联合同模板未配置分类(居间/普通)!");
+        }
+        if (Objects.equals(selfTemplate.getTemplateKind(), relatedTemplate.getTemplateKind())) {
+            throw new BusinessException("仅允许居间合同与普通合同互相一对一关联!");
+        }
+
+        if (related.getRelatedContractId() != null
+                && (selfId == null || !related.getRelatedContractId().equals(selfId))) {
+            throw new BusinessException("目标合同已被其他合同关联,请先解除原关联!");
+        }
+        Integer occupied = vppContractMapper.selectCount(new LambdaQueryWrapper<VppContract>()
+                .eq(VppContract::getRelatedContractId, relatedId)
+                .eq(VppContract::getDeleteFlag, 0)
+                .ne(selfId != null, VppContract::getId, selfId));
+        if (occupied != null && occupied > 0) {
+            throw new BusinessException("目标合同已被其他合同关联,请先解除原关联!");
+        }
+    }
+
+    /**
+     * 同步对端一对一关联(调用前须已完成 validateRelatedContract)
+     */
+    private void syncPeerRelated(Long selfId, Long relatedId, Long previousRelatedId) {
+        if (previousRelatedId != null && !previousRelatedId.equals(relatedId)) {
+            clearRelatedLink(selfId, previousRelatedId);
+        }
+        if (relatedId == null || selfId == null) {
+            return;
+        }
+        VppContract related = vppContractMapper.selectOne(new LambdaQueryWrapper<VppContract>()
+                .eq(VppContract::getId, relatedId)
+                .eq(VppContract::getDeleteFlag, 0));
+        if (related == null) {
+            throw new BusinessException("关联合同不存在或已被删除!");
+        }
+        related.setRelatedContractId(selfId);
+        VppAuditHelper.fillUpdate(related);
+        vppContractMapper.updateById(related);
+    }
+
+    private void clearRelatedLink(Long selfId, Long relatedId) {
+        if (relatedId == null) {
+            return;
+        }
+        VppContract related = vppContractMapper.selectById(relatedId);
+        if (related != null && Objects.equals(related.getRelatedContractId(), selfId)) {
+            related.setRelatedContractId(null);
+            VppAuditHelper.fillUpdate(related);
+            vppContractMapper.updateById(related);
+        }
+    }
+
     /**
      * 根据模板ID查询模板信息
      */

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

@@ -58,11 +58,15 @@ public class VppContractTemplateServiceImpl implements VppContractTemplateServic
 
         // ========== templateCode 全租户唯一性校验 ==========
         checkTemplateCodeUnique(vo.getTemplateCode(), null, tenantId);
+        if (vo.getTemplateKind() != null && vo.getTemplateKind() != 1 && vo.getTemplateKind() != 2) {
+            throw new BusinessException("模板分类无效,仅支持 1居间/层间 2普通/直网!");
+        }
 
         VppContractTemplate template = new VppContractTemplate();
         template.setTemplateCode(vo.getTemplateCode());
         template.setTemplateName(vo.getTemplateName());
         template.setContractType(vo.getContractType());
+        template.setTemplateKind(vo.getTemplateKind());
         template.setVersion("V1.0");
         template.setFileUrl(vo.getFileUrl());
         template.setVariablesJson(VppFieldValidator.validateJson(vo.getVariablesJson(), "占位符变量定义"));
@@ -115,6 +119,13 @@ public class VppContractTemplateServiceImpl implements VppContractTemplateServic
             existingRecord.setContractType(vo.getContractType());
             hasNonEnabledChange = true;
         }
+        if (vo.getTemplateKind() != null) {
+            if (vo.getTemplateKind() != 1 && vo.getTemplateKind() != 2) {
+                throw new BusinessException("模板分类无效,仅支持 1居间/层间 2普通/直网!");
+            }
+            existingRecord.setTemplateKind(vo.getTemplateKind());
+            hasNonEnabledChange = true;
+        }
         if (StringUtils.isNotBlank(vo.getFileUrl())) {
             existingRecord.setFileUrl(vo.getFileUrl());
             hasNonEnabledChange = true;
@@ -170,6 +181,7 @@ public class VppContractTemplateServiceImpl implements VppContractTemplateServic
             responseVO.setTemplateCode(entity.getTemplateCode());
             responseVO.setTemplateName(entity.getTemplateName());
             responseVO.setContractType(entity.getContractType());
+            responseVO.setTemplateKind(entity.getTemplateKind());
             responseVO.setVersion(entity.getVersion());
             responseVO.setFileUrl(entity.getFileUrl());
             responseVO.setVariablesJson(entity.getVariablesJson());

+ 72 - 8
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppCustomerServiceImpl.java

@@ -10,10 +10,18 @@ import com.usky.vpp.domain.VppContract;
 import com.usky.vpp.domain.VppCustomer;
 import com.usky.vpp.domain.VppCustomerAccess;
 import com.usky.vpp.domain.VppCustomerContact;
+import com.usky.vpp.domain.VppDrInvitation;
+import com.usky.vpp.domain.VppDrParticipation;
+import com.usky.vpp.domain.VppSettlementBill;
+import com.usky.vpp.domain.VppSite;
 import com.usky.vpp.mapper.VppContractMapper;
 import com.usky.vpp.mapper.VppCustomerAccessMapper;
 import com.usky.vpp.mapper.VppCustomerContactMapper;
 import com.usky.vpp.mapper.VppCustomerMapper;
+import com.usky.vpp.mapper.VppDrInvitationMapper;
+import com.usky.vpp.mapper.VppDrParticipationMapper;
+import com.usky.vpp.mapper.VppSettlementBillMapper;
+import com.usky.vpp.mapper.VppSiteMapper;
 import com.usky.vpp.service.VppCustomerService;
 import com.usky.vpp.service.VppSiteService;
 import com.usky.vpp.service.vo.CustomerAccessAuditRequest;
@@ -73,6 +81,14 @@ public class VppCustomerServiceImpl implements VppCustomerService {
     private VppCustomerContactMapper contactMapper;
     @Autowired
     private VppContractMapper contractMapper;
+    @Autowired
+    private VppSiteMapper siteMapper;
+    @Autowired
+    private VppDrInvitationMapper invitationMapper;
+    @Autowired
+    private VppDrParticipationMapper participationMapper;
+    @Autowired
+    private VppSettlementBillMapper settlementBillMapper;
 
     // ==================== 准入管理 ====================
 
@@ -390,6 +406,10 @@ public class VppCustomerServiceImpl implements VppCustomerService {
         customer.setDrUpCapacityKw(vo.getDrUpCapacityKw());
         customer.setDrDownCapacityKw(vo.getDrDownCapacityKw());
         customer.setProvince(vo.getProvince());
+        customer.setIsGroupUser(vo.getIsGroupUser());
+        customer.setIsGreenPower(vo.getIsGreenPower());
+        customer.setIsCoopDevUser(vo.getIsCoopDevUser());
+        customer.setCoopPartnerName(vo.getCoopPartnerName());
         customer.setCity(vo.getCity());
         customer.setDistrict(vo.getDistrict());
         customer.setAddress(vo.getAddress());
@@ -584,6 +604,18 @@ public class VppCustomerServiceImpl implements VppCustomerService {
         if (vo.getProvince() != null) {
             existing.setProvince(vo.getProvince());
         }
+        if (vo.getIsGroupUser() != null) {
+            existing.setIsGroupUser(vo.getIsGroupUser());
+        }
+        if (vo.getIsGreenPower() != null) {
+            existing.setIsGreenPower(vo.getIsGreenPower());
+        }
+        if (vo.getIsCoopDevUser() != null) {
+            existing.setIsCoopDevUser(vo.getIsCoopDevUser());
+        }
+        if (vo.getCoopPartnerName() != null) {
+            existing.setCoopPartnerName(vo.getCoopPartnerName());
+        }
         if (vo.getCity() != null) {
             existing.setCity(vo.getCity());
         }
@@ -677,19 +709,51 @@ public class VppCustomerServiceImpl implements VppCustomerService {
     @Transactional(rollbackFor = Exception.class)
     public Boolean deleteCustomer(Long id) {
         VppCustomer customer = findCustomerById(id);
+        assertCustomerDeletable(id);
 
-        // 客户下存在合同(不论状态)则不允许删除
-        LambdaQueryWrapper<VppContract> contractWrapper = new LambdaQueryWrapper<>();
-        contractWrapper.eq(VppContract::getCustomerId, id)
-                .eq(VppContract::getDeleteFlag, 0);
-        Integer contractCount = contractMapper.selectCount(contractWrapper);
+        VppAuditHelper.fillSoftDelete(customer);
+        int result = customerMapper.updateById(customer);
+        return result > 0;
+    }
+
+    /**
+     * 客户下存在合同、站点、邀约、参与记录等业务数据时不允许删除。
+     */
+    private void assertCustomerDeletable(Long customerId) {
+        Integer contractCount = contractMapper.selectCount(new LambdaQueryWrapper<VppContract>()
+                .eq(VppContract::getCustomerId, customerId)
+                .eq(VppContract::getDeleteFlag, VppAuditHelper.NOT_DELETED));
         if (contractCount != null && contractCount > 0) {
             throw new BusinessException("该客户下存在合同,无法删除!");
         }
 
-        VppAuditHelper.fillSoftDelete(customer);
-        int result = customerMapper.updateById(customer);
-        return result > 0;
+        Integer siteCount = siteMapper.selectCount(new LambdaQueryWrapper<VppSite>()
+                .eq(VppSite::getCustomerId, customerId)
+                .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        if (siteCount != null && siteCount > 0) {
+            throw new BusinessException("该客户下存在站点,无法删除!");
+        }
+
+        Integer invitationCount = invitationMapper.selectCount(new LambdaQueryWrapper<VppDrInvitation>()
+                .eq(VppDrInvitation::getCustomerId, customerId)
+                .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        if (invitationCount != null && invitationCount > 0) {
+            throw new BusinessException("该客户下存在需求响应邀约,无法删除!");
+        }
+
+        Integer participationCount = participationMapper.selectCount(new LambdaQueryWrapper<VppDrParticipation>()
+                .eq(VppDrParticipation::getCustomerId, customerId)
+                .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        if (participationCount != null && participationCount > 0) {
+            throw new BusinessException("该客户下存在需求响应参与记录,无法删除!");
+        }
+
+        Integer billCount = settlementBillMapper.selectCount(new LambdaQueryWrapper<VppSettlementBill>()
+                .eq(VppSettlementBill::getCustomerId, customerId)
+                .eq(VppSettlementBill::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        if (billCount != null && billCount > 0) {
+            throw new BusinessException("该客户下存在结算账单,无法删除!");
+        }
     }
 
     @Override

+ 7 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDeviceServiceImpl.java

@@ -172,6 +172,9 @@ public class VppDeviceServiceImpl implements VppDeviceService {
         if (params.get("deviceType") != null) {
             wrapper.eq(VppDevice::getDeviceType, params.get("deviceType").toString());
         }
+        if (params.get("unResourceId") != null && StringUtils.hasText(params.get("unResourceId").toString())) {
+            wrapper.eq(VppDevice::getUnResourceId, params.get("unResourceId").toString().trim());
+        }
         applyRunStatusFilter(wrapper, params);
         return wrapper;
     }
@@ -228,6 +231,9 @@ public class VppDeviceServiceImpl implements VppDeviceService {
             throw new BusinessException("设备名称不能为空");
         }
         ensureUniqueField(VppDevice::getDeviceCode, request.getDeviceCode(), excludeId, "设备编号已存在");
+        if (StringUtils.hasText(request.getUnResourceId())) {
+            ensureUniqueField(VppDevice::getUnResourceId, request.getUnResourceId().trim(), excludeId, "运管资源ID已存在");
+        }
     }
 
     private void ensureUniqueField(com.baomidou.mybatisplus.core.toolkit.support.SFunction<VppDevice, ?> field,
@@ -252,6 +258,7 @@ public class VppDeviceServiceImpl implements VppDeviceService {
         device.setRatedPowerKw(request.getRatedPowerKw());
         device.setFirmwareVersion(request.getFirmwareVersion());
         device.setGatewayId(request.getGatewayId());
+        device.setUnResourceId(StringUtils.hasText(request.getUnResourceId()) ? request.getUnResourceId().trim() : null);
         device.setRemark(request.getRemark());
     }
 

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

@@ -489,34 +489,27 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
         if (CollectionUtils.isEmpty(invitations)) {
             return Collections.emptyList();
         }
-        Set<Long> eventIds = invitations.stream().map(VppDrInvitation::getDrEventId).collect(Collectors.toSet());
         Set<Long> customerIds = invitations.stream().map(VppDrInvitation::getCustomerId).collect(Collectors.toSet());
 
-        Map<Long, VppDrEvent> eventMap = eventMapper.selectBatchIds(eventIds).stream()
-                .collect(Collectors.toMap(VppDrEvent::getId, e -> e, (a, b) -> a));
         Map<Long, VppCustomer> customerMap = customerMapper.selectBatchIds(customerIds).stream()
                 .collect(Collectors.toMap(VppCustomer::getId, c -> c, (a, b) -> a));
 
         return invitations.stream()
-                .map(inv -> toVo(inv, eventMap.get(inv.getDrEventId()), customerMap.get(inv.getCustomerId()), null))
+                .map(inv -> toVo(inv, customerMap.get(inv.getCustomerId()), null))
                 .collect(Collectors.toList());
     }
 
     private DrInvitationVO toVo(VppDrInvitation invitation) {
-        VppDrEvent event = eventMapper.selectById(invitation.getDrEventId());
         VppCustomer customer = customerMapper.selectById(invitation.getCustomerId());
         VppCustomerContact contact = invitation.getSmsContactId() != null
                 ? contactMapper.selectById(invitation.getSmsContactId())
                 : null;
-        return toVo(invitation, event, customer, contact);
+        return toVo(invitation, customer, contact);
     }
 
-    private DrInvitationVO toVo(VppDrInvitation invitation, VppDrEvent event, VppCustomer customer, VppCustomerContact contact) {
+    private DrInvitationVO toVo(VppDrInvitation invitation, VppCustomer customer, VppCustomerContact contact) {
         DrInvitationVO vo = new DrInvitationVO();
         BeanUtils.copyProperties(invitation, vo);
-        if (event != null) {
-            vo.setDrEventCode(event.getEventId());
-        }
         if (customer != null) {
             vo.setCustomerName(customer.getCustomerName());
         }

+ 116 - 89
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrMonitorServiceImpl.java

@@ -8,10 +8,12 @@ import com.usky.common.security.utils.SecurityUtils;
 import com.usky.vpp.domain.VppDrEvent;
 import com.usky.vpp.domain.VppDrInvitation;
 import com.usky.vpp.domain.VppDrParticipation;
+import com.usky.vpp.domain.VppDrSubsidyPrediction;
 import com.usky.vpp.domain.VppResourcePoint;
 import com.usky.vpp.mapper.VppDrEventMapper;
 import com.usky.vpp.mapper.VppDrInvitationMapper;
 import com.usky.vpp.mapper.VppDrParticipationMapper;
+import com.usky.vpp.mapper.VppDrSubsidyPredictionMapper;
 import com.usky.vpp.mapper.VppResourcePointMapper;
 import com.usky.vpp.service.VppBaselineService;
 import com.usky.vpp.service.VppDrMonitorService;
@@ -20,6 +22,7 @@ import com.usky.vpp.service.vo.DrMonitorCurveVO;
 import com.usky.vpp.service.vo.DrMonitorRecordVO;
 import com.usky.vpp.service.vo.DrMonitorSummaryVO;
 import com.usky.vpp.service.vo.SiteBaselineVO;
+import com.usky.vpp.service.vo.SiteDeclaredCapacityVO;
 import com.usky.vpp.util.VppAuditHelper;
 import com.usky.vpp.util.VppPageHelper;
 import org.springframework.beans.BeanUtils;
@@ -36,13 +39,10 @@ import java.time.LocalTime;
 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.HashSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -76,6 +76,8 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
     private VppResourcePointMapper resourcePointMapper;
     @Autowired
     private VppBaselineService baselineService;
+    @Autowired
+    private VppDrSubsidyPredictionMapper subsidyPredictionMapper;
 
     @Override
     public DrMonitorSummaryVO getSummary(String startDate, String endDate, List<Long> siteIds) {
@@ -201,9 +203,11 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
         Map<Long, List<VppDrInvitation>> byEvent = invitations.stream()
                 .filter(i -> i.getDrEventId() != null)
                 .collect(Collectors.groupingBy(VppDrInvitation::getDrEventId));
+        Map<Long, BigDecimal> subsidyByEvent = loadSubsidyAmountByEvent(eventIds, peakSiteIds, tenantId);
 
         List<DrMonitorRecordVO> records = events.stream()
-                .map(e -> toRecordVo(e, byEvent.getOrDefault(e.getId(), Collections.emptyList())))
+                .map(e -> toRecordVo(e, byEvent.getOrDefault(e.getId(), Collections.emptyList()),
+                        subsidyByEvent.get(e.getId())))
                 .collect(Collectors.toList());
 
         Page<?> page = VppPageHelper.of(params);
@@ -218,104 +222,117 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
     }
 
     @Override
-    public DrMonitorCurveVO getCurve(Long eventId, List<Long> siteIds) {
-        if (eventId == null) {
-            throw new BusinessException("事件ID不能为空");
-        }
-        requireSiteIds(siteIds);
-        Integer tenantId = SecurityUtils.getTenantId();
-        VppDrEvent event = eventMapper.selectById(eventId);
-        if (event == null || VppAuditHelper.isDeleted(event.getDeleteFlag())) {
-            throw new BusinessException("事件不存在");
-        }
-        if (tenantId != null && event.getTenantId() != null && !tenantId.equals(event.getTenantId())) {
-            throw new BusinessException("事件不存在");
+    public DrMonitorCurveVO getCurve(Long siteId, Integer baselineDays) {
+        if (siteId == null) {
+            throw new BusinessException("站点ID不能为空");
         }
-        if (event.getStartTime() == null) {
-            throw new BusinessException("事件缺少开始时间");
+        if (baselineDays != null && baselineDays != 3 && baselineDays != 5) {
+            throw new BusinessException("基线负荷仅支持3天或5天维度查询!");
         }
-
-        List<VppResourcePoint> peakResources = listPeakResources(siteIds, tenantId);
+        Integer tenantId = SecurityUtils.getTenantId();
+        List<VppResourcePoint> peakResources = listPeakResources(siteId, tenantId);
         if (peakResources.isEmpty()) {
-            throw new BusinessException("所选站点下无可参与调峰的资源点");
+            throw new BusinessException("所选站点下无可参与调峰的资源点!");
         }
         int resourceCount = peakResources.size();
-        Map<Long, Long> siteResourceCount = new HashMap<>();
-        for (VppResourcePoint resource : peakResources) {
-            siteResourceCount.merge(resource.getSiteId(), 1L, Long::sum);
+
+        LocalDate today = LocalDate.now();
+        LocalDateTime windowStart = today.atStartOfDay();
+        LocalDateTime windowEnd = today.atTime(LocalTime.MAX);
+        String responseStartTime = windowStart.format(DATE_TIME_FMT);
+        String responseEndTime = windowEnd.format(DATE_TIME_FMT);
+        String actualEndTime = windowEnd.format(DATE_TIME_FMT);
+        int requiredDays = (baselineDays != null && baselineDays == 3) ? 3 : 5;
+
+        BigDecimal actualAvg = avg(resolveSiteActualResponseKw(
+                siteId, responseStartTime, responseEndTime, BigDecimal.ZERO), resourceCount);
+
+        SiteBaselineVO baseline = baselineService.getSiteBaseline(
+                siteId, responseStartTime, requiredDays, actualEndTime);
+
+        DrMonitorCurveVO curve = new DrMonitorCurveVO();
+        curve.setResponseDate(today.format(DATE_FMT));
+        curve.setSiteId(siteId);
+        curve.setActualResponseCapacityKw(actualAvg);
+        if (baseline != null && StringUtils.hasText(baseline.getDataSource())) {
+            curve.setDataSource(baseline.getDataSource());
         }
-        Set<Long> peakSiteIds = siteResourceCount.keySet();
+        // if (dataSources.isEmpty()) {
+        //     curve.setDataSource("mock");
+        // }
 
-        List<VppDrInvitation> invitations = listInvitations(
-                Collections.singleton(event.getId()), peakSiteIds, tenantId);
-        BigDecimal declaredSum = BigDecimal.ZERO;
-        BigDecimal actualSum = BigDecimal.ZERO;
-        for (VppDrInvitation invitation : invitations) {
-            declaredSum = declaredSum.add(nz(invitation.getDeclaredCapacityKw()));
-            actualSum = actualSum.add(nz(invitation.getActualResponseCapacityKw()));
-        }
-        BigDecimal declaredAvg = avg(declaredSum, resourceCount);
-        BigDecimal actualAvg = avg(actualSum, resourceCount);
-
-        String responseStartTime = event.getStartTime().format(DATE_TIME_FMT);
-        // 按资源点加权:站点曲线 × 该站可调峰资源数,再 / 总资源数
-        Map<String, BigDecimal> baselineWeighted = new LinkedHashMap<>();
-        Map<String, BigDecimal> loadWeighted = new LinkedHashMap<>();
-        Set<String> dataSources = new LinkedHashSet<>();
-        List<Long> usedSiteIds = new ArrayList<>();
-
-        for (Map.Entry<Long, Long> entry : siteResourceCount.entrySet()) {
-            Long siteId = entry.getKey();
-            BigDecimal weight = BigDecimal.valueOf(entry.getValue());
-            SiteBaselineVO baseline = baselineService.getSiteBaseline(siteId, responseStartTime);
-            if (baseline == null || CollectionUtils.isEmpty(baseline.getPoints())) {
+        if (baseline == null || CollectionUtils.isEmpty(baseline.getPoints())) {
+            return curve;
+        }
+        for (BaselinePointVO point : baseline.getPoints()) {
+            if (!StringUtils.hasText(point.getTime()) || !inCurveWindow(point.getTime(), windowStart, windowEnd)) {
                 continue;
             }
-            usedSiteIds.add(siteId);
-            if (StringUtils.hasText(baseline.getDataSource())) {
-                dataSources.add(baseline.getDataSource());
+            DrMonitorCurveVO.Point curvePoint = new DrMonitorCurveVO.Point();
+            curvePoint.setTime(point.getTime());
+            curvePoint.setBaselineKw(nz(point.getPredictedBaselineKw()).setScale(SCALE, RoundingMode.HALF_UP));
+            curvePoint.setActualResponseCapacityKw(actualAvg);
+            curve.getPoints().add(curvePoint);
+        }
+        return curve;
+    }
+
+    /**
+     * 实际响应容量复用申报容量链路 {@link VppBaselineService#getSiteDeclaredCapacity},
+     * 取预估响应容量;无数据时回退邀约实际响应容量。
+     */
+    private BigDecimal resolveSiteActualResponseKw(Long siteId, String responseStartTime,
+                                                   String responseEndTime, BigDecimal fallbackKw) {
+        if (siteId == null || !StringUtils.hasText(responseStartTime) || !StringUtils.hasText(responseEndTime)) {
+            return nz(fallbackKw);
+        }
+        try {
+            SiteDeclaredCapacityVO capacity = baselineService.getSiteDeclaredCapacity(
+                    siteId, responseStartTime, responseEndTime);
+            if (capacity != null && capacity.getEstimatedResponseCapacityKw() != null
+                    && capacity.getEstimatedResponseCapacityKw().compareTo(BigDecimal.ZERO) > 0) {
+                return capacity.getEstimatedResponseCapacityKw();
             }
-            for (BaselinePointVO point : baseline.getPoints()) {
-                if (!StringUtils.hasText(point.getTime())) {
-                    continue;
-                }
-                baselineWeighted.merge(point.getTime(),
-                        nz(point.getPredictedBaselineKw()).multiply(weight), BigDecimal::add);
-                loadWeighted.merge(point.getTime(),
-                        nz(point.getTodayActualKw()).multiply(weight), BigDecimal::add);
+        } catch (Exception ignored) {
+            // 回退邀约实际响应容量,保持曲线可返回
+        }
+        return nz(fallbackKw);
+    }
+
+    private boolean inCurveWindow(String timeKey, LocalDateTime windowStart, LocalDateTime windowEnd) {
+        try {
+            LocalTime t = LocalTime.parse(timeKey.length() == 5 ? timeKey : timeKey.substring(0, 5));
+            LocalDate date = windowStart.toLocalDate();
+            LocalDateTime point = LocalDateTime.of(date, t);
+            // 跨日窗口兜底:若点早于 windowStart 且 windowEnd 跨日,放到次日
+            if (point.isBefore(windowStart) && windowEnd.toLocalDate().isAfter(date)) {
+                point = point.plusDays(1);
             }
+            return !point.isBefore(windowStart) && !point.isAfter(windowEnd);
+        } catch (Exception ex) {
+            return true;
         }
+    }
 
-        BigDecimal divisor = BigDecimal.valueOf(resourceCount);
-        DrMonitorCurveVO curve = new DrMonitorCurveVO();
-        curve.setId(event.getId());
-        curve.setEventId(event.getEventId());
-        curve.setResponseDate(event.getStartTime().toLocalDate().format(DATE_FMT));
-        curve.setSiteIds(usedSiteIds);
-        curve.setDeclaredCapacityKw(declaredAvg);
-        curve.setActualResponseCapacityKw(actualAvg);
-        if (dataSources.isEmpty()) {
-            curve.setDataSource("mock");
-        } else if (dataSources.size() == 1) {
-            curve.setDataSource(dataSources.iterator().next());
-        } else {
-            curve.setDataSource("mixed");
-        }
-
-        List<String> times = new ArrayList<>(baselineWeighted.keySet());
-        times.sort(Comparator.naturalOrder());
-        for (String time : times) {
-            DrMonitorCurveVO.Point point = new DrMonitorCurveVO.Point();
-            point.setTime(time);
-            point.setBaselineKw(baselineWeighted.getOrDefault(time, BigDecimal.ZERO)
-                    .divide(divisor, SCALE, RoundingMode.HALF_UP));
-            point.setLoadKw(loadWeighted.getOrDefault(time, BigDecimal.ZERO)
-                    .divide(divisor, SCALE, RoundingMode.HALF_UP));
-            point.setDeclaredCapacityKw(declaredAvg);
-            point.setActualResponseCapacityKw(actualAvg);
-            curve.getPoints().add(point);
+    private Map<Long, BigDecimal> loadSubsidyAmountByEvent(Set<Long> eventIds, Set<Long> siteIds, Integer tenantId) {
+        if (CollectionUtils.isEmpty(eventIds)) {
+            return Collections.emptyMap();
         }
-        return curve;
+        LambdaQueryWrapper<VppDrSubsidyPrediction> wrapper = new LambdaQueryWrapper<>();
+        wrapper.in(VppDrSubsidyPrediction::getDrEventId, eventIds)
+                .eq(tenantId != null, VppDrSubsidyPrediction::getTenantId, tenantId);
+        if (!CollectionUtils.isEmpty(siteIds)) {
+            wrapper.in(VppDrSubsidyPrediction::getSiteId, siteIds);
+        }
+        List<VppDrSubsidyPrediction> list = subsidyPredictionMapper.selectList(wrapper);
+        Map<Long, BigDecimal> result = new HashMap<>();
+        for (VppDrSubsidyPrediction item : list) {
+            if (item.getDrEventId() == null || item.getEstimatedSubsidyAmount() == null) {
+                continue;
+            }
+            result.merge(item.getDrEventId(), item.getEstimatedSubsidyAmount(), BigDecimal::add);
+        }
+        return result;
     }
 
     // ---------- loaders ----------
@@ -326,6 +343,14 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
         }
     }
 
+    private List<VppResourcePoint> listPeakResources(Long siteId, Integer tenantId) {
+        return resourcePointMapper.selectList(new LambdaQueryWrapper<VppResourcePoint>()
+                .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(VppResourcePoint::getIsSupportPeak, SUPPORT_PEAK)
+                .eq(VppResourcePoint::getSiteId, siteId)
+                .eq(tenantId != null, VppResourcePoint::getTenantId, tenantId));
+    }
+
     private List<VppResourcePoint> listPeakResources(List<Long> siteIds, Integer tenantId) {
         return resourcePointMapper.selectList(new LambdaQueryWrapper<VppResourcePoint>()
                 .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
@@ -391,9 +416,11 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
                 .eq(tenantId != null, VppDrInvitation::getTenantId, tenantId));
     }
 
-    private DrMonitorRecordVO toRecordVo(VppDrEvent event, List<VppDrInvitation> invitations) {
+    private DrMonitorRecordVO toRecordVo(VppDrEvent event, List<VppDrInvitation> invitations,
+                                         BigDecimal estimatedSubsidyAmount) {
         DrMonitorRecordVO vo = new DrMonitorRecordVO();
         BeanUtils.copyProperties(event, vo);
+        vo.setEstimatedSubsidyAmount(estimatedSubsidyAmount);
         if (CollectionUtils.isEmpty(invitations)) {
             vo.setResponseCompletionRate(null);
             vo.setQualified(false);

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

@@ -156,6 +156,7 @@ public class VppDrServiceImpl implements VppDrService {
         event.setAttachmentUrl(request.getAttachmentUrl());
         event.setAttachmentName(request.getAttachmentName());
         event.setSubsidyPrice(request.getSubsidyPrice());
+        event.setFloatingCoefficient(request.getFloatingCoefficient());
         event.setEventStatus(EVENT_STATUS_PENDING);
         VppAuditHelper.fillCreate(event);
         eventMapper.insert(event);
@@ -188,6 +189,7 @@ public class VppDrServiceImpl implements VppDrService {
         event.setAttachmentUrl(request.getAttachmentUrl());
         event.setAttachmentName(request.getAttachmentName());
         event.setSubsidyPrice(request.getSubsidyPrice());
+        event.setFloatingCoefficient(request.getFloatingCoefficient());
         VppAuditHelper.fillUpdate(event);
         eventMapper.updateById(event);
     }
@@ -837,6 +839,9 @@ public class VppDrServiceImpl implements VppDrService {
         if (request.getSubsidyPrice() != null && request.getSubsidyPrice().compareTo(BigDecimal.ZERO) < 0) {
             throw new BusinessException("补贴标准不能为负数");
         }
+        if (request.getFloatingCoefficient() == null) {
+            throw new BusinessException("下浮系数不能为空");
+        }
     }
 
     private void ensureEventIdUnique(String eventId, Long excludeId) {
@@ -889,6 +894,7 @@ public class VppDrServiceImpl implements VppDrService {
         vo.setAttachmentUrl(event.getAttachmentUrl());
         vo.setAttachmentName(event.getAttachmentName());
         vo.setSubsidyPrice(event.getSubsidyPrice());
+        vo.setFloatingCoefficient(event.getFloatingCoefficient());
         vo.setEventStatus(event.getEventStatus());
         vo.setParticipations(loadParticipationVos(event.getId()));
         return vo;

+ 441 - 50
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrSubsidyPredictionServiceImpl.java

@@ -4,17 +4,24 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.common.core.exception.BusinessException;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.vpp.domain.VppCustomer;
 import com.usky.vpp.domain.VppDrEvent;
 import com.usky.vpp.domain.VppDrInvitation;
 import com.usky.vpp.domain.VppDrSubsidyPrediction;
+import com.usky.vpp.domain.VppResponseDeviationPrice;
 import com.usky.vpp.domain.VppSite;
-import com.usky.vpp.mapper.VppDrEventMapper;
-import com.usky.vpp.mapper.VppDrInvitationMapper;
-import com.usky.vpp.mapper.VppDrSubsidyPredictionMapper;
-import com.usky.vpp.mapper.VppSiteMapper;
+import com.usky.vpp.mapper.*;
+import com.usky.vpp.mapper.VppCustomerMapper;
+import com.usky.vpp.mapper.VppResponseDeviationPriceMapper;
+import com.usky.vpp.service.VppBaselineService;
 import com.usky.vpp.service.VppDrSubsidyPredictionService;
+import com.usky.vpp.service.vo.DrSubsidyPredictionCalculateRequest;
+import com.usky.vpp.service.vo.DrSubsidyPredictionRequest;
 import com.usky.vpp.service.vo.DrSubsidyPredictionVO;
+import com.usky.vpp.service.vo.SiteDeclaredCapacityVO;
 import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppResponseCoefficientHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -27,12 +34,12 @@ import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.time.Duration;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
-import java.util.function.Function;
 import java.util.stream.Collectors;
 
 /**
@@ -42,6 +49,8 @@ import java.util.stream.Collectors;
 public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPredictionService {
 
     private static final Logger log = LoggerFactory.getLogger(VppDrSubsidyPredictionServiceImpl.class);
+    private static final BigDecimal MINUTES_PER_HOUR = BigDecimal.valueOf(60);
+    private static final DateTimeFormatter DATE_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
 
     @Autowired
     private VppDrSubsidyPredictionMapper subsidyPredictionMapper;
@@ -51,18 +60,30 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
     private VppDrEventMapper eventMapper;
     @Autowired
     private VppSiteMapper siteMapper;
+    @Autowired
+    private VppCustomerMapper customerMapper;
+    @Autowired
+    private VppResponseDeviationPriceMapper priceAdjustmentMapper;
+    @Autowired
+    private VppBaselineService vppBaselineService;
 
     @Override
-    public CommonPage<DrSubsidyPredictionVO> page(String siteName, String startTime, String endTime,
+    public CommonPage<DrSubsidyPredictionVO> page(String siteName, String customerName,
+                                                   String startTime, String endTime,
                                                    Integer current, Integer size) {
         Page<VppDrSubsidyPrediction> page = new Page<>(
                 current != null ? current : 1,
                 size != null && size <= 100 ? size : 20);
 
         LambdaQueryWrapper<VppDrSubsidyPrediction> wrapper = new LambdaQueryWrapper<>();
+        Integer tenantId = SecurityUtils.getTenantId();
+        wrapper.eq(tenantId != null, VppDrSubsidyPrediction::getTenantId, tenantId);
         if (StringUtils.hasText(siteName)) {
             wrapper.like(VppDrSubsidyPrediction::getSiteName, siteName);
         }
+        if (StringUtils.hasText(customerName)) {
+            wrapper.like(VppDrSubsidyPrediction::getCustomerName, customerName);
+        }
         if (StringUtils.hasText(startTime)) {
             wrapper.ge(VppDrSubsidyPrediction::getEventStartTime, startTime);
         }
@@ -72,14 +93,151 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         wrapper.orderByDesc(VppDrSubsidyPrediction::getCreateTime);
 
         Page<VppDrSubsidyPrediction> result = subsidyPredictionMapper.selectPage(page, wrapper);
-
+        Map<Long, String> invitationNoMap = loadInvitationNoMap(result.getRecords());
         List<DrSubsidyPredictionVO> list = result.getRecords().stream()
-                .map(this::toVo)
+                .map(item -> toVo(item, invitationNoMap))
                 .collect(Collectors.toList());
 
         return new CommonPage<>(list, result.getTotal(), size != null ? size : 20, current != null ? current : 1);
     }
 
+    @Override
+    public DrSubsidyPredictionVO calculate(DrSubsidyPredictionCalculateRequest request) {
+        if (request == null) {
+            throw new BusinessException("请求参数不能为空");
+        }
+        if (!StringUtils.hasText(request.getInvitationNo())) {
+            throw new BusinessException("邀约编号不能为空");
+        }
+        if (request.getActualResponseGridKw() == null) {
+            throw new BusinessException("实际响应容量(电网)不能为空");
+        }
+
+        VppDrInvitation invitation = findInvitationByNo(request.getInvitationNo().trim());
+        VppDrEvent event = invitation.getDrEventId() == null ? null : eventMapper.selectById(invitation.getDrEventId());
+        if (event == null || VppAuditHelper.isDeleted(event.getDeleteFlag())) {
+            throw new BusinessException("邀约关联的需求响应事件不存在");
+        }
+
+        VppDrSubsidyPrediction entity = new VppDrSubsidyPrediction();
+        entity.setInvitationId(invitation.getId());
+        entity.setDrEventId(event.getId());
+        entity.setSiteId(invitation.getSiteId());
+        fillSiteName(entity);
+        entity.setCustomerId(invitation.getCustomerId());
+        fillCustomerName(entity);
+        entity.setEventStartTime(event.getStartTime());
+        entity.setEventEndTime(event.getEndTime());
+        entity.setSubsidyPrice(event.getSubsidyPrice());
+
+        BigDecimal declaredKw = invitation.getDeclaredCapacityKw() != null
+                ? invitation.getDeclaredCapacityKw()
+                : invitation.getActualClearedCapacityKw();
+        BigDecimal actualClearedKw = invitation.getActualClearedCapacityKw() != null
+                ? invitation.getActualClearedCapacityKw()
+                : declaredKw;
+        BigDecimal platformKw = resolvePlatformActualResponseKw(
+                invitation.getSiteId(), event.getStartTime(), event.getEndTime(),
+                invitation.getEstimatedResponseCapacityKw());
+
+        entity.setActualResponseGridKw(request.getActualResponseGridKw());
+        entity.setActualResponsePlatformKw(platformKw);
+        entity.setDeclaredClearedKw(declaredKw);
+        entity.setEstimatedCapacityKw(platformKw);
+        entity.setClearedCapacityKw(actualClearedKw);
+        entity.setTenantId(event.getTenantId() != null ? event.getTenantId() : invitation.getTenantId());
+
+        recalculate(entity, loadActivePriceAdjustments(
+                entity.getTenantId() != null ? entity.getTenantId() : SecurityUtils.getTenantId()));
+
+        Map<Long, String> invitationNoMap = Collections.singletonMap(invitation.getId(), invitation.getInvitationNo());
+        return toVo(entity, invitationNoMap);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean create(DrSubsidyPredictionRequest request) {
+        if (request == null) {
+            throw new BusinessException("请求参数不能为空");
+        }
+        assertInvitationNotDuplicated(request.getInvitationId(), null);
+
+        VppDrSubsidyPrediction entity = new VppDrSubsidyPrediction();
+        applyRequest(entity, request);
+        fillCustomerFromInvitationIfAbsent(entity);
+        fillSiteName(entity);
+        fillCustomerName(entity);
+        entity.setTenantId(SecurityUtils.getTenantId());
+        entity.setCreateTime(LocalDateTime.now());
+        entity.setUpdateTime(LocalDateTime.now());
+        return subsidyPredictionMapper.insert(entity) > 0;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean update(DrSubsidyPredictionRequest request) {
+        if (request == null || request.getId() == null) {
+            throw new BusinessException("主键ID不能为空");
+        }
+        VppDrSubsidyPrediction existing = subsidyPredictionMapper.selectById(request.getId());
+        if (existing == null) {
+            throw new BusinessException("补贴预测记录不存在");
+        }
+        Integer tenantId = SecurityUtils.getTenantId();
+        if (tenantId != null && existing.getTenantId() != null && !tenantId.equals(existing.getTenantId())) {
+            throw new BusinessException("补贴预测记录不存在");
+        }
+        Long invitationId = request.getInvitationId() != null
+                ? request.getInvitationId()
+                : existing.getInvitationId();
+        assertInvitationNotDuplicated(invitationId, existing.getId());
+
+        applyRequest(existing, request);
+        if (request.getSiteId() != null) {
+            fillSiteName(existing);
+        }
+        if (request.getCustomerId() != null) {
+            fillCustomerName(existing);
+        }
+        existing.setUpdateTime(LocalDateTime.now());
+        return subsidyPredictionMapper.updateById(existing) > 0;
+    }
+
+    /**
+     * 同一邀约不可重复新增补贴预测。
+     */
+    private void assertInvitationNotDuplicated(Long invitationId, Long excludeId) {
+        if (invitationId == null) {
+            return;
+        }
+        Integer tenantId = SecurityUtils.getTenantId();
+        Integer count = subsidyPredictionMapper.selectCount(
+                new LambdaQueryWrapper<VppDrSubsidyPrediction>()
+                        .eq(VppDrSubsidyPrediction::getInvitationId, invitationId)
+                        .eq(tenantId != null, VppDrSubsidyPrediction::getTenantId, tenantId)
+                        .ne(excludeId != null, VppDrSubsidyPrediction::getId, excludeId));
+        if (count != null && count > 0) {
+            throw new BusinessException("该邀约事件已添加补贴金额预测,不能重复添加!");
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean delete(Long id) {
+        if (id == null) {
+            throw new BusinessException("主键ID不能为空");
+        }
+        VppDrSubsidyPrediction existing = subsidyPredictionMapper.selectById(id);
+        if (existing == null) {
+            throw new BusinessException("补贴预测记录不存在");
+        }
+        Integer tenantId = SecurityUtils.getTenantId();
+        if (tenantId != null && existing.getTenantId() != null && !tenantId.equals(existing.getTenantId())) {
+            throw new BusinessException("补贴预测记录不存在");
+        }
+        return subsidyPredictionMapper.deleteById(id) > 0;
+    }
+
     @Override
     @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
     public void generateByEventId(Long drEventId) {
@@ -87,26 +245,22 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
             throw new BusinessException("事件ID不能为空");
         }
 
-        // 查询事件
         VppDrEvent event = eventMapper.selectById(drEventId);
         if (event == null) {
             log.warn("补贴预测生成跳过:事件不存在 drEventId={}", drEventId);
             return;
         }
 
-        // 查询事件下所有邀约
         List<VppDrInvitation> invitations = invitationMapper.selectList(
                 new LambdaQueryWrapper<VppDrInvitation>()
                         .eq(VppDrInvitation::getDrEventId, drEventId)
                         .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
         );
-
         if (invitations.isEmpty()) {
             log.info("补贴预测生成跳过:事件下无邀约 drEventId={}", drEventId);
             return;
         }
 
-        // 批量查询站点名称
         Set<Long> siteIds = invitations.stream()
                 .map(VppDrInvitation::getSiteId)
                 .filter(Objects::nonNull)
@@ -115,18 +269,25 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
                 : siteMapper.selectBatchIds(siteIds).stream()
                         .collect(Collectors.toMap(VppSite::getId, VppSite::getSiteName, (a, b) -> a));
 
-        // 逐条计算并 upsert
+        Set<Long> customerIds = invitations.stream()
+                .map(VppDrInvitation::getCustomerId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        Map<Long, String> customerNameMap = loadCustomerNameMap(customerIds);
+
+        List<VppResponseDeviationPrice> adjustments = loadActivePriceAdjustments(event.getTenantId());
+
         for (VppDrInvitation invitation : invitations) {
-            VppDrSubsidyPrediction prediction = buildPrediction(event, invitation, siteNameMap);
+            VppDrSubsidyPrediction prediction = buildPrediction(
+                    event, invitation, siteNameMap, customerNameMap, adjustments);
 
-            // 按 invitation_id 唯一键,存在则更新,不存在则新增
             VppDrSubsidyPrediction existing = subsidyPredictionMapper.selectOne(
                     new LambdaQueryWrapper<VppDrSubsidyPrediction>()
                             .eq(VppDrSubsidyPrediction::getInvitationId, invitation.getId())
             );
-
             if (existing != null) {
                 prediction.setId(existing.getId());
+                prediction.setCreateTime(existing.getCreateTime());
                 prediction.setUpdateTime(LocalDateTime.now());
                 subsidyPredictionMapper.updateById(prediction);
             } else {
@@ -139,71 +300,301 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         log.info("补贴预测生成完成 drEventId={}, invitationCount={}", drEventId, invitations.size());
     }
 
-    /**
-     * 构建单条补贴预测记录
-     */
     private VppDrSubsidyPrediction buildPrediction(VppDrEvent event, VppDrInvitation invitation,
-                                                    Map<Long, String> siteNameMap) {
+                                                    Map<Long, String> siteNameMap,
+                                                    Map<Long, String> customerNameMap,
+                                                    List<VppResponseDeviationPrice> adjustments) {
         VppDrSubsidyPrediction p = new VppDrSubsidyPrediction();
         p.setInvitationId(invitation.getId());
         p.setDrEventId(event.getId());
         p.setSiteId(invitation.getSiteId());
-        p.setSiteName(invitation.getSiteId() == null
-                ? null
-                : siteNameMap.getOrDefault(invitation.getSiteId(), null));
+        p.setSiteName(invitation.getSiteId() == null ? null : siteNameMap.get(invitation.getSiteId()));
+        p.setCustomerId(invitation.getCustomerId());
+        p.setCustomerName(invitation.getCustomerId() == null ? null : customerNameMap.get(invitation.getCustomerId()));
         p.setEventStartTime(event.getStartTime());
         p.setEventEndTime(event.getEndTime());
+        p.setSubsidyPrice(event.getSubsidyPrice());
 
-        // 响应时长(分钟)
-        int durationMin = 0;
-        if (event.getStartTime() != null && event.getEndTime() != null
-                && event.getEndTime().isAfter(event.getStartTime())) {
-            durationMin = (int) Duration.between(event.getStartTime(), event.getEndTime()).toMinutes();
+        BigDecimal platformKw = resolvePlatformActualResponseKw(
+                invitation.getSiteId(), event.getStartTime(), event.getEndTime(),
+                invitation.getEstimatedResponseCapacityKw());
+        BigDecimal gridKw = invitation.getActualResponseCapacityKw();
+        BigDecimal declaredKw = invitation.getDeclaredCapacityKw() != null
+                ? invitation.getDeclaredCapacityKw()
+                : invitation.getActualClearedCapacityKw();
+        BigDecimal actualClearedKw = invitation.getActualClearedCapacityKw() != null
+                ? invitation.getActualClearedCapacityKw()
+                : declaredKw;
+
+        p.setActualResponsePlatformKw(platformKw);
+        p.setActualResponseGridKw(gridKw);
+        p.setDeclaredClearedKw(declaredKw);
+        p.setEstimatedCapacityKw(platformKw);
+        p.setClearedCapacityKw(actualClearedKw);
+        p.setTenantId(event.getTenantId());
+        recalculate(p, adjustments);
+        return p;
+    }
+
+    /**
+     * 平台实际响应量:复用申报容量链路(可调峰资源点→设备→TSDB 响应时段有功功率均值)。
+     * TSDB 无数据时回退邀约上的预估响应容量。
+     */
+    private BigDecimal resolvePlatformActualResponseKw(Long siteId,
+                                                       LocalDateTime startTime,
+                                                       LocalDateTime endTime,
+                                                       BigDecimal fallbackKw) {
+        if (siteId == null || startTime == null || endTime == null || !endTime.isAfter(startTime)) {
+            return fallbackKw;
         }
-        p.setResponseDurationMin(durationMin);
+        try {
+            SiteDeclaredCapacityVO capacity = vppBaselineService.getSiteDeclaredCapacity(
+                    siteId,
+                    startTime.format(DATE_TIME_FMT),
+                    endTime.format(DATE_TIME_FMT));
+            if (capacity != null && capacity.getEstimatedResponseCapacityKw() != null
+                    && capacity.getEstimatedResponseCapacityKw().compareTo(BigDecimal.ZERO) > 0) {
+                return capacity.getEstimatedResponseCapacityKw();
+            }
+        } catch (Exception ex) {
+            log.warn("查询平台实际响应量失败,回退邀约预估容量 siteId={}, err={}", siteId, ex.getMessage());
+        }
+        return fallbackKw;
+    }
 
-        // 补贴标准
-        p.setSubsidyPrice(event.getSubsidyPrice());
+    private VppDrInvitation findInvitationByNo(String invitationNo) {
+        Integer tenantId = SecurityUtils.getTenantId();
+        VppDrInvitation invitation = invitationMapper.selectOne(
+                new LambdaQueryWrapper<VppDrInvitation>()
+                        .eq(VppDrInvitation::getInvitationNo, invitationNo)
+                        .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .eq(tenantId != null, VppDrInvitation::getTenantId, tenantId)
+                        .last("LIMIT 1"));
+        if (invitation == null) {
+            throw new BusinessException("邀约不存在:" + invitationNo);
+        }
+        return invitation;
+    }
 
-        // 预估响应容量
-        p.setEstimatedCapacityKw(invitation.getEstimatedResponseCapacityKw());
+    private void applyRequest(VppDrSubsidyPrediction entity, DrSubsidyPredictionRequest request) {
+        if (request.getInvitationId() != null) {
+            entity.setInvitationId(request.getInvitationId());
+        }
+        if (request.getDrEventId() != null) {
+            entity.setDrEventId(request.getDrEventId());
+        }
+        if (request.getSiteId() != null) {
+            entity.setSiteId(request.getSiteId());
+        }
+        if (request.getCustomerId() != null) {
+            entity.setCustomerId(request.getCustomerId());
+        }
+        if (StringUtils.hasText(request.getCustomerName())) {
+            entity.setCustomerName(request.getCustomerName().trim());
+        }
+        if (request.getEventStartTime() != null) {
+            entity.setEventStartTime(request.getEventStartTime());
+        }
+        if (request.getEventEndTime() != null) {
+            entity.setEventEndTime(request.getEventEndTime());
+        }
+        if (request.getSubsidyPrice() != null) {
+            entity.setSubsidyPrice(request.getSubsidyPrice());
+        }
+        if (request.getActualResponseGridKw() != null) {
+            entity.setActualResponseGridKw(request.getActualResponseGridKw());
+        }
+        if (request.getActualResponsePlatformKw() != null) {
+            entity.setActualResponsePlatformKw(request.getActualResponsePlatformKw());
+            entity.setEstimatedCapacityKw(request.getActualResponsePlatformKw());
+        }
+        if (request.getDeclaredClearedKw() != null) {
+            entity.setDeclaredClearedKw(request.getDeclaredClearedKw());
+            entity.setClearedCapacityKw(request.getDeclaredClearedKw());
+        }
+        if (request.getShareRatio() != null) {
+            entity.setShareRatio(normalizeShareRatio(request.getShareRatio()));
+        }
+        if (request.getResponseDurationMin() != null) {
+            entity.setResponseDurationMin(request.getResponseDurationMin());
+        }
+        if (request.getDeviationRate() != null) {
+            entity.setDeviationRate(request.getDeviationRate());
+        }
+        if (request.getCompletionRate() != null) {
+            entity.setCompletionRate(request.getCompletionRate());
+        }
+        if (request.getPriceAdjustmentCoefficient() != null) {
+            entity.setPriceAdjustmentCoefficient(request.getPriceAdjustmentCoefficient());
+        }
+        if (request.getEstimatedSubsidyAmount() != null) {
+            entity.setEstimatedSubsidyAmount(request.getEstimatedSubsidyAmount());
+        }
+    }
 
-        // 实际出清
-        p.setClearedCapacityKw(invitation.getActualClearedCapacityKw());
+    private void fillCustomerFromInvitationIfAbsent(VppDrSubsidyPrediction entity) {
+        if (entity.getCustomerId() != null || entity.getInvitationId() == null) {
+            return;
+        }
+        VppDrInvitation invitation = invitationMapper.selectById(entity.getInvitationId());
+        if (invitation != null) {
+            entity.setCustomerId(invitation.getCustomerId());
+        }
+    }
 
-        // 预估完成率 = 预估容量 / 实际出清;实际出清为NULL或0时记为0
-        BigDecimal completionRate = BigDecimal.ZERO;
-        BigDecimal estimatedKw = invitation.getEstimatedResponseCapacityKw();
-        BigDecimal actualKw = invitation.getActualClearedCapacityKw();
-        if (estimatedKw != null && actualKw != null && actualKw.compareTo(BigDecimal.ZERO) > 0) {
-            completionRate = estimatedKw.divide(actualKw, 4, RoundingMode.HALF_UP);
+    private void fillSiteName(VppDrSubsidyPrediction entity) {
+        if (entity.getSiteId() == null) {
+            entity.setSiteName(null);
+            return;
         }
-        p.setCompletionRate(completionRate);
+        VppSite site = siteMapper.selectById(entity.getSiteId());
+        entity.setSiteName(site != null ? site.getSiteName() : null);
+    }
 
-        // 预估偏差率 = 1 - 预估完成率
-        BigDecimal deviationRate = BigDecimal.ONE.subtract(completionRate);
-        p.setDeviationRate(deviationRate);
+    private void fillCustomerName(VppDrSubsidyPrediction entity) {
+        if (entity.getCustomerId() == null) {
+            if (!StringUtils.hasText(entity.getCustomerName())) {
+                entity.setCustomerName(null);
+            }
+            return;
+        }
+        if (StringUtils.hasText(entity.getCustomerName())) {
+            return;
+        }
+        VppCustomer customer = customerMapper.selectById(entity.getCustomerId());
+        entity.setCustomerName(customer != null ? customer.getCustomerName() : null);
+    }
 
-        // 预估补贴金额 = 补贴标准(为NULL时记为NULL)
-        p.setEstimatedSubsidyAmount(event.getSubsidyPrice());
+    private Map<Long, String> loadCustomerNameMap(Set<Long> customerIds) {
+        if (customerIds == null || customerIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        return customerMapper.selectBatchIds(customerIds).stream()
+                .collect(Collectors.toMap(VppCustomer::getId, VppCustomer::getCustomerName, (a, b) -> a));
+    }
 
-        p.setTenantId(event.getTenantId());
-        return p;
+    private List<VppResponseDeviationPrice> loadActivePriceAdjustments(Integer tenantId) {
+        return priceAdjustmentMapper.selectList(
+                new LambdaQueryWrapper<VppResponseDeviationPrice>()
+                        .eq(VppResponseDeviationPrice::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .eq(tenantId != null, VppResponseDeviationPrice::getTenantId, tenantId)
+                        .orderByAsc(VppResponseDeviationPrice::getId));
+    }
+
+    private BigDecimal resolvePriceAdjustmentCoefficient(BigDecimal completionRate,
+                                                         List<VppResponseDeviationPrice> adjustments) {
+        if (adjustments != null) {
+            for (VppResponseDeviationPrice adjustment : adjustments) {
+                if (VppResponseCoefficientHelper.matches(
+                        adjustment.getCoeffLower(),
+                        adjustment.getCoeffUpper(),
+                        adjustment.getLowerInclusive(),
+                        adjustment.getUpperInclusive(),
+                        completionRate)) {
+                    return adjustment.getPriceAdjustmentCoefficient();
+                }
+            }
+        }
+        throw new BusinessException("未匹配到响应量偏差价格调整配置!");
+    }
+
+    private Map<Long, String> loadInvitationNoMap(List<VppDrSubsidyPrediction> predictions) {
+        Set<Long> invitationIds = predictions.stream()
+                .map(VppDrSubsidyPrediction::getInvitationId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        if (invitationIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        return invitationMapper.selectBatchIds(invitationIds).stream()
+                .collect(Collectors.toMap(VppDrInvitation::getId, VppDrInvitation::getInvitationNo,
+                        (left, right) -> left));
+    }
+
+    /**
+     * 自动计算:响应时长、偏差率、完成率、补贴金额
+     * <ul>
+     *   <li>完成率(%)= 实际响应容量(电网) / 实际出清 × 100(实际出清为 null 或 ≤0 时不计算)</li>
+     *   <li>偏差率(%)= (1 − 完成率) × 100</li>
+     *   <li>价格调整系数:按完成率比例匹配响应量偏差价格调整配置</li>
+     *   <li>补贴金额 = 电网实际响应量 × 响应时长(h) × 价格调整系数 × 补贴标准</li>
+     * </ul>
+     */
+    private void recalculate(VppDrSubsidyPrediction entity,
+                             List<VppResponseDeviationPrice> adjustments) {
+        int durationMin = 0;
+        if (entity.getEventStartTime() != null && entity.getEventEndTime() != null
+                && entity.getEventEndTime().isAfter(entity.getEventStartTime())) {
+            durationMin = (int) Duration.between(entity.getEventStartTime(), entity.getEventEndTime()).toMinutes();
+        }
+        entity.setResponseDurationMin(durationMin);
+
+        BigDecimal gridKw = entity.getActualResponseGridKw();
+        BigDecimal clearedKw = entity.getClearedCapacityKw() != null
+                ? entity.getClearedCapacityKw()
+                : entity.getDeclaredClearedKw();
+
+        if (clearedKw == null || clearedKw.compareTo(BigDecimal.ZERO) <= 0 || gridKw == null) {
+            entity.setDeviationRate(null);
+            entity.setCompletionRate(null);
+            entity.setPriceAdjustmentCoefficient(null);
+            entity.setEstimatedSubsidyAmount(null);
+            return;
+        }
+
+        BigDecimal completionRatio = gridKw.divide(clearedKw, 6, RoundingMode.HALF_UP);
+        BigDecimal deviationRatio = BigDecimal.ONE.subtract(completionRatio);
+        BigDecimal priceAdjustmentCoefficient = resolvePriceAdjustmentCoefficient(completionRatio, adjustments);
+
+        entity.setCompletionRate(completionRatio.multiply(BigDecimal.valueOf(100)).setScale(4, RoundingMode.HALF_UP));
+        entity.setDeviationRate(deviationRatio.multiply(BigDecimal.valueOf(100)).setScale(4, RoundingMode.HALF_UP));
+        entity.setPriceAdjustmentCoefficient(priceAdjustmentCoefficient);
+
+        // 补贴按电网核定实际响应量结算
+        BigDecimal subsidyAmount = null;
+        if (entity.getSubsidyPrice() != null && durationMin > 0) {
+            BigDecimal hours = BigDecimal.valueOf(durationMin).divide(MINUTES_PER_HOUR, 6, RoundingMode.HALF_UP);
+            subsidyAmount = gridKw
+                    .multiply(hours)
+                    .multiply(priceAdjustmentCoefficient)
+                    .multiply(entity.getSubsidyPrice())
+                    .setScale(4, RoundingMode.HALF_UP);
+        }
+        entity.setEstimatedSubsidyAmount(subsidyAmount);
+    }
+
+    /** 分成比例统一为 0~1;若传入值 &gt; 1 则按百分数换算 */
+    private BigDecimal normalizeShareRatio(BigDecimal raw) {
+        if (raw == null) {
+            return null;
+        }
+        if (raw.compareTo(BigDecimal.ONE) > 0) {
+            return raw.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP);
+        }
+        return raw;
     }
 
-    private DrSubsidyPredictionVO toVo(VppDrSubsidyPrediction entity) {
+    private DrSubsidyPredictionVO toVo(VppDrSubsidyPrediction entity, Map<Long, String> invitationNoMap) {
         DrSubsidyPredictionVO vo = new DrSubsidyPredictionVO();
         vo.setId(entity.getId());
         vo.setInvitationId(entity.getInvitationId());
+        vo.setInvitationNo(invitationNoMap.get(entity.getInvitationId()));
         vo.setDrEventId(entity.getDrEventId());
         vo.setSiteId(entity.getSiteId());
         vo.setSiteName(entity.getSiteName());
+        vo.setCustomerId(entity.getCustomerId());
+        vo.setCustomerName(entity.getCustomerName());
         vo.setEventStartTime(entity.getEventStartTime());
         vo.setEventEndTime(entity.getEventEndTime());
         vo.setResponseDurationMin(entity.getResponseDurationMin());
         vo.setSubsidyPrice(entity.getSubsidyPrice());
         vo.setEstimatedCapacityKw(entity.getEstimatedCapacityKw());
         vo.setClearedCapacityKw(entity.getClearedCapacityKw());
+        vo.setActualResponseGridKw(entity.getActualResponseGridKw());
+        vo.setActualResponsePlatformKw(entity.getActualResponsePlatformKw());
+        vo.setDeclaredClearedKw(entity.getDeclaredClearedKw());
+        vo.setShareRatio(entity.getShareRatio());
+        vo.setPriceAdjustmentCoefficient(entity.getPriceAdjustmentCoefficient());
         vo.setCompletionRate(entity.getCompletionRate());
         vo.setDeviationRate(entity.getDeviationRate());
         vo.setEstimatedSubsidyAmount(entity.getEstimatedSubsidyAmount());

+ 13 - 7
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResourceBoardServiceImpl.java

@@ -17,7 +17,7 @@ import com.usky.vpp.service.VppTsdbQueryService;
 import com.usky.vpp.service.vo.ResourceBoardDetailVO;
 import com.usky.vpp.service.vo.ResourceBoardItemVO;
 import com.usky.vpp.util.VppAuditHelper;
-import com.usky.vpp.util.VppResourceMockHelper;
+import com.usky.vpp.util.VppResourceTypeHelper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
@@ -184,7 +184,7 @@ public class VppResourceBoardServiceImpl implements VppResourceBoardService {
         item.setResourceCode(resource.getResourceCode());
         item.setResourceName(resource.getResourceName());
         item.setResourceType(resource.getResourceType());
-        item.setResourceTypeLabel(VppResourceMockHelper.resourceTypeLabel(resource.getResourceType()));
+        item.setResourceTypeLabel(VppResourceTypeHelper.resourceTypeLabel(resource.getResourceType()));
         item.setSiteId(resource.getSiteId());
         item.setCapacityKw(resource.getCapacityKw());
         if (site != null) {
@@ -193,19 +193,25 @@ public class VppResourceBoardServiceImpl implements VppResourceBoardService {
             item.setCity(site.getCity());
             item.setDistrict(site.getDistrict());
         }
-        applyTsdbMetrics(item, tsdbMetrics);
-        item.setSocPercent(VppResourceMockHelper.mockSocPercent(resource));
-        item.setEvcsChargePowerKw(VppResourceMockHelper.mockEvcsChargePowerKw(resource));
+        applyTsdbMetrics(item, tsdbMetrics, resource.getResourceType());
+        // item.setSocPercent(VppResourceMockHelper.mockSocPercent(resource));
+        // item.setEvcsChargePowerKw(VppResourceMockHelper.mockEvcsChargePowerKw(resource));
     }
 
-    private void applyTsdbMetrics(ResourceBoardItemVO item, Map<String, BigDecimal> tsdbMetrics) {
+    private void applyTsdbMetrics(ResourceBoardItemVO item, Map<String, BigDecimal> tsdbMetrics, String resourceType) {
         BigDecimal deviceStatus = getMetric(tsdbMetrics, "device_status");
         boolean abnormal = deviceStatus != null && deviceStatus.compareTo(BigDecimal.ZERO) == 0;
         item.setAbnormal(abnormal);
         item.setRunStatusLabel(resolveRunStatusLabel(deviceStatus));
-        item.setRealtimePowerKw(getMetric(tsdbMetrics, "p"));
+        item.setRealtimePowerKw(getMetric(tsdbMetrics, VppTsdbConstants.METRIC_P));
         item.setVoltageV(getMetric(tsdbMetrics, "ua"));
         item.setCurrentA(getMetric(tsdbMetrics, "ia"));
+        if ("ESS".equalsIgnoreCase(resourceType)) {
+            item.setSocPercent(getMetric(tsdbMetrics, VppTsdbConstants.METRIC_SOC));
+        }
+        if ("EVCS".equalsIgnoreCase(resourceType)) {
+            item.setEvcsChargePowerKw(getMetric(tsdbMetrics, VppTsdbConstants.METRIC_P));
+        }
     }
 
     private Map<String, Map<String, BigDecimal>> loadTsdbMetricsMap(Collection<VppDevice> devices) {

+ 15 - 4
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResourceOverviewServiceImpl.java

@@ -17,7 +17,7 @@ import com.usky.vpp.service.vo.ResourceCurveSeriesVO;
 import com.usky.vpp.service.vo.ResourceOverviewRealtimeCurveVO;
 import com.usky.vpp.service.vo.ResourceTypeStatVO;
 import com.usky.vpp.util.VppAuditHelper;
-import com.usky.vpp.util.VppResourceMockHelper;
+import com.usky.vpp.util.VppResourceTypeHelper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
@@ -71,7 +71,8 @@ public class VppResourceOverviewServiceImpl implements VppResourceOverviewServic
 
     @Override
     public List<AlarmLevelStatVO> getAlarmStats() {
-        return buildAlarmStatsMock();
+        // return buildAlarmStatsMock();
+        return buildAlarmStats();
     }
 
     @Override
@@ -277,7 +278,7 @@ public class VppResourceOverviewServiceImpl implements VppResourceOverviewServic
         for (String type : RESOURCE_TYPES) {
             ResourceTypeStatVO stat = new ResourceTypeStatVO();
             stat.setResourceType(type);
-            stat.setResourceTypeLabel(VppResourceMockHelper.resourceTypeLabel(type));
+            stat.setResourceTypeLabel(VppResourceTypeHelper.resourceTypeLabel(type));
             stat.setTotalCount(0L);
             stat.setTotalCapacityKw(BigDecimal.ZERO);
             stat.setTotalAdjustableCapacityKw(BigDecimal.ZERO);
@@ -288,7 +289,7 @@ public class VppResourceOverviewServiceImpl implements VppResourceOverviewServic
             ResourceTypeStatVO stat = statMap.computeIfAbsent(type, key -> {
                 ResourceTypeStatVO s = new ResourceTypeStatVO();
                 s.setResourceType(key);
-                s.setResourceTypeLabel(VppResourceMockHelper.resourceTypeLabel(key));
+                s.setResourceTypeLabel(VppResourceTypeHelper.resourceTypeLabel(key));
                 s.setTotalCount(0L);
                 s.setTotalCapacityKw(BigDecimal.ZERO);
                 s.setTotalAdjustableCapacityKw(BigDecimal.ZERO);
@@ -442,6 +443,15 @@ public class VppResourceOverviewServiceImpl implements VppResourceOverviewServic
         return new ArrayList<>(uuids);
     }
 
+    private List<AlarmLevelStatVO> buildAlarmStats() {
+        List<AlarmLevelStatVO> stats = new ArrayList<>();
+        stats.add(buildAlarmStat(1, "紧急", 0L));
+        stats.add(buildAlarmStat(2, "重要", 0L));
+        stats.add(buildAlarmStat(3, "一般", 0L));
+        return stats;
+    }
+
+    /*
     private List<AlarmLevelStatVO> buildAlarmStatsMock() {
         List<AlarmLevelStatVO> stats = new ArrayList<>();
         stats.add(buildAlarmStat(1, "紧急", 3L));
@@ -449,6 +459,7 @@ public class VppResourceOverviewServiceImpl implements VppResourceOverviewServic
         stats.add(buildAlarmStat(3, "一般", 15L));
         return stats;
     }
+    */
 
     private AlarmLevelStatVO buildAlarmStat(int level, String label, long count) {
         AlarmLevelStatVO stat = new AlarmLevelStatVO();

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

@@ -16,7 +16,7 @@ import com.usky.vpp.service.vo.ResourcePointListVO;
 import com.usky.vpp.service.vo.ResourcePointRequest;
 import com.usky.vpp.util.VppAuditHelper;
 import com.usky.vpp.util.VppPageHelper;
-import com.usky.vpp.util.VppResourceMockHelper;
+import com.usky.vpp.util.VppResourceTypeHelper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -178,7 +178,7 @@ public class VppResourcePointServiceImpl implements VppResourcePointService {
             vo.setSiteId(resource.getSiteId());
             vo.setDeviceId(resource.getDeviceId());
             vo.setResourceType(resource.getResourceType());
-            vo.setResourceTypeLabel(VppResourceMockHelper.resourceTypeLabel(resource.getResourceType()));
+            vo.setResourceTypeLabel(VppResourceTypeHelper.resourceTypeLabel(resource.getResourceType()));
             vo.setCapacityKw(resource.getCapacityKw());
             vo.setIsControl(resource.getIsControl());
             vo.setIsSupportPeak(resource.getIsSupportPeak());

+ 205 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResponseDeviationPriceServiceImpl.java

@@ -0,0 +1,205 @@
+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.common.security.utils.SecurityUtils;
+import com.usky.vpp.domain.VppResponseDeviationPrice;
+import com.usky.vpp.mapper.VppResponseDeviationPriceMapper;
+import com.usky.vpp.service.VppResponseDeviationPriceService;
+import com.usky.vpp.service.vo.ResponseDeviationPriceAdjustmentRequest;
+import com.usky.vpp.service.vo.ResponseDeviationPriceAdjustmentVO;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppPageHelper;
+import com.usky.vpp.util.VppResponseCoefficientHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 响应量偏差价格调整配置服务实现。
+ */
+@Service
+public class VppResponseDeviationPriceServiceImpl implements VppResponseDeviationPriceService {
+
+    @Autowired
+    private VppResponseDeviationPriceMapper adjustmentMapper;
+
+    @Override
+    public CommonPage<ResponseDeviationPriceAdjustmentVO> page(BigDecimal completionRate,
+                                                                Integer current,
+                                                                Integer size) {
+        Map<String, Object> pageParams = new HashMap<>(2);
+        pageParams.put("current", current);
+        pageParams.put("size", size);
+        Page<VppResponseDeviationPrice> page = VppPageHelper.of(pageParams);
+
+        LambdaQueryWrapper<VppResponseDeviationPrice> wrapper =
+                new LambdaQueryWrapper<VppResponseDeviationPrice>()
+                        .eq(VppResponseDeviationPrice::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .orderByAsc(VppResponseDeviationPrice::getId);
+        applyTenantFilter(wrapper);
+        if (completionRate != null) {
+            applyCompletionRateFilter(wrapper, completionRate);
+        }
+
+        Page<VppResponseDeviationPrice> result = adjustmentMapper.selectPage(page, wrapper);
+        List<ResponseDeviationPriceAdjustmentVO> records = result.getRecords().stream()
+                .map(this::toVo)
+                .collect(Collectors.toList());
+        return new CommonPage<>(records, result.getTotal(), result.getCurrent(), result.getSize());
+    }
+
+    @Override
+    public ResponseDeviationPriceAdjustmentVO get(Long id) {
+        return toVo(requireAdjustment(id));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long create(ResponseDeviationPriceAdjustmentRequest request) {
+        validateRequest(request, null);
+        VppResponseDeviationPrice adjustment = new VppResponseDeviationPrice();
+        fillFromRequest(adjustment, request);
+        VppAuditHelper.fillCreate(adjustment);
+        adjustmentMapper.insert(adjustment);
+        return adjustment.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void update(Long id, ResponseDeviationPriceAdjustmentRequest request) {
+        VppResponseDeviationPrice adjustment = requireAdjustment(id);
+        validateRequest(request, id);
+        fillFromRequest(adjustment, request);
+        VppAuditHelper.fillUpdate(adjustment);
+        adjustmentMapper.updateById(adjustment);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void delete(Long id) {
+        VppResponseDeviationPrice adjustment = requireAdjustment(id);
+        VppAuditHelper.fillSoftDelete(adjustment);
+        adjustmentMapper.updateById(adjustment);
+    }
+
+    private VppResponseDeviationPrice requireAdjustment(Long id) {
+        if (id == null) {
+            throw new BusinessException("主键ID不能为空");
+        }
+        VppResponseDeviationPrice adjustment = adjustmentMapper.selectById(id);
+        if (adjustment == null || VppAuditHelper.isDeleted(adjustment.getDeleteFlag())) {
+            throw new BusinessException("响应量偏差价格调整配置不存在");
+        }
+        Integer tenantId = SecurityUtils.getTenantId();
+        if (tenantId != null && adjustment.getTenantId() != null
+                && !tenantId.equals(adjustment.getTenantId())) {
+            throw new BusinessException("响应量偏差价格调整配置不存在");
+        }
+        return adjustment;
+    }
+
+    private void fillFromRequest(VppResponseDeviationPrice adjustment,
+                                 ResponseDeviationPriceAdjustmentRequest request) {
+        VppResponseCoefficientHelper.Range range = toRange(request);
+        adjustment.setCoeffLower(range.getLower());
+        adjustment.setCoeffUpper(range.getUpper());
+        adjustment.setLowerInclusive(range.isLowerInclusive());
+        adjustment.setUpperInclusive(range.isUpperInclusive());
+        adjustment.setPriceAdjustmentCoefficient(request.getPriceAdjustmentCoefficient());
+        adjustment.setRemark(request.getRemark());
+    }
+
+    private void validateRequest(ResponseDeviationPriceAdjustmentRequest request, Long excludeId) {
+        if (request == null) {
+            throw new BusinessException("请求体不能为空");
+        }
+        VppResponseCoefficientHelper.Range requestRange = toRange(request);
+        if (request.getPriceAdjustmentCoefficient() == null) {
+            throw new BusinessException("价格调整系数不能为空");
+        }
+        if (request.getPriceAdjustmentCoefficient().compareTo(BigDecimal.ZERO) < 0) {
+            throw new BusinessException("价格调整系数不能小于0");
+        }
+
+        Integer tenantId = SecurityUtils.getTenantId();
+        List<VppResponseDeviationPrice> existing = adjustmentMapper.selectList(
+                new LambdaQueryWrapper<VppResponseDeviationPrice>()
+                        .eq(VppResponseDeviationPrice::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .eq(tenantId != null, VppResponseDeviationPrice::getTenantId, tenantId)
+                        .ne(excludeId != null, VppResponseDeviationPrice::getId, excludeId));
+        for (VppResponseDeviationPrice item : existing) {
+            VppResponseCoefficientHelper.Range existingRange = toRange(item);
+            if (VppResponseCoefficientHelper.overlaps(requestRange, existingRange)) {
+                throw new BusinessException("响应量系数区间与已有配置重叠:"
+                        + VppResponseCoefficientHelper.format(existingRange));
+            }
+        }
+    }
+
+    private VppResponseCoefficientHelper.Range toRange(ResponseDeviationPriceAdjustmentRequest request) {
+        return VppResponseCoefficientHelper.of(
+                request.getCoeffLower(),
+                request.getCoeffUpper(),
+                request.getLowerInclusive(),
+                request.getUpperInclusive());
+    }
+
+    private VppResponseCoefficientHelper.Range toRange(VppResponseDeviationPrice adjustment) {
+        return VppResponseCoefficientHelper.of(
+                adjustment.getCoeffLower(),
+                adjustment.getCoeffUpper(),
+                adjustment.getLowerInclusive(),
+                adjustment.getUpperInclusive());
+    }
+
+    /**
+     * SQL 层按完成率匹配结构化区间。
+     */
+    private void applyCompletionRateFilter(LambdaQueryWrapper<VppResponseDeviationPrice> wrapper,
+                                           BigDecimal completionRate) {
+        wrapper.apply(
+                "(coeff_lower IS NULL OR "
+                        + "(lower_inclusive = 1 AND {0} >= coeff_lower) OR "
+                        + "(lower_inclusive = 0 AND {0} > coeff_lower))"
+                        + " AND "
+                        + "(coeff_upper IS NULL OR "
+                        + "(upper_inclusive = 1 AND {0} <= coeff_upper) OR "
+                        + "(upper_inclusive = 0 AND {0} < coeff_upper))",
+                completionRate);
+    }
+
+    private ResponseDeviationPriceAdjustmentVO toVo(VppResponseDeviationPrice adjustment) {
+        ResponseDeviationPriceAdjustmentVO vo = new ResponseDeviationPriceAdjustmentVO();
+        vo.setId(adjustment.getId());
+        vo.setCoeffLower(adjustment.getCoeffLower());
+        vo.setCoeffUpper(adjustment.getCoeffUpper());
+        vo.setLowerInclusive(Boolean.TRUE.equals(adjustment.getLowerInclusive()));
+        vo.setUpperInclusive(Boolean.TRUE.equals(adjustment.getUpperInclusive()));
+        vo.setResponseCoefficientLabel(VppResponseCoefficientHelper.format(
+                adjustment.getCoeffLower(),
+                adjustment.getCoeffUpper(),
+                adjustment.getLowerInclusive(),
+                adjustment.getUpperInclusive()));
+        vo.setPriceAdjustmentCoefficient(adjustment.getPriceAdjustmentCoefficient());
+        vo.setRemark(adjustment.getRemark());
+        vo.setCreateTime(adjustment.getCreateTime());
+        vo.setUpdateTime(adjustment.getUpdateTime());
+        return vo;
+    }
+
+    private void applyTenantFilter(LambdaQueryWrapper<VppResponseDeviationPrice> wrapper) {
+        Integer tenantId = SecurityUtils.getTenantId();
+        if (tenantId != null) {
+            wrapper.eq(VppResponseDeviationPrice::getTenantId, tenantId);
+        }
+    }
+}

+ 12 - 11
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResponseMonitorServiceImpl.java

@@ -27,7 +27,6 @@ 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.VppBaselineHelper;
-import com.usky.vpp.util.VppCapabilityEvalHelper;
 import com.usky.vpp.util.VppResponseMonitorHelper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -161,7 +160,7 @@ public class VppResponseMonitorServiceImpl implements VppResponseMonitorService
         BigDecimal declared = VppResponseMonitorHelper.resolveDeclaredCapacity(
                 drContext.event, drContext.participations, drContext.executions);
 
-        CurveBuildResult curve = buildCurve(resources, devices, siteId, statDate, declared, drContext);
+            CurveBuildResult curve = buildCurve(resources, devices, statDate, declared, drContext);
         result.setPoints(curve.points);
         result.setDataSource(curve.dataSource);
         result.setStatistics(VppResponseMonitorHelper.buildStatistics(curve.points, statDate));
@@ -177,13 +176,14 @@ public class VppResponseMonitorServiceImpl implements VppResponseMonitorService
         LocalDate statDate = parseDate(date);
         ResponseCompareVO result = new ResponseCompareVO();
         result.setDate(statDate.format(DATE_FMT));
-        String dataSource = "mock";
+        // String dataSource = "mock";
+        String dataSource = null;
 
         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());
+            CurveBuildResult curve = buildCurve(resources, devices, statDate, BigDecimal.ZERO, DrContext.empty());
             if ("tsdb".equals(curve.dataSource)) {
                 dataSource = "tsdb";
             }
@@ -206,15 +206,15 @@ public class VppResponseMonitorServiceImpl implements VppResponseMonitorService
 
     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()) {
-            return new CurveBuildResult(
-                    VppResponseMonitorHelper.buildMockCurve(resources, devices, siteId, statDate, declaredCapacityKw),
-                    "mock");
+            // return new CurveBuildResult(
+            //         VppResponseMonitorHelper.buildMockCurve(resources, devices, siteId, statDate, declaredCapacityKw),
+            //         "mock");
+            return new CurveBuildResult(Collections.emptyList(), null);
         }
 
         // 确定响应时段(有事件用事件时段,无事件用全天)
@@ -246,9 +246,10 @@ public class VppResponseMonitorServiceImpl implements VppResponseMonitorService
                 LocalTime.MIN, LocalTime.of(23, 59, 59),
                 VppBaselineHelper.POWER_METRICS, interval, null);
         if (originalBaseline.isEmpty()) {
-            return new CurveBuildResult(
-                    VppResponseMonitorHelper.buildMockCurve(resources, devices, siteId, statDate, declaredCapacityKw),
-                    "mock");
+            // return new CurveBuildResult(
+            //         VppResponseMonitorHelper.buildMockCurve(resources, devices, siteId, statDate, declaredCapacityKw),
+            //         "mock");
+            return new CurveBuildResult(Collections.emptyList(), "tsdb");
         }
 
         // 今日实际负荷(全天)

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

@@ -171,8 +171,8 @@ public class VppSiteServiceImpl implements VppSiteService {
             if (params.get("siteCode") != null) {
                 wrapper.eq(VppSite::getSiteCode, params.get("siteCode").toString());
             }
-            if (params.get("province") != null) {
-                wrapper.eq(VppSite::getProvince, params.get("province").toString());
+            if (params.get("province") != null && StringUtils.hasText(params.get("province").toString())) {
+                wrapper.like(VppSite::getProvince, params.get("province").toString());
             }
             if (params.get("city") != null) {
                 wrapper.eq(VppSite::getCity, params.get("city").toString());

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

@@ -3,7 +3,7 @@ package com.usky.vpp.service.vo;
 import lombok.Data;
 
 /**
- * 告警级别统计(模拟数据,可跳转告警列表)
+ * 告警级别统计(可跳转告警列表)
  */
 @Data
 public class AlarmLevelStatVO {

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

@@ -14,7 +14,7 @@ public class CapabilityLoadCurveVO {
     private String siteName;
     /** 统计日期 yyyy-MM-dd */
     private String date;
-    /** 数据来源:tsdb / mock */
+    /** 数据来源:tsdb */
     private String dataSource;
     private List<CapabilityLoadPointVO> points;
 }

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

@@ -77,6 +77,26 @@ public class CustomerRequestVO {
      */
     private String province;
 
+    /**
+     * 是否集团内用户 0否 1是
+     */
+    private Integer isGroupUser;
+
+    /**
+     * 是否绿电 0否 1是
+     */
+    private Integer isGreenPower;
+
+    /**
+     * 是否合作开发用户 0否 1是
+     */
+    private Integer isCoopDevUser;
+
+    /**
+     * 合作开发单位名称
+     */
+    private String coopPartnerName;
+
     /**
      * 市
      */

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

@@ -62,6 +62,18 @@ public class CustomerResponseVO {
 
     private String province;
 
+    /** 是否集团内用户 0否 1是 */
+    private Integer isGroupUser;
+
+    /** 是否绿电 0否 1是 */
+    private Integer isGreenPower;
+
+    /** 是否合作开发用户 0否 1是 */
+    private Integer isCoopDevUser;
+
+    /** 合作开发单位名称 */
+    private String coopPartnerName;
+
     private String city;
 
     private String district;

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

@@ -14,7 +14,7 @@ public class DashboardCurveVO {
 
     /** 统计日期 yyyy-MM-dd */
     private String statDate;
-    /** 数据来源:tsdb / mock */
+    /** 数据来源:tsdb */
     private String dataSource;
     private List<Point> points = new ArrayList<>();
 

+ 4 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DeviceRequest.java

@@ -20,6 +20,10 @@ public class DeviceRequest {
     private Integer runStatus;
     private String firmwareVersion;
     private String gatewayId;
+    /**
+     * 运管资源ID,全局唯一
+     */
+    private String unResourceId;
     private String remark;
 
     private String productCode;

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

@@ -34,6 +34,8 @@ public class DrEventDetailVO {
     /** 附件名称 */
     private String attachmentName;
     private BigDecimal subsidyPrice;
+    /** 下浮系数 */
+    private BigDecimal floatingCoefficient;
     private Integer eventStatus;
     private List<DrParticipationVO> participations;
 }

+ 2 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrEventRequest.java

@@ -35,4 +35,6 @@ public class DrEventRequest {
     /** 附件名称 */
     private String attachmentName;
     private BigDecimal subsidyPrice;
+    /** 下浮系数(必填) */
+    private BigDecimal floatingCoefficient;
 }

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

@@ -15,7 +15,6 @@ public class DrInvitationVO {
     private Long id;
     private String invitationNo;
     private Long drEventId;
-    private String drEventCode;
     private Long customerId;
     private String customerName;
     private LocalDate executeStartDate;

+ 4 - 10
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrMonitorCurveVO.java

@@ -7,7 +7,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 /**
- * 响应监测 - 折线图(基线/实际负荷 + 申报/实际响应容量)
+ * 响应监测 - 折线图(基线负荷 + 实际响应容量)
  */
 @Data
 public class DrMonitorCurveVO {
@@ -15,11 +15,9 @@ public class DrMonitorCurveVO {
     private Long id;
     private String eventId;
     private String responseDate;
-    /** tsdb / mock / mixed */
+    /** 数据来源:tsdb */
     private String dataSource;
-    private List<Long> siteIds = new ArrayList<>();
-    /** 申报容量合计 kW(邀约) */
-    private BigDecimal declaredCapacityKw = BigDecimal.ZERO;
+    private Long siteId;
     /** 实际响应容量合计 kW(邀约) */
     private BigDecimal actualResponseCapacityKw = BigDecimal.ZERO;
     private List<Point> points = new ArrayList<>();
@@ -28,12 +26,8 @@ public class DrMonitorCurveVO {
     public static class Point {
         /** 时刻 HH:mm */
         private String time;
-        /** 基线负荷(预测)kW */
+        /** 基线负荷 kW */
         private BigDecimal baselineKw;
-        /** 实际负荷 kW */
-        private BigDecimal loadKw;
-        /** 申报容量 kW */
-        private BigDecimal declaredCapacityKw;
         /** 实际响应容量 kW */
         private BigDecimal actualResponseCapacityKw;
     }

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

@@ -1,5 +1,6 @@
 package com.usky.vpp.service.vo;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -16,19 +17,33 @@ public class DrMonitorRecordVO {
     private String eventName;
     private Integer responseType;
     private Integer eventType;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime startTime;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime endTime;
+    /** 响应截止时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime declareDeadline;
+
+    /** 邀约范围 */
     private String inviteScope;
+
+    /** 邀约容量 */
     private BigDecimal targetCapacityKw;
+
+    /** 出清容量 */
     private BigDecimal clearedCapacityKw;
     private String remark;
     private String attachmentUrl;
     private BigDecimal subsidyPrice;
+    /** 下浮系数 */
+    private BigDecimal floatingCoefficient;
     private Integer eventStatus;
     private String rawPayload;
     private Integer tenantId;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime createTime;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime updateTime;
     private String createdBy;
     private String updatedBy;
@@ -37,4 +52,6 @@ public class DrMonitorRecordVO {
     private BigDecimal responseCompletionRate;
     /** 是否达标:关联邀约 response_completion_rate = 100% */
     private Boolean qualified;
+    /** 补贴金额(预测) */
+    private BigDecimal estimatedSubsidyAmount;
 }

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

@@ -0,0 +1,19 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 补贴金额预测试算请求。
+ * <p>前端仅传邀约编号与电网实际响应容量,其余计算所需数据由服务端按邀约/事件查询。</p>
+ */
+@Data
+public class DrSubsidyPredictionCalculateRequest {
+
+    /** 邀约编号(vpp_dr_invitation.invitation_no) */
+    private String invitationNo;
+
+    /** 实际响应容量-电网(kW) */
+    private BigDecimal actualResponseGridKw;
+}

+ 70 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrSubsidyPredictionRequest.java

@@ -0,0 +1,70 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 补贴金额预测 请求VO(新增/编辑)。
+ * <p>新增时由前端携带试算结果直接入库,服务端不再重算。</p>
+ */
+@Data
+public class DrSubsidyPredictionRequest {
+
+    /** 主键(更新时必传) */
+    private Long id;
+
+    /** 邀约ID(可选) */
+    private Long invitationId;
+
+    /** 需求响应事件ID(可选) */
+    private Long drEventId;
+
+    /** 站点ID(手动选择) */
+    private Long siteId;
+
+    /** 企业/客户ID */
+    private Long customerId;
+
+    /** 企业名称 */
+    private String customerName;
+
+    /** 响应开始时间(手动选择) */
+    private LocalDateTime eventStartTime;
+
+    /** 响应结束时间(手动选择) */
+    private LocalDateTime eventEndTime;
+
+    /** 补贴标准(元/kWh) */
+    private BigDecimal subsidyPrice;
+
+    /** 实际响应量-电网(kW) */
+    private BigDecimal actualResponseGridKw;
+
+    /** 实际响应量-平台(kW,手动填写) */
+    private BigDecimal actualResponsePlatformKw;
+
+    /** 申报出清量(kW) */
+    private BigDecimal declaredClearedKw;
+
+    /**
+     * 分成比例(0~1,如 0.8 表示 80%)
+     */
+    private BigDecimal shareRatio;
+
+    /** 响应时长(分钟,试算结果) */
+    private Integer responseDurationMin;
+
+    /** 偏差率(百分比)= (1 − 完成率) × 100 */
+    private BigDecimal deviationRate;
+
+    /** 完成率(百分比)= 实际响应容量(电网) / 实际出清 × 100 */
+    private BigDecimal completionRate;
+
+    /** 价格调整系数(试算结果) */
+    private BigDecimal priceAdjustmentCoefficient;
+
+    /** 预估补贴金额(试算结果) */
+    private BigDecimal estimatedSubsidyAmount;
+}

+ 31 - 45
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrSubsidyPredictionVO.java

@@ -13,78 +13,64 @@ public class DrSubsidyPredictionVO {
 
     private Long id;
 
-    /**
-     * 邀约ID
-     */
     private Long invitationId;
 
-    /**
-     * 需求响应事件ID
-     */
+    /** 邀约编号,替代界面展示的 drEventCode。 */
+    private String invitationNo;
+
     private Long drEventId;
 
-    /**
-     * 站点ID
-     */
     private Long siteId;
 
-    /**
-     * 站点名称
-     */
     private String siteName;
 
-    /**
-     * 响应开始时间
-     */
+    /** 企业/客户ID */
+    private Long customerId;
+
+    /** 企业名称 */
+    private String customerName;
+
     private LocalDateTime eventStartTime;
 
-    /**
-     * 响应结束时间
-     */
     private LocalDateTime eventEndTime;
 
-    /**
-     * 响应时长(分钟)
-     */
+    /** 响应时长(分钟) */
     private Integer responseDurationMin;
 
-    /**
-     * 补贴标准(元/kWh)
-     */
+    /** 补贴标准(元/kWh) */
     private BigDecimal subsidyPrice;
 
-    /**
-     * 预估响应容量(kW)
-     */
+    /** 预估响应容量(kW,兼容) */
     private BigDecimal estimatedCapacityKw;
 
-    /**
-     * 实际出清(kW)
-     */
+    /** 实际出清(kW,兼容) */
     private BigDecimal clearedCapacityKw;
 
-    /**
-     * 预估完成率(0~1)
-     */
+    /** 实际响应量-电网(kW) */
+    private BigDecimal actualResponseGridKw;
+
+    /** 实际响应量-平台(kW) */
+    private BigDecimal actualResponsePlatformKw;
+
+    /** 申报出清量(kW) */
+    private BigDecimal declaredClearedKw;
+
+    /** 分成比例(0~1) */
+    private BigDecimal shareRatio;
+
+    /** 按完成率匹配得到的价格调整系数。 */
+    private BigDecimal priceAdjustmentCoefficient;
+
+    /** 完成率(百分比)= 实际响应容量(电网) / 实际出清 × 100 */
     private BigDecimal completionRate;
 
-    /**
-     * 预估偏差率(0~1)
-     */
+    /** 偏差率(百分比)= (1 − 完成率) × 100 */
     private BigDecimal deviationRate;
 
-    /**
-     * 预估补贴金额
-     */
+    /** 预估补贴金额(元) */
     private BigDecimal estimatedSubsidyAmount;
 
-    /**
-     * 创建时间
-     */
     private LocalDateTime createTime;
 
-    /**
-     * 更新时间
-     */
     private LocalDateTime updateTime;
 }

+ 29 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResponseDeviationPriceAdjustmentRequest.java

@@ -0,0 +1,29 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 响应量偏差价格调整配置新增/修改请求。
+ */
+@Data
+public class ResponseDeviationPriceAdjustmentRequest {
+
+    /** 响应量系数下限,null 表示无下界(如 &lt;0.6)。 */
+    private BigDecimal coeffLower;
+
+    /** 响应量系数上限,null 表示无上界(如 &gt;1.4)。 */
+    private BigDecimal coeffUpper;
+
+    /** 下限是否闭区间;无下界时可忽略。 */
+    private Boolean lowerInclusive;
+
+    /** 上限是否闭区间;无上界时可忽略。 */
+    private Boolean upperInclusive;
+
+    /** 价格调整系数。 */
+    private BigDecimal priceAdjustmentCoefficient;
+
+    private String remark;
+}

+ 36 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResponseDeviationPriceAdjustmentVO.java

@@ -0,0 +1,36 @@
+package com.usky.vpp.service.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 响应量偏差价格调整配置展示对象。
+ */
+@Data
+public class ResponseDeviationPriceAdjustmentVO {
+
+    private Long id;
+
+    private BigDecimal coeffLower;
+
+    private BigDecimal coeffUpper;
+
+    private Boolean lowerInclusive;
+
+    private Boolean upperInclusive;
+
+    /** 只读展示串,如 [0.6,0.8)、&lt;0.6、&gt;1.4。 */
+    private String responseCoefficientLabel;
+
+    private BigDecimal priceAdjustmentCoefficient;
+
+    private String remark;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updateTime;
+}

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

@@ -16,7 +16,7 @@ public class ResponseLoadAnalysisVO {
     private String date;
     private Long eventId;
     private String eventName;
-    /** tsdb / mock */
+    /** tsdb */
     private String dataSource;
     private String granularity = "15m";
     private List<ResponseLoadPointVO> points = new ArrayList<>();

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

@@ -24,7 +24,7 @@ public class SiteBaselineVO {
     private String responseDayType;
     /** 响应日类型中文 */
     private String responseDayTypeLabel;
-    /** 数据来源:tsdb / mock */
+    /** 数据来源:tsdb */
     private String dataSource;
     /** 全天曲线(整点) */
     private List<BaselinePointVO> points;

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

@@ -30,7 +30,7 @@ public class SiteDeclaredCapacityVO {
     private String responseDayType;
     /** 响应日类型中文 */
     private String responseDayTypeLabel;
-    /** 数据来源:tsdb / mock */
+    /** 数据来源:tsdb */
     private String dataSource;
     /** 响应时段内各时间点申报容量 */
     private List<DeclaredCapacityPointVO> points;

+ 31 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractOverviewVO.java

@@ -0,0 +1,31 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+/**
+ * 合同概况
+ */
+@Data
+public class VppContractOverviewVO {
+
+    private Long id;
+    private String contractNo;
+    private String contractName;
+    /** 甲方 */
+    private String partyA;
+    /** 乙方 */
+    private String partyB;
+    /** 签订日期 */
+    private LocalDate signDate;
+    /** 合同金额 */
+    private BigDecimal contractAmount;
+    /** 生效日期 */
+    private LocalDate effectiveDate;
+    /** 到期日期 */
+    private LocalDate expireDate;
+    /** 生效状态(解析后) */
+    private Integer contractStatus;
+}

+ 25 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractRequestVO.java

@@ -19,6 +19,11 @@ public class VppContractRequestVO {
      */
     private Long id;
 
+    /**
+     * 合同编号(手动输入,必填)
+     */
+    private String contractNo;
+
     /**
      * 客户ID
      */
@@ -29,6 +34,11 @@ public class VppContractRequestVO {
      */
     private Long templateId;
 
+    /**
+     * 关联合同ID(居间/普通一对一)
+     */
+    private Long relatedContractId;
+
     /**
      * 合同类型
      */
@@ -39,6 +49,21 @@ public class VppContractRequestVO {
      */
     private String contractName;
 
+    /**
+     * 甲方
+     */
+    private String partyA;
+
+    /**
+     * 乙方
+     */
+    private String partyB;
+
+    /**
+     * 合同金额
+     */
+    private BigDecimal contractAmount;
+
     /**
      * 合同状态
      */

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

@@ -18,7 +18,7 @@ public class VppContractResponseVO {
     private Long id;
 
     /**
-     * 合同编号(自动生成
+     * 合同编号(手动输入
      */
     private String contractNo;
 
@@ -42,6 +42,21 @@ public class VppContractResponseVO {
      */
     private String templateName;
 
+    /**
+     * 关联合同ID
+     */
+    private Long relatedContractId;
+
+    /**
+     * 关联合同编号
+     */
+    private String relatedContractNo;
+
+    /**
+     * 关联合同名称
+     */
+    private String relatedContractName;
+
     /**
      * 合同类型
      */
@@ -52,6 +67,21 @@ public class VppContractResponseVO {
      */
     private String contractName;
 
+    /**
+     * 甲方
+     */
+    private String partyA;
+
+    /**
+     * 乙方
+     */
+    private String partyB;
+
+    /**
+     * 合同金额
+     */
+    private BigDecimal contractAmount;
+
     /**
      * 合同状态(0草稿 1审核中 2待生效 3已生效 4已驳回 5已到期 6已终止)
      */

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

@@ -31,6 +31,11 @@ public class VppContractTemplateRequestVO {
      */
     private Integer contractType;
 
+    /**
+     * 模板分类:1居间/层间 2普通/直网
+     */
+    private Integer templateKind;
+
     /**
      * 版本号
      */

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

@@ -34,6 +34,11 @@ public class VppContractTemplateResponseVO {
      */
     private Integer contractType;
 
+    /**
+     * 模板分类:1居间/层间 2普通/直网
+     */
+    private Integer templateKind;
+
     /**
      * 版本号
      */

+ 21 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppBaselineHelper.java

@@ -201,7 +201,21 @@ public final class VppBaselineHelper {
                                                             Map<String, BigDecimal> predictedBaselineByTime,
                                                             Map<String, BigDecimal> todayActualByTime,
                                                             Map<String, BigDecimal> yesterdayActualByTime) {
+        return buildBaselinePoints(responseDate, responseStartTime, responseStartTime,
+                predictedBaselineByTime, todayActualByTime, yesterdayActualByTime);
+    }
+
+    /**
+     * @param todayActualUntil 当日实测曲线填充截止时间(含),用于响应后窗口展示
+     */
+    public static List<BaselinePointVO> buildBaselinePoints(LocalDate responseDate,
+                                                            LocalDateTime responseStartTime,
+                                                            LocalDateTime todayActualUntil,
+                                                            Map<String, BigDecimal> predictedBaselineByTime,
+                                                            Map<String, BigDecimal> todayActualByTime,
+                                                            Map<String, BigDecimal> yesterdayActualByTime) {
         List<BaselinePointVO> points = new ArrayList<>();
+        LocalDateTime actualUntil = todayActualUntil != null ? todayActualUntil : responseStartTime;
         for (Map.Entry<String, BigDecimal> entry : predictedBaselineByTime.entrySet()) {
             String timeKey = entry.getKey();
             LocalTime time = parseTimeKey(timeKey);
@@ -211,7 +225,7 @@ public final class VppBaselineHelper {
             point.setTime(timeKey);
             point.setPredictedBaselineKw(scale(entry.getValue()));
 
-            if (!pointTime.isAfter(responseStartTime)) {
+            if (!pointTime.isAfter(actualUntil)) {
                 point.setTodayActualKw(scale(todayActualByTime.get(timeKey)));
             }
             point.setYesterdayActualKw(scale(yesterdayActualByTime.get(timeKey)));
@@ -224,6 +238,7 @@ public final class VppBaselineHelper {
         return VppCapabilityEvalHelper.hasTsdbPowerData(deviceMetricData, POWER_METRICS);
     }
 
+    /*
     public static List<BaselinePointVO> buildMockBaselinePoints(Long siteId,
                                                                 LocalDate responseDate,
                                                                 LocalDateTime responseStartTime) {
@@ -264,6 +279,7 @@ public final class VppBaselineHelper {
         }
         return k;
     }
+    */
 
     public static String formatDate(LocalDate date) {
         return date.format(DATE_FMT);
@@ -345,6 +361,7 @@ public final class VppBaselineHelper {
         return points;
     }
 
+    /*
     public static BigDecimal mockDeclaredCapacityKw(Long siteId,
                                                     LocalDate responseDate,
                                                     LocalDateTime windowStart,
@@ -385,6 +402,7 @@ public final class VppBaselineHelper {
         }
         return mockHistorical;
     }
+    */
 
     private static BigDecimal averageValuesInWindow(Map<String, BigDecimal> valuesByTime,
                                                     LocalDateTime windowStart,
@@ -499,6 +517,7 @@ public final class VppBaselineHelper {
         return value.setScale(2, RoundingMode.HALF_UP);
     }
 
+    /*
     private static double mockHourFactor(LocalTime time) {
         int hour = time.getHour();
         int minute = time.getMinute();
@@ -528,4 +547,5 @@ public final class VppBaselineHelper {
         long mixed = seed * 31L + minuteOfDay * 17L + seriesSalt * 13L;
         return (mixed % 1000) / 1000.0;
     }
+    */
 }

+ 5 - 9
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppCapabilityEvalHelper.java

@@ -1,7 +1,5 @@
 package com.usky.vpp.util;
 
-import com.usky.vpp.domain.VppDevice;
-import com.usky.vpp.domain.VppResourcePoint;
 import com.usky.vpp.service.vo.CapabilityLoadPointVO;
 
 import java.math.BigDecimal;
@@ -20,7 +18,7 @@ import java.util.Set;
 import java.util.TreeMap;
 
 /**
- * 可调能力评估辅助(TSDB 聚合 + 模拟数据兜底
+ * 可调能力评估辅助(TSDB 聚合)
  */
 public final class VppCapabilityEvalHelper {
 
@@ -162,9 +160,7 @@ public final class VppCapabilityEvalHelper {
         return false;
     }
 
-    /**
-     * 生成当日负荷曲线(每 10 分钟一个点,00:00 ~ 23:50)- 模拟兜底
-     */
+    /*
     public static List<CapabilityLoadPointVO> buildLoadCurve(List<VppResourcePoint> resources,
                                                            List<VppDevice> devices,
                                                            Long siteId,
@@ -212,6 +208,7 @@ public final class VppCapabilityEvalHelper {
         }
         return total.setScale(2, RoundingMode.HALF_UP);
     }
+    */
 
     private static BigDecimal sumBucketPower(Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> deviceData,
                                              List<String> metricCandidates,
@@ -294,6 +291,7 @@ public final class VppCapabilityEvalHelper {
         return value.setScale(2, RoundingMode.HALF_UP);
     }
 
+    /*
     private static VppDevice findDevice(List<VppDevice> devices, Long deviceId) {
         if (devices == null || deviceId == null) {
             return null;
@@ -301,9 +299,6 @@ public final class VppCapabilityEvalHelper {
         return devices.stream().filter(d -> deviceId.equals(d.getId())).findFirst().orElse(null);
     }
 
-    /**
-     * 日内负荷形态:夜间低、早晚峰、午间次峰
-     */
     private static double timeFactor(LocalTime time) {
         int hour = time.getHour();
         int minute = time.getMinute();
@@ -358,4 +353,5 @@ public final class VppCapabilityEvalHelper {
         long mixed = seed * 31L + minuteOfDay * 17L + seriesSalt * 13L;
         return (mixed % 1000) / 1000.0;
     }
+    */
 }

+ 11 - 9
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppResourceMockHelper.java

@@ -1,21 +1,22 @@
 package com.usky.vpp.util;
 
-import com.usky.vpp.domain.VppDevice;
-import com.usky.vpp.domain.VppResourcePoint;
-
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.time.LocalDateTime;
-import java.util.concurrent.ThreadLocalRandom;
-
 /**
- * 资源看板/概览模拟数据生成(联调阶段使用,后续可替换为 TSDB/IoT 实时数据
+ * 资源看板/概览模拟数据生成(已停用,保留注释备查)
  */
 public final class VppResourceMockHelper {
 
     private VppResourceMockHelper() {
     }
 
+    /*
+    import com.usky.vpp.domain.VppDevice;
+    import com.usky.vpp.domain.VppResourcePoint;
+
+    import java.math.BigDecimal;
+    import java.math.RoundingMode;
+    import java.time.LocalDateTime;
+    import java.util.concurrent.ThreadLocalRandom;
+
     public static BigDecimal mockSocPercent(VppResourcePoint resource) {
         if (!"ESS".equalsIgnoreCase(resource.getResourceType())) {
             return null;
@@ -59,4 +60,5 @@ public final class VppResourceMockHelper {
         long mixed = seed * 31L + salt + LocalDateTime.now().getMinute();
         return (mixed % 1000) / 1000.0;
     }
+    */
 }

+ 30 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppResourceTypeHelper.java

@@ -0,0 +1,30 @@
+package com.usky.vpp.util;
+
+/**
+ * 资源类型中文标签
+ */
+public final class VppResourceTypeHelper {
+
+    private VppResourceTypeHelper() {
+    }
+
+    public static String resourceTypeLabel(String resourceType) {
+        if (resourceType == null) {
+            return "未知";
+        }
+        switch (resourceType.toUpperCase()) {
+            case "PV":
+                return "光伏";
+            case "ESS":
+                return "储能";
+            case "EVCS":
+                return "充电桩";
+            case "IND_LOAD":
+                return "工业负荷";
+            case "COM_BLDG":
+                return "商业楼宇";
+            default:
+                return resourceType;
+        }
+    }
+}

+ 167 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppResponseCoefficientHelper.java

@@ -0,0 +1,167 @@
+package com.usky.vpp.util;
+
+import com.usky.common.core.exception.BusinessException;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+
+/**
+ * 响应量系数区间:结构化边界校验、匹配、重叠判断与展示串拼装。
+ */
+public final class VppResponseCoefficientHelper {
+
+    private VppResponseCoefficientHelper() {
+    }
+
+    /**
+     * 由结构化边界构建区间并校验。
+     */
+    public static Range of(BigDecimal lower, BigDecimal upper,
+                           Boolean lowerInclusive, Boolean upperInclusive) {
+        if (lower == null && upper == null) {
+            throw new BusinessException("响应量系数下限与上限不能同时为空");
+        }
+        boolean lowerInc = lower != null && Boolean.TRUE.equals(lowerInclusive);
+        boolean upperInc = upper != null && Boolean.TRUE.equals(upperInclusive);
+        if (lower != null && upper != null && lower.compareTo(upper) >= 0) {
+            throw new BusinessException("响应量系数区间下限必须小于上限");
+        }
+        return new Range(lower, upper, lowerInc, upperInc);
+    }
+
+    public static boolean matches(BigDecimal lower, BigDecimal upper,
+                                  Boolean lowerInclusive, Boolean upperInclusive,
+                                  BigDecimal coefficient) {
+        return coefficient != null && of(lower, upper, lowerInclusive, upperInclusive).contains(coefficient);
+    }
+
+    public static boolean matches(Range range, BigDecimal coefficient) {
+        return coefficient != null && range != null && range.contains(coefficient);
+    }
+
+    public static boolean overlaps(Range left, Range right) {
+        return left != null && right != null && left.overlaps(right);
+    }
+
+    /** 拼装展示串,如 [0.6,0.8)、&lt;0.6、&gt;1.4。 */
+    public static String format(BigDecimal lower, BigDecimal upper,
+                                Boolean lowerInclusive, Boolean upperInclusive) {
+        return of(lower, upper, lowerInclusive, upperInclusive).format();
+    }
+
+    public static String format(Range range) {
+        return range == null ? null : range.format();
+    }
+
+    /**
+     * 解析历史字符串区间,仅供数据迁移使用。
+     */
+    public static Range parse(String expression) {
+        if (!StringUtils.hasText(expression)) {
+            throw new BusinessException("响应量系数不能为空");
+        }
+        String value = expression.trim().replace(',', ',').replace(" ", "");
+        try {
+            if (value.startsWith("<=")) {
+                return new Range(null, new BigDecimal(value.substring(2)), false, true);
+            }
+            if (value.startsWith("<")) {
+                return new Range(null, new BigDecimal(value.substring(1)), false, false);
+            }
+            if (value.startsWith(">=")) {
+                return new Range(new BigDecimal(value.substring(2)), null, true, false);
+            }
+            if (value.startsWith(">")) {
+                return new Range(new BigDecimal(value.substring(1)), null, false, false);
+            }
+            if ((value.startsWith("[") || value.startsWith("("))
+                    && (value.endsWith("]") || value.endsWith(")"))) {
+                String[] bounds = value.substring(1, value.length() - 1).split(",", -1);
+                if (bounds.length != 2) {
+                    throw new IllegalArgumentException();
+                }
+                BigDecimal lower = new BigDecimal(bounds[0]);
+                BigDecimal upper = new BigDecimal(bounds[1]);
+                if (lower.compareTo(upper) >= 0) {
+                    throw new BusinessException("响应量系数区间下限必须小于上限");
+                }
+                return new Range(lower, upper, value.startsWith("["), value.endsWith("]"));
+            }
+        } catch (NumberFormatException ex) {
+            throw new BusinessException("响应量系数格式无效");
+        }
+        throw new BusinessException("响应量系数格式无效,示例:[0.6,0.8)、<0.6、>1.4");
+    }
+
+    public static final class Range {
+        private final BigDecimal lower;
+        private final BigDecimal upper;
+        private final boolean lowerInclusive;
+        private final boolean upperInclusive;
+
+        private Range(BigDecimal lower, BigDecimal upper, boolean lowerInclusive, boolean upperInclusive) {
+            this.lower = lower;
+            this.upper = upper;
+            this.lowerInclusive = lowerInclusive;
+            this.upperInclusive = upperInclusive;
+        }
+
+        public BigDecimal getLower() {
+            return lower;
+        }
+
+        public BigDecimal getUpper() {
+            return upper;
+        }
+
+        public boolean isLowerInclusive() {
+            return lowerInclusive;
+        }
+
+        public boolean isUpperInclusive() {
+            return upperInclusive;
+        }
+
+        public boolean contains(BigDecimal value) {
+            if (lower != null) {
+                int comparison = value.compareTo(lower);
+                if (comparison < 0 || (comparison == 0 && !lowerInclusive)) {
+                    return false;
+                }
+            }
+            if (upper != null) {
+                int comparison = value.compareTo(upper);
+                if (comparison > 0 || (comparison == 0 && !upperInclusive)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        public boolean overlaps(Range other) {
+            return upperAllows(other.lower, other.lowerInclusive)
+                    && other.upperAllows(lower, lowerInclusive);
+        }
+
+        public String format() {
+            if (lower == null && upper != null) {
+                return (upperInclusive ? "<=" : "<") + upper.stripTrailingZeros().toPlainString();
+            }
+            if (upper == null && lower != null) {
+                return (lowerInclusive ? ">=" : ">") + lower.stripTrailingZeros().toPlainString();
+            }
+            String left = lowerInclusive ? "[" : "(";
+            String right = upperInclusive ? "]" : ")";
+            return left + lower.stripTrailingZeros().toPlainString()
+                    + "," + upper.stripTrailingZeros().toPlainString() + right;
+        }
+
+        private boolean upperAllows(BigDecimal otherLower, boolean otherLowerInclusive) {
+            if (upper == null || otherLower == null) {
+                return true;
+            }
+            int comparison = upper.compareTo(otherLower);
+            return comparison > 0 || (comparison == 0 && upperInclusive && otherLowerInclusive);
+        }
+    }
+}

+ 6 - 2
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppResponseMonitorHelper.java

@@ -1,10 +1,8 @@
 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;
 
@@ -76,6 +74,7 @@ public final class VppResponseMonitorHelper {
         return points;
     }
 
+    /*
     public static List<ResponseLoadPointVO> buildMockCurve(List<VppResourcePoint> resources,
                                                          List<VppDevice> devices,
                                                          Long siteId,
@@ -101,6 +100,7 @@ public final class VppResponseMonitorHelper {
         }
         return points;
     }
+    */
 
     /**
      * 基于预计算的预测基线和今日实际,构建曲线点(替代直接操作 TSDB 原始时序)。
@@ -232,6 +232,7 @@ public final class VppResponseMonitorHelper {
         return row;
     }
 
+    /*
     private static BigDecimal mockAggregate(List<VppResourcePoint> resources,
                                             List<VppDevice> devices,
                                             long seed,
@@ -255,6 +256,7 @@ public final class VppResponseMonitorHelper {
         }
         return total.setScale(2, RoundingMode.HALF_UP);
     }
+    */
 
     private static BigDecimal sumBucketPower(Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> deviceData,
                                              List<String> metricCandidates,
@@ -334,6 +336,7 @@ public final class VppResponseMonitorHelper {
         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;
@@ -373,4 +376,5 @@ public final class VppResponseMonitorHelper {
         }
         return 0.55 - (h - 21) / 3 * 0.15;
     }
+    */
 }

+ 24 - 23
service-vpp/service-vpp-biz/src/main/resources/bootstrap.yml

@@ -18,27 +18,28 @@ spring:
         shared-configs:
           - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
 
+#配置已经移到nacos中!
 # 运管平台 UN 对接(也可在 Nacos 中配置 vpp.un.*)
-vpp:
-  un:
-    outbound-enabled: false
-    poll-enabled: false
-    crypto-enabled: false
-    poll-interval-sec: 10
-    price-down-coeff: "0.8"
-    token-header: Authorization
-    token-prefix: "Bearer "
-    token-ttl-minutes: 25
-    auto-ack-clearing: false
-    auto-register-on-startup: false
-  # 阿里云短信(密钥复用 Nacos 共享配置 sms.ali.*)
-  sms:
-    enabled: true
-    sign-name: 上海永天科技股份有限公司
-    invitation-template-code: SMS_465362899
-  report:
-    retry-max: 3
-  # 月度电量/电费核算同步(每月1号00:35拉上月数据)
-  settlement:
-    monthly-sync-enabled: true
-    monthly-sync-cron: "0 35 0 1 * ?"
+#vpp:
+#  un:
+#    outbound-enabled: false
+#    poll-enabled: false
+#    crypto-enabled: false
+#    poll-interval-sec: 10
+#    price-down-coeff: "0.8"
+#    token-header: Authorization
+#    token-prefix: "Bearer "
+#    token-ttl-minutes: 25
+#    auto-ack-clearing: false
+#    auto-register-on-startup: false
+#  # 阿里云短信(密钥复用 Nacos 共享配置 sms.ali.*)
+#  sms:
+#    enabled: true
+#    sign-name: 上海永天科技股份有限公司
+#    invitation-template-code: SMS_465362899
+#  report:
+#    retry-max: 3
+#  # 月度电量/电费核算同步(每月1号00:35拉上月数据)
+#  settlement:
+#    monthly-sync-enabled: true
+#    monthly-sync-cron: "0 35 0 1 * ?"

+ 149 - 34
service-vpp/service-vpp-biz/src/main/resources/sql/vpp_schema.sql

@@ -13,6 +13,10 @@ CREATE TABLE `vpp_customer` (
     `dr_notify_minutes` INT NULL COMMENT '需求响应提前通知分钟数,默认30',
     `dr_up_capacity_kw` DECIMAL(12,4) NULL COMMENT '登记上调能力 kW',
     `dr_down_capacity_kw` DECIMAL(12,4) NULL COMMENT '登记下调能力 kW',
+    `is_group_user` TINYINT NULL COMMENT '是否集团内用户 0否 1是',
+    `is_green_power` TINYINT NULL COMMENT '是否绿电用户 0否 1是',
+    `is_coop_dev_user` TINYINT NULL COMMENT '是否合作开发用户 0否 1是',
+    `coop_partner_name` VARCHAR(200) NULL COMMENT '合作开发单位名称',
     `province` VARCHAR(50) NULL COMMENT '省',
     `city` VARCHAR(50) NULL COMMENT '市',
     `district` VARCHAR(50) NULL COMMENT '区',
@@ -26,6 +30,17 @@ CREATE TABLE `vpp_customer` (
     `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT '删除标识 0未删除 1已删除',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
+    `is_vpp_resource` TINYINT NOT NULL COMMENT '是否虚拟电厂资源 0否 1是',
+    `vpp_category` VARCHAR(32) NULL COMMENT '虚拟电厂分类',
+    `dr_resource_type` VARCHAR(32) NULL COMMENT '需求响应资源分类',
+    `vpp_proxy_code` VARCHAR(64) NULL COMMENT '虚拟电厂代理编码',
+    `industry` VARCHAR(32) NULL COMMENT '所属行业',
+    `industry_sector` VARCHAR(32) NULL COMMENT '所属产业',
+    `running_capacity` DECIMAL(12,4) NULL COMMENT '运行容量 kW',
+    `production_start_date` TIME NULL COMMENT '生产经营开始时间',
+    `production_end_date` TIME NULL COMMENT '生产经营结束时间',
+    `power_address` VARCHAR(500) NULL COMMENT '用电地址',
+    `signing_date` DATETIME(3) NULL COMMENT '签约时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_customer_account_no` (account_no),
     KEY `idx_customer_name` (customer_name),
@@ -83,14 +98,19 @@ CREATE TABLE `vpp_contract` (
     `contract_no` VARCHAR(64) NOT NULL COMMENT '合同编号',
     `customer_id` BIGINT NOT NULL COMMENT '客户ID',
     `template_id` BIGINT NULL COMMENT '模板ID',
+    `related_contract_id` BIGINT NULL COMMENT '关联合同ID',
     `contract_type` TINYINT NOT NULL COMMENT '1购售电 2需求响应合作 3聚合代理 4服务代理 5居民充电桩 6自有资产',
     `contract_name` VARCHAR(200) NOT NULL COMMENT '合同名称',
+    `party_a` VARCHAR(200) NULL COMMENT '甲方',
+    `party_b` VARCHAR(200) NULL COMMENT '乙方',
+    `contract_amount` DECIMAL(18,4) NULL COMMENT '合同金额',
     `contract_status` TINYINT NOT NULL COMMENT '0草稿 1审核中 2已生效 3已到期 4已终止',
     `sign_date` DATE NULL COMMENT '签订日期',
     `effective_date` DATE NULL COMMENT '生效日期',
     `expire_date` DATE NULL COMMENT '到期日期',
     `file_url` VARCHAR(500) NULL COMMENT '合同文件URL',
-    `share_ratio` DECIMAL(5,2) NULL COMMENT '分成比例%',
+    `customer_ratio` DECIMAL(6,4) NULL COMMENT '客户分成比例',
+    `operator_ratio` DECIMAL(6,4) NULL COMMENT '运营商分成比例',
     `price_json` JSON NULL COMMENT '电价条款JSON',
     `account_info_json` JSON NULL COMMENT '分成账户信息',
     `remark` VARCHAR(500) NULL COMMENT '备注',
@@ -104,6 +124,7 @@ CREATE TABLE `vpp_contract` (
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_contract_no` (contract_no),
     KEY `idx_contract_customer` (customer_id),
+    KEY `idx_contract_related` (related_contract_id),
     KEY `idx_contract_status` (contract_status, sign_date)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同';
 
@@ -111,7 +132,8 @@ CREATE TABLE `vpp_contract_template` (
     `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `template_code` VARCHAR(32) NOT NULL COMMENT '模板编码',
     `template_name` VARCHAR(200) NOT NULL COMMENT '模板名称',
-    `contract_type` TINYINT NOT NULL COMMENT '合同类型',
+    `contract_type` TINYINT NOT NULL COMMENT '合同类型 ',
+    `template_kind` TINYINT NULL COMMENT '模板分类 1居间/层间 2普通/直网',
     `version` VARCHAR(20) NOT NULL COMMENT '版本号',
     `file_url` VARCHAR(500) NOT NULL COMMENT '模板文件URL',
     `variables_json` JSON NULL COMMENT '占位符变量定义',
@@ -143,22 +165,16 @@ CREATE TABLE `vpp_resource_point` (
     `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `resource_code` VARCHAR(64) NOT NULL COMMENT '资源编号',
     `resource_name` VARCHAR(200) NOT NULL COMMENT '资源名称',
-    `customer_id` BIGINT NOT NULL COMMENT '所属客户ID',
+    `site_id` BIGINT NULL COMMENT '所属站点ID',
+    `device_id` BIGINT NULL COMMENT '关联设备ID',
     `resource_type` VARCHAR(16) NOT NULL COMMENT 'PV/ESS/EVCS/IND_LOAD/COM_BLDG',
     `capacity_kw` DECIMAL(12,4) NOT NULL COMMENT '装机容量 kW',
-    `adjustable_kw` DECIMAL(12,4) NULL COMMENT '可调容量 kW',
-    `province` VARCHAR(50) NULL COMMENT '省',
-    `city` VARCHAR(50) NULL COMMENT '市',
-    `district` VARCHAR(50) NULL COMMENT '区/区域',
-    `address` VARCHAR(500) NULL COMMENT '地址',
-    `longitude` DECIMAL(10,6) NULL COMMENT '经度',
-    `latitude` DECIMAL(10,6) NULL COMMENT '纬度',
-    `owner_name` VARCHAR(200) NULL COMMENT '业主名称',
-    `contact_name` VARCHAR(100) NULL COMMENT '联系人',
-    `contact_phone` VARCHAR(20) NULL COMMENT '联系电话',
-    `run_status` TINYINT NOT NULL COMMENT '0离线 1在线 2故障 3维护',
-    `response_priority` TINYINT NULL COMMENT '响应优先级1-10',
-    `un_resource_id` VARCHAR(64) NULL COMMENT '运管平台分路资源ID',
+    `is_control` TINYINT NULL COMMENT '是否可控 0否 1是',
+    `is_support_peak` TINYINT NULL COMMENT '是否支持削峰 0否 1是',
+    `is_support_fm` TINYINT NULL COMMENT '是否支持调频 0否 1是',
+    `response_min` INT NULL COMMENT '最小响应时长 分钟',
+    `max_up_kw` DECIMAL(12,4) NULL COMMENT '最大上调能力 kW',
+    `min_down_kw` DECIMAL(12,4) NULL COMMENT '最小下调能力 kW',
     `remark` VARCHAR(500) NULL COMMENT '备注',
     `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
@@ -169,13 +185,14 @@ CREATE TABLE `vpp_resource_point` (
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_resource_code` (resource_code),
-    KEY `idx_resource_customer` (customer_id),
-    KEY `idx_resource_type_status` (resource_type, run_status, deleted_at)
+    KEY `idx_resource_site` (site_id),
+    KEY `idx_resource_device` (device_id),
+    KEY `idx_resource_type_deleted` (resource_type, deleted_at)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='资源点';
 
-CREATE TABLE `vpp_resource_point_config` (
+CREATE TABLE `vpp_site_config` (
     `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
-    `resource_id` BIGINT NOT NULL COMMENT '资源点ID',
+    `site_id` BIGINT NOT NULL COMMENT '站点ID',
     `collect_interval_sec` INT NOT NULL COMMENT '采集频率秒 60/300/900',
     `power_upper_limit` DECIMAL(12,4) NULL COMMENT '功率上限告警',
     `power_lower_limit` DECIMAL(12,4) NULL COMMENT '功率下限告警',
@@ -191,14 +208,15 @@ CREATE TABLE `vpp_resource_point_config` (
     `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT '删除标识 0未删除 1已删除',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
-    UNIQUE KEY `uk_config_resource` (resource_id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='资源点运行参数';
+    UNIQUE KEY `uk_site_config_site` (site_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='点运行参数';
 
 CREATE TABLE `vpp_device` (
     `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `device_code` VARCHAR(64) NOT NULL COMMENT '设备编号',
+    `device_uuid` VARCHAR(64) NULL COMMENT '物联网设备唯一标识',
     `device_name` VARCHAR(200) NOT NULL COMMENT '设备名称',
-    `resource_id` BIGINT NOT NULL COMMENT '所属资源点ID',
+    `site_id` BIGINT NULL COMMENT '所属站点ID',
     `device_type` VARCHAR(32) NOT NULL COMMENT 'INVERTER/PCS/EVCS/METER/Gateway等',
     `manufacturer` VARCHAR(100) NULL COMMENT '厂商',
     `model` VARCHAR(100) NULL COMMENT '型号',
@@ -206,6 +224,7 @@ CREATE TABLE `vpp_device` (
     `firmware_version` VARCHAR(50) NULL COMMENT '固件版本',
     `last_online_at` DATETIME(3) NULL COMMENT '最后在线时间',
     `gateway_id` VARCHAR(64) NULL COMMENT 'IoT网关标识',
+    `un_resource_id` VARCHAR(64) NULL COMMENT '运管资源ID',
     `remark` VARCHAR(500) NULL COMMENT '备注',
     `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
@@ -216,8 +235,9 @@ CREATE TABLE `vpp_device` (
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_device_code` (device_code),
-    KEY `idx_device_resource` (resource_id),
-    KEY `idx_device_comm` (comm_status, run_status)
+    UNIQUE KEY `uk_device_un_resource_id` (`un_resource_id`),
+    KEY `idx_device_uuid` (device_uuid),
+    KEY `idx_device_site` (site_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备';
 
 CREATE TABLE `vpp_device_control_log` (
@@ -238,7 +258,7 @@ CREATE TABLE `vpp_energy_reading_monthly` (
     `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `customer_id` BIGINT NOT NULL COMMENT '客户ID',
     `site_id` BIGINT NULL COMMENT '站点ID,可空表示户号级',
-    `settle_year` SMALLINT NOT NULL COMMENT '结算年',
+    `settle_year` VARCHAR(4) NOT NULL COMMENT '结算年',
     `settle_month` TINYINT NOT NULL COMMENT '结算月',
     `total_energy_kwh` DECIMAL(18,4) NOT NULL COMMENT '总用电量 kWh',
     `peak_energy_kwh` DECIMAL(18,4) NULL COMMENT '峰电量',
@@ -287,7 +307,7 @@ CREATE TABLE `vpp_settlement_bill` (
     `bill_no` VARCHAR(64) NOT NULL COMMENT '账单编号',
     `customer_id` BIGINT NOT NULL COMMENT '客户ID',
     `site_id` BIGINT NULL COMMENT '站点ID,可空表示户号级',
-    `settle_year` SMALLINT NOT NULL COMMENT '结算年',
+    `settle_year` VARCHAR(4) NOT NULL COMMENT '结算年',
     `settle_month` TINYINT NOT NULL COMMENT '结算月',
     `total_energy_kwh` DECIMAL(18,4) NOT NULL COMMENT '总用电量',
     `total_amount` DECIMAL(18,4) NOT NULL COMMENT '应付金额',
@@ -432,6 +452,7 @@ CREATE TABLE `vpp_dr_event` (
     `attachment_url` VARCHAR(500) NULL COMMENT '附件URL',
     `attachment_name` VARCHAR(200) NULL COMMENT '附件名称',
     `subsidy_price` DECIMAL(10,4) NULL COMMENT '补贴标准 元/kWh',
+    `floating_coefficient` DECIMAL(10,4) NULL COMMENT '下浮系数',
     `event_status` TINYINT NOT NULL COMMENT '0待参与 1已申报 2执行中 3已结束 4已取消',
     `raw_payload` JSON NULL COMMENT '原始邀约报文',
     `tenant_id` INT NULL COMMENT '租户ID',
@@ -614,14 +635,103 @@ CREATE TABLE `vpp_report_log` (
     KEY `idx_vpp_report_type_time` (report_type, reported_at)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='数据上报日志';
 
+CREATE TABLE `vpp_site` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+    `site_code` VARCHAR(64) NOT NULL COMMENT '站点编号',
+    `site_name` VARCHAR(200) NOT NULL COMMENT '站点名称',
+    `customer_id` BIGINT NOT NULL COMMENT '客户ID',
+    `province` VARCHAR(50) NULL COMMENT '省',
+    `city` VARCHAR(50) NULL COMMENT '市',
+    `district` VARCHAR(50) NULL COMMENT '区县',
+    `address` VARCHAR(500) NULL COMMENT '地址',
+    `longitude` DECIMAL(10,6) NULL COMMENT '经度',
+    `latitude` DECIMAL(10,6) NULL COMMENT '纬度',
+    `owner_name` VARCHAR(200) NULL COMMENT '业主名称',
+    `contact_name` VARCHAR(100) NULL COMMENT '联系人',
+    `contact_phone` VARCHAR(20) NULL COMMENT '联系电话',
+    `response_priority` INT NULL COMMENT '响应优先级',
+    `un_resource_id` VARCHAR(64) NULL COMMENT '运管平台资源ID',
+    `remark` VARCHAR(500) NULL COMMENT '备注',
+    `tenant_id` INT NULL COMMENT '租户ID',
+    `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
+    `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
+    `created_by` BIGINT NULL COMMENT '创建人',
+    `updated_by` BIGINT NULL COMMENT '更新人',
+    `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT '删除标识 0未删除 1已删除',
+    `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
+    `account_no` VARCHAR(32) NULL COMMENT '电力户号',
+    `street_town` VARCHAR(100) NULL COMMENT '街道乡镇',
+    `avg_completion_rate` DECIMAL(12,4) NULL COMMENT '平均完成率',
+    `run_status` INT NULL COMMENT '运行状态',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_site_code` (`site_code`),
+    KEY `idx_site_customer` (`customer_id`),
+    KEY `idx_site_region` (`province`, `city`, `district`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='站点';
+
+CREATE TABLE `vpp_dr_subsidy_prediction` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+    `invitation_id` BIGINT NULL COMMENT '邀约ID',
+    `dr_event_id` BIGINT NULL COMMENT '需求响应事件ID',
+    `site_id` BIGINT NULL COMMENT '站点ID',
+    `site_name` VARCHAR(200) NULL COMMENT '站点名称',
+    `customer_id` BIGINT NULL COMMENT '企业/客户ID',
+    `customer_name` VARCHAR(200) NULL COMMENT '企业名称',
+    `event_start_time` DATETIME(3) NULL COMMENT '事件开始时间',
+    `event_end_time` DATETIME(3) NULL COMMENT '事件结束时间',
+    `response_duration_min` INT NULL COMMENT '响应时长 分钟',
+    `subsidy_price` DECIMAL(5,2) NULL COMMENT '补贴单价',
+    `estimated_capacity_kw` DECIMAL(12,2) NULL COMMENT '预估响应容量 kW',
+    `cleared_capacity_kw` DECIMAL(12,2) NULL COMMENT '出清容量 kW',
+    `actual_response_grid_kw` DECIMAL(12,2) NULL COMMENT '电网实际响应容量 kW',
+    `actual_response_platform_kw` DECIMAL(12,2) NULL COMMENT '平台实际响应容量 kW',
+    `declared_cleared_kw` DECIMAL(12,2) NULL COMMENT '申报出清容量 kW',
+    `share_ratio` DECIMAL(6,4) NULL COMMENT '分成比例',
+    `price_adjustment_coefficient` DECIMAL(12,4) NULL COMMENT '价格调整系数',
+    `completion_rate` DECIMAL(12,4) NULL COMMENT '完成率(百分比)=平台实际响应量/申报出清量×100',
+    `deviation_rate` DECIMAL(12,4) NULL COMMENT '偏差率(百分比)=(电网实际响应量-平台实际响应量)/申报出清量×100',
+    `estimated_subsidy_amount` DECIMAL(18,4) NULL COMMENT '预估补贴金额',
+    `tenant_id` INT NULL COMMENT '租户ID',
+    `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
+    `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_subsidy_prediction_invitation` (`invitation_id`),
+    KEY `idx_subsidy_prediction_event_site` (`dr_event_id`, `site_id`),
+    KEY `idx_subsidy_prediction_customer` (`customer_id`, `customer_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='需求响应补贴预测';
+
+CREATE TABLE `vpp_response_deviation_price` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+    `coeff_lower` DECIMAL(5,2) NULL COMMENT '响应量系数下限,NULL表示无下界',
+    `coeff_upper` DECIMAL(5,2) NULL COMMENT '响应量系数上限,NULL表示无上界',
+    `lower_inclusive` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '下限是否闭区间 1闭 0开',
+    `upper_inclusive` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '上限是否闭区间 1闭 0开',
+    `price_adjustment_coefficient` DECIMAL(5,2) NOT NULL COMMENT '价格调整系数',
+    `remark` VARCHAR(500) NULL COMMENT '备注',
+    `tenant_id` INT NULL COMMENT '租户ID',
+    `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
+    `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
+    `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT '删除标识 0未删除 1已删除',
+    `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_response_deviation_price_tenant` (`tenant_id`, `delete_flag`),
+    KEY `idx_response_deviation_price_bounds` (`coeff_lower`, `coeff_upper`),
+    KEY `idx_response_deviation_price_deleted` (`delete_flag`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='响应量偏差价格调整表';
+
 CREATE TABLE vpp_file_archive (
     id          BIGINT         NOT NULL AUTO_INCREMENT COMMENT '主键',
+    archive_name VARCHAR(200)  NOT NULL COMMENT '档案名称',
     file_name   VARCHAR(255)   NOT NULL COMMENT '原始文件名',
     file_url    VARCHAR(500)   NOT NULL COMMENT '存储路径',
-    file_type   VARCHAR(32)    NOT NULL COMMENT '文件类型',
-    file_size   BIGINT         NULL COMMENT '文件大小字节',
-    biz_type    VARCHAR(32)    NOT NULL COMMENT '业务类型',
-    biz_id      BIGINT         NOT NULL COMMENT '业务主键',
+    archive_type TINYINT       NOT NULL COMMENT '档案类型',
+    file_type   VARCHAR(32)    NULL COMMENT '文件类型',
+    file_size   DOUBLE         NULL COMMENT '文件大小 KB',
+    biz_type    VARCHAR(32)    NULL COMMENT '业务类型',
+    biz_id      BIGINT         NULL COMMENT '业务主键',
+    site_id     BIGINT         NOT NULL COMMENT '站点ID',
     remark      VARCHAR(500)   NULL COMMENT '备注',
     tenant_id   INT            NULL COMMENT '租户ID',
     create_time DATETIME(3)    NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
@@ -630,8 +740,10 @@ CREATE TABLE vpp_file_archive (
     updated_by  VARCHAR(30)    NULL COMMENT '更新人',
     delete_flag INT(1)         NOT NULL DEFAULT 0 COMMENT '删除标识 0未删除 1已删除',
     deleted_at  DATETIME(3)    NULL COMMENT '软删除时间',
+    version     VARCHAR(20)    NULL DEFAULT 'V1.0' COMMENT '版本号',
     PRIMARY KEY (id),
-    KEY idx_file_archive_biz (biz_type, biz_id)
+    KEY idx_file_archive_biz (biz_type, biz_id),
+    KEY idx_file_archive_site (site_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='电子档案';
 
 CREATE TABLE `vpp_bidding_config` (
@@ -647,7 +759,6 @@ CREATE TABLE `vpp_bidding_config` (
     `notice_max_hours` decimal(5,1) DEFAULT NULL COMMENT '提前通知最大小时数(上限,含本值,如 24 表示 ≤24)',
     `max_price` DECIMAL(12,4) NOT NULL COMMENT '最高价格(元)',
     `min_price` DECIMAL(12,4) NOT NULL COMMENT '最低价格(元)',
-    `site_ids` JSON NULL COMMENT '关联站点ID列表',
     `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
@@ -658,4 +769,8 @@ CREATE TABLE `vpp_bidding_config` (
     PRIMARY KEY (`id`),
     KEY `idx_bidding_config_effective` (effective_start, effective_end),
     KEY `idx_bidding_config_name` (config_name)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='竞价配置';
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='竞价配置';
+
+-- 已有库升级:设备表新增运管资源ID(全局唯一,NULL 不参与唯一冲突)
+-- ALTER TABLE `vpp_device` ADD COLUMN `un_resource_id` VARCHAR(64) NULL COMMENT '运管资源ID' AFTER `gateway_id`;
+-- ALTER TABLE `vpp_device` ADD UNIQUE KEY `uk_device_un_resource_id` (`un_resource_id`);