فهرست منبع

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

hanzhengyi 3 روز پیش
والد
کامیت
828c8993fa
58فایلهای تغییر یافته به همراه2364 افزوده شده و 1064 حذف شده
  1. 24 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppSocialBenefitConstants.java
  2. 10 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppTsdbConstants.java
  3. 32 12
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DashboardController.java
  4. 24 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrController.java
  5. 8 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppBiddingConfig.java
  6. 11 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContract.java
  7. 5 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDevice.java
  8. 16 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrEvent.java
  9. 20 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrInvitation.java
  10. 1 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrParticipation.java
  11. 79 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrSubsidyPrediction.java
  12. 46 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/enums/DeviceTypeMetricEnum.java
  13. 40 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/enums/ElectricSeasonEnum.java
  14. 11 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/enums/ElectricUserTypeEnum.java
  15. 58 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/enums/TimeSegmentEnum.java
  16. 37 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/job/VppSettlementMonthlyScheduler.java
  17. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/job/VppUnPollScheduler.java
  18. 12 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/mapper/VppDrSubsidyPredictionMapper.java
  19. 20 7
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDashboardService.java
  20. 29 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrSubsidyPredictionService.java
  21. 3 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppSettlementService.java
  22. 23 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppSettlementSyncService.java
  23. 9 4
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppContractServiceImpl.java
  24. 617 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDashboardServiceImpl.java
  25. 37 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrInvitationServiceImpl.java
  26. 41 6
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrServiceImpl.java
  27. 214 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrSubsidyPredictionServiceImpl.java
  28. 382 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppSettlementSyncServiceImpl.java
  29. 3 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/BiddingConfigRequest.java
  30. 4 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/BiddingConfigVO.java
  31. 30 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DashboardCurveVO.java
  32. 28 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DashboardMapVO.java
  33. 112 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DashboardSummaryVO.java
  34. 12 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrEventDetailVO.java
  35. 12 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrEventRequest.java
  36. 12 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInvitationRequest.java
  37. 12 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInvitationVO.java
  38. 2 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrParticipateRequest.java
  39. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrParticipationVO.java
  40. 90 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrSubsidyPredictionVO.java
  41. 7 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractRequestVO.java
  42. 7 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractResponseVO.java
  43. 59 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/ElectricFeeCalcUtil.java
  44. 65 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/ElectricTimeUtil.java
  45. 5 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppAuditHelper.java
  46. 13 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppDrParticipationHelper.java
  47. 59 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppEnergyUsageHelper.java
  48. 4 0
      service-vpp/service-vpp-biz/src/main/resources/bootstrap.yml
  49. 0 33
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_audit_field_unify_migration.sql
  50. 0 35
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_bidding_config_migration.sql
  51. 0 191
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_delete_flag_migration.sql
  52. 0 41
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_dr_invitation_migration.sql
  53. 0 24
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_dr_participation_completion_rate_migration.sql
  54. 0 284
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_id_auto_increment_migration.sql
  55. 0 362
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_id_reset_from_one_migration.sql
  56. 0 13
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_report_task_site_ids_migration.sql
  57. 17 8
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_schema.sql
  58. 0 32
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_tenant_migration.sql

+ 24 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppSocialBenefitConstants.java

@@ -0,0 +1,24 @@
+package com.usky.vpp.constant;
+
+import java.math.BigDecimal;
+
+/**
+ * 大屏社会效益折算系数(按响应电量 MWh)
+ */
+public final class VppSocialBenefitConstants {
+
+    /** 1 MWh → 减碳量(吨 CO₂) */
+    public static final BigDecimal CARBON_TON_PER_MWH = new BigDecimal("0.785");
+
+    /** 1 MWh → 节约标准煤(吨) */
+    public static final BigDecimal COAL_TON_PER_MWH = new BigDecimal("0.404");
+
+    /** 1 吨 CO₂ → 等效植树(棵) */
+    public static final BigDecimal TREES_PER_CARBON_TON = new BigDecimal("1.44");
+
+    /** 1 MWh → 等效绿证(张) */
+    public static final BigDecimal GREEN_CERT_PER_MWH = BigDecimal.ONE;
+
+    private VppSocialBenefitConstants() {
+    }
+}

+ 10 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppTsdbConstants.java

@@ -45,11 +45,21 @@ public final class VppTsdbConstants {
     /** 正向总有功电能(用电累计) */
     public static final String METRIC_EPP = "epp";
 
+    /** 反向总有功电能(发电累计) */
+    public static final String METRIC_EPN = "epn";
+
+    /** 尖/峰/平/谷 正向有功电度 */
+    public static final String METRIC_EPP1 = "epp1";
+    public static final String METRIC_EPP2 = "epp2";
+    public static final String METRIC_EPP3 = "epp3";
+    public static final String METRIC_EPP4 = "epp4";
+
     /** 总有功功率 kW */
     public static final String METRIC_P = "p";
 
     /** 储能 SOC % */
     public static final String METRIC_SOC = "soc";
 
+
     public static final String TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
 }

+ 32 - 12
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DashboardController.java

@@ -2,12 +2,18 @@ package com.usky.vpp.controller.web;
 
 import com.usky.common.core.bean.ApiResult;
 import com.usky.vpp.service.VppDashboardService;
+import com.usky.vpp.service.vo.DashboardCurveVO;
+import com.usky.vpp.service.vo.DashboardMapVO;
+import com.usky.vpp.service.vo.DashboardSummaryVO;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
 
 /**
- * 虚拟电厂 - Dashboard 接口
- * 网关前缀: /prod-api/service-vpp
+ * 虚拟电厂 - 运行管理监控大屏
+ * <p>网关前缀: /prod-api/service-vpp</p>
  */
 @RestController
 @RequestMapping("/dashboard")
@@ -16,16 +22,30 @@ public class DashboardController {
     @Autowired
     private VppDashboardService vppDashboardService;
 
-    @GetMapping(value = "/summary")
-    public ApiResult<Object> summary(@RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+    /**
+     * 大屏汇总 KPI
+     * <p>租户隔离;削峰/填谷/补贴/当年调控/社会效益/收益排名仅统计已结束事件。</p>
+     */
+    @GetMapping("/summary")
+    public ApiResult<DashboardSummaryVO> summary() {
+        return ApiResult.success(vppDashboardService.getSummary());
     }
-    @GetMapping(value = "/map")
-    public ApiResult<Object> mapResources(@RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+
+    /**
+     * 地图站点点位(含经纬度与装机容量)
+     */
+    @GetMapping("/map")
+    public ApiResult<DashboardMapVO> mapResources() {
+        return ApiResult.success(vppDashboardService.getMap());
     }
-    @GetMapping(value = "/curve")
-    public ApiResult<Object> curve(@RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+
+    /**
+     * 当日用电负荷曲线(基础功率 vs 负荷功率)
+     *
+     * @param date 统计日期 yyyy-MM-dd,默认当天
+     */
+    @GetMapping("/curve")
+    public ApiResult<DashboardCurveVO> curve(@RequestParam(value = "date", required = false) String date) {
+        return ApiResult.success(vppDashboardService.getCurve(date));
     }
 }

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

@@ -7,6 +7,7 @@ import com.usky.vpp.domain.VppDrEvaluation;
 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.vo.DrClearingRequest;
 import com.usky.vpp.service.vo.DrEventDetailVO;
 import com.usky.vpp.service.vo.DrEventRequest;
@@ -18,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.DrSubsidyPredictionVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
@@ -35,6 +37,8 @@ public class DrController {
     private VppDrService vppDrService;
     @Autowired
     private VppDrInvitationService vppDrInvitationService;
+    @Autowired
+    private VppDrSubsidyPredictionService vppDrSubsidyPredictionService;
 
     @GetMapping(value = "/event")
     public ApiResult<CommonPage<VppDrEvent>> pageEvent(@RequestParam(required = false) Map<String, Object> params) {
@@ -173,4 +177,24 @@ public class DrController {
                                                        @RequestBody DrInvitationReplyRequest body) {
         return ApiResult.success(vppDrInvitationService.replyInvitation(id, body));
     }
+
+    // ==================== 补贴金额预测 ====================
+
+    /**
+     * 分页查询补贴金额预测
+     * siteName  站点名称(模糊匹配)
+     * startTime 响应开始时间起始
+     * endTime   响应开始时间截止
+     * current   页码
+     * size      页大小
+     */
+    @GetMapping(value = "/subsidyPrediction")
+    public ApiResult<CommonPage<DrSubsidyPredictionVO>> pageSubsidyPrediction(
+            @RequestParam(value = "siteName", required = false) String siteName,
+            @RequestParam(value = "startTime", required = false) String startTime,
+            @RequestParam(value = "endTime", required = false) String endTime,
+            @RequestParam(value = "current", required = false, defaultValue = "1") Integer current,
+            @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
+        return ApiResult.success(vppDrSubsidyPredictionService.page(siteName, startTime, endTime, current, size));
+    }
 }

+ 8 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppBiddingConfig.java

@@ -4,6 +4,7 @@ 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 com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
@@ -47,16 +48,23 @@ public class VppBiddingConfig implements Serializable {
     private String siteIds;
     @TableField("tenant_id")
     private Integer tenantId;
+
     @TableField("create_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime createTime;
+
     @TableField("update_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime updateTime;
+
     @TableField("created_by")
     private String createdBy;
     @TableField("updated_by")
     private String updatedBy;
     @TableField("delete_flag")
     private Integer deleteFlag;
+
     @TableField("deleted_at")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime deletedAt;
 }

+ 11 - 2
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContract.java

@@ -72,8 +72,17 @@ public class VppContract implements Serializable {
     @TableField("file_url")
     private String fileUrl;
 
-    @TableField("share_ratio")
-    private BigDecimal shareRatio;
+    /**
+     * 客户比例
+    */
+    @TableField("customer_ratio")
+    private BigDecimal customerRatio;
+
+    /**
+     * 运营商比例
+     */
+    @TableField("operator_ratio")
+    private BigDecimal operatorRatio;
 
     @TableField("price_json")
     private String priceJson;

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

@@ -57,7 +57,11 @@ public class VppDevice implements Serializable {
     private Integer deleteFlag;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
+
+    /**
+     * 物联网实时状态(来自 dmp_device_status,非本表字段)
+     * 1:在线,2:离线
+     */
     @TableField(exist = false)
-    /** 1:在线,2:离线 */
     private Integer deviceStatus;
 }

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

@@ -34,10 +34,26 @@ public class VppDrEvent implements Serializable {
     private LocalDateTime startTime;
     @TableField("end_time")
     private LocalDateTime endTime;
+    /** 申报截止时间 */
+    @TableField("declare_deadline")
+    private LocalDateTime declareDeadline;
+    /** 邀约范围 */
+    @TableField("invite_scope")
+    private String inviteScope;
+    /** 邀约规模 kW */
     @TableField("target_capacity_kw")
     private BigDecimal targetCapacityKw;
+    /** 出清规模 kW */
     @TableField("cleared_capacity_kw")
     private BigDecimal clearedCapacityKw;
+    /** 备注 */
+    private String remark;
+    /** 附件 URL */
+    @TableField("attachment_url")
+    private String attachmentUrl;
+    /** 附件名称 */
+    @TableField("attachment_name")
+    private String attachmentName;
     @TableField("subsidy_price")
     private BigDecimal subsidyPrice;
     @TableField("event_status")

+ 20 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrInvitation.java

@@ -30,6 +30,8 @@ public class VppDrInvitation implements Serializable {
     private Long drEventId;
     @TableField("customer_id")
     private Long customerId;
+    @TableField("site_id")
+    private Long siteId;
     @TableField("execute_start_date")
     private LocalDate executeStartDate;
     @TableField("execute_end_date")
@@ -44,8 +46,26 @@ public class VppDrInvitation implements Serializable {
     private Integer responseType;
     @TableField("demand_capacity_kw")
     private BigDecimal demandCapacityKw;
+    /** 申报出清容量 kW */
     @TableField("declared_capacity_kw")
     private BigDecimal declaredCapacityKw;
+    /** 实际出清 kW(电网审核通过) */
+    @TableField("actual_cleared_capacity_kw")
+    private BigDecimal actualClearedCapacityKw;
+    /** 预估响应容量 kW(响应完成后本平台评估) */
+    @TableField("estimated_response_capacity_kw")
+    private BigDecimal estimatedResponseCapacityKw;
+    /** 实际响应容量 kW(电网评估最终结果) */
+    @TableField("actual_response_capacity_kw")
+    private BigDecimal actualResponseCapacityKw;
+    /**
+     * 响应完成率 = 实际响应容量 / 邀约规模(需求容量);展示时 ×100%
+     */
+    @TableField("response_completion_rate")
+    private BigDecimal responseCompletionRate;
+    /** 是否中标 0否 1是(单选,集中标记) */
+    @TableField("is_winning_bid")
+    private Integer isWinningBid;
     @TableField("reply_status")
     private Integer replyStatus;
     @TableField("response_status")

+ 1 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrParticipation.java

@@ -30,6 +30,7 @@ public class VppDrParticipation implements Serializable {
     private Long resourceId;
     @TableField("participate_status")
     private Integer participateStatus;
+    /** 申报出清容量 */
     @TableField("declared_capacity_kw")
     private BigDecimal declaredCapacityKw;
     @TableField("cleared_capacity_kw")

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

@@ -0,0 +1,79 @@
+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 com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * vpp_dr_subsidy_prediction - 需求响应补贴金额预测
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("vpp_dr_subsidy_prediction")
+public class VppDrSubsidyPrediction implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("invitation_id")
+    private Long invitationId;
+
+    @TableField("dr_event_id")
+    private Long drEventId;
+
+    @TableField("site_id")
+    private Long siteId;
+
+    @TableField("site_name")
+    private String siteName;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField("event_start_time")
+    private LocalDateTime eventStartTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField("event_end_time")
+    private LocalDateTime eventEndTime;
+
+    @TableField("response_duration_min")
+    private Integer responseDurationMin;
+
+    @TableField("subsidy_price")
+    private BigDecimal subsidyPrice;
+
+    @TableField("estimated_capacity_kw")
+    private BigDecimal estimatedCapacityKw;
+
+    @TableField("cleared_capacity_kw")
+    private BigDecimal clearedCapacityKw;
+
+    @TableField("completion_rate")
+    private BigDecimal completionRate;
+
+    @TableField("deviation_rate")
+    private BigDecimal deviationRate;
+
+    @TableField("estimated_subsidy_amount")
+    private BigDecimal estimatedSubsidyAmount;
+
+    @TableField("tenant_id")
+    private Integer tenantId;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField("create_time")
+    private LocalDateTime createTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+}

+ 46 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/enums/DeviceTypeMetricEnum.java

@@ -0,0 +1,46 @@
+package com.usky.vpp.enums;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * 设备类型与时序电量属性映射(用于月度电量核算)
+ */
+public enum DeviceTypeMetricEnum {
+
+    /**
+     * 充电桩 product/device_type = 809
+     */
+    EV_CHARGER_809(
+            "809",
+            Arrays.asList("epp1", "epp2", "epp3", "epp4", "epn1", "epn2", "epn3", "epn4", "epp", "epn")
+    );
+
+    private final String deviceType;
+    private final List<String> metrics;
+
+    DeviceTypeMetricEnum(String deviceType, List<String> metrics) {
+        this.deviceType = deviceType;
+        this.metrics = Collections.unmodifiableList(metrics);
+    }
+
+    public String getDeviceType() {
+        return deviceType;
+    }
+
+    public List<String> getMetrics() {
+        return metrics;
+    }
+
+    public static Optional<DeviceTypeMetricEnum> ofDeviceType(String deviceType) {
+        if (deviceType == null || deviceType.trim().isEmpty()) {
+            return Optional.empty();
+        }
+        String key = deviceType.trim();
+        return Arrays.stream(values())
+                .filter(item -> item.deviceType.equalsIgnoreCase(key))
+                .findFirst();
+    }
+}

+ 40 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/enums/ElectricSeasonEnum.java

@@ -0,0 +1,40 @@
+package com.usky.vpp.enums;
+
+import java.time.Month;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 上海分时电价季节分类(沪发改价管〔2022〕50号)
+ */
+public enum ElectricSeasonEnum {
+
+    /** 夏季:7,8,9月 */
+    SUMMER(EnumSet.of(Month.JULY, Month.AUGUST, Month.SEPTEMBER)),
+    /** 冬季尖峰月:1,12月 */
+    WINTER_SPIKE(EnumSet.of(Month.JANUARY, Month.DECEMBER)),
+    /** 普通月份:3,4,5,6,10,11月 */
+    NORMAL(EnumSet.of(Month.MARCH, Month.APRIL, Month.MAY, Month.JUNE, Month.OCTOBER, Month.NOVEMBER));
+
+    private final Set<Integer> monthNumbers;
+
+    ElectricSeasonEnum(Set<Month> months) {
+        this.monthNumbers = Collections.unmodifiableSet(months.stream()
+                .map(Month::getValue)
+                .collect(Collectors.toSet()));
+    }
+
+    public static ElectricSeasonEnum getByMonth(int month) {
+        return Arrays.stream(values())
+                .filter(season -> season.monthNumbers.contains(month))
+                .findFirst()
+                .orElseThrow(() -> new IllegalArgumentException("非法月份:" + month));
+    }
+
+    public Set<Integer> getMonthNumbers() {
+        return monthNumbers;
+    }
+}

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

@@ -0,0 +1,11 @@
+package com.usky.vpp.enums;
+
+/**
+ * 工商业电价用户类型
+ */
+public enum ElectricUserTypeEnum {
+    /** 两部制:尖/峰/平/谷 */
+    TWO_PART,
+    /** 单一制:仅峰/谷 */
+    SINGLE_PART
+}

+ 58 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/enums/TimeSegmentEnum.java

@@ -0,0 +1,58 @@
+package com.usky.vpp.enums;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+
+/**
+ * 分时时段:尖峰、高峰、平段、低谷
+ */
+public enum TimeSegmentEnum {
+
+    SPIKE(1, "epp1", "epn1", new BigDecimal("1.20"), 4),
+    PEAK(2, "epp2", "epn2", null, 3),
+    FLAT(3, "epp3", "epn3", new BigDecimal("1.00"), 2),
+    VALLEY(4, "epp4", "epn4", new BigDecimal("0.50"), 1);
+
+    /** 账单明细 time_period:1尖 2峰 3平 4谷 */
+    private final int periodCode;
+    private final String forwardMetric;
+    private final String reverseMetric;
+    private final BigDecimal rateFactor;
+    private final int priority;
+
+    TimeSegmentEnum(int periodCode, String forwardMetric, String reverseMetric,
+                    BigDecimal rateFactor, int priority) {
+        this.periodCode = periodCode;
+        this.forwardMetric = forwardMetric;
+        this.reverseMetric = reverseMetric;
+        this.rateFactor = rateFactor;
+        this.priority = priority;
+    }
+
+    public int getPeriodCode() {
+        return periodCode;
+    }
+
+    public String getForwardMetric() {
+        return forwardMetric;
+    }
+
+    public String getReverseMetric() {
+        return reverseMetric;
+    }
+
+    public BigDecimal getRateFactor() {
+        return rateFactor;
+    }
+
+    public int getPriority() {
+        return priority;
+    }
+
+    public static TimeSegmentEnum fromPeriodCode(int periodCode) {
+        return Arrays.stream(values())
+                .filter(item -> item.periodCode == periodCode)
+                .findFirst()
+                .orElseThrow(() -> new IllegalArgumentException("未知时段编码:" + periodCode));
+    }
+}

+ 37 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/job/VppSettlementMonthlyScheduler.java

@@ -0,0 +1,37 @@
+package com.usky.vpp.job;
+
+import com.usky.vpp.service.VppSettlementSyncService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * 每月 1 号 00:35 同步上月电量核算与电费账单(电费金额为 0)
+ */
+@Component
+public class VppSettlementMonthlyScheduler {
+
+    private static final Logger log = LoggerFactory.getLogger(VppSettlementMonthlyScheduler.class);
+
+    @Value("${vpp.settlement.monthly-sync-enabled:true}")
+    private boolean enabled;
+
+    @Autowired
+    private VppSettlementSyncService settlementSyncService;
+
+    @Scheduled(cron = "${vpp.settlement.monthly-sync-cron:0 35 0 1 * ?}")
+    public void syncLastMonthSettlement() {
+        if (!enabled) {
+            return;
+        }
+        try {
+            int success = settlementSyncService.syncLastMonth();
+            log.info("月度电量/电费同步任务完成, successSites={}", success);
+        } catch (Exception ex) {
+            log.error("月度电量/电费同步任务失败: {}", ex.getMessage(), ex);
+        }
+    }
+}

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

@@ -22,7 +22,7 @@ public class VppUnPollScheduler {
     @Autowired
     private VppUnIntegrationService integrationService;
 
-    @Scheduled(fixedDelayString = "#{${vpp.un.poll-interval-sec:10} * 1000}")
+    //@Scheduled(fixedDelayString = "#{${vpp.un.poll-interval-sec:10} * 1000}")
     public void pollUn() {
         if (!properties.isPollActive()) {
             return;

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

@@ -0,0 +1,12 @@
+package com.usky.vpp.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.vpp.domain.VppDrSubsidyPrediction;
+import org.springframework.stereotype.Repository;
+
+/**
+ * vpp_dr_subsidy_prediction Mapper
+ */
+@Repository
+public interface VppDrSubsidyPredictionMapper extends CrudMapper<VppDrSubsidyPrediction> {
+}

+ 20 - 7
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDashboardService.java

@@ -1,15 +1,28 @@
 package com.usky.vpp.service;
 
-import com.usky.common.core.bean.CommonPage;
-
-import java.util.Map;
+import com.usky.vpp.service.vo.DashboardCurveVO;
+import com.usky.vpp.service.vo.DashboardMapVO;
+import com.usky.vpp.service.vo.DashboardSummaryVO;
 
 /**
- * VppDashboardService 业务接口
+ * 运行管理监控大屏
  */
 public interface VppDashboardService {
 
-    default Object stub(String action, Map<String, Object> params) {
-        return null;
-    }
+    /**
+     * 大屏汇总 KPI(租户隔离;DR 相关仅统计已结束事件)
+     */
+    DashboardSummaryVO getSummary();
+
+    /**
+     * 当日用电负荷曲线(基础功率 vs 负荷功率)
+     *
+     * @param date yyyy-MM-dd,为空默认当天
+     */
+    DashboardCurveVO getCurve(String date);
+
+    /**
+     * 地图站点点位
+     */
+    DashboardMapVO getMap();
 }

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

@@ -0,0 +1,29 @@
+package com.usky.vpp.service;
+
+import com.usky.common.core.bean.CommonPage;
+import com.usky.vpp.service.vo.DrSubsidyPredictionVO;
+
+/**
+ * 补贴金额预测 业务接口
+ */
+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);
+
+    /**
+     * 根据事件ID生成/更新补贴预测数据
+     *
+     * @param drEventId 需求响应事件ID
+     */
+    void generateByEventId(Long drEventId);
+}

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

@@ -16,6 +16,9 @@ public interface VppSettlementService {
         return null;
     }
 
+    /**
+     * 分页查询月度电量核算数据
+     */
     CommonPage<VppEnergyReadingMonthly> pageReading(Long id, String customerName, String calcCycle,
                                                     Integer current, Integer size);
 

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

@@ -0,0 +1,23 @@
+package com.usky.vpp.service;
+
+/**
+ * 月度电量/电费核算数据同步
+ */
+public interface VppSettlementSyncService {
+
+    /**
+     * 同步上月(相对当前时间)各站点电量与电费账单(电费金额为 0)。
+     *
+     * @return 成功处理的站点数量
+     */
+    int syncLastMonth();
+
+    /**
+     * 同步指定结算年月的电量与电费账单(电费金额为 0)。
+     *
+     * @param settleYear  结算年,如 2026
+     * @param settleMonth 结算月,1-12
+     * @return 成功处理的站点数量
+     */
+    int syncMonth(int settleYear, int settleMonth);
+}

+ 9 - 4
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppContractServiceImpl.java

@@ -80,7 +80,8 @@ public class VppContractServiceImpl implements VppContractService {
         contract.setEffectiveDate(vo.getEffectiveDate());
         contract.setExpireDate(vo.getExpireDate());
         contract.setFileUrl(vo.getFileUrl());
-        contract.setShareRatio(vo.getShareRatio());
+        contract.setCustomerRatio(vo.getCustomerRatio());
+        contract.setOperatorRatio(vo.getOperatorRatio());
         contract.setPriceJson(VppFieldValidator.validateJson(vo.getPriceJson(), "价格定义"));
         contract.setAccountInfoJson(VppFieldValidator.validateJson(vo.getAccountInfoJson(), "账户信息"));
         contract.setRemark(vo.getRemark());
@@ -131,8 +132,11 @@ public class VppContractServiceImpl implements VppContractService {
         if (StringUtils.isNotBlank(vo.getFileUrl())) {
             existingRecord.setFileUrl(vo.getFileUrl());
         }
-        if (vo.getShareRatio() != null) {
-            existingRecord.setShareRatio(vo.getShareRatio());
+        if (vo.getCustomerRatio() != null) {
+            existingRecord.setCustomerRatio(vo.getCustomerRatio());
+        }
+        if (vo.getOperatorRatio() != null) {
+            existingRecord.setOperatorRatio(vo.getOperatorRatio());
         }
         if (vo.getPriceJson() != null) {
             existingRecord.setPriceJson(VppFieldValidator.validateJson(vo.getPriceJson(), "价格定义"));
@@ -207,7 +211,8 @@ public class VppContractServiceImpl implements VppContractService {
             vo.setEffectiveDate(entity.getEffectiveDate());
             vo.setExpireDate(entity.getExpireDate());
             vo.setFileUrl(entity.getFileUrl());
-            vo.setShareRatio(entity.getShareRatio());
+            vo.setCustomerRatio(entity.getCustomerRatio());
+            vo.setOperatorRatio(entity.getOperatorRatio());
             vo.setPriceJson(entity.getPriceJson());
             vo.setAccountInfoJson(entity.getAccountInfoJson());
             vo.setRemark(entity.getRemark());

+ 617 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDashboardServiceImpl.java

@@ -1,11 +1,627 @@
 package com.usky.vpp.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.vpp.constant.VppSocialBenefitConstants;
+import com.usky.vpp.domain.DmpDeviceStatus;
+import com.usky.vpp.domain.VppCustomer;
+import com.usky.vpp.domain.VppDevice;
+import com.usky.vpp.domain.VppDrEvaluation;
+import com.usky.vpp.domain.VppDrEvent;
+import com.usky.vpp.domain.VppDrInvitation;
+import com.usky.vpp.domain.VppDrParticipation;
+import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.domain.VppSite;
+import com.usky.vpp.mapper.DmpDeviceStatusMapper;
+import com.usky.vpp.mapper.VppCustomerMapper;
+import com.usky.vpp.mapper.VppDeviceMapper;
+import com.usky.vpp.mapper.VppDrEvaluationMapper;
+import com.usky.vpp.mapper.VppDrEventMapper;
+import com.usky.vpp.mapper.VppDrInvitationMapper;
+import com.usky.vpp.mapper.VppDrParticipationMapper;
+import com.usky.vpp.mapper.VppResourcePointMapper;
+import com.usky.vpp.mapper.VppSiteMapper;
+import com.usky.vpp.service.VppCapabilityEvalService;
 import com.usky.vpp.service.VppDashboardService;
+import com.usky.vpp.service.vo.CapabilityEvalSummaryVO;
+import com.usky.vpp.service.vo.CapabilityLoadCurveVO;
+import com.usky.vpp.service.vo.CapabilityLoadPointVO;
+import com.usky.vpp.service.vo.DashboardCurveVO;
+import com.usky.vpp.service.vo.DashboardMapVO;
+import com.usky.vpp.service.vo.DashboardSummaryVO;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppCapabilityEvalHelper;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.Year;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
- * VppDashboardService 默认实现(待按业务完善)
+ * 运行管理监控大屏
+ * <p>租户隔离;DR 相关指标仅统计 event_status=已结束 的历史事件。</p>
  */
 @Service
 public class VppDashboardServiceImpl implements VppDashboardService {
+
+    private static final int EVENT_STATUS_ENDED = 3;
+    private static final int EVENT_TYPE_PEAK = 1;
+    private static final int EVENT_TYPE_VALLEY = 2;
+    private static final int REPLY_ACCEPT = 1;
+    /** dmp_device_status.device_status:1 在线,2 离线 */
+    private static final int DMP_STATUS_ONLINE = 1;
+    private static final int IS_CONTROL = 1;
+    private static final int IS_WINNING = 1;
+    private static final int SCALE = 4;
+    private static final int MONEY_SCALE = 2;
+    private static final BigDecimal KW_PER_MW = new BigDecimal("1000");
+
+    @Autowired
+    private VppSiteMapper siteMapper;
+    @Autowired
+    private VppCustomerMapper customerMapper;
+    @Autowired
+    private VppResourcePointMapper resourcePointMapper;
+    @Autowired
+    private VppDeviceMapper deviceMapper;
+    @Autowired
+    private DmpDeviceStatusMapper dmpDeviceStatusMapper;
+    @Autowired
+    private VppDrEventMapper eventMapper;
+    @Autowired
+    private VppDrEvaluationMapper evaluationMapper;
+    @Autowired
+    private VppDrInvitationMapper invitationMapper;
+    @Autowired
+    private VppDrParticipationMapper participationMapper;
+    @Autowired
+    private VppCapabilityEvalService capabilityEvalService;
+
+    @Override
+    public DashboardSummaryVO getSummary() {
+        Integer tenantId = SecurityUtils.getTenantId();
+        DashboardSummaryVO vo = new DashboardSummaryVO();
+
+        List<VppSite> sites = listSites(tenantId);
+        List<VppResourcePoint> resources = listResources(tenantId);
+        List<VppDevice> devices = listDevices(tenantId);
+        List<VppDrEvent> endedEvents = listEndedEvents(tenantId);
+        Map<Long, VppDrEvaluation> evaluationMap = loadEvaluationMap(endedEvents);
+        List<VppDrInvitation> endedInvitations = listInvitationsByEvents(endedEvents, tenantId);
+        Map<Long, List<VppDrInvitation>> invitationsByEvent = endedInvitations.stream()
+                .filter(i -> i.getDrEventId() != null)
+                .collect(Collectors.groupingBy(VppDrInvitation::getDrEventId));
+
+        fillAccess(vo.getAccess(), sites, resources);
+        fillBasic(vo.getBasic(), tenantId, resources, endedEvents, evaluationMap, invitationsByEvent);
+        fillDeviceStatus(vo.getDeviceStatus(), devices);
+        fillSubsidy(vo.getSubsidy(), evaluationMap);
+        fillYearRegulation(vo.getYearRegulation(), tenantId, endedEvents, evaluationMap, invitationsByEvent);
+        fillSocialBenefit(vo.getSocialBenefit(), endedEvents, evaluationMap, invitationsByEvent);
+        fillEarningsRank(vo, sites, resources, endedEvents, evaluationMap, endedInvitations);
+        return vo;
+    }
+
+    @Override
+    public DashboardCurveVO getCurve(String date) {
+        CapabilityLoadCurveVO source = capabilityEvalService.getLoadCurve(null, date);
+        DashboardCurveVO curve = new DashboardCurveVO();
+        curve.setStatDate(source.getDate());
+        curve.setDataSource(source.getDataSource());
+        if (!CollectionUtils.isEmpty(source.getPoints())) {
+            List<DashboardCurveVO.Point> points = new ArrayList<>(source.getPoints().size());
+            for (CapabilityLoadPointVO p : source.getPoints()) {
+                DashboardCurveVO.Point point = new DashboardCurveVO.Point();
+                point.setTime(p.getTime());
+                point.setBaselineKw(p.getBaselineLoadKw());
+                point.setLoadKw(p.getActualLoadKw());
+                points.add(point);
+            }
+            curve.setPoints(points);
+        }
+        return curve;
+    }
+
+    @Override
+    public DashboardMapVO getMap() {
+        Integer tenantId = SecurityUtils.getTenantId();
+        List<VppSite> sites = listSites(tenantId);
+        List<VppResourcePoint> resources = listResources(tenantId);
+        Map<Long, BigDecimal> capacityBySite = new HashMap<>();
+        for (VppResourcePoint resource : resources) {
+            if (resource.getSiteId() == null) {
+                continue;
+            }
+            BigDecimal cap = nz(resource.getCapacityKw());
+            capacityBySite.merge(resource.getSiteId(), cap, BigDecimal::add);
+        }
+
+        DashboardMapVO map = new DashboardMapVO();
+        for (VppSite site : sites) {
+            if (site.getLongitude() == null || site.getLatitude() == null) {
+                continue;
+            }
+            DashboardMapVO.SitePoint point = new DashboardMapVO.SitePoint();
+            point.setSiteId(site.getId());
+            point.setSiteName(site.getSiteName());
+            point.setLongitude(site.getLongitude());
+            point.setLatitude(site.getLatitude());
+            point.setProvince(site.getProvince());
+            point.setCity(site.getCity());
+            point.setCapacityKw(capacityBySite.getOrDefault(site.getId(), BigDecimal.ZERO)
+                    .setScale(SCALE, RoundingMode.HALF_UP));
+            map.getSites().add(point);
+        }
+        return map;
+    }
+
+    // ---------- access / basic / device ----------
+
+    private void fillAccess(DashboardSummaryVO.Access access, List<VppSite> sites, List<VppResourcePoint> resources) {
+        access.setSiteCount((long) sites.size());
+        BigDecimal total = BigDecimal.ZERO;
+        BigDecimal pv = BigDecimal.ZERO;
+        BigDecimal ess = BigDecimal.ZERO;
+        BigDecimal adjustable = BigDecimal.ZERO;
+        for (VppResourcePoint resource : resources) {
+            BigDecimal cap = nz(resource.getCapacityKw());
+            total = total.add(cap);
+            String category = VppCapabilityEvalHelper.resolveCategory(resource.getResourceType());
+            if (VppCapabilityEvalHelper.CATEGORY_GENERATION.equals(category)) {
+                pv = pv.add(cap);
+            } else if (VppCapabilityEvalHelper.CATEGORY_STORAGE.equals(category)) {
+                ess = ess.add(cap);
+            }
+            if (Objects.equals(resource.getIsControl(), IS_CONTROL)) {
+                adjustable = adjustable.add(cap);
+            }
+        }
+        access.setTotalCapacityKw(total.setScale(SCALE, RoundingMode.HALF_UP));
+        access.setPvCapacityKw(pv.setScale(SCALE, RoundingMode.HALF_UP));
+        access.setEssCapacityKw(ess.setScale(SCALE, RoundingMode.HALF_UP));
+        access.setAdjustableCapacityKw(adjustable.setScale(SCALE, RoundingMode.HALF_UP));
+    }
+
+    private void fillBasic(DashboardSummaryVO.Basic basic, Integer tenantId,
+                           List<VppResourcePoint> resources,
+                           List<VppDrEvent> endedEvents, Map<Long, VppDrEvaluation> evaluationMap,
+                           Map<Long, List<VppDrInvitation>> invitationsByEvent) {
+        long customerCount = customerMapper.selectCount(
+                new LambdaQueryWrapper<VppCustomer>()
+                        .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .eq(tenantId != null, VppCustomer::getTenantId, tenantId));
+        basic.setCustomerCount(customerCount);
+
+        BigDecimal loadCapacity = BigDecimal.ZERO;
+        Set<Long> controlDeviceIds = new HashSet<>();
+        long controlResourceWithoutDevice = 0L;
+        BigDecimal maxUp = BigDecimal.ZERO;
+        BigDecimal maxDown = BigDecimal.ZERO;
+        for (VppResourcePoint resource : resources) {
+            if (VppCapabilityEvalHelper.CATEGORY_LOAD.equals(
+                    VppCapabilityEvalHelper.resolveCategory(resource.getResourceType()))) {
+                loadCapacity = loadCapacity.add(nz(resource.getCapacityKw()));
+            }
+            if (Objects.equals(resource.getIsControl(), IS_CONTROL)) {
+                if (resource.getDeviceId() != null) {
+                    controlDeviceIds.add(resource.getDeviceId());
+                } else {
+                    controlResourceWithoutDevice++;
+                }
+                maxUp = maxUp.add(nz(resource.getMaxUpKw()));
+                maxDown = maxDown.add(nz(resource.getMinDownKw()));
+            }
+        }
+        basic.setLoadCapacityKw(loadCapacity.setScale(SCALE, RoundingMode.HALF_UP));
+        basic.setAdjustableDeviceCount(controlDeviceIds.size() + controlResourceWithoutDevice);
+
+        BigDecimal peak = BigDecimal.ZERO;
+        BigDecimal valley = BigDecimal.ZERO;
+        for (VppDrEvent event : endedEvents) {
+            BigDecimal responseKw = resolveEventActualResponseKw(
+                    event, evaluationMap.get(event.getId()), invitationsByEvent.get(event.getId()));
+            if (Objects.equals(event.getEventType(), EVENT_TYPE_PEAK)) {
+                peak = peak.add(responseKw);
+            } else if (Objects.equals(event.getEventType(), EVENT_TYPE_VALLEY)) {
+                valley = valley.add(responseKw);
+            }
+        }
+        basic.setPeakShaveTotalKw(peak.setScale(SCALE, RoundingMode.HALF_UP));
+        basic.setValleyFillTotalKw(valley.setScale(SCALE, RoundingMode.HALF_UP));
+
+        CapabilityEvalSummaryVO capability = capabilityEvalService.getSummary(null);
+        if (capability != null) {
+            basic.setMaxUpKw(nz(capability.getMaxUpKw()).setScale(SCALE, RoundingMode.HALF_UP));
+            basic.setMaxDownKw(nz(capability.getMaxDownKw()).setScale(SCALE, RoundingMode.HALF_UP));
+        } else {
+            basic.setMaxUpKw(maxUp.setScale(SCALE, RoundingMode.HALF_UP));
+            basic.setMaxDownKw(maxDown.setScale(SCALE, RoundingMode.HALF_UP));
+        }
+    }
+
+    /**
+     * 设备状态:从 vpp_device 取 device_uuid,再查 dmp_device_status(1在线 / 2离线)。
+     * 无 uuid 或 dmp 无记录的设备计为离线;dmp 无故障态,fault 固定为 0。
+     */
+    private void fillDeviceStatus(DashboardSummaryVO.DeviceStatus status, List<VppDevice> devices) {
+        status.setTotal((long) devices.size());
+        status.setFault(0L);
+        if (CollectionUtils.isEmpty(devices)) {
+            status.setOnline(0L);
+            status.setOffline(0L);
+            return;
+        }
+
+        List<String> deviceUuids = devices.stream()
+                .map(VppDevice::getDeviceUuid)
+                .filter(StringUtils::hasText)
+                .map(String::trim)
+                .distinct()
+                .collect(Collectors.toList());
+
+        Map<String, Integer> statusByUuid = Collections.emptyMap();
+        if (!deviceUuids.isEmpty()) {
+            statusByUuid = dmpDeviceStatusMapper.selectList(
+                            new LambdaQueryWrapper<DmpDeviceStatus>()
+                                    .select(DmpDeviceStatus::getDeviceUuid, DmpDeviceStatus::getDeviceStatus)
+                                    .in(DmpDeviceStatus::getDeviceUuid, deviceUuids))
+                    .stream()
+                    .filter(s -> StringUtils.hasText(s.getDeviceUuid()))
+                    .collect(Collectors.toMap(DmpDeviceStatus::getDeviceUuid, DmpDeviceStatus::getDeviceStatus, (a, b) -> a));
+        }
+
+        long online = 0;
+        long offline = 0;
+        for (VppDevice device : devices) {
+            String uuid = StringUtils.hasText(device.getDeviceUuid()) ? device.getDeviceUuid().trim() : null;
+            Integer dmpStatus = uuid == null ? null : statusByUuid.get(uuid);
+            if (Objects.equals(dmpStatus, DMP_STATUS_ONLINE)) {
+                online++;
+            } else {
+                // 2离线,或无 uuid / dmp 无记录
+                offline++;
+            }
+        }
+        status.setOnline(online);
+        status.setOffline(offline);
+    }
+
+    private void fillSubsidy(DashboardSummaryVO.Subsidy subsidy, Map<Long, VppDrEvaluation> evaluationMap) {
+        BigDecimal total = BigDecimal.ZERO;
+        for (VppDrEvaluation evaluation : evaluationMap.values()) {
+            total = total.add(nz(evaluation.getSubsidyAmount()));
+        }
+        subsidy.setTotalAmount(total.setScale(MONEY_SCALE, RoundingMode.HALF_UP));
+    }
+
+    private void fillYearRegulation(DashboardSummaryVO.YearRegulation yearRegulation, Integer tenantId,
+                                    List<VppDrEvent> endedEvents, Map<Long, VppDrEvaluation> evaluationMap,
+                                    Map<Long, List<VppDrInvitation>> invitationsByEvent) {
+        int year = Year.now().getValue();
+        yearRegulation.setYear(year);
+        LocalDateTime yearStart = LocalDate.of(year, 1, 1).atStartOfDay();
+        LocalDateTime yearEnd = LocalDate.of(year, 12, 31).atTime(23, 59, 59);
+
+        List<VppDrEvent> yearEvents = endedEvents.stream()
+                .filter(e -> e.getStartTime() != null
+                        && !e.getStartTime().isBefore(yearStart)
+                        && !e.getStartTime().isAfter(yearEnd))
+                .collect(Collectors.toList());
+        Set<Long> yearEventIds = yearEvents.stream().map(VppDrEvent::getId).collect(Collectors.toSet());
+
+        List<VppDrInvitation> yearInvitations = yearEventIds.isEmpty()
+                ? Collections.emptyList()
+                : yearEventIds.stream()
+                .flatMap(id -> invitationsByEvent.getOrDefault(id, Collections.emptyList()).stream())
+                .collect(Collectors.toList());
+
+        List<BigDecimal> deviationRates = new ArrayList<>();
+        List<BigDecimal> completionRates = new ArrayList<>();
+        BigDecimal effectiveKw = BigDecimal.ZERO;
+        BigDecimal totalClearedKw = BigDecimal.ZERO;
+        BigDecimal durationHour = BigDecimal.ZERO;
+        boolean anyWinning = yearInvitations.stream()
+                .anyMatch(i -> Objects.equals(i.getIsWinningBid(), IS_WINNING));
+
+        for (VppDrInvitation invitation : yearInvitations) {
+            BigDecimal actual = resolveInvitationActualResponseKw(invitation);
+            BigDecimal cleared = nz(invitation.getActualClearedCapacityKw());
+            BigDecimal inviteScale = nz(invitation.getDemandCapacityKw());
+
+            if (cleared.compareTo(BigDecimal.ZERO) > 0) {
+                BigDecimal absDiff = actual.subtract(cleared).abs();
+                BigDecimal rate = BigDecimal.ONE.subtract(
+                        absDiff.divide(cleared, SCALE, RoundingMode.HALF_UP));
+                if (rate.compareTo(BigDecimal.ZERO) < 0) {
+                    rate = BigDecimal.ZERO;
+                }
+                deviationRates.add(rate);
+            }
+
+            BigDecimal completion = invitation.getResponseCompletionRate();
+            if (completion == null && inviteScale.compareTo(BigDecimal.ZERO) > 0) {
+                completion = actual.divide(inviteScale, SCALE, RoundingMode.HALF_UP);
+            }
+            if (completion != null) {
+                completionRates.add(completion);
+            }
+
+            boolean countAsEffective = anyWinning
+                    ? Objects.equals(invitation.getIsWinningBid(), IS_WINNING)
+                    : actual.compareTo(BigDecimal.ZERO) > 0;
+            if (countAsEffective) {
+                effectiveKw = effectiveKw.add(actual);
+            }
+            totalClearedKw = totalClearedKw.add(cleared.compareTo(BigDecimal.ZERO) > 0 ? cleared : inviteScale);
+        }
+
+        for (VppDrEvent event : yearEvents) {
+            durationHour = durationHour.add(resolveDurationHour(event, evaluationMap.get(event.getId())));
+            // 无邀约时回退事件出清规模
+            if (!invitationsByEvent.containsKey(event.getId())
+                    || invitationsByEvent.get(event.getId()).isEmpty()) {
+                totalClearedKw = totalClearedKw.add(nz(event.getClearedCapacityKw()).compareTo(BigDecimal.ZERO) > 0
+                        ? nz(event.getClearedCapacityKw())
+                        : nz(event.getTargetCapacityKw()));
+            }
+        }
+
+        yearRegulation.setExecutionDeviationRate(avg(deviationRates));
+        yearRegulation.setResponseCompletionRate(avg(completionRates));
+        yearRegulation.setEffectiveCapacityMw(toMw(effectiveKw));
+        yearRegulation.setTotalCapacityMw(toMw(totalClearedKw));
+        yearRegulation.setTotalDurationHour(durationHour.setScale(SCALE, RoundingMode.HALF_UP));
+        yearRegulation.setInvitationCount((long) yearInvitations.size());
+        yearRegulation.setParticipationCount(yearInvitations.stream()
+                .filter(i -> Objects.equals(i.getReplyStatus(), REPLY_ACCEPT))
+                .count());
+    }
+
+    private void fillSocialBenefit(DashboardSummaryVO.SocialBenefit socialBenefit,
+                                   List<VppDrEvent> endedEvents,
+                                   Map<Long, VppDrEvaluation> evaluationMap,
+                                   Map<Long, List<VppDrInvitation>> invitationsByEvent) {
+        BigDecimal responseMwh = BigDecimal.ZERO;
+        for (VppDrEvent event : endedEvents) {
+            VppDrEvaluation evaluation = evaluationMap.get(event.getId());
+            BigDecimal kw = resolveEventActualResponseKw(event, evaluation, invitationsByEvent.get(event.getId()));
+            BigDecimal hours = resolveDurationHour(event, evaluation);
+            responseMwh = responseMwh.add(kw.multiply(hours).divide(KW_PER_MW, 8, RoundingMode.HALF_UP));
+        }
+
+        BigDecimal carbon = responseMwh.multiply(VppSocialBenefitConstants.CARBON_TON_PER_MWH)
+                .setScale(SCALE, RoundingMode.HALF_UP);
+        BigDecimal coal = responseMwh.multiply(VppSocialBenefitConstants.COAL_TON_PER_MWH)
+                .setScale(SCALE, RoundingMode.HALF_UP);
+        long trees = carbon.multiply(VppSocialBenefitConstants.TREES_PER_CARBON_TON)
+                .setScale(0, RoundingMode.HALF_UP).longValue();
+        long greenCert = responseMwh.multiply(VppSocialBenefitConstants.GREEN_CERT_PER_MWH)
+                .setScale(0, RoundingMode.HALF_UP).longValue();
+
+        socialBenefit.setCarbonReductionTon(carbon);
+        socialBenefit.setCoalSavedTon(coal);
+        socialBenefit.setEquivalentTrees(trees);
+        socialBenefit.setEquivalentGreenCert(greenCert);
+    }
+
+    private void fillEarningsRank(DashboardSummaryVO vo, List<VppSite> sites,
+                                  List<VppResourcePoint> resources,
+                                  List<VppDrEvent> endedEvents,
+                                  Map<Long, VppDrEvaluation> evaluationMap,
+                                  List<VppDrInvitation> endedInvitations) {
+        if (endedEvents.isEmpty()) {
+            vo.setEarningsRank(emptyRank(sites));
+            return;
+        }
+
+        Set<Long> eventIds = endedEvents.stream().map(VppDrEvent::getId).collect(Collectors.toSet());
+        Map<Long, Long> resourceSiteMap = resources.stream()
+                .filter(r -> r.getId() != null && r.getSiteId() != null)
+                .collect(Collectors.toMap(VppResourcePoint::getId, VppResourcePoint::getSiteId, (a, b) -> a));
+
+        List<VppDrParticipation> participations = participationMapper.selectList(
+                new LambdaQueryWrapper<VppDrParticipation>()
+                        .in(VppDrParticipation::getEventId, eventIds)
+                        .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+
+        Map<Long, Set<Long>> eventSiteIds = new HashMap<>();
+        for (VppDrParticipation participation : participations) {
+            Long siteId = resourceSiteMap.get(participation.getResourceId());
+            if (siteId == null) {
+                continue;
+            }
+            eventSiteIds.computeIfAbsent(participation.getEventId(), k -> new LinkedHashSet<>()).add(siteId);
+        }
+
+        for (VppDrInvitation invitation : endedInvitations) {
+            if (invitation.getSiteId() == null || invitation.getSiteId() <= 0) {
+                continue;
+            }
+            eventSiteIds.computeIfAbsent(invitation.getDrEventId(), k -> new LinkedHashSet<>())
+                    .add(invitation.getSiteId());
+        }
+
+        Map<Long, BigDecimal> earningsBySite = new HashMap<>();
+        for (VppSite site : sites) {
+            earningsBySite.put(site.getId(), BigDecimal.ZERO);
+        }
+        for (VppDrEvent event : endedEvents) {
+            VppDrEvaluation evaluation = evaluationMap.get(event.getId());
+            BigDecimal amount = evaluation != null ? nz(evaluation.getSubsidyAmount()) : BigDecimal.ZERO;
+            if (amount.compareTo(BigDecimal.ZERO) <= 0) {
+                continue;
+            }
+            Set<Long> siteIds = eventSiteIds.getOrDefault(event.getId(), Collections.emptySet());
+            if (siteIds.isEmpty()) {
+                continue;
+            }
+            BigDecimal share = amount.divide(BigDecimal.valueOf(siteIds.size()), 8, RoundingMode.HALF_UP);
+            for (Long siteId : siteIds) {
+                earningsBySite.merge(siteId, share, BigDecimal::add);
+            }
+        }
+
+        Map<Long, String> siteNameMap = sites.stream()
+                .collect(Collectors.toMap(VppSite::getId, VppSite::getSiteName, (a, b) -> a));
+        List<DashboardSummaryVO.EarningsRankItem> rank = earningsBySite.entrySet().stream()
+                .map(e -> {
+                    DashboardSummaryVO.EarningsRankItem item = new DashboardSummaryVO.EarningsRankItem();
+                    item.setSiteId(e.getKey());
+                    item.setSiteName(siteNameMap.getOrDefault(e.getKey(), String.valueOf(e.getKey())));
+                    item.setEarnings(e.getValue().setScale(MONEY_SCALE, RoundingMode.HALF_UP));
+                    return item;
+                })
+                .sorted(Comparator.comparing(DashboardSummaryVO.EarningsRankItem::getEarnings).reversed())
+                .collect(Collectors.toList());
+        vo.setEarningsRank(rank);
+    }
+
+    private List<DashboardSummaryVO.EarningsRankItem> emptyRank(List<VppSite> sites) {
+        return sites.stream().map(site -> {
+            DashboardSummaryVO.EarningsRankItem item = new DashboardSummaryVO.EarningsRankItem();
+            item.setSiteId(site.getId());
+            item.setSiteName(site.getSiteName());
+            item.setEarnings(BigDecimal.ZERO.setScale(MONEY_SCALE, RoundingMode.HALF_UP));
+            return item;
+        }).collect(Collectors.toList());
+    }
+
+    // ---------- data loaders ----------
+
+    private List<VppSite> listSites(Integer tenantId) {
+        return siteMapper.selectList(new LambdaQueryWrapper<VppSite>()
+                .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(tenantId != null, VppSite::getTenantId, tenantId));
+    }
+
+    private List<VppResourcePoint> listResources(Integer tenantId) {
+        return resourcePointMapper.selectList(new LambdaQueryWrapper<VppResourcePoint>()
+                .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(tenantId != null, VppResourcePoint::getTenantId, tenantId));
+    }
+
+    private List<VppDevice> listDevices(Integer tenantId) {
+        return deviceMapper.selectList(new LambdaQueryWrapper<VppDevice>()
+                .eq(VppDevice::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(tenantId != null, VppDevice::getTenantId, tenantId));
+    }
+
+    private List<VppDrEvent> listEndedEvents(Integer tenantId) {
+        return eventMapper.selectList(new LambdaQueryWrapper<VppDrEvent>()
+                .eq(VppDrEvent::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(VppDrEvent::getEventStatus, EVENT_STATUS_ENDED)
+                .eq(tenantId != null, VppDrEvent::getTenantId, tenantId));
+    }
+
+    private List<VppDrInvitation> listInvitationsByEvents(List<VppDrEvent> events, Integer tenantId) {
+        if (events.isEmpty()) {
+            return Collections.emptyList();
+        }
+        Set<Long> eventIds = events.stream().map(VppDrEvent::getId).collect(Collectors.toSet());
+        return invitationMapper.selectList(new LambdaQueryWrapper<VppDrInvitation>()
+                .in(VppDrInvitation::getDrEventId, eventIds)
+                .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(tenantId != null, VppDrInvitation::getTenantId, tenantId));
+    }
+
+    private Map<Long, VppDrEvaluation> loadEvaluationMap(List<VppDrEvent> events) {
+        if (events.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        Set<Long> eventIds = events.stream().map(VppDrEvent::getId).collect(Collectors.toSet());
+        return evaluationMapper.selectList(new LambdaQueryWrapper<VppDrEvaluation>()
+                        .in(VppDrEvaluation::getEventId, eventIds)
+                        .eq(VppDrEvaluation::getDeleteFlag, VppAuditHelper.NOT_DELETED))
+                .stream()
+                .collect(Collectors.toMap(VppDrEvaluation::getEventId, e -> e, (a, b) -> a));
+    }
+
+    // ---------- helpers ----------
+
+    private static BigDecimal resolveEventActualResponseKw(VppDrEvent event, VppDrEvaluation evaluation,
+                                                           List<VppDrInvitation> invitations) {
+        if (!CollectionUtils.isEmpty(invitations)) {
+            BigDecimal sum = BigDecimal.ZERO;
+            boolean any = false;
+            for (VppDrInvitation invitation : invitations) {
+                BigDecimal kw = resolveInvitationActualResponseKw(invitation);
+                if (kw.compareTo(BigDecimal.ZERO) > 0
+                        || invitation.getActualResponseCapacityKw() != null
+                        || invitation.getEstimatedResponseCapacityKw() != null) {
+                    sum = sum.add(kw);
+                    any = true;
+                }
+            }
+            if (any) {
+                return sum;
+            }
+        }
+        if (evaluation != null && evaluation.getActualCapacityKw() != null) {
+            return nz(evaluation.getActualCapacityKw());
+        }
+        return BigDecimal.ZERO;
+    }
+
+    private static BigDecimal resolveInvitationActualResponseKw(VppDrInvitation invitation) {
+        if (invitation.getActualResponseCapacityKw() != null) {
+            return nz(invitation.getActualResponseCapacityKw());
+        }
+        if (invitation.getEstimatedResponseCapacityKw() != null) {
+            return nz(invitation.getEstimatedResponseCapacityKw());
+        }
+        return BigDecimal.ZERO;
+    }
+
+    private static BigDecimal resolveDurationHour(VppDrEvent event, VppDrEvaluation evaluation) {
+        if (evaluation != null && evaluation.getResponseDurationMin() != null
+                && evaluation.getResponseDurationMin() > 0) {
+            return BigDecimal.valueOf(evaluation.getResponseDurationMin())
+                    .divide(BigDecimal.valueOf(60), SCALE, RoundingMode.HALF_UP);
+        }
+        if (event.getStartTime() != null && event.getEndTime() != null
+                && event.getEndTime().isAfter(event.getStartTime())) {
+            long minutes = ChronoUnit.MINUTES.between(event.getStartTime(), event.getEndTime());
+            if (minutes > 0) {
+                return BigDecimal.valueOf(minutes)
+                        .divide(BigDecimal.valueOf(60), SCALE, RoundingMode.HALF_UP);
+            }
+        }
+        return BigDecimal.ZERO;
+    }
+
+    private static BigDecimal avg(List<BigDecimal> values) {
+        if (values == null || values.isEmpty()) {
+            return null;
+        }
+        BigDecimal sum = BigDecimal.ZERO;
+        for (BigDecimal value : values) {
+            sum = sum.add(value);
+        }
+        return sum.divide(BigDecimal.valueOf(values.size()), SCALE, RoundingMode.HALF_UP);
+    }
+
+    private static BigDecimal toMw(BigDecimal kw) {
+        return nz(kw).divide(KW_PER_MW, SCALE, RoundingMode.HALF_UP);
+    }
+
+    private static BigDecimal nz(BigDecimal value) {
+        return value == null ? BigDecimal.ZERO : value;
+    }
 }

+ 37 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrInvitationServiceImpl.java

@@ -123,6 +123,12 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
         invitation.setResponseType(request.getResponseType() != null ? request.getResponseType() : event.getResponseType());
         invitation.setDemandCapacityKw(request.getDemandCapacityKw());
         invitation.setDeclaredCapacityKw(request.getDeclaredCapacityKw() != null ? request.getDeclaredCapacityKw() : BigDecimal.ZERO);
+        invitation.setActualClearedCapacityKw(request.getActualClearedCapacityKw());
+        invitation.setEstimatedResponseCapacityKw(request.getEstimatedResponseCapacityKw());
+        invitation.setActualResponseCapacityKw(request.getActualResponseCapacityKw());
+        invitation.setIsWinningBid(request.getIsWinningBid());
+        invitation.setResponseCompletionRate(resolveCompletionRate(request, invitation.getDemandCapacityKw()));
+        invitation.setSiteId(request.getSiteId());
         invitation.setReplyStatus(REPLY_PENDING);
         invitation.setResponseStatus(RESPONSE_PENDING);
         invitation.setSmsNotifyStatus(SMS_NOT_SENT);
@@ -161,6 +167,22 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
         if (request.getDeclaredCapacityKw() != null) {
             invitation.setDeclaredCapacityKw(request.getDeclaredCapacityKw());
         }
+        if (request.getActualClearedCapacityKw() != null) {
+            invitation.setActualClearedCapacityKw(request.getActualClearedCapacityKw());
+        }
+        if (request.getEstimatedResponseCapacityKw() != null) {
+            invitation.setEstimatedResponseCapacityKw(request.getEstimatedResponseCapacityKw());
+        }
+        if (request.getActualResponseCapacityKw() != null) {
+            invitation.setActualResponseCapacityKw(request.getActualResponseCapacityKw());
+        }
+        if (request.getIsWinningBid() != null) {
+            invitation.setIsWinningBid(request.getIsWinningBid());
+        }
+        if (request.getSiteId() != null) {
+            invitation.setSiteId(request.getSiteId());
+        }
+        invitation.setResponseCompletionRate(resolveCompletionRate(request, invitation.getDemandCapacityKw()));
         if (request.getTransactionType() != null) {
             invitation.setTransactionType(request.getTransactionType());
         }
@@ -258,7 +280,7 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
                     ? request.getDeclaredCapacityKw()
                     : invitation.getDeclaredCapacityKw();
             if (declared == null || declared.compareTo(BigDecimal.ZERO) <= 0) {
-                throw new BusinessException("申报容量必须大于0");
+                throw new BusinessException("申报出清容量必须大于0");
             }
 
             VppDrParticipation participation = findOrCreateParticipation(invitation, declared);
@@ -505,6 +527,20 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
         return vo;
     }
 
+    /**
+     * 优先用请求显式完成率;否则按 实际响应容量 / 需求容量(邀约规模) 计算。
+     */
+    private BigDecimal resolveCompletionRate(DrInvitationRequest request, BigDecimal demandCapacityKw) {
+        if (request == null) {
+            return null;
+        }
+        if (request.getResponseCompletionRate() != null) {
+            return request.getResponseCompletionRate();
+        }
+        BigDecimal actual = request.getActualResponseCapacityKw();
+        return VppDrParticipationHelper.calcResponseCompletionRate(actual, demandCapacityKw);
+    }
+
     private String resolveTemplateCode(DrInvitationNotifyRequest request) {
         if (request != null && StringUtils.hasText(request.getTemplateCode())) {
             return request.getTemplateCode().trim();

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

@@ -84,6 +84,8 @@ public class VppDrServiceImpl implements VppDrService {
     private VppSiteResourceHelper siteResourceHelper;
     @Autowired
     private VppUnIntegrationService unIntegrationService;
+    @Autowired
+    private VppDrSubsidyPredictionServiceImpl subsidyPredictionService;
 
     @Override
     public CommonPage<VppDrEvent> pageEvent(Map<String, Object> params) {
@@ -145,8 +147,13 @@ public class VppDrServiceImpl implements VppDrService {
         event.setEventType(request.getEventType());
         event.setStartTime(request.getStartTime());
         event.setEndTime(request.getEndTime());
+        event.setDeclareDeadline(request.getDeclareDeadline());
+        event.setInviteScope(request.getInviteScope());
         event.setTargetCapacityKw(request.getTargetCapacityKw());
         event.setClearedCapacityKw(request.getClearedCapacityKw());
+        event.setRemark(request.getRemark());
+        event.setAttachmentUrl(request.getAttachmentUrl());
+        event.setAttachmentName(request.getAttachmentName());
         event.setSubsidyPrice(request.getSubsidyPrice());
         event.setEventStatus(EVENT_STATUS_PENDING);
         VppAuditHelper.fillCreate(event);
@@ -172,8 +179,13 @@ public class VppDrServiceImpl implements VppDrService {
         event.setEventType(request.getEventType());
         event.setStartTime(request.getStartTime());
         event.setEndTime(request.getEndTime());
+        event.setDeclareDeadline(request.getDeclareDeadline());
+        event.setInviteScope(request.getInviteScope());
         event.setTargetCapacityKw(request.getTargetCapacityKw());
         event.setClearedCapacityKw(request.getClearedCapacityKw());
+        event.setRemark(request.getRemark());
+        event.setAttachmentUrl(request.getAttachmentUrl());
+        event.setAttachmentName(request.getAttachmentName());
         event.setSubsidyPrice(request.getSubsidyPrice());
         VppAuditHelper.fillUpdate(event);
         eventMapper.updateById(event);
@@ -267,7 +279,7 @@ public class VppDrServiceImpl implements VppDrService {
 
         if (Boolean.TRUE.equals(request.getParticipate())) {
             if (request.getDeclaredCapacityKw() == null || request.getDeclaredCapacityKw().compareTo(BigDecimal.ZERO) <= 0) {
-                throw new BusinessException("申报容量必须大于0");
+                throw new BusinessException("申报出清容量必须大于0");
             }
 
             if (request.getResources() == null || request.getResources().isEmpty()) {
@@ -286,7 +298,7 @@ public class VppDrServiceImpl implements VppDrService {
                     throw new BusinessException("资源ID不能为空");
                 }
                 if (resource.getDeclaredCapacityKw() == null || resource.getDeclaredCapacityKw().compareTo(BigDecimal.ZERO) <= 0) {
-                    throw new BusinessException("资源申报容量必须大于0");
+                    throw new BusinessException("资源申报出清容量必须大于0");
                 }
                 VppResourcePoint rp = resourcePointMapper.selectById(resource.getResourceId());
                 if (rp == null || VppAuditHelper.isDeleted(rp.getDeleteFlag())) {
@@ -303,7 +315,7 @@ public class VppDrServiceImpl implements VppDrService {
             }
 
             if (resourceDeclaredTotal.compareTo(request.getDeclaredCapacityKw()) != 0) {
-                throw new BusinessException("各资源申报容量之和与总申报容量不一致");
+                throw new BusinessException("各资源申报出清容量之和与总申报出清容量不一致");
             }
 
             for (DrParticipateRequest.DrResourceDeclare resource : request.getResources()) {
@@ -394,7 +406,7 @@ public class VppDrServiceImpl implements VppDrService {
             }
             if (participation.getDeclaredCapacityKw() != null
                     && resource.getClearedCapacityKw().compareTo(participation.getDeclaredCapacityKw()) > 0) {
-                throw new BusinessException("出清容量不能超过申报容量");
+                throw new BusinessException("出清容量不能超过申报出清容量");
             }
             participation.setClearedCapacityKw(resource.getClearedCapacityKw());
             VppDrParticipationHelper.refreshCompletionRate(participation);
@@ -420,6 +432,13 @@ public class VppDrServiceImpl implements VppDrService {
         }
 
         syncClearingToUn(eventId);
+
+        // ========== 出清完成后,自动生成补贴金额预测 ==========
+        try {
+            subsidyPredictionService.generateByEventId(eventId);
+        } catch (Exception ex) {
+            log.warn("补贴预测生成失败 eventId={}: {}", eventId, ex.getMessage());
+        }
     }
 
     private void syncClearingToUn(Long eventId) {
@@ -570,6 +589,13 @@ public class VppDrServiceImpl implements VppDrService {
         VppAuditHelper.fillUpdate(event);
         eventMapper.updateById(event);
 
+        // ========== 事件结束后,自动生成补贴金额预测 ==========
+        try {
+            subsidyPredictionService.generateByEventId(eventId);
+        } catch (Exception ex) {
+            log.warn("补贴预测生成失败 eventId={}: {}", eventId, ex.getMessage());
+        }
+
         VppDrEvaluation evaluation = evaluationMapper.selectOne(
                 new LambdaQueryWrapper<VppDrEvaluation>()
                         .eq(VppDrEvaluation::getEventId, eventId)
@@ -798,10 +824,14 @@ public class VppDrServiceImpl implements VppDrService {
             throw new BusinessException("响应结束时间不能早于开始时间");
         }
         if (request.getTargetCapacityKw() == null || request.getTargetCapacityKw().compareTo(BigDecimal.ZERO) <= 0) {
-            throw new BusinessException("目标容量必须大于0");
+            throw new BusinessException("邀约规模必须大于0");
         }
         if (request.getClearedCapacityKw() != null && request.getClearedCapacityKw().compareTo(BigDecimal.ZERO) < 0) {
-            throw new BusinessException("出清容量不能为负数");
+            throw new BusinessException("出清规模不能为负数");
+        }
+        if (request.getDeclareDeadline() != null && request.getStartTime() != null
+                && request.getDeclareDeadline().isAfter(request.getStartTime())) {
+            throw new BusinessException("申报截止时间不能晚于响应开始时间");
         }
         if (request.getSubsidyPrice() != null && request.getSubsidyPrice().compareTo(BigDecimal.ZERO) < 0) {
             throw new BusinessException("补贴标准不能为负数");
@@ -850,8 +880,13 @@ public class VppDrServiceImpl implements VppDrService {
         vo.setEventType(event.getEventType());
         vo.setStartTime(event.getStartTime());
         vo.setEndTime(event.getEndTime());
+        vo.setDeclareDeadline(event.getDeclareDeadline());
+        vo.setInviteScope(event.getInviteScope());
         vo.setTargetCapacityKw(event.getTargetCapacityKw());
         vo.setClearedCapacityKw(event.getClearedCapacityKw());
+        vo.setRemark(event.getRemark());
+        vo.setAttachmentUrl(event.getAttachmentUrl());
+        vo.setAttachmentName(event.getAttachmentName());
         vo.setSubsidyPrice(event.getSubsidyPrice());
         vo.setEventStatus(event.getEventStatus());
         vo.setParticipations(loadParticipationVos(event.getId()));

+ 214 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrSubsidyPredictionServiceImpl.java

@@ -0,0 +1,214 @@
+package com.usky.vpp.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.vpp.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.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.DrSubsidyPredictionVO;
+import com.usky.vpp.util.VppAuditHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 补贴金额预测 业务实现
+ */
+@Service
+public class VppDrSubsidyPredictionServiceImpl implements VppDrSubsidyPredictionService {
+
+    private static final Logger log = LoggerFactory.getLogger(VppDrSubsidyPredictionServiceImpl.class);
+
+    @Autowired
+    private VppDrSubsidyPredictionMapper subsidyPredictionMapper;
+    @Autowired
+    private VppDrInvitationMapper invitationMapper;
+    @Autowired
+    private VppDrEventMapper eventMapper;
+    @Autowired
+    private VppSiteMapper siteMapper;
+
+    @Override
+    public CommonPage<DrSubsidyPredictionVO> page(String siteName, String startTime, String endTime,
+                                                   Integer current, Integer size) {
+        Page<VppDrSubsidyPrediction> page = new Page<>(
+                current != null ? current : 1,
+                size != null && size <= 100 ? size : 20);
+
+        LambdaQueryWrapper<VppDrSubsidyPrediction> wrapper = new LambdaQueryWrapper<>();
+        if (StringUtils.hasText(siteName)) {
+            wrapper.like(VppDrSubsidyPrediction::getSiteName, siteName);
+        }
+        if (StringUtils.hasText(startTime)) {
+            wrapper.ge(VppDrSubsidyPrediction::getEventStartTime, startTime);
+        }
+        if (StringUtils.hasText(endTime)) {
+            wrapper.le(VppDrSubsidyPrediction::getEventStartTime, endTime);
+        }
+        wrapper.orderByDesc(VppDrSubsidyPrediction::getCreateTime);
+
+        Page<VppDrSubsidyPrediction> result = subsidyPredictionMapper.selectPage(page, wrapper);
+
+        List<DrSubsidyPredictionVO> list = result.getRecords().stream()
+                .map(this::toVo)
+                .collect(Collectors.toList());
+
+        return new CommonPage<>(list, result.getTotal(), size != null ? size : 20, current != null ? current : 1);
+    }
+
+    @Override
+    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
+    public void generateByEventId(Long drEventId) {
+        if (drEventId == null) {
+            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)
+                .collect(Collectors.toSet());
+        Map<Long, String> siteNameMap = siteIds.isEmpty() ? Collections.emptyMap()
+                : siteMapper.selectBatchIds(siteIds).stream()
+                        .collect(Collectors.toMap(VppSite::getId, VppSite::getSiteName, (a, b) -> a));
+
+        // 逐条计算并 upsert
+        for (VppDrInvitation invitation : invitations) {
+            VppDrSubsidyPrediction prediction = buildPrediction(event, invitation, siteNameMap);
+
+            // 按 invitation_id 唯一键,存在则更新,不存在则新增
+            VppDrSubsidyPrediction existing = subsidyPredictionMapper.selectOne(
+                    new LambdaQueryWrapper<VppDrSubsidyPrediction>()
+                            .eq(VppDrSubsidyPrediction::getInvitationId, invitation.getId())
+            );
+
+            if (existing != null) {
+                prediction.setId(existing.getId());
+                prediction.setUpdateTime(LocalDateTime.now());
+                subsidyPredictionMapper.updateById(prediction);
+            } else {
+                prediction.setCreateTime(LocalDateTime.now());
+                prediction.setUpdateTime(LocalDateTime.now());
+                subsidyPredictionMapper.insert(prediction);
+            }
+        }
+
+        log.info("补贴预测生成完成 drEventId={}, invitationCount={}", drEventId, invitations.size());
+    }
+
+    /**
+     * 构建单条补贴预测记录
+     */
+    private VppDrSubsidyPrediction buildPrediction(VppDrEvent event, VppDrInvitation invitation,
+                                                    Map<Long, String> siteNameMap) {
+        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.setEventStartTime(event.getStartTime());
+        p.setEventEndTime(event.getEndTime());
+
+        // 响应时长(分钟)
+        int durationMin = 0;
+        if (event.getStartTime() != null && event.getEndTime() != null
+                && event.getEndTime().isAfter(event.getStartTime())) {
+            durationMin = (int) Duration.between(event.getStartTime(), event.getEndTime()).toMinutes();
+        }
+        p.setResponseDurationMin(durationMin);
+
+        // 补贴标准
+        p.setSubsidyPrice(event.getSubsidyPrice());
+
+        // 预估响应容量
+        p.setEstimatedCapacityKw(invitation.getEstimatedResponseCapacityKw());
+
+        // 实际出清
+        p.setClearedCapacityKw(invitation.getActualClearedCapacityKw());
+
+        // 预估完成率 = 预估容量 / 实际出清;实际出清为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);
+        }
+        p.setCompletionRate(completionRate);
+
+        // 预估偏差率 = 1 - 预估完成率
+        BigDecimal deviationRate = BigDecimal.ONE.subtract(completionRate);
+        p.setDeviationRate(deviationRate);
+
+        // 预估补贴金额 = 补贴标准(为NULL时记为NULL)
+        p.setEstimatedSubsidyAmount(event.getSubsidyPrice());
+
+        p.setTenantId(event.getTenantId());
+        return p;
+    }
+
+    private DrSubsidyPredictionVO toVo(VppDrSubsidyPrediction entity) {
+        DrSubsidyPredictionVO vo = new DrSubsidyPredictionVO();
+        vo.setId(entity.getId());
+        vo.setInvitationId(entity.getInvitationId());
+        vo.setDrEventId(entity.getDrEventId());
+        vo.setSiteId(entity.getSiteId());
+        vo.setSiteName(entity.getSiteName());
+        vo.setEventStartTime(entity.getEventStartTime());
+        vo.setEventEndTime(entity.getEventEndTime());
+        vo.setResponseDurationMin(entity.getResponseDurationMin());
+        vo.setSubsidyPrice(entity.getSubsidyPrice());
+        vo.setEstimatedCapacityKw(entity.getEstimatedCapacityKw());
+        vo.setClearedCapacityKw(entity.getClearedCapacityKw());
+        vo.setCompletionRate(entity.getCompletionRate());
+        vo.setDeviationRate(entity.getDeviationRate());
+        vo.setEstimatedSubsidyAmount(entity.getEstimatedSubsidyAmount());
+        vo.setCreateTime(entity.getCreateTime());
+        vo.setUpdateTime(entity.getUpdateTime());
+        return vo;
+    }
+}

+ 382 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppSettlementSyncServiceImpl.java

@@ -0,0 +1,382 @@
+package com.usky.vpp.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.vpp.domain.VppDevice;
+import com.usky.vpp.domain.VppEnergyReadingMonthly;
+import com.usky.vpp.domain.VppSettlementBill;
+import com.usky.vpp.domain.VppSettlementBillDetail;
+import com.usky.vpp.domain.VppSite;
+import com.usky.vpp.enums.DeviceTypeMetricEnum;
+import com.usky.vpp.enums.TimeSegmentEnum;
+import com.usky.vpp.mapper.VppDeviceMapper;
+import com.usky.vpp.mapper.VppEnergyReadingMonthlyMapper;
+import com.usky.vpp.mapper.VppSettlementBillDetailMapper;
+import com.usky.vpp.mapper.VppSettlementBillMapper;
+import com.usky.vpp.mapper.VppSiteMapper;
+import com.usky.vpp.service.VppSettlementSyncService;
+import com.usky.vpp.service.VppTsdbQueryService;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppEnergyUsageHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.support.TransactionTemplate;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.YearMonth;
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * 每月从时序库拉取站点分时电量,写入月度核算与电费账单(电费金额固定为 0)
+ */
+@Service
+public class VppSettlementSyncServiceImpl implements VppSettlementSyncService {
+
+    private static final Logger log = LoggerFactory.getLogger(VppSettlementSyncServiceImpl.class);
+
+    private static final int CALC_STATUS_PENDING = 0;
+    private static final int PAYMENT_STATUS_UNPAID = 0;
+    private static final int SCALE = 4;
+
+    @Autowired
+    private VppSiteMapper siteMapper;
+    @Autowired
+    private VppDeviceMapper deviceMapper;
+    @Autowired
+    private VppEnergyReadingMonthlyMapper energyReadingMonthlyMapper;
+    @Autowired
+    private VppSettlementBillMapper settlementBillMapper;
+    @Autowired
+    private VppSettlementBillDetailMapper settlementBillDetailMapper;
+    @Autowired
+    private VppTsdbQueryService vppTsdbQueryService;
+
+    private TransactionTemplate transactionTemplate;
+
+    @Autowired
+    public void setTransactionManager(PlatformTransactionManager transactionManager) {
+        this.transactionTemplate = new TransactionTemplate(transactionManager);
+    }
+
+    @Override
+    public int syncLastMonth() {
+        YearMonth lastMonth = YearMonth.now().minusMonths(1);
+        return syncMonth(lastMonth.getYear(), lastMonth.getMonthValue());
+    }
+
+    @Override
+    public int syncMonth(int settleYear, int settleMonth) {
+        YearMonth yearMonth = YearMonth.of(settleYear, settleMonth);
+        LocalDateTime startInclusive = yearMonth.atDay(1).atStartOfDay();
+        LocalDateTime endExclusive = yearMonth.plusMonths(1).atDay(1).atStartOfDay();
+        LocalDateTime queryEnd = endExclusive.minusSeconds(1);
+        String settleYearStr = String.valueOf(settleYear);
+        LocalDate dueDate = yearMonth.plusMonths(1).atEndOfMonth();
+
+        List<VppSite> sites = siteMapper.selectList(new LambdaQueryWrapper<VppSite>()
+                .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        if (CollectionUtils.isEmpty(sites)) {
+            log.info("月度结算同步:无可用站点, settle={}-{}", settleYear, settleMonth);
+            return 0;
+        }
+
+        int success = 0;
+        for (VppSite site : sites) {
+            try {
+                Boolean synced = transactionTemplate.execute(status ->
+                        syncSite(site, settleYearStr, settleMonth, startInclusive, endExclusive, queryEnd, dueDate));
+                if (Boolean.TRUE.equals(synced)) {
+                    success++;
+                }
+            } catch (Exception ex) {
+                log.error("月度结算同步失败 siteId={}, settle={}-{}, err={}",
+                        site.getId(), settleYear, settleMonth, ex.getMessage(), ex);
+            }
+        }
+        log.info("月度结算同步完成 settle={}-{}, sites={}, success={}",
+                settleYear, settleMonth, sites.size(), success);
+        return success;
+    }
+
+    private boolean syncSite(VppSite site,
+                             String settleYear,
+                             int settleMonth,
+                             LocalDateTime startInclusive,
+                             LocalDateTime endExclusive,
+                             LocalDateTime queryEnd,
+                             LocalDate dueDate) {
+        if (site == null || site.getId() == null) {
+            return false;
+        }
+        if (existsReading(site.getCustomerId(), site.getId(), settleYear, settleMonth)
+                || existsBill(site.getCustomerId(), site.getId(), settleYear, settleMonth)) {
+            log.info("月度结算已存在,跳过 siteId={}, settle={}-{}", site.getId(), settleYear, settleMonth);
+            return false;
+        }
+
+        SiteEnergyAggregate aggregate = querySiteEnergy(site.getId(), startInclusive, endExclusive, queryEnd);
+
+        VppEnergyReadingMonthly reading = buildReading(site, settleYear, settleMonth, aggregate);
+        energyReadingMonthlyMapper.insert(reading);
+
+        VppSettlementBill bill = buildBill(site, settleYear, settleMonth, reading.getTotalEnergyKwh(), dueDate);
+        settlementBillMapper.insert(bill);
+
+        for (VppSettlementBillDetail detail : buildBillDetails(bill.getId(), site.getTenantId(), aggregate)) {
+            settlementBillDetailMapper.insert(detail);
+        }
+        return true;
+    }
+
+    private SiteEnergyAggregate querySiteEnergy(Long siteId,
+                                                LocalDateTime startInclusive,
+                                                LocalDateTime endExclusive,
+                                                LocalDateTime queryEnd) {
+        List<VppDevice> devices = deviceMapper.selectList(new LambdaQueryWrapper<VppDevice>()
+                .eq(VppDevice::getSiteId, siteId)
+                .eq(VppDevice::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        if (CollectionUtils.isEmpty(devices)) {
+            return SiteEnergyAggregate.empty();
+        }
+
+        Map<DeviceTypeMetricEnum, List<String>> typeDeviceUuids = new HashMap<>();
+        for (VppDevice device : devices) {
+            if (device == null || !StringUtils.hasText(device.getDeviceUuid())) {
+                continue;
+            }
+            DeviceTypeMetricEnum.ofDeviceType(device.getDeviceType()).ifPresent(typeEnum ->
+                    typeDeviceUuids.computeIfAbsent(typeEnum, key -> new ArrayList<>())
+                            .add(device.getDeviceUuid().trim()));
+        }
+        if (typeDeviceUuids.isEmpty()) {
+            log.warn("站点无匹配电量设备类型 siteId={}", siteId);
+            return SiteEnergyAggregate.empty();
+        }
+
+        SiteEnergyAggregate aggregate = SiteEnergyAggregate.empty();
+        for (Map.Entry<DeviceTypeMetricEnum, List<String>> entry : typeDeviceUuids.entrySet()) {
+            DeviceTypeMetricEnum typeEnum = entry.getKey();
+            List<String> deviceUuids = new ArrayList<>(new LinkedHashSet<>(entry.getValue()));
+            Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> history =
+                    vppTsdbQueryService.queryDeviceMetricHistory(
+                            deviceUuids, startInclusive, queryEnd, typeEnum.getMetrics());
+            aggregate.merge(sumUsageBySegment(deviceUuids, history, startInclusive, endExclusive));
+        }
+        return aggregate;
+    }
+
+    private SiteEnergyAggregate sumUsageBySegment(List<String> deviceUuids,
+                                                  Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> history,
+                                                  LocalDateTime startInclusive,
+                                                  LocalDateTime endExclusive) {
+        SiteEnergyAggregate aggregate = SiteEnergyAggregate.empty();
+        if (CollectionUtils.isEmpty(deviceUuids) || history == null || history.isEmpty()) {
+            return aggregate;
+        }
+
+        Map<TimeSegmentEnum, BigDecimal> forward = new EnumMap<>(TimeSegmentEnum.class);
+        Map<TimeSegmentEnum, BigDecimal> reverse = new EnumMap<>(TimeSegmentEnum.class);
+        for (TimeSegmentEnum segment : TimeSegmentEnum.values()) {
+            forward.put(segment, BigDecimal.ZERO);
+            reverse.put(segment, BigDecimal.ZERO);
+        }
+        BigDecimal totalForward = BigDecimal.ZERO;
+        BigDecimal totalReverse = BigDecimal.ZERO;
+        boolean hasData = false;
+
+        for (String deviceUuid : deviceUuids) {
+            Map<String, TreeMap<LocalDateTime, BigDecimal>> metricMap = history.get(deviceUuid);
+            if (metricMap == null || metricMap.isEmpty()) {
+                continue;
+            }
+            for (TimeSegmentEnum segment : TimeSegmentEnum.values()) {
+                BigDecimal fwd = VppEnergyUsageHelper.calcUsage(
+                        metricMap.get(segment.getForwardMetric()), startInclusive, endExclusive);
+                if (fwd != null) {
+                    hasData = true;
+                    forward.put(segment, forward.get(segment).add(fwd));
+                }
+                BigDecimal rev = VppEnergyUsageHelper.calcUsage(
+                        metricMap.get(segment.getReverseMetric()), startInclusive, endExclusive);
+                if (rev != null) {
+                    hasData = true;
+                    reverse.put(segment, reverse.get(segment).add(rev));
+                }
+            }
+            BigDecimal epp = VppEnergyUsageHelper.calcUsage(metricMap.get("epp"), startInclusive, endExclusive);
+            if (epp != null) {
+                hasData = true;
+                totalForward = totalForward.add(epp);
+            }
+            BigDecimal epn = VppEnergyUsageHelper.calcUsage(metricMap.get("epn"), startInclusive, endExclusive);
+            if (epn != null) {
+                hasData = true;
+                totalReverse = totalReverse.add(epn);
+            }
+        }
+
+        BigDecimal sharp = scale(forward.get(TimeSegmentEnum.SPIKE));
+        BigDecimal peak = scale(forward.get(TimeSegmentEnum.PEAK));
+        BigDecimal flat = scale(forward.get(TimeSegmentEnum.FLAT));
+        BigDecimal valley = scale(forward.get(TimeSegmentEnum.VALLEY));
+        BigDecimal segmentSum = sharp.add(peak).add(flat).add(valley);
+
+        BigDecimal totalEnergy = totalForward.compareTo(BigDecimal.ZERO) > 0 ? scale(totalForward) : segmentSum;
+        BigDecimal reverseSum = reverse.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
+        BigDecimal genEnergy = totalReverse.compareTo(BigDecimal.ZERO) > 0 ? scale(totalReverse) : scale(reverseSum);
+
+        aggregate.hasData = hasData;
+        aggregate.sharpEnergyKwh = sharp;
+        aggregate.peakEnergyKwh = peak;
+        aggregate.flatEnergyKwh = flat;
+        aggregate.valleyEnergyKwh = valley;
+        aggregate.totalEnergyKwh = totalEnergy;
+        aggregate.genEnergyKwh = genEnergy;
+        return aggregate;
+    }
+
+    private VppEnergyReadingMonthly buildReading(VppSite site,
+                                                 String settleYear,
+                                                 int settleMonth,
+                                                 SiteEnergyAggregate aggregate) {
+        VppEnergyReadingMonthly reading = new VppEnergyReadingMonthly();
+        reading.setCustomerId(site.getCustomerId());
+        reading.setSiteId(site.getId());
+        reading.setSettleYear(settleYear);
+        reading.setSettleMonth(settleMonth);
+        reading.setSharpEnergyKwh(aggregate.sharpEnergyKwh);
+        reading.setPeakEnergyKwh(aggregate.peakEnergyKwh);
+        reading.setFlatEnergyKwh(aggregate.flatEnergyKwh);
+        reading.setValleyEnergyKwh(aggregate.valleyEnergyKwh);
+        reading.setTotalEnergyKwh(aggregate.totalEnergyKwh);
+        reading.setGenEnergyKwh(aggregate.genEnergyKwh);
+        reading.setCalcStatus(CALC_STATUS_PENDING);
+        reading.setCalcAt(null);
+        reading.setTenantId(site.getTenantId());
+        VppAuditHelper.fillCreate(reading);
+        if (reading.getTenantId() == null) {
+            reading.setTenantId(site.getTenantId());
+        }
+        return reading;
+    }
+
+    private VppSettlementBill buildBill(VppSite site,
+                                        String settleYear,
+                                        int settleMonth,
+                                        BigDecimal totalEnergyKwh,
+                                        LocalDate dueDate) {
+        VppSettlementBill bill = new VppSettlementBill();
+        bill.setBillNo(VppAuditHelper.nextBillNo());
+        bill.setCustomerId(site.getCustomerId());
+        bill.setSiteId(site.getId());
+        bill.setSettleYear(settleYear);
+        bill.setSettleMonth(settleMonth);
+        bill.setTotalEnergyKwh(VppEnergyUsageHelper.zeroIfNull(totalEnergyKwh));
+        // 当前阶段电费统一为 0
+        bill.setTotalAmount(BigDecimal.ZERO.setScale(SCALE, RoundingMode.HALF_UP));
+        bill.setPaidAmount(BigDecimal.ZERO.setScale(SCALE, RoundingMode.HALF_UP));
+        bill.setPaymentStatus(PAYMENT_STATUS_UNPAID);
+        bill.setDueDate(dueDate);
+        bill.setGeneratedAt(LocalDateTime.now());
+        bill.setTenantId(site.getTenantId());
+        VppAuditHelper.fillCreate(bill);
+        if (bill.getTenantId() == null) {
+            bill.setTenantId(site.getTenantId());
+        }
+        return bill;
+    }
+
+    private List<VppSettlementBillDetail> buildBillDetails(Long billId,
+                                                           Integer tenantId,
+                                                           SiteEnergyAggregate aggregate) {
+        List<VppSettlementBillDetail> details = new ArrayList<>(4);
+        details.add(buildDetail(billId, tenantId, TimeSegmentEnum.SPIKE, aggregate.sharpEnergyKwh));
+        details.add(buildDetail(billId, tenantId, TimeSegmentEnum.PEAK, aggregate.peakEnergyKwh));
+        details.add(buildDetail(billId, tenantId, TimeSegmentEnum.FLAT, aggregate.flatEnergyKwh));
+        details.add(buildDetail(billId, tenantId, TimeSegmentEnum.VALLEY, aggregate.valleyEnergyKwh));
+        return details;
+    }
+
+    private VppSettlementBillDetail buildDetail(Long billId,
+                                                Integer tenantId,
+                                                TimeSegmentEnum segment,
+                                                BigDecimal energyKwh) {
+        VppSettlementBillDetail detail = new VppSettlementBillDetail();
+        detail.setBillId(billId);
+        detail.setTimePeriod(segment.getPeriodCode());
+        detail.setEnergyKwh(VppEnergyUsageHelper.zeroIfNull(energyKwh));
+        detail.setPrice(BigDecimal.ZERO.setScale(SCALE, RoundingMode.HALF_UP));
+        detail.setAmount(BigDecimal.ZERO.setScale(SCALE, RoundingMode.HALF_UP));
+        detail.setTenantId(tenantId);
+        return detail;
+    }
+
+    private boolean existsReading(Long customerId, Long siteId, String settleYear, int settleMonth) {
+        return energyReadingMonthlyMapper.selectCount(new LambdaQueryWrapper<VppEnergyReadingMonthly>()
+                .eq(VppEnergyReadingMonthly::getCustomerId, customerId)
+                .eq(VppEnergyReadingMonthly::getSiteId, siteId)
+                .eq(VppEnergyReadingMonthly::getSettleYear, settleYear)
+                .eq(VppEnergyReadingMonthly::getSettleMonth, settleMonth)
+                .eq(VppEnergyReadingMonthly::getDeleteFlag, VppAuditHelper.NOT_DELETED)) > 0;
+    }
+
+    private boolean existsBill(Long customerId, Long siteId, String settleYear, int settleMonth) {
+        return settlementBillMapper.selectCount(new LambdaQueryWrapper<VppSettlementBill>()
+                .eq(VppSettlementBill::getCustomerId, customerId)
+                .eq(VppSettlementBill::getSiteId, siteId)
+                .eq(VppSettlementBill::getSettleYear, settleYear)
+                .eq(VppSettlementBill::getSettleMonth, settleMonth)
+                .eq(VppSettlementBill::getDeleteFlag, VppAuditHelper.NOT_DELETED)) > 0;
+    }
+
+    private static BigDecimal scale(BigDecimal value) {
+        return VppEnergyUsageHelper.zeroIfNull(value).setScale(SCALE, RoundingMode.HALF_UP);
+    }
+
+    private static final class SiteEnergyAggregate {
+        private boolean hasData;
+        private BigDecimal sharpEnergyKwh = BigDecimal.ZERO;
+        private BigDecimal peakEnergyKwh = BigDecimal.ZERO;
+        private BigDecimal flatEnergyKwh = BigDecimal.ZERO;
+        private BigDecimal valleyEnergyKwh = BigDecimal.ZERO;
+        private BigDecimal totalEnergyKwh = BigDecimal.ZERO;
+        private BigDecimal genEnergyKwh = BigDecimal.ZERO;
+
+        static SiteEnergyAggregate empty() {
+            SiteEnergyAggregate aggregate = new SiteEnergyAggregate();
+            aggregate.sharpEnergyKwh = BigDecimal.ZERO.setScale(SCALE, RoundingMode.HALF_UP);
+            aggregate.peakEnergyKwh = BigDecimal.ZERO.setScale(SCALE, RoundingMode.HALF_UP);
+            aggregate.flatEnergyKwh = BigDecimal.ZERO.setScale(SCALE, RoundingMode.HALF_UP);
+            aggregate.valleyEnergyKwh = BigDecimal.ZERO.setScale(SCALE, RoundingMode.HALF_UP);
+            aggregate.totalEnergyKwh = BigDecimal.ZERO.setScale(SCALE, RoundingMode.HALF_UP);
+            aggregate.genEnergyKwh = BigDecimal.ZERO.setScale(SCALE, RoundingMode.HALF_UP);
+            return aggregate;
+        }
+
+        void merge(SiteEnergyAggregate other) {
+            if (other == null) {
+                return;
+            }
+            this.hasData = this.hasData || other.hasData;
+            this.sharpEnergyKwh = this.sharpEnergyKwh.add(other.sharpEnergyKwh);
+            this.peakEnergyKwh = this.peakEnergyKwh.add(other.peakEnergyKwh);
+            this.flatEnergyKwh = this.flatEnergyKwh.add(other.flatEnergyKwh);
+            this.valleyEnergyKwh = this.valleyEnergyKwh.add(other.valleyEnergyKwh);
+            this.totalEnergyKwh = this.totalEnergyKwh.add(other.totalEnergyKwh);
+            this.genEnergyKwh = this.genEnergyKwh.add(other.genEnergyKwh);
+        }
+    }
+}

+ 3 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/BiddingConfigRequest.java

@@ -1,5 +1,6 @@
 package com.usky.vpp.service.vo;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -13,7 +14,9 @@ import java.util.List;
 public class BiddingConfigRequest {
 
     private String configName;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime effectiveStart;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime effectiveEnd;
     private String attachmentUrl;
     private String attachmentName;

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

@@ -1,5 +1,6 @@
 package com.usky.vpp.service.vo;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -14,7 +15,9 @@ public class BiddingConfigVO {
 
     private Long id;
     private String configName;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime effectiveStart;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime effectiveEnd;
     /** 适用时间段展示,如 2025-01-01 00:00:00 - 2025-06-30 23:59:59 */
     private String effectivePeriod;
@@ -30,5 +33,6 @@ public class BiddingConfigVO {
     private BigDecimal minPrice;
     private List<Long> siteIds;
     private List<BiddingConfigSiteVO> sites;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime updateTime;
 }

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

@@ -0,0 +1,30 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 运行管理监控大屏 - 当日用电负荷曲线
+ */
+@Data
+public class DashboardCurveVO {
+
+    /** 统计日期 yyyy-MM-dd */
+    private String statDate;
+    /** 数据来源:tsdb / mock */
+    private String dataSource;
+    private List<Point> points = new ArrayList<>();
+
+    @Data
+    public static class Point {
+        /** 时刻 HH:mm */
+        private String time;
+        /** 基础功率 kW */
+        private BigDecimal baselineKw;
+        /** 负荷功率 kW */
+        private BigDecimal loadKw;
+    }
+}

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

@@ -0,0 +1,28 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 运行管理监控大屏 - 地图站点
+ */
+@Data
+public class DashboardMapVO {
+
+    private List<SitePoint> sites = new ArrayList<>();
+
+    @Data
+    public static class SitePoint {
+        private Long siteId;
+        private String siteName;
+        private BigDecimal longitude;
+        private BigDecimal latitude;
+        private String province;
+        private String city;
+        /** 站点下资源装机容量合计 kW */
+        private BigDecimal capacityKw = BigDecimal.ZERO;
+    }
+}

+ 112 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DashboardSummaryVO.java

@@ -0,0 +1,112 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 运行管理监控大屏 - 汇总 KPI
+ */
+@Data
+public class DashboardSummaryVO {
+
+    private Access access = new Access();
+    private Basic basic = new Basic();
+    private DeviceStatus deviceStatus = new DeviceStatus();
+    private Subsidy subsidy = new Subsidy();
+    private YearRegulation yearRegulation = new YearRegulation();
+    private SocialBenefit socialBenefit = new SocialBenefit();
+    private List<EarningsRankItem> earningsRank = new ArrayList<>();
+
+    @Data
+    public static class Access {
+        /** 接入站点数 */
+        private Long siteCount = 0L;
+        /** 接入装机容量 kW */
+        private BigDecimal totalCapacityKw = BigDecimal.ZERO;
+        /** 接入光伏 kW */
+        private BigDecimal pvCapacityKw = BigDecimal.ZERO;
+        /** 接入储能资源 kW */
+        private BigDecimal essCapacityKw = BigDecimal.ZERO;
+        /** 接入可调资源 kW */
+        private BigDecimal adjustableCapacityKw = BigDecimal.ZERO;
+    }
+
+    @Data
+    public static class Basic {
+        /** 接入用户数量 */
+        private Long customerCount = 0L;
+        /** 负荷容量 kW(负荷类资源) */
+        private BigDecimal loadCapacityKw = BigDecimal.ZERO;
+        /** 可调设备数量 */
+        private Long adjustableDeviceCount = 0L;
+        /** 累计削峰量 kW(仅已结束事件) */
+        private BigDecimal peakShaveTotalKw = BigDecimal.ZERO;
+        /** 累计填谷量 kW(仅已结束事件) */
+        private BigDecimal valleyFillTotalKw = BigDecimal.ZERO;
+        /** 最大上调功率 kW */
+        private BigDecimal maxUpKw = BigDecimal.ZERO;
+        /** 最大下调功率 kW */
+        private BigDecimal maxDownKw = BigDecimal.ZERO;
+    }
+
+    @Data
+    public static class DeviceStatus {
+        /** 设备总数 */
+        private Long total = 0L;
+        /** 在线设备数 */
+        private Long online = 0L;
+        /** 离线设备数 */
+        private Long offline = 0L;
+        /** 故障设备数 */
+        private Long fault = 0L;
+    }
+
+    @Data
+    public static class Subsidy {
+        /** 累计补贴总额(元,仅已结束事件评价) */
+        private BigDecimal totalAmount = BigDecimal.ZERO;
+    }
+
+    @Data
+    public static class YearRegulation {
+        /** 年份 */
+        private Integer year;
+        /** 执行偏差率 0~1 */
+        private BigDecimal executionDeviationRate;
+        /** 响应完成率 0~1 */
+        private BigDecimal responseCompletionRate;
+        /** 有效调控容量 MW */
+        private BigDecimal effectiveCapacityMw = BigDecimal.ZERO;
+        /** 总调控容量 MW */
+        private BigDecimal totalCapacityMw = BigDecimal.ZERO;
+        /** 总调控时长 H */
+        private BigDecimal totalDurationHour = BigDecimal.ZERO;
+        /** 总邀约次数 */
+        private Long invitationCount = 0L;
+        /** 总参与次数 */
+        private Long participationCount = 0L;
+    }
+
+    @Data
+    public static class SocialBenefit {
+        /** 减碳量(吨) */
+        private BigDecimal carbonReductionTon = BigDecimal.ZERO;
+        /** 节约标准煤(吨) */
+        private BigDecimal coalSavedTon = BigDecimal.ZERO;
+        /** 等效植树量(棵) */
+        private Long equivalentTrees = 0L;
+        /** 等效绿证数(张) */
+        private Long equivalentGreenCert = 0L;
+    }
+
+    @Data
+    public static class EarningsRankItem {
+        private Long siteId;
+        private String siteName;
+        /** 收益(元) */
+        private BigDecimal earnings = BigDecimal.ZERO;
+    }
+}

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

@@ -19,8 +19,20 @@ public class DrEventDetailVO {
     private Integer eventType;
     private LocalDateTime startTime;
     private LocalDateTime endTime;
+    /** 申报截止时间 */
+    private LocalDateTime declareDeadline;
+    /** 邀约范围 */
+    private String inviteScope;
+    /** 邀约规模 kW */
     private BigDecimal targetCapacityKw;
+    /** 出清规模 kW */
     private BigDecimal clearedCapacityKw;
+    /** 备注 */
+    private String remark;
+    /** 附件 URL */
+    private String attachmentUrl;
+    /** 附件名称 */
+    private String attachmentName;
     private BigDecimal subsidyPrice;
     private Integer eventStatus;
     private List<DrParticipationVO> participations;

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

@@ -20,7 +20,19 @@ public class DrEventRequest {
     private Integer eventType;
     private LocalDateTime startTime;
     private LocalDateTime endTime;
+    /** 申报截止时间 */
+    private LocalDateTime declareDeadline;
+    /** 邀约范围 */
+    private String inviteScope;
+    /** 邀约规模 kW */
     private BigDecimal targetCapacityKw;
+    /** 出清规模 kW */
     private BigDecimal clearedCapacityKw;
+    /** 备注 */
+    private String remark;
+    /** 附件 URL */
+    private String attachmentUrl;
+    /** 附件名称 */
+    private String attachmentName;
     private BigDecimal subsidyPrice;
 }

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

@@ -18,11 +18,23 @@ public class DrInvitationRequest {
     private LocalDate executeEndDate;
     private LocalDateTime replyDeadline;
     private BigDecimal demandCapacityKw;
+    /** 申报出清容量 kW */
     private BigDecimal declaredCapacityKw;
+    /** 实际出清 kW(电网审核通过) */
+    private BigDecimal actualClearedCapacityKw;
+    /** 预估响应容量 kW(响应完成后本平台评估) */
+    private BigDecimal estimatedResponseCapacityKw;
+    /** 实际响应容量 kW(电网评估最终结果) */
+    private BigDecimal actualResponseCapacityKw;
+    /** 响应完成率 = 实际响应容量 / 邀约规模 */
+    private BigDecimal responseCompletionRate;
+    /** 是否中标 0否 1是 */
+    private Integer isWinningBid;
     /** 1削峰 2填谷 */
     private Integer transactionType;
     /** 1日前 2日内 3秒级 */
     private Integer responseType;
+    private Long siteId;
     private Long smsContactId;
     private String remark;
 }

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

@@ -25,7 +25,18 @@ public class DrInvitationVO {
     private Integer transactionType;
     private Integer responseType;
     private BigDecimal demandCapacityKw;
+    /** 申报出清容量 kW */
     private BigDecimal declaredCapacityKw;
+    /** 实际出清 kW(电网审核通过) */
+    private BigDecimal actualClearedCapacityKw;
+    /** 预估响应容量 kW(响应完成后本平台评估) */
+    private BigDecimal estimatedResponseCapacityKw;
+    /** 实际响应容量 kW(电网评估最终结果) */
+    private BigDecimal actualResponseCapacityKw;
+    /** 响应完成率 = 实际响应容量 / 邀约规模 */
+    private BigDecimal responseCompletionRate;
+    /** 是否中标 0否 1是 */
+    private Integer isWinningBid;
     private Integer replyStatus;
     private Integer responseStatus;
     private Integer smsNotifyStatus;
@@ -34,6 +45,7 @@ public class DrInvitationVO {
     private String smsContactName;
     private String smsContactPhone;
     private Long participationId;
+    private Long siteId;
     private String remark;
     private Integer tenantId;
     private LocalDateTime createTime;

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

@@ -13,6 +13,7 @@ public class DrParticipateRequest {
 
     private Boolean participate;
     private Long strategyId;
+    /** 申报出清容量 kW */
     private BigDecimal declaredCapacityKw;
     private String reason;
     private List<DrResourceDeclare> resources;
@@ -20,6 +21,7 @@ public class DrParticipateRequest {
     @Data
     public static class DrResourceDeclare {
         private Long resourceId;
+        /** 申报出清容量 kW */
         private BigDecimal declaredCapacityKw;
     }
 }

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

@@ -19,7 +19,7 @@ public class DrParticipationVO {
     private Integer participateStatus;
     private BigDecimal declaredCapacityKw;
     private BigDecimal clearedCapacityKw;
-    /** 完成率 = 出清容量 / 申报容量 */
+    /** 完成率 = 出清容量 / 申报出清容量 */
     private BigDecimal completionRate;
     private LocalDateTime declaredAt;
 }

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

@@ -0,0 +1,90 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 补贴金额预测 响应VO
+ */
+@Data
+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)
+     */
+    private BigDecimal subsidyPrice;
+
+    /**
+     * 预估响应容量(kW)
+     */
+    private BigDecimal estimatedCapacityKw;
+
+    /**
+     * 实际出清(kW)
+     */
+    private BigDecimal clearedCapacityKw;
+
+    /**
+     * 预估完成率(0~1)
+     */
+    private BigDecimal completionRate;
+
+    /**
+     * 预估偏差率(0~1)
+     */
+    private BigDecimal deviationRate;
+
+    /**
+     * 预估补贴金额
+     */
+    private BigDecimal estimatedSubsidyAmount;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+}

+ 7 - 2
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractRequestVO.java

@@ -65,9 +65,14 @@ public class VppContractRequestVO {
     private String fileUrl;
 
     /**
-     * 分成比例
+     * 客户比例
      */
-    private BigDecimal shareRatio;
+    private BigDecimal customerRatio;
+
+    /**
+     * 运营商比例
+     */
+    private BigDecimal operatorRatio;
 
     /**
      * 价格定义(JSON)

+ 7 - 2
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractResponseVO.java

@@ -78,9 +78,14 @@ public class VppContractResponseVO {
     private String fileUrl;
 
     /**
-     * 分成比例
+     * 客户比例
      */
-    private BigDecimal shareRatio;
+    private BigDecimal customerRatio;
+
+    /**
+     * 运营商比例
+     */
+    private BigDecimal operatorRatio;
 
     /**
      * 价格定义(JSON)

+ 59 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/ElectricFeeCalcUtil.java

@@ -0,0 +1,59 @@
+package com.usky.vpp.util;
+
+import com.usky.vpp.enums.ElectricSeasonEnum;
+import com.usky.vpp.enums.TimeSegmentEnum;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * 分时电费计算(平段基准价 × 季节浮动系数 × 电量)
+ */
+public final class ElectricFeeCalcUtil {
+
+    private static final BigDecimal PEAK_RATE_SUMMER_WINTER = new BigDecimal("1.50");
+    private static final BigDecimal PEAK_RATE_NORMAL = new BigDecimal("1.30");
+    private static final int SCALE = 4;
+
+    private ElectricFeeCalcUtil() {
+    }
+
+    public static BigDecimal calcSegmentFee(BigDecimal flatUnitPrice,
+                                            ElectricSeasonEnum season,
+                                            TimeSegmentEnum segment,
+                                            BigDecimal useKwh) {
+        if (flatUnitPrice == null || useKwh == null || useKwh.compareTo(BigDecimal.ZERO) <= 0) {
+            return BigDecimal.ZERO.setScale(SCALE, RoundingMode.HALF_UP);
+        }
+        BigDecimal segUnitPrice = resolveSegmentUnitPrice(flatUnitPrice, season, segment);
+        return segUnitPrice.multiply(useKwh).setScale(SCALE, RoundingMode.HALF_UP);
+    }
+
+    public static BigDecimal resolveSegmentUnitPrice(BigDecimal flatUnitPrice,
+                                                     ElectricSeasonEnum season,
+                                                     TimeSegmentEnum segment) {
+        if (flatUnitPrice == null || segment == null) {
+            return BigDecimal.ZERO;
+        }
+        switch (segment) {
+            case FLAT:
+                return flatUnitPrice;
+            case VALLEY:
+                return flatUnitPrice.multiply(TimeSegmentEnum.VALLEY.getRateFactor());
+            case PEAK:
+                return flatUnitPrice.multiply(peakRate(season));
+            case SPIKE:
+                return flatUnitPrice.multiply(peakRate(season))
+                        .multiply(TimeSegmentEnum.SPIKE.getRateFactor());
+            default:
+                throw new IllegalArgumentException("未知时段");
+        }
+    }
+
+    private static BigDecimal peakRate(ElectricSeasonEnum season) {
+        if (ElectricSeasonEnum.SUMMER == season || ElectricSeasonEnum.WINTER_SPIKE == season) {
+            return PEAK_RATE_SUMMER_WINTER;
+        }
+        return PEAK_RATE_NORMAL;
+    }
+}

+ 65 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/ElectricTimeUtil.java

@@ -0,0 +1,65 @@
+package com.usky.vpp.util;
+
+import com.usky.vpp.enums.ElectricSeasonEnum;
+import com.usky.vpp.enums.ElectricUserTypeEnum;
+import com.usky.vpp.enums.TimeSegmentEnum;
+
+import java.time.LocalDateTime;
+
+/**
+ * 分时电价工具:根据时间、用户类型判断当前属于哪个时段
+ */
+public final class ElectricTimeUtil {
+
+    private ElectricTimeUtil() {
+    }
+
+    public static TimeSegmentEnum getCurrentSegment(LocalDateTime dateTime, ElectricUserTypeEnum userType) {
+        int month = dateTime.getMonthValue();
+        int hour = dateTime.getHour();
+        ElectricSeasonEnum season = ElectricSeasonEnum.getByMonth(month);
+
+        if (ElectricUserTypeEnum.SINGLE_PART == userType) {
+            return hour >= 6 && hour < 22 ? TimeSegmentEnum.PEAK : TimeSegmentEnum.VALLEY;
+        }
+
+        if (isSpikeHour(hour, season)) {
+            return TimeSegmentEnum.SPIKE;
+        }
+        if (isPeakHour(hour, season)) {
+            return TimeSegmentEnum.PEAK;
+        }
+        if (isFlatHour(hour, season)) {
+            return TimeSegmentEnum.FLAT;
+        }
+        return TimeSegmentEnum.VALLEY;
+    }
+
+    private static boolean isSpikeHour(int hour, ElectricSeasonEnum season) {
+        if (ElectricSeasonEnum.SUMMER == season) {
+            return hour >= 12 && hour < 14;
+        }
+        if (ElectricSeasonEnum.WINTER_SPIKE == season) {
+            return hour >= 19 && hour < 21;
+        }
+        return false;
+    }
+
+    private static boolean isPeakHour(int hour, ElectricSeasonEnum season) {
+        if (ElectricSeasonEnum.SUMMER == season) {
+            return (hour >= 8 && hour < 15) || (hour >= 18 && hour < 21);
+        }
+        return (hour >= 8 && hour < 11) || (hour >= 18 && hour < 21);
+    }
+
+    private static boolean isFlatHour(int hour, ElectricSeasonEnum season) {
+        if (ElectricSeasonEnum.SUMMER == season) {
+            return (hour >= 6 && hour < 8) || (hour >= 15 && hour < 18) || (hour >= 21 && hour < 22);
+        }
+        return (hour >= 6 && hour < 8) || (hour >= 11 && hour < 18) || (hour >= 21 && hour < 22);
+    }
+
+    public static boolean isValleyHour(int hour) {
+        return hour >= 22 || hour < 6;
+    }
+}

+ 5 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppAuditHelper.java

@@ -79,6 +79,11 @@ public final class VppAuditHelper {
                 + UUID.randomUUID().toString().substring(0, 4).toUpperCase();
     }
 
+    public static String nextBillNo() {
+        return "BILL_" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now())
+                + UUID.randomUUID().toString().substring(0, 4).toUpperCase();
+    }
+
     private static String currentUsername() {
         try {
             return SecurityUtils.getUsername();

+ 13 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppDrParticipationHelper.java

@@ -16,7 +16,7 @@ public final class VppDrParticipationHelper {
     }
 
     /**
-     * 完成率 = 出清容量 / 申报容量;申报容量为空或 ≤0、出清容量为空时返回 null。
+     * 完成率 = 出清容量 / 申报出清容量;申报出清容量为空或 ≤0、出清容量为空时返回 null。
      */
     public static BigDecimal calcCompletionRate(BigDecimal clearedCapacityKw, BigDecimal declaredCapacityKw) {
         if (clearedCapacityKw == null || declaredCapacityKw == null
@@ -26,6 +26,18 @@ public final class VppDrParticipationHelper {
         return clearedCapacityKw.divide(declaredCapacityKw, RATE_SCALE, RoundingMode.HALF_UP);
     }
 
+    /**
+     * 响应完成率 = 实际响应容量 / 邀约规模;邀约规模为空或 ≤0、实际响应容量为空时返回 null。
+     */
+    public static BigDecimal calcResponseCompletionRate(BigDecimal actualResponseCapacityKw,
+                                                        BigDecimal inviteScaleKw) {
+        if (actualResponseCapacityKw == null || inviteScaleKw == null
+                || inviteScaleKw.compareTo(BigDecimal.ZERO) <= 0) {
+            return null;
+        }
+        return actualResponseCapacityKw.divide(inviteScaleKw, RATE_SCALE, RoundingMode.HALF_UP);
+    }
+
     public static void refreshCompletionRate(VppDrParticipation participation) {
         if (participation == null) {
             return;

+ 59 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppEnergyUsageHelper.java

@@ -0,0 +1,59 @@
+package com.usky.vpp.util;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * 累计电能表时序差值计算(与 EMS 分段用量逻辑一致)
+ */
+public final class VppEnergyUsageHelper {
+
+    private static final int SCALE = 4;
+
+    private VppEnergyUsageHelper() {
+    }
+
+    /**
+     * 计算区间内累计电能增量:末值 - 首值;若回零则取末值。
+     *
+     * @return 无有效读数时返回 null
+     */
+    public static BigDecimal calcUsage(TreeMap<LocalDateTime, BigDecimal> timeValueMap,
+                                       LocalDateTime startInclusive,
+                                       LocalDateTime endExclusive) {
+        if (timeValueMap == null || timeValueMap.isEmpty() || startInclusive == null || endExclusive == null) {
+            return null;
+        }
+        TreeMap<LocalDateTime, BigDecimal> segment = new TreeMap<>();
+        for (Map.Entry<LocalDateTime, BigDecimal> entry : timeValueMap.entrySet()) {
+            LocalDateTime ts = entry.getKey();
+            if (!ts.isBefore(startInclusive) && ts.isBefore(endExclusive)) {
+                segment.put(ts, entry.getValue());
+            }
+        }
+        if (segment.size() < 2) {
+            return null;
+        }
+        BigDecimal first = segment.firstEntry().getValue();
+        BigDecimal last = segment.lastEntry().getValue();
+        if (first == null || last == null) {
+            return null;
+        }
+        BigDecimal usage = last.subtract(first);
+        if (usage.compareTo(BigDecimal.ZERO) < 0) {
+            usage = last;
+        }
+        return usage.setScale(SCALE, RoundingMode.HALF_UP);
+    }
+
+    public static BigDecimal zeroIfNull(BigDecimal value) {
+        return value == null ? BigDecimal.ZERO.setScale(SCALE, RoundingMode.HALF_UP) : value;
+    }
+
+    public static BigDecimal addNullable(BigDecimal left, BigDecimal right) {
+        return zeroIfNull(left).add(zeroIfNull(right));
+    }
+}

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

@@ -38,3 +38,7 @@ vpp:
     invitation-template-code: SMS_465362899
   report:
     retry-max: 3
+  # 月度电量/电费核算同步(每月1号00:35拉上月数据)
+  settlement:
+    monthly-sync-enabled: true
+    monthly-sync-cron: "0 35 0 1 * ?"

+ 0 - 33
service-vpp/service-vpp-biz/src/main/resources/sql/vpp_audit_field_unify_migration.sql

@@ -1,33 +0,0 @@
--- VPP 审计字段统一为 created_by 风格(已有库执行)
--- operator_id / recorded_by / audit_by -> created_by VARCHAR(30)
--- operated_at -> create_time(日志类表)
-
--- 准入申请:审核人复用 updated_by,移除 audit_by
-ALTER TABLE `vpp_customer_access` DROP COLUMN `audit_by`;
-
--- 合同审核流水
-ALTER TABLE `vpp_contract_audit_log`
-    DROP COLUMN `operator_name`,
-    CHANGE COLUMN `operator_id` `created_by` VARCHAR(30) NULL COMMENT '创建人',
-    CHANGE COLUMN `operated_at` `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
-    DROP INDEX `idx_contract_audit_contract`,
-    ADD KEY `idx_contract_audit_contract` (`contract_id`, `create_time`);
-
--- 设备远程控制日志
-ALTER TABLE `vpp_device_control_log`
-    CHANGE COLUMN `operator_id` `created_by` VARCHAR(30) NULL COMMENT '创建人',
-    CHANGE COLUMN `operated_at` `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
-    DROP INDEX `idx_control_device`,
-    ADD KEY `idx_control_device` (`device_id`, `create_time`);
-
--- 缴费记录
-ALTER TABLE `vpp_payment_record`
-    CHANGE COLUMN `recorded_by` `created_by` VARCHAR(30) NULL COMMENT '创建人',
-    ADD COLUMN `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间' AFTER `tenant_id`;
-
--- 报送审核流水
-ALTER TABLE `vpp_report_audit_log`
-    CHANGE COLUMN `operator_id` `created_by` VARCHAR(30) NULL COMMENT '创建人',
-    CHANGE COLUMN `operated_at` `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
-    DROP INDEX `idx_report_audit_record`,
-    ADD KEY `idx_report_audit_record` (`record_id`, `create_time`);

+ 0 - 35
service-vpp/service-vpp-biz/src/main/resources/sql/vpp_bidding_config_migration.sql

@@ -1,35 +0,0 @@
--- 竞价配置表(已有库执行,可重复执行)
-
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables
-     WHERE table_schema = DATABASE()
-       AND table_name = 'vpp_bidding_config') = 0,
-    'CREATE TABLE `vpp_bidding_config` (
-        `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键'',
-        `config_name` VARCHAR(200) NOT NULL COMMENT ''配置名称'',
-        `effective_start` DATETIME(3) NOT NULL COMMENT ''适用开始时间'',
-        `effective_end` DATETIME(3) NOT NULL COMMENT ''适用结束时间'',
-        `attachment_url` VARCHAR(500) NULL COMMENT ''附件URL'',
-        `attachment_name` VARCHAR(200) NULL COMMENT ''附件原始文件名'',
-        `bidding_mode` TINYINT NOT NULL COMMENT ''1分时段竞价 2整体竞价'',
-        `typical_coefficient` DECIMAL(6,4) NOT NULL COMMENT ''典型系数'',
-        `advance_close_hours` INT NOT 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 ''更新时间'',
-        `created_by` VARCHAR(30) NULL COMMENT ''创建人'',
-        `updated_by` VARCHAR(30) NULL COMMENT ''更新人'',
-        `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'',
-        `deleted_at` DATETIME(3) NULL COMMENT ''软删除时间'',
-        PRIMARY KEY (`id`),
-        KEY `idx_bidding_config_effective` (effective_start, effective_end),
-        KEY `idx_bidding_config_name` (config_name)
-    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT=''竞价配置''',
-    'SELECT 1'
-);
-PREPARE stmt FROM @sql;
-EXECUTE stmt;
-DEALLOCATE PREPARE stmt;

+ 0 - 191
service-vpp/service-vpp-biz/src/main/resources/sql/vpp_delete_flag_migration.sql

@@ -1,191 +0,0 @@
--- VPP 软删除标识字段(已有库执行,可重复执行)
--- 含 deleted_at 的表新增 delete_flag INT(1) NOT NULL DEFAULT 0
-
--- vpp_customer
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_customer' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_customer` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_customer.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_customer` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_customer_access
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_customer_access' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_customer_access` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_customer_access.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_customer_access` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_customer_contact
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_customer_contact' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_customer_contact` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_customer_contact.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_customer_contact` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_contract
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_contract' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_contract` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_contract.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_contract` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_contract_template
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_contract_template' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_contract_template` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_contract_template.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_contract_template` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_resource_point
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_resource_point' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_resource_point` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_resource_point.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_resource_point` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_resource_point_config
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_resource_point_config' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_resource_point_config` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_resource_point_config.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_resource_point_config` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_device
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_device' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_device` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_device.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_device` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_energy_reading_monthly
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_energy_reading_monthly' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_energy_reading_monthly` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_energy_reading_monthly.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_energy_reading_monthly` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_energy_summary_daily
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_energy_summary_daily' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_energy_summary_daily` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_energy_summary_daily.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_energy_summary_daily` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_settlement_bill
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_settlement_bill' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_settlement_bill` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_settlement_bill.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_settlement_bill` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_report_task
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_report_task' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_report_task` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_report_task.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_report_task` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_report_record
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_report_record' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_report_record` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_report_record.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_report_record` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_report_data
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_report_data' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_report_data` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_report_data.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_report_data` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_dr_event
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_event' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_dr_event` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_dr_event.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_dr_event` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_dr_participation
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_participation' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_dr_participation` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_dr_participation.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_dr_participation` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_dr_strategy
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_strategy' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_dr_strategy` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_dr_strategy.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_dr_strategy` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_dr_execution
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_execution' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_dr_execution` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_dr_execution.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_dr_execution` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_dr_evaluation
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_evaluation' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_dr_evaluation` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_dr_evaluation.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_dr_evaluation` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_registration
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_registration' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_registration` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_registration.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_registration` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;
-
--- vpp_file_archive
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_file_archive' AND column_name = 'delete_flag') = 0,
-    'ALTER TABLE `vpp_file_archive` ADD COLUMN `delete_flag` INT(1) NOT NULL DEFAULT 0 COMMENT ''删除标识 0未删除 1已删除'' AFTER `updated_by`',
-    'SELECT ''skip: vpp_file_archive.delete_flag exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-UPDATE `vpp_file_archive` SET `delete_flag` = 1 WHERE `deleted_at` IS NOT NULL AND `delete_flag` = 0;

+ 0 - 41
service-vpp/service-vpp-biz/src/main/resources/sql/vpp_dr_invitation_migration.sql

@@ -1,41 +0,0 @@
--- VPP 需求响应企业邀约表(已有库执行,可重复执行)
--- 对应 Web「邀约管理」界面,详见 虚拟电厂平台数据库设计文档 V1.1 §5.23
-
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_invitation') = 0,
-    'CREATE TABLE `vpp_dr_invitation` (
-        `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键'',
-        `invitation_no` VARCHAR(32) NOT NULL COMMENT ''邀约编号'',
-        `dr_event_id` BIGINT NOT NULL COMMENT ''需求响应事件ID'',
-        `customer_id` BIGINT NOT NULL COMMENT ''客户ID'',
-        `execute_start_date` DATE NOT NULL COMMENT ''执行开始日期'',
-        `execute_end_date` DATE NOT NULL COMMENT ''执行结束日期'',
-        `reply_deadline` DATETIME(3) NOT NULL COMMENT ''邀约回复截止日期'',
-        `issue_time` DATETIME(3) NOT NULL COMMENT ''下发时间'',
-        `transaction_type` TINYINT NULL COMMENT ''交易类型 1削峰 2填谷'',
-        `response_type` TINYINT NOT NULL COMMENT ''1日前 2日内 3秒级'',
-        `demand_capacity_kw` DECIMAL(12,4) NOT NULL COMMENT ''需求容量 kW'',
-        `declared_capacity_kw` DECIMAL(12,4) NULL COMMENT ''已申报容量 kW'',
-        `reply_status` TINYINT NOT NULL COMMENT ''0待回复 1参与 2拒绝 3超时'',
-        `response_status` TINYINT NOT NULL COMMENT ''0待响应 1已申报 2执行中 3已结束 4已取消'',
-        `sms_notify_status` TINYINT NOT NULL DEFAULT 0 COMMENT ''0未发送 1已发送 2发送失败'',
-        `sms_notify_at` DATETIME(3) NULL COMMENT ''短信通知时间'',
-        `sms_contact_id` BIGINT NULL COMMENT ''通知联系人ID'',
-        `participation_id` BIGINT 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` 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_dr_invitation_no` (`invitation_no`),
-        UNIQUE KEY `uk_dr_invitation_event_customer` (`dr_event_id`, `customer_id`),
-        KEY `idx_dr_invitation_customer` (`customer_id`, `reply_status`),
-        KEY `idx_dr_invitation_issue` (`issue_time`, `response_type`)
-    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT=''需求响应企业邀约''',
-    'SELECT ''skip: vpp_dr_invitation already exists'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;

+ 0 - 24
service-vpp/service-vpp-biz/src/main/resources/sql/vpp_dr_participation_completion_rate_migration.sql

@@ -1,24 +0,0 @@
--- 事件参与记录增加完成率(已有库执行,可重复执行)
--- completion_rate = cleared_capacity_kw / declared_capacity_kw
-
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns
-     WHERE table_schema = DATABASE()
-       AND table_name = 'vpp_dr_participation'
-       AND column_name = 'completion_rate') = 0,
-    'ALTER TABLE `vpp_dr_participation` ADD COLUMN `completion_rate` DECIMAL(12,4) NULL COMMENT ''完成率=出清容量/申报容量'' AFTER `cleared_capacity_kw`',
-    'SELECT 1'
-);
-PREPARE stmt FROM @sql;
-EXECUTE stmt;
-DEALLOCATE PREPARE stmt;
-
--- 回填已有出清数据
-UPDATE `vpp_dr_participation`
-SET `completion_rate` = ROUND(`cleared_capacity_kw` / `declared_capacity_kw`, 4)
-WHERE `delete_flag` = 0
-  AND `cleared_capacity_kw` IS NOT NULL
-  AND `declared_capacity_kw` IS NOT NULL
-  AND `declared_capacity_kw` > 0
-  AND (`completion_rate` IS NULL
-       OR `completion_rate` <> ROUND(`cleared_capacity_kw` / `declared_capacity_kw`, 4));

+ 0 - 284
service-vpp/service-vpp-biz/src/main/resources/sql/vpp_id_auto_increment_migration.sql

@@ -1,284 +0,0 @@
--- VPP 主键自增迁移(已有库执行,可重复执行)
--- 将各表 id 列改为 BIGINT AUTO_INCREMENT,与 MyBatis-Plus IdType.AUTO 对齐
--- 表不存在时自动跳过,适用于部分表尚未创建的库
-
--- vpp_customer
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_customer') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_customer' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_customer` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_customer.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_customer_access
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_customer_access') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_customer_access' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_customer_access` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_customer_access.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_customer_contact
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_customer_contact') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_customer_contact' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_customer_contact` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_customer_contact.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_contract
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_contract') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_contract' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_contract` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_contract.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_contract_template
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_contract_template') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_contract_template' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_contract_template` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_contract_template.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_contract_audit_log
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_contract_audit_log') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_contract_audit_log' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_contract_audit_log` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_contract_audit_log.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_site
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_site') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_site' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_site` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_site.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_site_config
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_site_config') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_site_config' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_site_config` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_site_config.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_resource_point
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_resource_point') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_resource_point' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_resource_point` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_resource_point.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_resource_point_config
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_resource_point_config') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_resource_point_config' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_resource_point_config` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_resource_point_config.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_device
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_device') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_device' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_device` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_device.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_device_control_log
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_device_control_log') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_device_control_log' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_device_control_log` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_device_control_log.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_energy_reading_monthly
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_energy_reading_monthly') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_energy_reading_monthly' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_energy_reading_monthly` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_energy_reading_monthly.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_energy_summary_daily
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_energy_summary_daily') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_energy_summary_daily' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_energy_summary_daily` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_energy_summary_daily.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_settlement_bill
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_settlement_bill') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_settlement_bill' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_settlement_bill` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_settlement_bill.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_settlement_bill_detail
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_settlement_bill_detail') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_settlement_bill_detail' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_settlement_bill_detail` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_settlement_bill_detail.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_payment_record
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_payment_record') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_payment_record' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_payment_record` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_payment_record.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_report_task
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_report_task') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_report_task' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_report_task` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_report_task.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_report_record
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_report_record') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_report_record' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_report_record` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_report_record.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_report_data
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_report_data') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_report_data' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_report_data` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_report_data.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_report_audit_log
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_report_audit_log') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_report_audit_log' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_report_audit_log` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_report_audit_log.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_dr_event
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_event') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_event' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_dr_event` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_dr_event.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_dr_participation
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_participation') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_participation' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_dr_participation` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_dr_participation.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_dr_invitation
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_invitation') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_invitation' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_dr_invitation` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_dr_invitation.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_dr_strategy
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_strategy') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_strategy' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_dr_strategy` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_dr_strategy.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_dr_strategy_resource
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_strategy_resource') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_strategy_resource' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_dr_strategy_resource` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_dr_strategy_resource.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_dr_execution
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_execution') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_execution' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_dr_execution` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_dr_execution.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_dr_evaluation
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_evaluation') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_evaluation' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_dr_evaluation` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_dr_evaluation.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_registration
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_registration') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_registration' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_registration` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_registration.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_report_log
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_report_log') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_report_log' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_report_log` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_report_log.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- vpp_file_archive
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_file_archive') > 0
-    AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'vpp_file_archive' AND column_name = 'id' AND EXTRA LIKE '%auto_increment%') = 0,
-    'ALTER TABLE `vpp_file_archive` MODIFY COLUMN `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT ''主键''',
-    'SELECT ''skip: vpp_file_archive.id already auto_increment or table missing'''
-);
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
--- 若需将自增计数与数据 ID 均重置为从 1 开始,请执行 vpp_id_reset_from_one_migration.sql

+ 0 - 362
service-vpp/service-vpp-biz/src/main/resources/sql/vpp_id_reset_from_one_migration.sql

@@ -1,362 +0,0 @@
--- VPP 主键 ID 重置为从 1 开始
--- 执行前请备份数据库!
---
--- 提供两种方案(二选一执行,不要同时执行):
---   方案一:清空全部 vpp_* 数据,TRUNCATE 后自增从 1 开始(开发/联调推荐)
---   方案二:保留现有数据,按依赖关系重编号为 1..n(需 MySQL 8.0+,支持 ROW_NUMBER)
-
--- =============================================================================
--- 方案一:清空数据并重置自增(会删除所有 vpp_* 业务数据)
--- 仅 TRUNCATE 当前库中已存在的表,可重复执行
--- =============================================================================
-
-SET FOREIGN_KEY_CHECKS = 0;
-
--- vpp_payment_record
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_payment_record') > 0, 'TRUNCATE TABLE `vpp_payment_record`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_settlement_bill_detail
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_settlement_bill_detail') > 0, 'TRUNCATE TABLE `vpp_settlement_bill_detail`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_device_control_log
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_device_control_log') > 0, 'TRUNCATE TABLE `vpp_device_control_log`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_contract_audit_log
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_contract_audit_log') > 0, 'TRUNCATE TABLE `vpp_contract_audit_log`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_customer_contact
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_customer_contact') > 0, 'TRUNCATE TABLE `vpp_customer_contact`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_resource_point_config
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_resource_point_config') > 0, 'TRUNCATE TABLE `vpp_resource_point_config`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_site_config
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_site_config') > 0, 'TRUNCATE TABLE `vpp_site_config`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_dr_participation
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_participation') > 0, 'TRUNCATE TABLE `vpp_dr_participation`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_dr_strategy_resource
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_strategy_resource') > 0, 'TRUNCATE TABLE `vpp_dr_strategy_resource`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_dr_execution
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_execution') > 0, 'TRUNCATE TABLE `vpp_dr_execution`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_dr_evaluation
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_evaluation') > 0, 'TRUNCATE TABLE `vpp_dr_evaluation`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_report_audit_log
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_report_audit_log') > 0, 'TRUNCATE TABLE `vpp_report_audit_log`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_report_data
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_report_data') > 0, 'TRUNCATE TABLE `vpp_report_data`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_energy_reading_monthly
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_energy_reading_monthly') > 0, 'TRUNCATE TABLE `vpp_energy_reading_monthly`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_energy_summary_daily
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_energy_summary_daily') > 0, 'TRUNCATE TABLE `vpp_energy_summary_daily`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_file_archive
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_file_archive') > 0, 'TRUNCATE TABLE `vpp_file_archive`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_device
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_device') > 0, 'TRUNCATE TABLE `vpp_device`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_resource_point
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_resource_point') > 0, 'TRUNCATE TABLE `vpp_resource_point`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_site
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_site') > 0, 'TRUNCATE TABLE `vpp_site`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_contract
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_contract') > 0, 'TRUNCATE TABLE `vpp_contract`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_settlement_bill
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_settlement_bill') > 0, 'TRUNCATE TABLE `vpp_settlement_bill`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_report_record
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_report_record') > 0, 'TRUNCATE TABLE `vpp_report_record`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_customer_access
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_customer_access') > 0, 'TRUNCATE TABLE `vpp_customer_access`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_dr_invitation
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_invitation') > 0, 'TRUNCATE TABLE `vpp_dr_invitation`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_dr_invitation
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_invitation') > 0, 'TRUNCATE TABLE `vpp_dr_invitation`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_dr_event
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_event') > 0, 'TRUNCATE TABLE `vpp_dr_event`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_dr_strategy
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_dr_strategy') > 0, 'TRUNCATE TABLE `vpp_dr_strategy`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_contract_template
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_contract_template') > 0, 'TRUNCATE TABLE `vpp_contract_template`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_report_task
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_report_task') > 0, 'TRUNCATE TABLE `vpp_report_task`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_registration
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_registration') > 0, 'TRUNCATE TABLE `vpp_registration`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_report_log
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_report_log') > 0, 'TRUNCATE TABLE `vpp_report_log`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
--- vpp_customer
-SET @sql = IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'vpp_customer') > 0, 'TRUNCATE TABLE `vpp_customer`', 'SELECT 1');
-PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-
-SET FOREIGN_KEY_CHECKS = 1;
-
--- TRUNCATE 后 InnoDB 自增值已自动重置为 1,无需再 ALTER AUTO_INCREMENT
-
--- =============================================================================
--- 方案二:保留数据,重编号为 1..n(若需使用,请注释掉上方方案一后执行本段)
--- 要求:MySQL 8.0+;assignee_id 等指向 sys_user 的字段不在此脚本范围内
--- =============================================================================
-
-/*
-SET FOREIGN_KEY_CHECKS = 0;
-SET UNIQUE_CHECKS = 0;
-
--- 1. 生成 ID 映射(old_id -> new_id)
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_contract_template;
-CREATE TEMPORARY TABLE tmp_map_vpp_contract_template AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_contract_template`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_customer;
-CREATE TEMPORARY TABLE tmp_map_vpp_customer AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_customer`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_dr_event;
-CREATE TEMPORARY TABLE tmp_map_vpp_dr_event AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_dr_event`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_dr_strategy;
-CREATE TEMPORARY TABLE tmp_map_vpp_dr_strategy AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_dr_strategy`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_registration;
-CREATE TEMPORARY TABLE tmp_map_vpp_registration AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_registration`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_report_task;
-CREATE TEMPORARY TABLE tmp_map_vpp_report_task AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_report_task`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_report_log;
-CREATE TEMPORARY TABLE tmp_map_vpp_report_log AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_report_log`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_customer_access;
-CREATE TEMPORARY TABLE tmp_map_vpp_customer_access AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_customer_access`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_customer_contact;
-CREATE TEMPORARY TABLE tmp_map_vpp_customer_contact AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_customer_contact`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_contract;
-CREATE TEMPORARY TABLE tmp_map_vpp_contract AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_contract`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_resource_point;
-CREATE TEMPORARY TABLE tmp_map_vpp_resource_point AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_resource_point`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_report_record;
-CREATE TEMPORARY TABLE tmp_map_vpp_report_record AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_report_record`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_contract_audit_log;
-CREATE TEMPORARY TABLE tmp_map_vpp_contract_audit_log AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_contract_audit_log`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_resource_point_config;
-CREATE TEMPORARY TABLE tmp_map_vpp_resource_point_config AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_resource_point_config`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_device;
-CREATE TEMPORARY TABLE tmp_map_vpp_device AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_device`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_energy_reading_monthly;
-CREATE TEMPORARY TABLE tmp_map_vpp_energy_reading_monthly AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_energy_reading_monthly`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_energy_summary_daily;
-CREATE TEMPORARY TABLE tmp_map_vpp_energy_summary_daily AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_energy_summary_daily`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_settlement_bill;
-CREATE TEMPORARY TABLE tmp_map_vpp_settlement_bill AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_settlement_bill`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_dr_participation;
-CREATE TEMPORARY TABLE tmp_map_vpp_dr_participation AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_dr_participation`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_dr_strategy_resource;
-CREATE TEMPORARY TABLE tmp_map_vpp_dr_strategy_resource AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_dr_strategy_resource`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_report_data;
-CREATE TEMPORARY TABLE tmp_map_vpp_report_data AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_report_data`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_report_audit_log;
-CREATE TEMPORARY TABLE tmp_map_vpp_report_audit_log AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_report_audit_log`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_device_control_log;
-CREATE TEMPORARY TABLE tmp_map_vpp_device_control_log AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_device_control_log`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_settlement_bill_detail;
-CREATE TEMPORARY TABLE tmp_map_vpp_settlement_bill_detail AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_settlement_bill_detail`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_payment_record;
-CREATE TEMPORARY TABLE tmp_map_vpp_payment_record AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_payment_record`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_dr_execution;
-CREATE TEMPORARY TABLE tmp_map_vpp_dr_execution AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_dr_execution`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_dr_evaluation;
-CREATE TEMPORARY TABLE tmp_map_vpp_dr_evaluation AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_dr_evaluation`;
-
-DROP TEMPORARY TABLE IF EXISTS tmp_map_vpp_file_archive;
-CREATE TEMPORARY TABLE tmp_map_vpp_file_archive AS
-SELECT `id` AS old_id, ROW_NUMBER() OVER (ORDER BY `id`) AS new_id FROM `vpp_file_archive`;
-
--- 2. 更新外键引用
-UPDATE `vpp_customer_access` t JOIN tmp_map_vpp_customer m ON t.`customer_id` = m.old_id SET t.`customer_id` = m.new_id;
-UPDATE `vpp_customer_contact` t JOIN tmp_map_vpp_customer m ON t.`customer_id` = m.old_id SET t.`customer_id` = m.new_id;
-UPDATE `vpp_contract` t JOIN tmp_map_vpp_customer m ON t.`customer_id` = m.old_id SET t.`customer_id` = m.new_id;
-UPDATE `vpp_contract` t JOIN tmp_map_vpp_contract_template m ON t.`template_id` = m.old_id SET t.`template_id` = m.new_id;
-UPDATE `vpp_resource_point` t JOIN tmp_map_vpp_customer m ON t.`customer_id` = m.old_id SET t.`customer_id` = m.new_id;
-UPDATE `vpp_energy_reading_monthly` t JOIN tmp_map_vpp_customer m ON t.`customer_id` = m.old_id SET t.`customer_id` = m.new_id;
-UPDATE `vpp_energy_reading_monthly` t JOIN tmp_map_vpp_resource_point m ON t.`resource_id` = m.old_id SET t.`resource_id` = m.new_id;
-UPDATE `vpp_energy_summary_daily` t JOIN tmp_map_vpp_resource_point m ON t.`resource_id` = m.old_id SET t.`resource_id` = m.new_id;
-UPDATE `vpp_settlement_bill` t JOIN tmp_map_vpp_customer m ON t.`customer_id` = m.old_id SET t.`customer_id` = m.new_id;
-UPDATE `vpp_contract_audit_log` t JOIN tmp_map_vpp_contract m ON t.`contract_id` = m.old_id SET t.`contract_id` = m.new_id;
-UPDATE `vpp_resource_point_config` t JOIN tmp_map_vpp_resource_point m ON t.`resource_id` = m.old_id SET t.`resource_id` = m.new_id;
-UPDATE `vpp_device` t JOIN tmp_map_vpp_resource_point m ON t.`resource_id` = m.old_id SET t.`resource_id` = m.new_id;
-UPDATE `vpp_device_control_log` t JOIN tmp_map_vpp_device m ON t.`device_id` = m.old_id SET t.`device_id` = m.new_id;
-UPDATE `vpp_settlement_bill_detail` t JOIN tmp_map_vpp_settlement_bill m ON t.`bill_id` = m.old_id SET t.`bill_id` = m.new_id;
-UPDATE `vpp_payment_record` t JOIN tmp_map_vpp_settlement_bill m ON t.`bill_id` = m.old_id SET t.`bill_id` = m.new_id;
-UPDATE `vpp_report_record` t JOIN tmp_map_vpp_report_task m ON t.`task_id` = m.old_id SET t.`task_id` = m.new_id;
-UPDATE `vpp_report_data` t JOIN tmp_map_vpp_report_record m ON t.`record_id` = m.old_id SET t.`record_id` = m.new_id;
-UPDATE `vpp_report_audit_log` t JOIN tmp_map_vpp_report_record m ON t.`record_id` = m.old_id SET t.`record_id` = m.new_id;
-UPDATE `vpp_dr_participation` t JOIN tmp_map_vpp_dr_event m ON t.`event_id` = m.old_id SET t.`event_id` = m.new_id;
-UPDATE `vpp_dr_participation` t JOIN tmp_map_vpp_customer m ON t.`customer_id` = m.old_id SET t.`customer_id` = m.new_id;
-UPDATE `vpp_dr_participation` t JOIN tmp_map_vpp_resource_point m ON t.`resource_id` = m.old_id SET t.`resource_id` = m.new_id;
-UPDATE `vpp_dr_strategy_resource` t JOIN tmp_map_vpp_dr_strategy m ON t.`strategy_id` = m.old_id SET t.`strategy_id` = m.new_id;
-UPDATE `vpp_dr_strategy_resource` t JOIN tmp_map_vpp_resource_point m ON t.`resource_id` = m.old_id SET t.`resource_id` = m.new_id;
-UPDATE `vpp_dr_execution` t JOIN tmp_map_vpp_dr_event m ON t.`event_id` = m.old_id SET t.`event_id` = m.new_id;
-UPDATE `vpp_dr_execution` t JOIN tmp_map_vpp_resource_point m ON t.`resource_id` = m.old_id SET t.`resource_id` = m.new_id;
-UPDATE `vpp_dr_evaluation` t JOIN tmp_map_vpp_dr_event m ON t.`event_id` = m.old_id SET t.`event_id` = m.new_id;
-
--- 电子档案 biz_id(按业务类型映射)
-UPDATE `vpp_file_archive` t JOIN tmp_map_vpp_contract m ON t.`biz_type` = 'CONTRACT' AND t.`biz_id` = m.old_id SET t.`biz_id` = m.new_id;
-UPDATE `vpp_file_archive` t JOIN tmp_map_vpp_customer m ON t.`biz_type` = 'CUSTOMER' AND t.`biz_id` = m.old_id SET t.`biz_id` = m.new_id;
-UPDATE `vpp_file_archive` t JOIN tmp_map_vpp_resource_point m ON t.`biz_type` = 'RESOURCE' AND t.`biz_id` = m.old_id SET t.`biz_id` = m.new_id;
-
--- 3. 重编号主键(先置负避免冲突,再转正)
-UPDATE `vpp_contract_template` t JOIN tmp_map_vpp_contract_template m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_customer` t JOIN tmp_map_vpp_customer m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_dr_event` t JOIN tmp_map_vpp_dr_event m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_dr_strategy` t JOIN tmp_map_vpp_dr_strategy m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_registration` t JOIN tmp_map_vpp_registration m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_report_task` t JOIN tmp_map_vpp_report_task m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_report_log` t JOIN tmp_map_vpp_report_log m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_customer_access` t JOIN tmp_map_vpp_customer_access m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_customer_contact` t JOIN tmp_map_vpp_customer_contact m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_contract` t JOIN tmp_map_vpp_contract m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_resource_point` t JOIN tmp_map_vpp_resource_point m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_report_record` t JOIN tmp_map_vpp_report_record m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_contract_audit_log` t JOIN tmp_map_vpp_contract_audit_log m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_resource_point_config` t JOIN tmp_map_vpp_resource_point_config m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_device` t JOIN tmp_map_vpp_device m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_energy_reading_monthly` t JOIN tmp_map_vpp_energy_reading_monthly m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_energy_summary_daily` t JOIN tmp_map_vpp_energy_summary_daily m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_settlement_bill` t JOIN tmp_map_vpp_settlement_bill m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_dr_participation` t JOIN tmp_map_vpp_dr_participation m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_dr_strategy_resource` t JOIN tmp_map_vpp_dr_strategy_resource m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_report_data` t JOIN tmp_map_vpp_report_data m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_report_audit_log` t JOIN tmp_map_vpp_report_audit_log m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_device_control_log` t JOIN tmp_map_vpp_device_control_log m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_settlement_bill_detail` t JOIN tmp_map_vpp_settlement_bill_detail m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_payment_record` t JOIN tmp_map_vpp_payment_record m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_dr_execution` t JOIN tmp_map_vpp_dr_execution m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_dr_evaluation` t JOIN tmp_map_vpp_dr_evaluation m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-UPDATE `vpp_file_archive` t JOIN tmp_map_vpp_file_archive m ON t.`id` = m.old_id SET t.`id` = -m.new_id;
-
-UPDATE `vpp_contract_template` SET `id` = -`id`;
-UPDATE `vpp_customer` SET `id` = -`id`;
-UPDATE `vpp_dr_event` SET `id` = -`id`;
-UPDATE `vpp_dr_strategy` SET `id` = -`id`;
-UPDATE `vpp_registration` SET `id` = -`id`;
-UPDATE `vpp_report_task` SET `id` = -`id`;
-UPDATE `vpp_report_log` SET `id` = -`id`;
-UPDATE `vpp_customer_access` SET `id` = -`id`;
-UPDATE `vpp_customer_contact` SET `id` = -`id`;
-UPDATE `vpp_contract` SET `id` = -`id`;
-UPDATE `vpp_resource_point` SET `id` = -`id`;
-UPDATE `vpp_report_record` SET `id` = -`id`;
-UPDATE `vpp_contract_audit_log` SET `id` = -`id`;
-UPDATE `vpp_resource_point_config` SET `id` = -`id`;
-UPDATE `vpp_device` SET `id` = -`id`;
-UPDATE `vpp_energy_reading_monthly` SET `id` = -`id`;
-UPDATE `vpp_energy_summary_daily` SET `id` = -`id`;
-UPDATE `vpp_settlement_bill` SET `id` = -`id`;
-UPDATE `vpp_dr_participation` SET `id` = -`id`;
-UPDATE `vpp_dr_strategy_resource` SET `id` = -`id`;
-UPDATE `vpp_report_data` SET `id` = -`id`;
-UPDATE `vpp_report_audit_log` SET `id` = -`id`;
-UPDATE `vpp_device_control_log` SET `id` = -`id`;
-UPDATE `vpp_settlement_bill_detail` SET `id` = -`id`;
-UPDATE `vpp_payment_record` SET `id` = -`id`;
-UPDATE `vpp_dr_execution` SET `id` = -`id`;
-UPDATE `vpp_dr_evaluation` SET `id` = -`id`;
-UPDATE `vpp_file_archive` SET `id` = -`id`;
-
--- 4. 重置自增计数(下一条插入从 max(id)+1,空表则为 1)
-ALTER TABLE `vpp_contract_template` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_customer` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_dr_event` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_dr_strategy` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_registration` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_report_task` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_report_log` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_customer_access` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_customer_contact` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_contract` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_resource_point` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_report_record` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_contract_audit_log` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_resource_point_config` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_device` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_energy_reading_monthly` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_energy_summary_daily` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_settlement_bill` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_dr_participation` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_dr_strategy_resource` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_report_data` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_report_audit_log` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_device_control_log` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_settlement_bill_detail` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_payment_record` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_dr_execution` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_dr_evaluation` AUTO_INCREMENT = 1;
-ALTER TABLE `vpp_file_archive` AUTO_INCREMENT = 1;
-
-SET UNIQUE_CHECKS = 1;
-SET FOREIGN_KEY_CHECKS = 1;
-*/

+ 0 - 13
service-vpp/service-vpp-biz/src/main/resources/sql/vpp_report_task_site_ids_migration.sql

@@ -1,13 +0,0 @@
--- 报送任务关联站点(已有库执行,可重复执行)
-
-SET @sql = IF(
-    (SELECT COUNT(*) FROM information_schema.columns
-     WHERE table_schema = DATABASE()
-       AND table_name = 'vpp_report_task'
-       AND column_name = 'site_ids') = 0,
-    'ALTER TABLE `vpp_report_task` ADD COLUMN `site_ids` JSON NULL COMMENT ''关联站点ID列表'' AFTER `remind_days`',
-    'SELECT 1'
-);
-PREPARE stmt FROM @sql;
-EXECUTE stmt;
-DEALLOCATE PREPARE stmt;

+ 17 - 8
service-vpp/service-vpp-biz/src/main/resources/sql/vpp_schema.sql

@@ -203,8 +203,6 @@ CREATE TABLE `vpp_device` (
     `manufacturer` VARCHAR(100) NULL COMMENT '厂商',
     `model` VARCHAR(100) NULL COMMENT '型号',
     `rated_power_kw` DECIMAL(12,4) NULL COMMENT '额定功率',
-    `comm_status` TINYINT NOT NULL COMMENT '0离线 1在线',
-    `run_status` TINYINT NOT NULL COMMENT '0停机 1运行 2故障',
     `firmware_version` VARCHAR(50) NULL COMMENT '固件版本',
     `last_online_at` DATETIME(3) NULL COMMENT '最后在线时间',
     `gateway_id` VARCHAR(64) NULL COMMENT 'IoT网关标识',
@@ -426,8 +424,13 @@ CREATE TABLE `vpp_dr_event` (
     `event_type` TINYINT NULL COMMENT '1削峰 2填谷',
     `start_time` DATETIME(3) NOT NULL COMMENT '响应开始',
     `end_time` DATETIME(3) NOT NULL COMMENT '响应结束',
-    `target_capacity_kw` DECIMAL(12,4) NOT NULL COMMENT '目标容量 kW',
-    `cleared_capacity_kw` DECIMAL(12,4) NULL COMMENT '出清容量 kW',
+    `declare_deadline` DATETIME(3) NULL COMMENT '申报截止时间',
+    `invite_scope` VARCHAR(500) NULL COMMENT '邀约范围',
+    `target_capacity_kw` DECIMAL(12,4) NOT NULL COMMENT '邀约规模 kW',
+    `cleared_capacity_kw` DECIMAL(12,4) NULL COMMENT '出清规模 kW',
+    `remark` VARCHAR(500) NULL COMMENT '备注',
+    `attachment_url` VARCHAR(500) NULL COMMENT '附件URL',
+    `attachment_name` VARCHAR(200) NULL COMMENT '附件名称',
     `subsidy_price` DECIMAL(10,4) NULL COMMENT '补贴标准 元/kWh',
     `event_status` TINYINT NOT NULL COMMENT '0待参与 1已申报 2执行中 3已结束 4已取消',
     `raw_payload` JSON NULL COMMENT '原始邀约报文',
@@ -449,9 +452,9 @@ CREATE TABLE `vpp_dr_participation` (
     `customer_id` BIGINT NOT NULL COMMENT '客户ID',
     `resource_id` BIGINT NULL COMMENT '资源点ID',
     `participate_status` TINYINT NOT NULL COMMENT '0待确认 1参与 2拒绝',
-    `declared_capacity_kw` DECIMAL(12,4) NULL COMMENT '申报容量',
+    `declared_capacity_kw` DECIMAL(12,4) NULL COMMENT '申报出清容量',
     `cleared_capacity_kw` DECIMAL(12,4) NULL COMMENT '出清容量',
-    `completion_rate` DECIMAL(12,4) NULL COMMENT '完成率=出清容量/申报容量',
+    `completion_rate` DECIMAL(12,4) NULL COMMENT '完成率=出清容量/申报出清容量',
     `declared_at` DATETIME(3) NULL COMMENT '申报时间',
     `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
@@ -470,14 +473,20 @@ CREATE TABLE `vpp_dr_invitation` (
     `invitation_no` VARCHAR(32) NOT NULL COMMENT '邀约编号',
     `dr_event_id` BIGINT NOT NULL COMMENT '需求响应事件ID',
     `customer_id` BIGINT NOT NULL COMMENT '客户ID',
+    `site_id` BIGINT NULL COMMENT '站点ID',
     `execute_start_date` DATE NOT NULL COMMENT '执行开始日期',
     `execute_end_date` DATE NOT NULL COMMENT '执行结束日期',
     `reply_deadline` DATETIME(3) NOT NULL COMMENT '邀约回复截止日期',
     `issue_time` DATETIME(3) NOT NULL COMMENT '下发时间',
     `transaction_type` TINYINT NULL COMMENT '交易类型 1削峰 2填谷',
     `response_type` TINYINT NOT NULL COMMENT '1日前 2日内 3秒级',
-    `demand_capacity_kw` DECIMAL(12,4) NOT NULL COMMENT '需求容量 kW',
-    `declared_capacity_kw` DECIMAL(12,4) NULL COMMENT '已申报容量 kW',
+    `demand_capacity_kw` DECIMAL(12,4) NOT NULL COMMENT '需求容量/邀约规模 kW',
+    `declared_capacity_kw` DECIMAL(12,4) NULL COMMENT '申报出清容量 kW',
+    `actual_cleared_capacity_kw` DECIMAL(12,4) NULL COMMENT '实际出清 kW(电网审核通过)',
+    `estimated_response_capacity_kw` DECIMAL(12,4) NULL COMMENT '预估响应容量 kW(响应完成后本平台评估)',
+    `actual_response_capacity_kw` DECIMAL(12,4) NULL COMMENT '实际响应容量 kW(电网评估最终结果)',
+    `response_completion_rate` DECIMAL(12,4) NULL COMMENT '响应完成率=实际响应容量/邀约规模',
+    `is_winning_bid` TINYINT NULL COMMENT '是否中标 0否 1是',
     `reply_status` TINYINT NOT NULL COMMENT '0待回复 1参与 2拒绝 3超时',
     `response_status` TINYINT NOT NULL COMMENT '0待响应 1已申报 2执行中 3已结束 4已取消',
     `sms_notify_status` TINYINT NOT NULL DEFAULT 0 COMMENT '0未发送 1已发送 2发送失败',

+ 0 - 32
service-vpp/service-vpp-biz/src/main/resources/sql/vpp_tenant_migration.sql

@@ -1,32 +0,0 @@
--- VPP 租户字段及审计字段类型变更(已有库执行)
--- created_by / updated_by: BIGINT -> VARCHAR(30)
--- 全表新增 tenant_id
-
-ALTER TABLE `vpp_customer` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `remark`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_customer_access` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `apply_at`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_customer_contact` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `position`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_contract` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `remark`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_contract_template` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `is_enabled`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_contract_audit_log` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `opinion`;
-ALTER TABLE `vpp_resource_point` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `remark`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_resource_point_config` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `alarm_rule_json`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_device` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `remark`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_device_control_log` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `result_message`;
-ALTER TABLE `vpp_energy_reading_monthly` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `calc_at`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_energy_summary_daily` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `avg_power_kw`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_settlement_bill` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `generated_at`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_settlement_bill_detail` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `amount`;
-ALTER TABLE `vpp_payment_record` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `remark`;
-ALTER TABLE `vpp_report_task` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `remind_days`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_report_record` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `archived_at`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_report_data` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `is_draft`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_report_audit_log` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `operator_ip`;
-ALTER TABLE `vpp_dr_event` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `raw_payload`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_dr_participation` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `declared_at`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_dr_strategy` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `is_enabled`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_dr_strategy_resource` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `max_adjust_kw`;
-ALTER TABLE `vpp_dr_execution` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `finished_at`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_dr_evaluation` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `evaluated_at`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_registration` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `meta_report_id`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '创建人', MODIFY COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人';
-ALTER TABLE `vpp_report_log` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `reported_at`;
-ALTER TABLE `vpp_file_archive` ADD COLUMN `tenant_id` INT NULL COMMENT '租户ID' AFTER `remark`, ADD COLUMN `updated_by` VARCHAR(30) NULL COMMENT '更新人' AFTER `created_by`, MODIFY COLUMN `created_by` VARCHAR(30) NULL COMMENT '上传人';