Selaa lähdekoodia

新增响应量偏差价格调整表&优化补贴金额预测

fuyuchuan 4 tuntia sitten
vanhempi
commit
71e5b46f97
14 muutettua tiedostoa jossa 656 lisäystä ja 103 poistoa
  1. 42 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrController.java
  2. 4 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrSubsidyPrediction.java
  3. 51 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppResponseDeviationPrice.java
  4. 12 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/mapper/VppResponseDeviationPriceMapper.java
  5. 21 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppResponseDeviationPriceService.java
  6. 3 10
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrInvitationServiceImpl.java
  7. 73 57
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrSubsidyPredictionServiceImpl.java
  8. 162 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResponseDeviationPriceServiceImpl.java
  9. 0 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInvitationVO.java
  10. 6 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrSubsidyPredictionVO.java
  11. 20 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResponseDeviationPriceAdjustmentRequest.java
  12. 24 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResponseDeviationPriceAdjustmentVO.java
  13. 103 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppResponseCoefficientHelper.java
  14. 135 34
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_schema.sql

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

@@ -8,6 +8,7 @@ import com.usky.vpp.domain.VppDrStrategy;
 import com.usky.vpp.service.VppDrInvitationService;
 import com.usky.vpp.service.VppDrService;
 import com.usky.vpp.service.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;
@@ -21,6 +22,8 @@ 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 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.*;
 
@@ -40,6 +43,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) {
@@ -225,4 +230,40 @@ public class DrController {
         return vppDrSubsidyPredictionService.delete(id)
                 ? ApiResult.success(true) : ApiResult.error("删除补贴预测失败!请重试!");
     }
-}
+
+    // ==================== 响应量偏差价格调整 ====================
+
+    @GetMapping(value = "/responseDeviationPriceAdjustment")
+    public ApiResult<CommonPage<ResponseDeviationPriceAdjustmentVO>> pageResponseDeviationPriceAdjustment(
+            @RequestParam(value = "responseCoefficient", required = false) String responseCoefficient,
+            @RequestParam(value = "current", required = false, defaultValue = "1") Integer current,
+            @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
+        return ApiResult.success(responseDeviationPriceService.page(responseCoefficient, current, size));
+    }
+
+    @GetMapping(value = "/responseDeviationPriceAdjustment/{id}")
+    public ApiResult<ResponseDeviationPriceAdjustmentVO> getResponseDeviationPriceAdjustment(
+            @PathVariable("id") Long id) {
+        return ApiResult.success(responseDeviationPriceService.get(id));
+    }
+
+    @PostMapping(value = "/responseDeviationPriceAdjustment")
+    public ApiResult<Long> createResponseDeviationPriceAdjustment(
+            @RequestBody ResponseDeviationPriceAdjustmentRequest request) {
+        return ApiResult.success(responseDeviationPriceService.create(request));
+    }
+
+    @PutMapping(value = "/responseDeviationPriceAdjustment/{id}")
+    public ApiResult<Void> updateResponseDeviationPriceAdjustment(
+            @PathVariable("id") Long id,
+            @RequestBody ResponseDeviationPriceAdjustmentRequest request) {
+        responseDeviationPriceService.update(id, request);
+        return ApiResult.success();
+    }
+
+    @DeleteMapping(value = "/responseDeviationPriceAdjustment/{id}")
+    public ApiResult<Void> deleteResponseDeviationPriceAdjustment(@PathVariable("id") Long id) {
+        responseDeviationPriceService.delete(id);
+        return ApiResult.success();
+    }
+}

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

@@ -75,6 +75,10 @@ public class VppDrSubsidyPrediction implements Serializable {
     @TableField("share_ratio")
     private BigDecimal shareRatio;
 
+    /** 按完成率命中响应量偏差价格调整表得到的价格调整系数。 */
+    @TableField("price_adjustment_coefficient")
+    private BigDecimal priceAdjustmentCoefficient;
+
     @TableField("completion_rate")
     private BigDecimal completionRate;
 

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

@@ -0,0 +1,51 @@
+package com.usky.vpp.domain;
+
+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;
+
+    /** 响应量系数区间,例如 [0.8, 1.2]。 */
+    @TableField("response_coefficient")
+    private String responseCoefficient;
+
+    /** 命中区间后用于补贴计算的价格调整系数。 */
+    @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> {
+}

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

@@ -0,0 +1,21 @@
+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;
+
+/**
+ * 响应量偏差价格调整配置服务。
+ */
+public interface VppResponseDeviationPriceService {
+
+    CommonPage<ResponseDeviationPriceAdjustmentVO> page(String responseCoefficient, Integer current, Integer size);
+
+    ResponseDeviationPriceAdjustmentVO get(Long id);
+
+    Long create(ResponseDeviationPriceAdjustmentRequest request);
+
+    void update(Long id, ResponseDeviationPriceAdjustmentRequest request);
+
+    void delete(Long id);
+}

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

+ 73 - 57
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrSubsidyPredictionServiceImpl.java

@@ -5,20 +5,18 @@ 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.VppResponseDeviationPrice;
 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.mapper.*;
+import com.usky.vpp.mapper.VppResponseDeviationPriceMapper;
 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 com.usky.vpp.util.VppResponseCoefficientHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -56,7 +54,7 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
     @Autowired
     private VppSiteMapper siteMapper;
     @Autowired
-    private VppContractMapper contractMapper;
+    private VppResponseDeviationPriceMapper priceAdjustmentMapper;
 
     @Override
     public CommonPage<DrSubsidyPredictionVO> page(String siteName, String startTime, String endTime,
@@ -80,8 +78,9 @@ 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);
@@ -106,7 +105,7 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         VppDrSubsidyPrediction entity = new VppDrSubsidyPrediction();
         applyRequest(entity, request);
         fillSiteName(entity);
-        recalculate(entity);
+        recalculate(entity, loadActivePriceAdjustments(SecurityUtils.getTenantId()));
         entity.setTenantId(SecurityUtils.getTenantId());
         entity.setCreateTime(LocalDateTime.now());
         entity.setUpdateTime(LocalDateTime.now());
@@ -132,7 +131,7 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         if (request.getSiteId() != null) {
             fillSiteName(existing);
         }
-        recalculate(existing);
+        recalculate(existing, loadActivePriceAdjustments(tenantId != null ? tenantId : existing.getTenantId()));
         existing.setUpdateTime(LocalDateTime.now());
         return subsidyPredictionMapper.updateById(existing) > 0;
     }
@@ -185,14 +184,10 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
                 : siteMapper.selectBatchIds(siteIds).stream()
                         .collect(Collectors.toMap(VppSite::getId, VppSite::getSiteName, (a, b) -> a));
 
-        Set<Long> customerIds = invitations.stream()
-                .map(VppDrInvitation::getCustomerId)
-                .filter(Objects::nonNull)
-                .collect(Collectors.toSet());
-        Map<Long, BigDecimal> shareRatioMap = loadCustomerShareRatio(customerIds);
+        List<VppResponseDeviationPrice> adjustments = loadActivePriceAdjustments(event.getTenantId());
 
         for (VppDrInvitation invitation : invitations) {
-            VppDrSubsidyPrediction prediction = buildPrediction(event, invitation, siteNameMap, shareRatioMap);
+            VppDrSubsidyPrediction prediction = buildPrediction(event, invitation, siteNameMap, adjustments);
 
             VppDrSubsidyPrediction existing = subsidyPredictionMapper.selectOne(
                     new LambdaQueryWrapper<VppDrSubsidyPrediction>()
@@ -215,7 +210,7 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
 
     private VppDrSubsidyPrediction buildPrediction(VppDrEvent event, VppDrInvitation invitation,
                                                     Map<Long, String> siteNameMap,
-                                                    Map<Long, BigDecimal> shareRatioMap) {
+                                                    List<VppResponseDeviationPrice> adjustments) {
         VppDrSubsidyPrediction p = new VppDrSubsidyPrediction();
         p.setInvitationId(invitation.getId());
         p.setDrEventId(event.getId());
@@ -236,32 +231,11 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         p.setDeclaredClearedKw(declaredKw);
         p.setEstimatedCapacityKw(platformKw);
         p.setClearedCapacityKw(declaredKw);
-        p.setShareRatio(normalizeShareRatio(shareRatioMap.get(invitation.getCustomerId())));
         p.setTenantId(event.getTenantId());
-        recalculate(p);
+        recalculate(p, adjustments);
         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());
-            }
-        }
-        return map;
-    }
-
     private void applyRequest(VppDrSubsidyPrediction entity, DrSubsidyPredictionRequest request) {
         if (request.getInvitationId() != null) {
             entity.setInvitationId(request.getInvitationId());
@@ -306,15 +280,50 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         entity.setSiteName(site != null ? site.getSiteName() : null);
     }
 
+    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.getResponseCoefficient(), completionRate)) {
+                    return adjustment.getPriceAdjustmentCoefficient();
+                }
+            }
+        }
+        throw new BusinessException("完成率 " + completionRate.stripTrailingZeros().toPlainString()
+                + " 未匹配到响应量偏差价格调整配置");
+    }
+
+    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>偏差率 = (电网实际响应量 − 平台实际响应量) / 申报出清量</li>
-     *   <li>完成率 = 平台实际响应量 / 申报出清量</li>
-     *   <li>补贴金额 = 平台实际响应量 × 响应时长(h) × 补贴单价 × 分成比例</li>
+     *   <li>完成率 = 1 − 偏差率</li>
+     *   <li>补贴金额 = 实际响应量 × 响应时长(h) × 价格调整系数 × 补贴标准</li>
      * </ul>
      */
-    private void recalculate(VppDrSubsidyPrediction entity) {
+    private void recalculate(VppDrSubsidyPrediction entity,
+                             List<VppResponseDeviationPrice> adjustments) {
         int durationMin = 0;
         if (entity.getEventStartTime() != null && entity.getEventEndTime() != null
                 && entity.getEventEndTime().isAfter(entity.getEventStartTime())) {
@@ -330,27 +339,32 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
                 ? entity.getDeclaredClearedKw()
                 : entity.getClearedCapacityKw();
 
-        BigDecimal completionRate = BigDecimal.ZERO;
-        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);
-            }
+        if (declaredKw == null || declaredKw.compareTo(BigDecimal.ZERO) <= 0
+                || gridKw == null || platformKw == null) {
+            entity.setDeviationRate(null);
+            entity.setCompletionRate(null);
+            entity.setPriceAdjustmentCoefficient(null);
+            entity.setEstimatedSubsidyAmount(null);
+            return;
         }
-        entity.setCompletionRate(completionRate);
+
+        BigDecimal deviationRate = gridKw.subtract(platformKw)
+                .divide(declaredKw, 4, RoundingMode.HALF_UP);
+        BigDecimal completionRate = BigDecimal.ONE.subtract(deviationRate)
+                .setScale(4, RoundingMode.HALF_UP);
+        BigDecimal priceAdjustmentCoefficient = resolvePriceAdjustmentCoefficient(completionRate, adjustments);
         entity.setDeviationRate(deviationRate);
+        entity.setCompletionRate(completionRate);
+        entity.setPriceAdjustmentCoefficient(priceAdjustmentCoefficient);
 
+        BigDecimal actualResponseKw = gridKw;
         BigDecimal subsidyAmount = null;
-        BigDecimal shareRatio = normalizeShareRatio(entity.getShareRatio());
-        if (platformKw != null && entity.getSubsidyPrice() != null && durationMin > 0 && shareRatio != null) {
+        if (entity.getSubsidyPrice() != null && durationMin > 0) {
             BigDecimal hours = BigDecimal.valueOf(durationMin).divide(MINUTES_PER_HOUR, 6, RoundingMode.HALF_UP);
-            subsidyAmount = platformKw
+            subsidyAmount = actualResponseKw
                     .multiply(hours)
+                    .multiply(priceAdjustmentCoefficient)
                     .multiply(entity.getSubsidyPrice())
-                    .multiply(shareRatio)
                     .setScale(4, RoundingMode.HALF_UP);
         }
         entity.setEstimatedSubsidyAmount(subsidyAmount);
@@ -367,10 +381,11 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         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());
@@ -384,6 +399,7 @@ public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPrediction
         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());

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

@@ -0,0 +1,162 @@
+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 org.springframework.util.StringUtils;
+
+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(String responseCoefficient,
+                                                                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 (StringUtils.hasText(responseCoefficient)) {
+            wrapper.like(VppResponseDeviationPrice::getResponseCoefficient,
+                    responseCoefficient.trim());
+        }
+
+        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) {
+        adjustment.setResponseCoefficient(request.getResponseCoefficient().trim().replace(',', ','));
+        adjustment.setPriceAdjustmentCoefficient(request.getPriceAdjustmentCoefficient());
+        adjustment.setRemark(request.getRemark());
+    }
+
+    private void validateRequest(ResponseDeviationPriceAdjustmentRequest request, Long excludeId) {
+        if (request == null) {
+            throw new BusinessException("请求体不能为空");
+        }
+        VppResponseCoefficientHelper.parse(request.getResponseCoefficient());
+        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) {
+            if (VppResponseCoefficientHelper.overlaps(request.getResponseCoefficient(),
+                    item.getResponseCoefficient())) {
+                throw new BusinessException("响应量系数区间与已有配置重叠:" + item.getResponseCoefficient());
+            }
+        }
+    }
+
+    private ResponseDeviationPriceAdjustmentVO toVo(VppResponseDeviationPrice adjustment) {
+        ResponseDeviationPriceAdjustmentVO vo = new ResponseDeviationPriceAdjustmentVO();
+        vo.setId(adjustment.getId());
+        vo.setResponseCoefficient(adjustment.getResponseCoefficient());
+        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);
+        }
+    }
+}

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

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

@@ -15,6 +15,9 @@ public class DrSubsidyPredictionVO {
 
     private Long invitationId;
 
+    /** 邀约编号,替代界面展示的 drEventCode。 */
+    private String invitationNo;
+
     private Long drEventId;
 
     private Long siteId;
@@ -49,6 +52,9 @@ public class DrSubsidyPredictionVO {
     /** 分成比例(0~1) */
     private BigDecimal shareRatio;
 
+    /** 按完成率匹配得到的价格调整系数。 */
+    private BigDecimal priceAdjustmentCoefficient;
+
     /** 完成率(0~1) */
     private BigDecimal completionRate;
 

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

@@ -0,0 +1,20 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 响应量偏差价格调整配置新增/修改请求。
+ */
+@Data
+public class ResponseDeviationPriceAdjustmentRequest {
+
+    /** 响应量系数区间,例如 <0.6、[0.6, 0.8)、(1.2, 1.4]、>1.4。 */
+    private String responseCoefficient;
+
+    /** 价格调整系数。 */
+    private BigDecimal priceAdjustmentCoefficient;
+
+    private String remark;
+}

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

@@ -0,0 +1,24 @@
+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 String responseCoefficient;
+    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;
+}

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

@@ -0,0 +1,103 @@
+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 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 boolean matches(String expression, BigDecimal coefficient) {
+        return coefficient != null && parse(expression).contains(coefficient);
+    }
+
+    public static boolean overlaps(String left, String right) {
+        return parse(left).overlaps(parse(right));
+    }
+
+    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;
+        }
+
+        private 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;
+        }
+
+        private boolean overlaps(Range other) {
+            return upperAllows(other.lower, other.lowerInclusive)
+                    && other.upperAllows(lower, lowerInclusive);
+        }
+
+        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);
+        }
+    }
+}

+ 135 - 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 '型号',
@@ -216,8 +234,8 @@ 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)
+    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 +256,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 +305,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 '应付金额',
@@ -615,14 +633,96 @@ 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 '站点名称',
+    `event_start_time` DATETIME(3) NULL COMMENT '事件开始时间',
+    `event_end_time` DATETIME(3) NULL COMMENT '事件结束时间',
+    `response_duration_min` INT NULL COMMENT '响应时长 分钟',
+    `subsidy_price` DECIMAL(12,4) NULL COMMENT '补贴单价',
+    `estimated_capacity_kw` DECIMAL(12,4) NULL COMMENT '预估响应容量 kW',
+    `cleared_capacity_kw` DECIMAL(12,4) NULL COMMENT '出清容量 kW',
+    `actual_response_grid_kw` DECIMAL(12,4) NULL COMMENT '电网实际响应容量 kW',
+    `actual_response_platform_kw` DECIMAL(12,4) NULL COMMENT '平台实际响应容量 kW',
+    `declared_cleared_kw` DECIMAL(12,4) 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 '完成率',
+    `deviation_rate` DECIMAL(12,4) NULL COMMENT '偏差率',
+    `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`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='需求响应补贴预测';
+
+CREATE TABLE `vpp_response_deviation_price` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+    `response_coefficient` VARCHAR(32) NOT NULL COMMENT '响应量系数区间',
+    `price_adjustment_coefficient` DECIMAL(12,4) 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`),
+    UNIQUE KEY `uk_response_deviation_price` (`tenant_id`, `response_coefficient`),
+    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 '创建时间',
@@ -631,8 +731,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` (
@@ -648,7 +750,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 '更新时间',
@@ -659,4 +760,4 @@ 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='竞价配置';