Explorar o código

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

fuyuchuan hai 9 horas
pai
achega
b50f7e81e1
Modificáronse 29 ficheiros con 931 adicións e 160 borrados
  1. 9 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ContractController.java
  2. 28 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrController.java
  3. 5 4
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrMonitorController.java
  4. 16 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContract.java
  5. 7 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContractTemplate.java
  6. 24 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppCustomer.java
  7. 18 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrSubsidyPrediction.java
  8. 11 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppBaselineService.java
  9. 6 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppContractService.java
  10. 3 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrMonitorService.java
  11. 16 8
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrSubsidyPredictionService.java
  12. 21 5
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppBaselineServiceImpl.java
  13. 192 32
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppContractServiceImpl.java
  14. 12 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppContractTemplateServiceImpl.java
  15. 16 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppCustomerServiceImpl.java
  16. 100 19
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrMonitorServiceImpl.java
  17. 223 43
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrSubsidyPredictionServiceImpl.java
  18. 20 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CustomerRequestVO.java
  19. 12 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CustomerResponseVO.java
  20. 5 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrMonitorCurveVO.java
  21. 8 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrMonitorRecordVO.java
  22. 48 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrSubsidyPredictionRequest.java
  23. 19 45
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrSubsidyPredictionVO.java
  24. 31 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractOverviewVO.java
  25. 25 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractRequestVO.java
  26. 31 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractResponseVO.java
  27. 5 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractTemplateRequestVO.java
  28. 5 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractTemplateResponseVO.java
  29. 15 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppBaselineHelper.java

+ 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));
+    }
+
     /**
      * 删除合同(软删除)
      */

+ 28 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrController.java

@@ -19,6 +19,7 @@ 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.DrSubsidyPredictionRequest;
 import com.usky.vpp.service.vo.DrSubsidyPredictionVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
@@ -197,4 +198,31 @@ public class DrController {
             @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
         return ApiResult.success(vppDrSubsidyPredictionService.page(siteName, startTime, endTime, current, size));
     }
+
+    /**
+     * 新增补贴金额预测(站点/响应时间手动选择;时长、偏差率、完成率、补贴金额自动计算)
+     */
+    @PostMapping(value = "/subsidyPrediction")
+    public ApiResult<Boolean> createSubsidyPrediction(@RequestBody DrSubsidyPredictionRequest request) {
+        return vppDrSubsidyPredictionService.create(request)
+                ? ApiResult.success(true) : ApiResult.error("新增补贴预测失败!请重试!");
+    }
+
+    /**
+     * 修改补贴金额预测
+     */
+    @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("删除补贴预测失败!请重试!");
+    }
 }

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

@@ -56,13 +56,14 @@ public class DrMonitorController {
     }
 
     /**
-     * 折线图:基线/实际负荷 + 申报/实际响应容量
-     * <p>必传站点;仅 is_support_peak=1 资源点;曲线按资源点加权平均。</p>
+     * 折线图:两条基线(5日/3日)+ 当日实测;时间窗为响应前2小时 + 响应时长 + 响应后1小时
+     * <p>baselineDays 传 3 或 5 时优先展示对应基线到 baselineKw;为空则同时返回两条。</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));
+                                             @RequestParam("siteIds") List<Long> siteIds,
+                                             @RequestParam(value = "baselineDays", required = false) Integer baselineDays) {
+        return ApiResult.success(drMonitorService.getCurve(id, siteIds, baselineDays));
     }
 
     // private List<Long> parseSiteIds(List<String> raw) {

+ 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;
+
     /**
      * 市
      */

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

@@ -51,12 +51,30 @@ 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("completion_rate")
     private BigDecimal completionRate;
 

+ 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);
 }

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

@@ -30,6 +30,8 @@ public interface VppDrMonitorService {
 
     /**
      * 折线图:基线/实际负荷按可调峰资源点加权平均;申报/实际响应按资源点平均
+     *
+     * @param baselineDays 基线参考天数,仅支持 3 或 5;为空时同时返回 3/5 日两条基线
      */
-    DrMonitorCurveVO getCurve(Long eventId, List<Long> siteIds);
+    DrMonitorCurveVO getCurve(Long eventId, List<Long> siteIds, Integer baselineDays);
 }

+ 16 - 8
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrSubsidyPredictionService.java

@@ -1,6 +1,7 @@
 package com.usky.vpp.service;
 
 import com.usky.common.core.bean.CommonPage;
+import com.usky.vpp.service.vo.DrSubsidyPredictionRequest;
 import com.usky.vpp.service.vo.DrSubsidyPredictionVO;
 
 /**
@@ -10,20 +11,27 @@ public interface VppDrSubsidyPredictionService {
 
     /**
      * 分页查询补贴金额预测
-     *
-     * @param siteName   站点名称(模糊匹配)
-     * @param startTime  响应开始时间起始
-     * @param endTime    响应开始时间截止
-     * @param current    页码
-     * @param size       页大小
      */
     CommonPage<DrSubsidyPredictionVO> page(String siteName, String startTime, String endTime,
                                            Integer current, Integer size);
 
+    /**
+     * 新增补贴预测(站点、响应时间可选手动;时长/偏差率/完成率/补贴金额自动计算)
+     */
+    Boolean create(DrSubsidyPredictionRequest request);
+
+    /**
+     * 修改补贴预测
+     */
+    Boolean update(DrSubsidyPredictionRequest request);
+
+    /**
+     * 删除补贴预测
+     */
+    Boolean delete(Long id);
+
     /**
      * 根据事件ID生成/更新补贴预测数据
-     *
-     * @param drEventId 需求响应事件ID
      */
     void generateByEventId(Long drEventId);
 }

+ 21 - 5
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppBaselineServiceImpl.java

@@ -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();
@@ -101,9 +110,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()
@@ -135,10 +149,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 +179,7 @@ public class VppBaselineServiceImpl implements VppBaselineService {
         List<BaselinePointVO> points = VppBaselineHelper.buildBaselinePoints(
                 responseDate,
                 startTime,
+                todayQueryEnd,
                 predictedBaselineByTime.isEmpty() ? applyCorrection(originalBaselineByTime, correctionFactorK)
                         : predictedBaselineByTime,
                 todayActualByTime,

+ 192 - 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;
@@ -62,11 +63,11 @@ public class VppContractServiceImpl implements VppContractService {
 
     @Override
     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);
 
         VppContract contract = new VppContract();
@@ -75,6 +76,9 @@ public class VppContractServiceImpl implements VppContractService {
         contract.setTemplateId(vo.getTemplateId());
         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,8 +92,10 @@ public class VppContractServiceImpl implements VppContractService {
         contract.setRemark(vo.getRemark());
 
         VppAuditHelper.fillCreate(contract);
-
         int insert = vppContractMapper.insert(contract);
+        if (insert > 0) {
+            bindRelatedContract(contract, vo.getRelatedContractId(), template, null);
+        }
         return insert > 0;
     }
 
@@ -108,7 +114,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 +135,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());
         }
@@ -155,8 +178,11 @@ public class VppContractServiceImpl implements VppContractService {
                 requestedStatus, existingRecord.getEffectiveDate(), existingRecord.getExpireDate()));
 
         VppAuditHelper.fillUpdate(existingRecord);
-
         int result = vppContractMapper.updateById(existingRecord);
+        if (result > 0 && vo.getRelatedContractId() != null) {
+            VppContractTemplate template = getTemplateById(existingRecord.getTemplateId());
+            bindRelatedContract(existingRecord, vo.getRelatedContractId(), template, previousRelatedId);
+        }
         return result > 0;
     }
 
@@ -204,6 +230,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 +240,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 +291,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 +403,118 @@ 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 bindRelatedContract(VppContract self, Long relatedId,
+                                     VppContractTemplate selfTemplate, Long previousRelatedId) {
+        if (relatedId == null) {
+            return;
+        }
+        if (self.getId() != null && relatedId.equals(self.getId())) {
+            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
+                && self.getId() != null
+                && !related.getRelatedContractId().equals(self.getId())) {
+            throw new BusinessException("目标合同已被其他合同关联,请先解除原关联!");
+        }
+        Integer occupied = vppContractMapper.selectCount(new LambdaQueryWrapper<VppContract>()
+                .eq(VppContract::getRelatedContractId, relatedId)
+                .eq(VppContract::getDeleteFlag, 0)
+                .ne(self.getId() != null, VppContract::getId, self.getId()));
+        if (occupied != null && occupied > 0) {
+            throw new BusinessException("目标合同已被其他合同关联,请先解除原关联!");
+        }
+
+        if (previousRelatedId != null && !previousRelatedId.equals(relatedId)) {
+            clearRelatedLink(self.getId(), previousRelatedId);
+        }
+
+        self.setRelatedContractId(relatedId);
+        VppAuditHelper.fillUpdate(self);
+        vppContractMapper.updateById(self);
+
+        related.setRelatedContractId(self.getId());
+        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());

+ 16 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppCustomerServiceImpl.java

@@ -390,6 +390,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 +588,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());
         }

+ 100 - 19
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;
@@ -76,6 +78,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 +205,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,11 +224,14 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
     }
 
     @Override
-    public DrMonitorCurveVO getCurve(Long eventId, List<Long> siteIds) {
+    public DrMonitorCurveVO getCurve(Long eventId, List<Long> siteIds, Integer baselineDays) {
         if (eventId == null) {
             throw new BusinessException("事件ID不能为空");
         }
         requireSiteIds(siteIds);
+        if (baselineDays != null && baselineDays != 3 && baselineDays != 5) {
+            throw new BusinessException("baselineDays 仅支持 3 或 5");
+        }
         Integer tenantId = SecurityUtils.getTenantId();
         VppDrEvent event = eventMapper.selectById(eventId);
         if (event == null || VppAuditHelper.isDeleted(event.getDeleteFlag())) {
@@ -257,9 +266,13 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
         BigDecimal declaredAvg = avg(declaredSum, resourceCount);
         BigDecimal actualAvg = avg(actualSum, resourceCount);
 
+        LocalDateTime windowStart = event.getStartTime().minusHours(2);
+        LocalDateTime windowEnd = (event.getEndTime() != null ? event.getEndTime() : event.getStartTime()).plusHours(1);
         String responseStartTime = event.getStartTime().format(DATE_TIME_FMT);
-        // 按资源点加权:站点曲线 × 该站可调峰资源数,再 / 总资源数
-        Map<String, BigDecimal> baselineWeighted = new LinkedHashMap<>();
+        String actualEndTime = windowEnd.format(DATE_TIME_FMT);
+
+        Map<String, BigDecimal> baseline5Weighted = new LinkedHashMap<>();
+        Map<String, BigDecimal> baseline3Weighted = new LinkedHashMap<>();
         Map<String, BigDecimal> loadWeighted = new LinkedHashMap<>();
         Set<String> dataSources = new LinkedHashSet<>();
         List<Long> usedSiteIds = new ArrayList<>();
@@ -267,22 +280,39 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
         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())) {
+            SiteBaselineVO baseline5 = baselineService.getSiteBaseline(siteId, responseStartTime, 5, actualEndTime);
+            SiteBaselineVO baseline3 = baselineService.getSiteBaseline(siteId, responseStartTime, 3, actualEndTime);
+            if ((baseline5 == null || CollectionUtils.isEmpty(baseline5.getPoints()))
+                    && (baseline3 == null || CollectionUtils.isEmpty(baseline3.getPoints()))) {
                 continue;
             }
             usedSiteIds.add(siteId);
-            if (StringUtils.hasText(baseline.getDataSource())) {
-                dataSources.add(baseline.getDataSource());
+            if (baseline5 != null && StringUtils.hasText(baseline5.getDataSource())) {
+                dataSources.add(baseline5.getDataSource());
             }
-            for (BaselinePointVO point : baseline.getPoints()) {
-                if (!StringUtils.hasText(point.getTime())) {
-                    continue;
+            if (baseline5 != null && !CollectionUtils.isEmpty(baseline5.getPoints())) {
+                for (BaselinePointVO point : baseline5.getPoints()) {
+                    if (!StringUtils.hasText(point.getTime()) || !inCurveWindow(point.getTime(), windowStart, windowEnd)) {
+                        continue;
+                    }
+                    baseline5Weighted.merge(point.getTime(),
+                            nz(point.getPredictedBaselineKw()).multiply(weight), BigDecimal::add);
+                    loadWeighted.merge(point.getTime(),
+                            nz(point.getTodayActualKw()).multiply(weight), BigDecimal::add);
+                }
+            }
+            if (baseline3 != null && !CollectionUtils.isEmpty(baseline3.getPoints())) {
+                for (BaselinePointVO point : baseline3.getPoints()) {
+                    if (!StringUtils.hasText(point.getTime()) || !inCurveWindow(point.getTime(), windowStart, windowEnd)) {
+                        continue;
+                    }
+                    baseline3Weighted.merge(point.getTime(),
+                            nz(point.getPredictedBaselineKw()).multiply(weight), BigDecimal::add);
+                    if (!loadWeighted.containsKey(point.getTime())) {
+                        loadWeighted.merge(point.getTime(),
+                                nz(point.getTodayActualKw()).multiply(weight), BigDecimal::add);
+                    }
                 }
-                baselineWeighted.merge(point.getTime(),
-                        nz(point.getPredictedBaselineKw()).multiply(weight), BigDecimal::add);
-                loadWeighted.merge(point.getTime(),
-                        nz(point.getTodayActualKw()).multiply(weight), BigDecimal::add);
             }
         }
 
@@ -302,13 +332,26 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
             curve.setDataSource("mixed");
         }
 
-        List<String> times = new ArrayList<>(baselineWeighted.keySet());
+        Set<String> timeSet = new LinkedHashSet<>();
+        timeSet.addAll(baseline5Weighted.keySet());
+        timeSet.addAll(baseline3Weighted.keySet());
+        timeSet.addAll(loadWeighted.keySet());
+        List<String> times = new ArrayList<>(timeSet);
         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));
+            BigDecimal b5 = baseline5Weighted.getOrDefault(time, BigDecimal.ZERO)
+                    .divide(divisor, SCALE, RoundingMode.HALF_UP);
+            BigDecimal b3 = baseline3Weighted.getOrDefault(time, BigDecimal.ZERO)
+                    .divide(divisor, SCALE, RoundingMode.HALF_UP);
+            point.setBaseline5DayKw(b5);
+            point.setBaseline3DayKw(b3);
+            if (baselineDays != null && baselineDays == 3) {
+                point.setBaselineKw(b3);
+            } else {
+                point.setBaselineKw(b5);
+            }
             point.setLoadKw(loadWeighted.getOrDefault(time, BigDecimal.ZERO)
                     .divide(divisor, SCALE, RoundingMode.HALF_UP));
             point.setDeclaredCapacityKw(declaredAvg);
@@ -318,6 +361,42 @@ public class VppDrMonitorServiceImpl implements VppDrMonitorService {
         return curve;
     }
 
+    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;
+        }
+    }
+
+    private Map<Long, BigDecimal> loadSubsidyAmountByEvent(Set<Long> eventIds, Set<Long> siteIds, Integer tenantId) {
+        if (CollectionUtils.isEmpty(eventIds)) {
+            return Collections.emptyMap();
+        }
+        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 ----------
 
     private void requireSiteIds(List<Long> siteIds) {
@@ -391,9 +470,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);

+ 223 - 43
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrSubsidyPredictionServiceImpl.java

@@ -4,15 +4,19 @@ 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.VppContract;
 import com.usky.vpp.domain.VppDrEvent;
 import com.usky.vpp.domain.VppDrInvitation;
 import com.usky.vpp.domain.VppDrSubsidyPrediction;
 import com.usky.vpp.domain.VppSite;
+import com.usky.vpp.mapper.VppContractMapper;
 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.service.VppDrSubsidyPredictionService;
+import com.usky.vpp.service.vo.DrSubsidyPredictionRequest;
 import com.usky.vpp.service.vo.DrSubsidyPredictionVO;
 import com.usky.vpp.util.VppAuditHelper;
 import org.slf4j.Logger;
@@ -32,7 +36,6 @@ 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 +45,7 @@ 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);
 
     @Autowired
     private VppDrSubsidyPredictionMapper subsidyPredictionMapper;
@@ -51,6 +55,8 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
     private VppDrEventMapper eventMapper;
     @Autowired
     private VppSiteMapper siteMapper;
+    @Autowired
+    private VppContractMapper contractMapper;
 
     @Override
     public CommonPage<DrSubsidyPredictionVO> page(String siteName, String startTime, String endTime,
@@ -60,6 +66,8 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
                 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);
         }
@@ -72,7 +80,6 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         wrapper.orderByDesc(VppDrSubsidyPrediction::getCreateTime);
 
         Page<VppDrSubsidyPrediction> result = subsidyPredictionMapper.selectPage(page, wrapper);
-
         List<DrSubsidyPredictionVO> list = result.getRecords().stream()
                 .map(this::toVo)
                 .collect(Collectors.toList());
@@ -80,6 +87,73 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         return new CommonPage<>(list, result.getTotal(), size != null ? size : 20, current != null ? current : 1);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean create(DrSubsidyPredictionRequest request) {
+        if (request == null) {
+            throw new BusinessException("请求参数不能为空");
+        }
+        if (request.getSiteId() == null) {
+            throw new BusinessException("站点不能为空");
+        }
+        if (request.getEventStartTime() == null || request.getEventEndTime() == null) {
+            throw new BusinessException("响应起止时间不能为空");
+        }
+        if (!request.getEventEndTime().isAfter(request.getEventStartTime())) {
+            throw new BusinessException("响应结束时间必须晚于开始时间");
+        }
+
+        VppDrSubsidyPrediction entity = new VppDrSubsidyPrediction();
+        applyRequest(entity, request);
+        fillSiteName(entity);
+        recalculate(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("补贴预测记录不存在");
+        }
+
+        applyRequest(existing, request);
+        if (request.getSiteId() != null) {
+            fillSiteName(existing);
+        }
+        recalculate(existing);
+        existing.setUpdateTime(LocalDateTime.now());
+        return subsidyPredictionMapper.updateById(existing) > 0;
+    }
+
+    @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 +161,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 +185,22 @@ 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, BigDecimal> shareRatioMap = loadCustomerShareRatio(customerIds);
+
         for (VppDrInvitation invitation : invitations) {
-            VppDrSubsidyPrediction prediction = buildPrediction(event, invitation, siteNameMap);
+            VppDrSubsidyPrediction prediction = buildPrediction(event, invitation, siteNameMap, shareRatioMap);
 
-            // 按 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,56 +213,158 @@ 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, BigDecimal> shareRatioMap) {
         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.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 = invitation.getEstimatedResponseCapacityKw();
+        BigDecimal gridKw = invitation.getActualResponseCapacityKw();
+        BigDecimal declaredKw = invitation.getDeclaredCapacityKw() != null
+                ? invitation.getDeclaredCapacityKw()
+                : invitation.getActualClearedCapacityKw();
+
+        p.setActualResponsePlatformKw(platformKw);
+        p.setActualResponseGridKw(gridKw);
+        p.setDeclaredClearedKw(declaredKw);
+        p.setEstimatedCapacityKw(platformKw);
+        p.setClearedCapacityKw(declaredKw);
+        p.setShareRatio(normalizeShareRatio(shareRatioMap.get(invitation.getCustomerId())));
+        p.setTenantId(event.getTenantId());
+        recalculate(p);
+        return p;
+    }
+
+    private Map<Long, BigDecimal> loadCustomerShareRatio(Set<Long> customerIds) {
+        if (customerIds == null || customerIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        List<VppContract> contracts = contractMapper.selectList(new LambdaQueryWrapper<VppContract>()
+                .in(VppContract::getCustomerId, customerIds)
+                .eq(VppContract::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .orderByDesc(VppContract::getCreateTime));
+        Map<Long, BigDecimal> map = new java.util.HashMap<>();
+        for (VppContract contract : contracts) {
+            if (contract.getCustomerId() == null || map.containsKey(contract.getCustomerId())) {
+                continue;
+            }
+            if (contract.getCustomerRatio() != null) {
+                map.put(contract.getCustomerId(), contract.getCustomerRatio());
+            }
         }
-        p.setResponseDurationMin(durationMin);
+        return map;
+    }
 
-        // 补贴标准
-        p.setSubsidyPrice(event.getSubsidyPrice());
+    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.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()));
+        }
+    }
 
-        // 预估响应容量
-        p.setEstimatedCapacityKw(invitation.getEstimatedResponseCapacityKw());
+    private void fillSiteName(VppDrSubsidyPrediction entity) {
+        if (entity.getSiteId() == null) {
+            entity.setSiteName(null);
+            return;
+        }
+        VppSite site = siteMapper.selectById(entity.getSiteId());
+        entity.setSiteName(site != null ? site.getSiteName() : null);
+    }
 
-        // 实际出清
-        p.setClearedCapacityKw(invitation.getActualClearedCapacityKw());
+    /**
+     * 自动计算:响应时长、偏差率、完成率、补贴金额
+     * <ul>
+     *   <li>偏差率 = (电网实际响应量 − 平台实际响应量) / 申报出清量</li>
+     *   <li>完成率 = 平台实际响应量 / 申报出清量</li>
+     *   <li>补贴金额 = 平台实际响应量 × 响应时长(h) × 补贴单价 × 分成比例</li>
+     * </ul>
+     */
+    private void recalculate(VppDrSubsidyPrediction entity) {
+        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 platformKw = entity.getActualResponsePlatformKw() != null
+                ? entity.getActualResponsePlatformKw()
+                : entity.getEstimatedCapacityKw();
+        BigDecimal gridKw = entity.getActualResponseGridKw();
+        BigDecimal declaredKw = entity.getDeclaredClearedKw() != null
+                ? entity.getDeclaredClearedKw()
+                : entity.getClearedCapacityKw();
 
-        // 预估完成率 = 预估容量 / 实际出清;实际出清为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);
+        BigDecimal deviationRate = BigDecimal.ZERO;
+        if (declaredKw != null && declaredKw.compareTo(BigDecimal.ZERO) > 0) {
+            if (platformKw != null) {
+                completionRate = platformKw.divide(declaredKw, 4, RoundingMode.HALF_UP);
+            }
+            if (gridKw != null && platformKw != null) {
+                deviationRate = gridKw.subtract(platformKw).divide(declaredKw, 4, RoundingMode.HALF_UP);
+            }
         }
-        p.setCompletionRate(completionRate);
-
-        // 预估偏差率 = 1 - 预估完成率
-        BigDecimal deviationRate = BigDecimal.ONE.subtract(completionRate);
-        p.setDeviationRate(deviationRate);
+        entity.setCompletionRate(completionRate);
+        entity.setDeviationRate(deviationRate);
 
-        // 预估补贴金额 = 补贴标准(为NULL时记为NULL)
-        p.setEstimatedSubsidyAmount(event.getSubsidyPrice());
+        BigDecimal subsidyAmount = null;
+        BigDecimal shareRatio = normalizeShareRatio(entity.getShareRatio());
+        if (platformKw != null && entity.getSubsidyPrice() != null && durationMin > 0 && shareRatio != null) {
+            BigDecimal hours = BigDecimal.valueOf(durationMin).divide(MINUTES_PER_HOUR, 6, RoundingMode.HALF_UP);
+            subsidyAmount = platformKw
+                    .multiply(hours)
+                    .multiply(entity.getSubsidyPrice())
+                    .multiply(shareRatio)
+                    .setScale(4, RoundingMode.HALF_UP);
+        }
+        entity.setEstimatedSubsidyAmount(subsidyAmount);
+    }
 
-        p.setTenantId(event.getTenantId());
-        return p;
+    /** 分成比例统一为 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) {
@@ -204,6 +380,10 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         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.setCompletionRate(entity.getCompletionRate());
         vo.setDeviationRate(entity.getDeviationRate());
         vo.setEstimatedSubsidyAmount(entity.getEstimatedSubsidyAmount());

+ 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;

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

@@ -28,8 +28,12 @@ public class DrMonitorCurveVO {
     public static class Point {
         /** 时刻 HH:mm */
         private String time;
-        /** 基线负荷(预测)kW */
+        /** 基线负荷(兼容旧字段,默认取 5 日或指定周期) */
         private BigDecimal baselineKw;
+        /** 5 日基线 kW */
+        private BigDecimal baseline5DayKw;
+        /** 3 日基线 kW */
+        private BigDecimal baseline3DayKw;
         /** 实际负荷 kW */
         private BigDecimal loadKw;
         /** 申报容量 kW */

+ 8 - 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,8 +17,11 @@ 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;
@@ -28,7 +32,9 @@ public class DrMonitorRecordVO {
     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 +43,6 @@ public class DrMonitorRecordVO {
     private BigDecimal responseCompletionRate;
     /** 是否达标:关联邀约 response_completion_rate = 100% */
     private Boolean qualified;
+    /** 补贴金额(预测) */
+    private BigDecimal estimatedSubsidyAmount;
 }

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

@@ -0,0 +1,48 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 补贴金额预测 请求VO(新增/编辑)
+ */
+@Data
+public class DrSubsidyPredictionRequest {
+
+    /** 主键(更新时必传) */
+    private Long id;
+
+    /** 邀约ID(可选) */
+    private Long invitationId;
+
+    /** 需求响应事件ID(可选) */
+    private Long drEventId;
+
+    /** 站点ID(手动选择) */
+    private Long siteId;
+
+    /** 响应开始时间(手动选择) */
+    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;
+}

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

@@ -13,78 +13,52 @@ public class DrSubsidyPredictionVO {
 
     private Long id;
 
-    /**
-     * 邀约ID
-     */
     private Long invitationId;
 
-    /**
-     * 需求响应事件ID
-     */
     private Long drEventId;
 
-    /**
-     * 站点ID
-     */
     private Long siteId;
 
-    /**
-     * 站点名称
-     */
     private String siteName;
 
-    /**
-     * 响应开始时间
-     */
     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;
+
+    /** 完成率(0~1) */
     private BigDecimal completionRate;
 
-    /**
-     * 预估偏差率(0~1)
-     */
+    /** 偏差率(0~1) */
     private BigDecimal deviationRate;
 
-    /**
-     * 预估补贴金额
-     */
+    /** 预估补贴金额(元) */
     private BigDecimal estimatedSubsidyAmount;
 
-    /**
-     * 创建时间
-     */
     private LocalDateTime createTime;
 
-    /**
-     * 更新时间
-     */
     private LocalDateTime updateTime;
 }

+ 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;
+
     /**
      * 版本号
      */

+ 15 - 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)));