Pārlūkot izejas kodu

Merge branch 'han' of uskycloud/usky-modules into server-165

hanzhengyi 1 dienu atpakaļ
vecāks
revīzija
bae1402b0c
98 mainītis faili ar 7006 papildinājumiem un 306 dzēšanām
  1. 2 15
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/config/mqtt/MqttBaseConfig.java
  2. 32 6
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/config/mqtt/MqttOutConfig.java
  3. 9 13
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/WeatherFetcher.java
  4. 13 0
      service-fire/service-fire-api/src/main/java/com/usky/fire/RemotePlanSonService.java
  5. 33 0
      service-fire/service-fire-api/src/main/java/com/usky/fire/factory/RemotePlanSonFactory.java
  6. 35 0
      service-fire/service-fire-biz/src/main/java/com/usky/fire/controller/api/planSonApi.java
  7. 5 5
      service-fire/service-fire-biz/src/main/java/com/usky/fire/controller/web/TaskController.java
  8. 10 0
      service-job/src/main/java/com/ruoyi/job/task/RyTask.java
  9. 23 0
      service-vpp/service-vpp-biz/pom.xml
  10. 23 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/config/VppSmsProperties.java
  11. 35 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppTsdbConstants.java
  12. 8 7
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ArchiveController.java
  13. 56 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/CapabilityEvalController.java
  14. 119 21
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ContractController.java
  15. 189 23
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/CustomerController.java
  16. 70 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrController.java
  17. 43 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContract.java
  18. 1 34
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContractAuditLog.java
  19. 17 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContractTemplate.java
  20. 112 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppCustomer.java
  21. 18 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppCustomerAccess.java
  22. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppCustomerContact.java
  23. 2 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDevice.java
  24. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDeviceControlLog.java
  25. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrEvaluation.java
  26. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrEvent.java
  27. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrExecution.java
  28. 76 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrInvitation.java
  29. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrParticipation.java
  30. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrStrategy.java
  31. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrStrategyResource.java
  32. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppEnergyReadingMonthly.java
  33. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppEnergySummaryDaily.java
  34. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppFileArchive.java
  35. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppPaymentRecord.java
  36. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppRegistration.java
  37. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppReportAuditLog.java
  38. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppReportData.java
  39. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppReportLog.java
  40. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppReportRecord.java
  41. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppReportTask.java
  42. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppResourcePoint.java
  43. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppSettlementBill.java
  44. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppSettlementBillDetail.java
  45. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppSite.java
  46. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppSiteConfig.java
  47. 2 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/mapper/VppContractTemplateMapper.java
  48. 12 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/mapper/VppDrInvitationMapper.java
  49. 2 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/mapper/VppFileArchiveMapper.java
  50. 16 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppAliyunSmsService.java
  51. 1 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppArchiveService.java
  52. 34 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppCapabilityEvalService.java
  53. 40 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppContractService.java
  54. 50 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppContractTemplateService.java
  55. 103 11
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppCustomerService.java
  56. 29 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrInvitationService.java
  57. 10 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrService.java
  58. 31 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppTsdbQueryService.java
  59. 87 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppAliyunSmsServiceImpl.java
  60. 110 27
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppArchiveServiceImpl.java
  61. 353 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppCapabilityEvalServiceImpl.java
  62. 325 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppContractServiceImpl.java
  63. 296 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppContractTemplateServiceImpl.java
  64. 1051 81
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppCustomerServiceImpl.java
  65. 514 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrInvitationServiceImpl.java
  66. 190 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrServiceImpl.java
  67. 170 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppTsdbQueryServiceImpl.java
  68. 21 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CapabilityCategoryStatVO.java
  69. 27 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CapabilityEvalSummaryVO.java
  70. 31 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CapabilityHistoryVO.java
  71. 20 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CapabilityLoadCurveVO.java
  72. 19 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CapabilityLoadPointVO.java
  73. 3 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CustomerAccessAuditRequest.java
  74. 73 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CustomerAccessRequestVO.java
  75. 109 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CustomerAccessResponseVO.java
  76. 174 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CustomerRequestVO.java
  77. 209 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CustomerResponseVO.java
  78. 26 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrEventRequest.java
  79. 13 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInvitationNotifyRequest.java
  80. 16 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInvitationReplyRequest.java
  81. 28 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInvitationRequest.java
  82. 43 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInvitationVO.java
  83. 19 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrStrategyRequest.java
  84. 36 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrStrategyVO.java
  85. 86 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractRequestVO.java
  86. 124 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractResponseVO.java
  87. 53 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractTemplateRequestVO.java
  88. 83 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractTemplateResponseVO.java
  89. 11 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppAuditHelper.java
  90. 364 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppCapabilityEvalHelper.java
  91. 141 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppFieldValidator.java
  92. 49 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppSmsTemplateHelper.java
  93. 5 0
      service-vpp/service-vpp-biz/src/main/resources/bootstrap.yml
  94. 94 0
      service-vpp/service-vpp-biz/src/main/resources/logback.xml
  95. 41 0
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_dr_invitation_migration.sql
  96. 284 0
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_id_auto_increment_migration.sql
  97. 362 0
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_id_reset_from_one_migration.sql
  98. 62 28
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_schema.sql

+ 2 - 15
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/config/mqtt/MqttBaseConfig.java

@@ -2,46 +2,33 @@ package com.usky.cdi.service.config.mqtt;
 
 import lombok.Data;
 import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.context.annotation.Bean;
 import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
 import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
-import org.springframework.stereotype.Component;
 
-@ConditionalOnProperty(prefix = "mqtt", value = {"enabled"}, havingValue = "true")
 @Data
-@Component
 public class MqttBaseConfig {
 
-    @Value("${mqtt.username}")
     private String username;
 
-    @Value("${mqtt.password}")
     private String password;
 
-    @Value("${mqtt.url}")
     private String hostUrl;
 
-    @Value("${mqtt.sub-topics:}")
     private String msgTopic;
 
     //心跳间隔
-    @Value("${mqtt.keep-alive-interval:60}")
     private int keepAliveInterval;
-    
+
     //完成超时
-    @Value("${mqtt.completionTimeout:3000}")
     private int completionTimeout;
 
 
-    @Bean
     public MqttPahoClientFactory mqttClientFactory() {
         DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
         MqttConnectOptions options = new MqttConnectOptions();
         options.setServerURIs(new String[]{this.getHostUrl()});
         options.setUserName(this.getUsername());
-        if (this.getPassword() != null && !this.getPassword().isEmpty()) {
+        if (this.getPassword() != null) {
             options.setPassword(this.getPassword().toCharArray());
         }
         options.setKeepAliveInterval(this.getKeepAliveInterval());

+ 32 - 6
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/config/mqtt/MqttOutConfig.java

@@ -1,11 +1,11 @@
 package com.usky.cdi.service.config.mqtt;
 
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
 import org.springframework.context.annotation.Bean;
 import org.springframework.integration.annotation.MessagingGateway;
 import org.springframework.integration.annotation.ServiceActivator;
 import org.springframework.integration.channel.DirectChannel;
-import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
+import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
 import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
 import org.springframework.integration.mqtt.support.MqttHeaders;
 import org.springframework.messaging.MessageChannel;
@@ -19,9 +19,9 @@ import java.util.Map;
  * @author han
  * @date 2025/03/20 14:31
  */
-@ConditionalOnProperty(prefix = "mqtt", value = {"enabled"}, havingValue = "true")
 @Component
 public class MqttOutConfig {
+    public MqttBaseConfig mqttBaseConfig;
 
     public static final String CHANNEL_NAME_OUT = "mqttOutboundChannel";
 
@@ -49,15 +49,41 @@ public class MqttOutConfig {
      */
     @Bean(name = MESSAGE_NAME)
     @ServiceActivator(inputChannel = CHANNEL_NAME_OUT)
-    public MessageHandler outbound(MqttPahoClientFactory factory) {
-        String clientId = "mqttx-out-" + System.currentTimeMillis();
+    public MessageHandler outbound(DefaultMqttPahoClientFactory factory) {
+        // 注意:这里的client-id暂时使用固定值,因为username在启动时还不可用
+        // 实际使用时,会在createMqttConnection方法中重新设置
+        String clientId = "mqttx-" + System.currentTimeMillis();
         MqttPahoMessageHandler messageHandler =
                 new MqttPahoMessageHandler(clientId, factory);
+        // 如果设置成true,发送消息时将不会阻塞。
         messageHandler.setAsync(true);
         messageHandler.setDefaultTopic(DEFAULT_TOPIC);
         return messageHandler;
     }
 
+    /**
+     * MQTT客户端工厂
+     * 注意:这个方法会被Spring自动创建,用于创建MQTT客户端
+     *
+     * @return DefaultMqttPahoClientFactory实例
+     */
+    @Bean
+    public DefaultMqttPahoClientFactory mqttClientFactory() {
+        // 创建默认的MqttPahoClientFactory
+        DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
+
+        // 设置默认的MqttConnectOptions,确保serverURIs不为null
+        // 使用时,会在createMqttConnection方法中重新配置
+        MqttConnectOptions options = new MqttConnectOptions();
+        // 设置默认的服务器地址
+        options.setServerURIs(new String[]{"ssl://114.80.201.143:8883"});
+        // 设置默认的心跳间隔
+        options.setKeepAliveInterval(60);
+        factory.setConnectionOptions(options);
+
+        return factory;
+    }
+
     // 注意:这个接口需要被Spring扫描到,所以我们保留@MessagingGateway注解
     // Spring会自动创建这个接口的实现类
     @MessagingGateway(defaultRequestChannel = CHANNEL_NAME_OUT)
@@ -94,4 +120,4 @@ public class MqttOutConfig {
          */
         void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, String payload);
     }
-}
+}

+ 9 - 13
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/WeatherFetcher.java

@@ -1,8 +1,5 @@
 package com.usky.cdi.service.util;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
-
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.*;
@@ -12,6 +9,7 @@ import java.net.HttpURLConnection;
 import java.net.URL;
 
 import lombok.extern.slf4j.Slf4j;
+import org.json.JSONObject;
 
 /**
  * 天气数据获取工具类
@@ -97,14 +95,14 @@ public class WeatherFetcher {
                 }
                 reader.close();
 
-                // 4. 解析JSON数据
-                JSONObject jsonResponse = JSON.parseObject(response.toString());
+                // 4. 解析JSON数据(使用org.json库)
+                JSONObject jsonResponse = new JSONObject(response.toString());
                 JSONObject main = jsonResponse.getJSONObject("main");
 
                 // 注意:温度默认是开尔文单位,转换为摄氏度需要 -273.15
                 double tempKelvin = main.getDouble("temp");
                 tempCelsius = tempKelvin - 273.15;
-                humidity = main.getInteger("humidity");
+                humidity = main.getInt("humidity");
                 double feelsLikeKelvin = main.getDouble("feels_like");
                 double feelsLikeCelsius = feelsLikeKelvin - 273.15;
 
@@ -115,16 +113,14 @@ public class WeatherFetcher {
                 // 记录日志
                 log.info("=== 天气解析结果 ===");
                 log.info("城市: {}", jsonResponse.getString("name"));
-                log.info("温度: {}°C (原始: {}K)", String.format("%.2f", tempCelsius), tempKelvin);
-                log.info("体感温度: {}°C", String.format("%.2f", feelsLikeCelsius));
+                log.info("温度: {:.2f}°C (原始: {}K)", tempCelsius, tempKelvin);
+                log.info("体感温度: {:.2f}°C", feelsLikeCelsius);
                 log.info("湿度: {}%", humidity);
                 log.info("天气状况: {}", description);
-                log.info("时区偏移: {}小时", (jsonResponse.getIntValue("timezone") / 3600));
+                log.info("时区偏移: {}小时", (jsonResponse.getInt("timezone") / 3600));
 
                 // 检查是否包含臭氧数据
-                if (jsonResponse.containsKey("air_quality")
-                        || jsonResponse.containsKey("o3")
-                        || jsonResponse.containsKey("components")) {
+                if (jsonResponse.has("air_quality") || jsonResponse.has("o3") || jsonResponse.has("components")) {
                     log.info("包含空气质量数据");
                 } else {
                     log.info("当前数据不包含臭氧浓度等空气质量指标");
@@ -174,4 +170,4 @@ public class WeatherFetcher {
     public static void main(String[] args) {
         fetchWeather();
     }
-}
+}

+ 13 - 0
service-fire/service-fire-api/src/main/java/com/usky/fire/RemotePlanSonService.java

@@ -0,0 +1,13 @@
+package com.usky.fire;
+
+
+import com.usky.fire.factory.RemoteFireFallbackFactory;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.*;
+
+@FeignClient(contextId = "remotePlanSonService", value = "service-fire" , fallbackFactory = RemoteFireFallbackFactory.class)
+public interface RemotePlanSonService {
+
+    @GetMapping("/addPlanSon")
+    void addPlanSon();
+}

+ 33 - 0
service-fire/service-fire-api/src/main/java/com/usky/fire/factory/RemotePlanSonFactory.java

@@ -0,0 +1,33 @@
+package com.usky.fire.factory;
+
+import com.usky.common.core.exception.FeignBadRequestException;
+import com.usky.fire.RemotePlanSonService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.cloud.openfeign.FallbackFactory;
+import org.springframework.stereotype.Component;
+
+
+/**
+ * 用户服务降级处理
+ *
+ * @author ruoyi
+ */
+@Component
+public class RemotePlanSonFactory implements FallbackFactory<RemotePlanSonService>
+{
+    private static final Logger log = LoggerFactory.getLogger(RemoteFireFallbackFactory.class);
+
+    @Override
+    public RemotePlanSonService create(Throwable throwable)
+    {
+        log.error("用户服务调用失败:{}", throwable.getMessage());
+        return new RemotePlanSonService()
+        {
+            @Override
+            public void addPlanSon() {
+                throw new FeignBadRequestException(500,"生成子计划数据异常"+throwable.getMessage());
+            }
+        };
+    }
+}

+ 35 - 0
service-fire/service-fire-biz/src/main/java/com/usky/fire/controller/api/planSonApi.java

@@ -0,0 +1,35 @@
+package com.usky.fire.controller.api;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.fire.RemoteFireService;
+import com.usky.fire.RemotePlanSonService;
+import com.usky.fire.service.PatrolInspectionPlanService;
+import com.usky.fire.service.PatrolInspectionPlanSonService;
+import com.usky.system.RemoteDeptService;
+import io.swagger.annotations.Api;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.openfeign.FallbackFactory;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>
+ *  数据统一查询 前端控制器
+ * </p>
+ *
+ */
+@RestController
+public class planSonApi implements RemotePlanSonService {
+
+    @Autowired
+    private PatrolInspectionPlanService patrolInspectionPlanService;
+
+    @Override
+    public void addPlanSon(){
+        patrolInspectionPlanService.addPatrolInspectionPlanSon();
+    }
+
+
+}

+ 5 - 5
service-fire/service-fire-biz/src/main/java/com/usky/fire/controller/web/TaskController.java

@@ -78,11 +78,11 @@ public class TaskController {
         demReportDataService.regularReportData();
     }
 
-    @Scheduled(cron = "0 0 23 * * ? ")//每天23点
-    public void task5(){
-        System.out.println(Thread.currentThread().getName() + "巡检计划分批次创建执行中");
-        patrolInspectionPlanService.addPatrolInspectionPlanSon();
-    }
+//    @Scheduled(cron = "0 0 23 * * ? ")//每天23点
+//    public void task5(){
+//        System.out.println(Thread.currentThread().getName() + "巡检计划分批次创建执行中");
+//        patrolInspectionPlanService.addPatrolInspectionPlanSon();
+//    }
 
 //    @Scheduled(cron = "0 5 * * * ?") //每个小时第五分钟执行
 //    public void task6(){

+ 10 - 0
service-job/src/main/java/com/ruoyi/job/task/RyTask.java

@@ -4,6 +4,7 @@ import com.usky.cdi.AlarmDataSyncTaskService;
 import com.usky.cdi.RemotecdiTaskService;
 import com.usky.common.core.utils.StringUtils;
 import com.usky.eg.RemoteEgService;
+import com.usky.fire.RemotePlanSonService;
 import com.usky.meeting.RemoteMeetingService;
 import com.usky.ems.RemoteEmsTaskService;
 import com.usky.fire.RemoteFireService;
@@ -43,6 +44,9 @@ public class RyTask {
     @Autowired
     private RemoteEgService remoteEgService;
 
+    @Autowired
+    private RemotePlanSonService remotePlanSonService;
+
     public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i) {
         System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i));
     }
@@ -130,4 +134,10 @@ public class RyTask {
         remoteEgService.egDeviceStatus();
     }
 
+    // 创建巡检子计划
+    public void addPlanSon(){
+        System.out.println(Thread.currentThread().getName() + "巡检计划分批次创建执行中");
+        remotePlanSonService.addPlanSon();
+    }
+
 }

+ 23 - 0
service-vpp/service-vpp-biz/pom.xml

@@ -91,6 +91,29 @@
             <artifactId>service-file</artifactId>
             <version>0.0.1</version>
         </dependency>
+
+        <!-- Apache POI(Excel 导出) -->
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi</artifactId>
+            <version>4.1.2</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi-ooxml</artifactId>
+            <version>4.1.2</version>
+        </dependency>
+        <!-- 阿里云短信(需求响应邀约通知) -->
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>aliyun-java-sdk-core</artifactId>
+            <version>4.5.16</version>
+        </dependency>
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
+            <version>1.1.0</version>
+        </dependency>
     </dependencies>
 
     <build>

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

@@ -0,0 +1,23 @@
+package com.usky.vpp.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 虚拟电厂阿里云短信配置
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "vpp.sms")
+public class VppSmsProperties {
+
+    /** 是否启用短信发送 */
+    private Boolean enabled = true;
+
+    /** 短信签名(阿里云控制台申请) */
+    private String signName;
+
+    /** 需求响应邀约默认模板 CODE */
+    private String invitationTemplateCode;
+}

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

@@ -0,0 +1,35 @@
+package com.usky.vpp.constant;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * VPP TDengine 指标常量(与 device_metrics 超级表及 EMS 存量表字段兼容)
+ */
+public final class VppTsdbConstants {
+
+    private VppTsdbConstants() {
+    }
+
+    /** 有功功率 / 实际负荷(优先匹配顺序) */
+    public static final List<String> ACTIVE_POWER_METRICS = Arrays.asList(
+            "active_power",
+            "totalActivePower",
+            "totalactivepower",
+            "activepowera"
+    );
+
+    /** 实时上调能力 */
+    public static final List<String> UP_CAPACITY_METRICS = Arrays.asList(
+            "up_capacity_kw",
+            "upCapacityKw"
+    );
+
+    /** 实时下调能力 */
+    public static final List<String> DOWN_CAPACITY_METRICS = Arrays.asList(
+            "down_capacity_kw",
+            "downCapacityKw"
+    );
+
+    public static final String TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
+}

+ 8 - 7
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ArchiveController.java

@@ -6,7 +6,6 @@ import com.usky.vpp.service.VppArchiveService;
 import com.usky.vpp.service.vo.VppFileArchiveRequestVO;
 import com.usky.vpp.service.vo.VppFileArchiveResponseVO;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletResponse;
@@ -33,23 +32,25 @@ public class ArchiveController {
 
     /**
      * 查询电子档案
+     * id          主键ID 精确匹配
      * archiveName 档案名称 模糊匹配
      * archiveType 档案类型 1图纸 2文档 3设计稿 4验收报告 5其他
      * siteId 站点id
      * bizType 业务类型
      * bizId 业务主键
-     * pageNum 页码
-     * pageSize 页大小
+     * current 页码
+     * size 页大小
      */
     @GetMapping
-    public ApiResult<CommonPage<VppFileArchiveResponseVO>> page(@RequestParam(value = "archiveName", required = false) String archiveName,
+    public ApiResult<CommonPage<VppFileArchiveResponseVO>> page(@RequestParam(value = "id", required = false) Long id,
+                                                                @RequestParam(value = "archiveName", required = false) String archiveName,
                                                                 @RequestParam(value = "archiveType", required = false) Integer archiveType,
                                                                 @RequestParam(value = "siteId", required = false) Long siteId,
                                                                 @RequestParam(value = "bizType", required = false) String bizType,
                                                                 @RequestParam(value = "bizId", required = false) Long bizId,
-                                                                @RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum,
-                                                                @RequestParam(value = "pageSize", required = false, defaultValue = "20") Integer pageSize) {
-        return ApiResult.success(vppArchiveService.page(archiveName, archiveType, siteId, bizType, bizId, pageNum, pageSize));
+                                                                @RequestParam(value = "current", required = false, defaultValue = "1") Integer current,
+                                                                @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
+        return ApiResult.success(vppArchiveService.page(id, archiveName, archiveType, siteId, bizType, bizId, current, size));
     }
 
     /**

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

@@ -0,0 +1,56 @@
+package com.usky.vpp.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.vpp.service.VppCapabilityEvalService;
+import com.usky.vpp.service.vo.CapabilityEvalSummaryVO;
+import com.usky.vpp.service.vo.CapabilityHistoryVO;
+import com.usky.vpp.service.vo.CapabilityLoadCurveVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+/**
+ * 可调能力评估接口
+ * <p>网关前缀: /prod-api/service-vpp/capability-eval</p>
+ */
+@RestController
+@RequestMapping("/capability-eval")
+public class CapabilityEvalController {
+
+    @Autowired
+    private VppCapabilityEvalService vppCapabilityEvalService;
+
+    /**
+     * 顶部汇总统计
+     * <p>支持按 siteId 筛选;返回总资源/负荷类/储能类/发电类户数与容量,及最大上下调能力。</p>
+     */
+    @GetMapping("/summary")
+    public ApiResult<CapabilityEvalSummaryVO> summary(@RequestParam(value = "siteId", required = false) Long siteId) {
+        return ApiResult.success(vppCapabilityEvalService.getSummary(siteId));
+    }
+
+    /**
+     * 可调能力曲线(基线负荷 vs 实际负荷)
+     * <p>默认统计当天,每 10 分钟一个点(00:00 ~ 23:50)。</p>
+     */
+    @GetMapping("/load-curve")
+    public ApiResult<CapabilityLoadCurveVO> loadCurve(@RequestParam(value = "siteId", required = false) Long siteId,
+                                                      @RequestParam(value = "date", required = false) String date) {
+        return ApiResult.success(vppCapabilityEvalService.getLoadCurve(siteId, date));
+    }
+
+    /**
+     * 历史响应记录(分页)
+     * <p>按站点下资源参与的需求响应事件倒序展示。</p>
+     */
+    @GetMapping("/history")
+    public ApiResult<CommonPage<CapabilityHistoryVO>> history(@RequestParam(value = "siteId", required = false) Long siteId,
+                                                            @RequestParam(required = false) Map<String, Object> params) {
+        return ApiResult.success(vppCapabilityEvalService.pageHistory(siteId, params));
+    }
+}

+ 119 - 21
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ContractController.java

@@ -1,10 +1,20 @@
 package com.usky.vpp.controller.web;
 
 import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.bean.CommonPage;
 import com.usky.vpp.service.VppContractService;
+import com.usky.vpp.service.VppContractTemplateService;
+import com.usky.vpp.service.vo.VppContractRequestVO;
+import com.usky.vpp.service.vo.VppContractResponseVO;
+import com.usky.vpp.service.vo.VppContractTemplateRequestVO;
+import com.usky.vpp.service.vo.VppContractTemplateResponseVO;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.format.annotation.DateTimeFormat;
 import org.springframework.web.bind.annotation.*;
 
+import javax.servlet.http.HttpServletResponse;
+import java.time.LocalDate;
+
 /**
  * 虚拟电厂 - Contract 接口
  * 网关前缀: /prod-api/service-vpp
@@ -13,35 +23,123 @@ import org.springframework.web.bind.annotation.*;
 @RequestMapping("/contract")
 public class ContractController {
 
+    @Autowired
+    private VppContractTemplateService vppContractTemplateService;
     @Autowired
     private VppContractService vppContractService;
 
-    @GetMapping(value = "/template")
-    public ApiResult<Object> listTemplate(@RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+    // ==================== 合同模板 ====================
+
+    /**
+     * 新增合同模板
+     */
+    @PostMapping("/template")
+    public ApiResult<Boolean> createTemplate(@RequestBody VppContractTemplateRequestVO vo) {
+        return vppContractTemplateService.create(vo) ? ApiResult.success(true) : ApiResult.error("新增合同模板失败!请重试!");
     }
-    @PostMapping
-    public ApiResult<Object> createContract() {
-        return ApiResult.success(null);
+
+    /**
+     * 修改合同模板
+     */
+    @PutMapping("/template")
+    public ApiResult<Boolean> updateTemplate(@RequestBody VppContractTemplateRequestVO vo) {
+        return vppContractTemplateService.update(vo) ? ApiResult.success(true) : ApiResult.error("修改或启用合同模板失败!请重试!");
     }
-    @GetMapping
-    public ApiResult<Object> pageContract() {
-        return ApiResult.success(null);
+
+    /**
+     * 分页查询合同模板
+     * id           主键ID 精确匹配
+     * templateName 模板名称 模糊匹配
+     * contractType 合同类型
+     * isEnabled    是否启用 0-禁用 1-启用
+     * current      页码
+     * size         页大小
+     */
+    @GetMapping("/template")
+    public ApiResult<CommonPage<VppContractTemplateResponseVO>> pageTemplate(
+            @RequestParam(value = "id", required = false) Long id,
+            @RequestParam(value = "templateName", required = false) String templateName,
+            @RequestParam(value = "contractType", required = false) Integer contractType,
+            @RequestParam(value = "isEnabled", required = false) Integer isEnabled,
+            @RequestParam(value = "current", required = false, defaultValue = "1") Integer current,
+            @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
+        return ApiResult.success(vppContractTemplateService.page(id, templateName, contractType, isEnabled, current, size));
+    }
+
+    /**
+     * 删除合同模板(软删除)
+     */
+    @DeleteMapping("/template/{id}")
+    public ApiResult<Boolean> deleteTemplate(@PathVariable("id") Long id) {
+        return vppContractTemplateService.delete(id) ? ApiResult.success(true) : ApiResult.error("删除合同模板失败!请重试!");
+    }
+
+    /**
+     * 下载合同模板文件(单个文件流下载,不打包)
+     */
+    @GetMapping("/template/{id}/download")
+    public void downloadTemplate(@PathVariable("id") Long id, HttpServletResponse response) {
+        vppContractTemplateService.download(id, response);
+    }
+
+    // ==================== 合同 ====================
+
+    /**
+     * 新增合同
+     */
+    @PostMapping
+    public ApiResult<Boolean> create(@RequestBody VppContractRequestVO vo) {
+        return vppContractService.create(vo) ? ApiResult.success(true) : ApiResult.error("新增合同失败!请重试!");
     }
-    @GetMapping(value = "/{id}")
-    public ApiResult<Object> getContract(@PathVariable("id") Long id, @RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+
+    /**
+     * 修改合同
+     */
+    @PutMapping
+    public ApiResult<Boolean> update(@RequestBody VppContractRequestVO vo) {
+        return vppContractService.update(vo) ? ApiResult.success(true) : ApiResult.error("修改合同失败!请重试!");
     }
-    @PostMapping(value = "/{id}/submit")
-    public ApiResult<Void> submitContract(@PathVariable("id") Long id, @RequestBody(required = false) Object body) {
-        return ApiResult.success();
+
+    /**
+     * 分页查询合同
+     * id             主键ID 精确匹配
+     * signDateStart  签约日期起始
+     * signDateEnd    签约日期截止
+     * templateId     模板ID
+     * contractType   合同类型
+     * customerId     客户ID
+     * contractStatus 合同状态
+     * current        页码
+     * size           页大小
+     */
+    @GetMapping
+    public ApiResult<CommonPage<VppContractResponseVO>> page(
+            @RequestParam(value = "id", required = false) Long id,
+            @RequestParam(value = "startDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
+            @RequestParam(value = "endDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
+            @RequestParam(value = "templateId", required = false) Long templateId,
+            @RequestParam(value = "contractType", required = false) Integer contractType,
+            @RequestParam(value = "customerId", required = false) Long customerId,
+            @RequestParam(value = "contractStatus", required = false) Integer contractStatus,
+            @RequestParam(value = "current", required = false, defaultValue = "1") Integer current,
+            @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
+        return ApiResult.success(vppContractService.page(id, startDate, endDate,
+                templateId, contractType, customerId, contractStatus, current, size));
     }
-    @PutMapping(value = "/{id}/audit")
-    public ApiResult<Void> auditContract(@PathVariable("id") Long id, @RequestBody(required = false) Object body) {
-        return ApiResult.success();
+
+    /**
+     * 删除合同(软删除)
+     */
+    @DeleteMapping("/{id}")
+    public ApiResult<Boolean> delete(@PathVariable("id") Long id) {
+        return vppContractService.delete(id) ? ApiResult.success(true) : ApiResult.error("删除合同失败!请重试!");
     }
-    @PostMapping(value = "/{id}/archive")
-    public ApiResult<Void> archiveContract(@PathVariable("id") Long id, @RequestBody(required = false) Object body) {
-        return ApiResult.success();
+
+    /**
+     * 下载合同文件(单个文件流下载,不打包)
+     */
+    @GetMapping("/{id}/download")
+    public void download(@PathVariable("id") Long id, HttpServletResponse response) {
+        vppContractService.download(id, response);
     }
 }

+ 189 - 23
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/CustomerController.java

@@ -2,21 +2,29 @@ package com.usky.vpp.controller.web;
 
 import com.usky.common.core.bean.ApiResult;
 import com.usky.common.core.bean.CommonPage;
-import com.usky.vpp.domain.VppCustomer;
-import com.usky.vpp.domain.VppCustomerAccess;
 import com.usky.vpp.domain.VppCustomerContact;
 import com.usky.vpp.service.VppCustomerService;
 import com.usky.vpp.service.vo.CustomerAccessAuditRequest;
-import com.usky.vpp.service.vo.CustomerAccessRequest;
+import com.usky.vpp.service.vo.CustomerAccessRequestVO;
+import com.usky.vpp.service.vo.CustomerAccessResponseVO;
 import com.usky.vpp.service.vo.CustomerContactRequest;
+import com.usky.vpp.service.vo.CustomerRequestVO;
+import com.usky.vpp.service.vo.CustomerResponseVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.Map;
+import javax.servlet.http.HttpServletResponse;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 
 /**
  * 客户管理接口
+ * <p>分为两大类:准入管理 和 客户管理</p>
  * 网关前缀: /prod-api/service-vpp/customer
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/6
  */
 @RestController
 @RequestMapping("/customer")
@@ -25,49 +33,203 @@ public class CustomerController {
     @Autowired
     private VppCustomerService vppCustomerService;
 
+    // ==================== 准入管理 ====================
+
+    /**
+     * 提交准入申请
+     */
     @PostMapping("/access")
-    public ApiResult<VppCustomerAccess> submitAccess(@RequestBody CustomerAccessRequest body) {
-        return ApiResult.success(vppCustomerService.submitAccess(body));
+    public ApiResult<Boolean> submitAccess(@RequestBody CustomerAccessRequestVO vo) {
+        return vppCustomerService.submitAccess(vo)
+                ? ApiResult.success(true)
+                : ApiResult.error("提交准入申请失败,请重试!");
     }
 
+    /**
+     * 分页查询准入申请
+     * <ul>
+     *   <li>id           主键ID 精确匹配</li>
+     *   <li>accessStatus 准入状态 0待审核 1通过 2拒绝</li>
+     *   <li>customerName 客户名称 模糊匹配</li>
+     *   <li>current      页码</li>
+     *   <li>size         页大小</li>
+     * </ul>
+     */
     @GetMapping("/access")
-    public ApiResult<CommonPage<VppCustomerAccess>> pageAccess(@RequestParam(required = false) Map<String, Object> params) {
-        return ApiResult.success(vppCustomerService.pageAccess(params));
+    public ApiResult<CommonPage<CustomerAccessResponseVO>> pageAccess(
+            @RequestParam(value = "id", required = false) Long id,
+            @RequestParam(value = "accessStatus", required = false) Integer accessStatus,
+            @RequestParam(value = "customerName", required = false) String customerName,
+            @RequestParam(value = "startTime", required = false) String startTime,
+            @RequestParam(value = "endTime", required = false) String endTime,
+            @RequestParam(value = "current", required = false, defaultValue = "1") Integer current,
+            @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+        LocalDateTime start = startTime != null && !startTime.isEmpty()
+                ? LocalDateTime.parse(startTime, formatter) : null;
+        LocalDateTime end = endTime != null && !endTime.isEmpty()
+                ? LocalDateTime.parse(endTime, formatter) : null;
+        return ApiResult.success(vppCustomerService.pageAccess(id, accessStatus, customerName, start, end, current, size));
     }
 
-    @PutMapping("/access/{id}/audit")
-    public ApiResult<Void> auditAccess(@PathVariable("id") Long id, @RequestBody CustomerAccessAuditRequest body) {
-        vppCustomerService.auditAccess(id, body);
+    /**
+     * 审核准入申请(通过/拒绝)
+     */
+    @PutMapping("/access/audit")
+    public ApiResult<Void> auditAccess(@RequestBody CustomerAccessAuditRequest body) {
+        vppCustomerService.auditAccess(body);
         return ApiResult.success();
     }
 
+    /**
+     * 删除准入申请(软删除)
+     */
+    @DeleteMapping("/access/{id}")
+    public ApiResult<Boolean> deleteAccess(@PathVariable("id") Long id) {
+        return vppCustomerService.deleteAccess(id)
+                ? ApiResult.success(true)
+                : ApiResult.error("删除准入申请失败,请重试!");
+    }
+
+    /**
+     * 导出准入申请数据(Excel)
+     * <ul>
+     *   <li>scope = "current":导出当前页,需传 current + size</li>
+     *   <li>scope = "all":导出当前租户全部未删除准入申请</li>
+     *   <li>scope = "filtered":导出筛选结果,需至少传一个筛选条件</li>
+     * </ul>
+     */
+    @GetMapping("/access/export")
+    public void exportAccess(
+            @RequestParam("scope") String scope,
+            @RequestParam("fileName") String fileName,
+            @RequestParam(value = "id", required = false) Long id,
+            @RequestParam(value = "accessStatus", required = false) Integer accessStatus,
+            @RequestParam(value = "customerName", required = false) String customerName,
+            @RequestParam(value = "startTime", required = false) String startTime,
+            @RequestParam(value = "endTime", required = false) String endTime,
+            @RequestParam(value = "current", required = false) Integer current,
+            @RequestParam(value = "size", required = false) Integer size,
+            HttpServletResponse response) {
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+        LocalDateTime start = startTime != null && !startTime.isEmpty()
+                ? LocalDateTime.parse(startTime, formatter) : null;
+        LocalDateTime end = endTime != null && !endTime.isEmpty()
+                ? LocalDateTime.parse(endTime, formatter) : null;
+        vppCustomerService.exportAccess(scope, fileName, id, accessStatus, customerName,
+                start, end, current, size, response);
+    }
+
+    // ==================== 客户管理 ====================
+
+    /**
+     * 直接新增客户(跳过准入流程)
+     */
+    @PostMapping
+    public ApiResult<Boolean> createCustomer(@RequestBody CustomerRequestVO vo) {
+        return vppCustomerService.createCustomer(vo)
+                ? ApiResult.success(true)
+                : ApiResult.error("新增客户失败,请重试!");
+    }
+
+    /**
+     * 分页查询客户(含关联合同)
+     * <ul>
+     *   <li>id               主键ID 精确匹配</li>
+     *   <li>customerName     客户名称 模糊匹配</li>
+     *   <li>accountNo        电力户号 精确匹配</li>
+     *   <li>lifecycleStatus  签约状态</li>
+     *   <li>customerType     客户类型</li>
+     *   <li>current          页码</li>
+     *   <li>size             页大小</li>
+     * </ul>
+     */
     @GetMapping
-    public ApiResult<CommonPage<VppCustomer>> pageCustomer(@RequestParam(required = false) Map<String, Object> params) {
-        return ApiResult.success(vppCustomerService.pageCustomer(params));
+    public ApiResult<CommonPage<CustomerResponseVO>> pageCustomer(
+            @RequestParam(value = "id", required = false) Long id,
+            @RequestParam(value = "customerName", required = false) String customerName,
+            @RequestParam(value = "accountNo", required = false) String accountNo,
+            @RequestParam(value = "lifecycleStatus", required = false) Integer lifecycleStatus,
+            @RequestParam(value = "customerType", required = false) Integer customerType,
+            @RequestParam(value = "current", required = false, defaultValue = "1") Integer current,
+            @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
+        return ApiResult.success(vppCustomerService.pageCustomer(
+                id, customerName, accountNo, lifecycleStatus, customerType, current, size));
     }
 
+    /**
+     * 查询客户详情(含关联合同)
+     */
     @GetMapping("/{id}")
-    public ApiResult<VppCustomer> getCustomer(@PathVariable("id") Long id) {
+    public ApiResult<CustomerResponseVO> getCustomer(@PathVariable("id") Long id) {
         return ApiResult.success(vppCustomerService.getCustomer(id));
     }
 
-    @PutMapping("/{id}")
-    public ApiResult<Void> updateCustomer(@PathVariable("id") Long id, @RequestBody VppCustomer body) {
-        vppCustomerService.updateCustomer(id, body);
-        return ApiResult.success();
+    /**
+     * 更新客户信息
+     */
+    @PutMapping()
+    public ApiResult<Boolean> updateCustomer(@RequestBody CustomerRequestVO vo) {
+        return vppCustomerService.updateCustomer(vo)
+                ? ApiResult.success(true)
+                : ApiResult.error("更新客户信息失败,请重试!");
+    }
+
+    /**
+     * 删除客户(软删除)
+     */
+    @DeleteMapping("/{id}")
+    public ApiResult<Boolean> deleteCustomer(@PathVariable("id") Long id) {
+        return vppCustomerService.deleteCustomer(id)
+                ? ApiResult.success(true)
+                : ApiResult.error("删除客户失败,请重试!");
     }
 
+    /**
+     * 导出客户数据(Excel)
+     * <ul>
+     *   <li>scope = "current":导出当前页,需传 current + size</li>
+     *   <li>scope = "all":导出当前租户全部未删除客户</li>
+     *   <li>scope = "filtered":导出筛选结果,需至少传一个筛选条件</li>
+     * </ul>
+     */
+    @GetMapping("/export")
+    public void exportCustomers(
+            @RequestParam("scope") String scope,
+            @RequestParam("fileName") String fileName,
+            @RequestParam(value = "customerName", required = false) String customerName,
+            @RequestParam(value = "lifecycleStatus", required = false) Integer lifecycleStatus,
+            @RequestParam(value = "current", required = false) Integer current,
+            @RequestParam(value = "size", required = false) Integer size,
+            HttpServletResponse response) {
+        vppCustomerService.exportCustomers(scope, fileName, customerName, lifecycleStatus, current, size, response);
+    }
+
+    // ==================== 联系人管理 ====================
+
+    /**
+     * 分页查询联系人
+     */
     @GetMapping("/{id}/contact")
-    public ApiResult<CommonPage<VppCustomerContact>> listContact(@PathVariable("id") Long id,
-                                                                  @RequestParam(required = false) Map<String, Object> params) {
-        return ApiResult.success(vppCustomerService.listContact(id, params));
+    public ApiResult<CommonPage<VppCustomerContact>> listContact(
+            @PathVariable("id") Long id,
+            @RequestParam(value = "current", required = false, defaultValue = "1") Integer current,
+            @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
+        return ApiResult.success(vppCustomerService.listContact(id, current, size));
     }
 
+    /**
+     * 新增联系人
+     */
     @PostMapping("/{id}/contact")
-    public ApiResult<VppCustomerContact> addContact(@PathVariable("id") Long id, @RequestBody CustomerContactRequest body) {
+    public ApiResult<VppCustomerContact> addContact(@PathVariable("id") Long id,
+                                                    @RequestBody CustomerContactRequest body) {
         return ApiResult.success(vppCustomerService.addContact(id, body));
     }
 
+    /**
+     * 更新联系人
+     */
     @PutMapping("/{id}/contact/{contactId}")
     public ApiResult<Void> updateContact(@PathVariable("id") Long id,
                                          @PathVariable("contactId") Long contactId,
@@ -76,8 +238,12 @@ public class CustomerController {
         return ApiResult.success();
     }
 
+    /**
+     * 删除联系人
+     */
     @DeleteMapping("/{id}/contact/{contactId}")
-    public ApiResult<Void> deleteContact(@PathVariable("id") Long id, @PathVariable("contactId") Long contactId) {
+    public ApiResult<Void> deleteContact(@PathVariable("id") Long id,
+                                         @PathVariable("contactId") Long contactId) {
         vppCustomerService.deleteContact(id, contactId);
         return ApiResult.success();
     }

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

@@ -5,11 +5,18 @@ import com.usky.common.core.bean.CommonPage;
 import com.usky.vpp.domain.VppDrEvent;
 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.vo.DrClearingRequest;
+import com.usky.vpp.service.vo.DrEventRequest;
 import com.usky.vpp.service.vo.DrInterveneRequest;
+import com.usky.vpp.service.vo.DrInvitationNotifyRequest;
+import com.usky.vpp.service.vo.DrInvitationReplyRequest;
+import com.usky.vpp.service.vo.DrInvitationRequest;
+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 org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
@@ -25,6 +32,8 @@ public class DrController {
 
     @Autowired
     private VppDrService vppDrService;
+    @Autowired
+    private VppDrInvitationService vppDrInvitationService;
 
     @GetMapping(value = "/event")
     public ApiResult<CommonPage<VppDrEvent>> pageEvent(@RequestParam(required = false) Map<String, Object> params) {
@@ -36,6 +45,23 @@ public class DrController {
         return ApiResult.success(vppDrService.getEvent(id));
     }
 
+    @PostMapping(value = "/event")
+    public ApiResult<VppDrEvent> createEvent(@RequestBody DrEventRequest body) {
+        return ApiResult.success(vppDrService.createEvent(body));
+    }
+
+    @PutMapping(value = "/event/{id}")
+    public ApiResult<Void> updateEvent(@PathVariable("id") Long id, @RequestBody DrEventRequest body) {
+        vppDrService.updateEvent(id, body);
+        return ApiResult.success();
+    }
+
+    @DeleteMapping(value = "/event/{id}")
+    public ApiResult<Void> deleteEvent(@PathVariable("id") Long id) {
+        vppDrService.deleteEvent(id);
+        return ApiResult.success();
+    }
+
     @GetMapping(value = "/event/{id}/capability")
     public ApiResult<Object> assessCapability(@PathVariable("id") Long id) {
         return ApiResult.success(vppDrService.assessCapability(id));
@@ -86,6 +112,11 @@ public class DrController {
         return ApiResult.success(vppDrService.pageStrategy(params));
     }
 
+    @GetMapping(value = "/strategy/{id}")
+    public ApiResult<DrStrategyVO> getStrategy(@PathVariable("id") Long id) {
+        return ApiResult.success(vppDrService.getStrategy(id));
+    }
+
     @PostMapping(value = "/strategy")
     public ApiResult<VppDrStrategy> createStrategy(@RequestBody DrStrategyRequest body) {
         return ApiResult.success(vppDrService.createStrategy(body));
@@ -102,4 +133,43 @@ public class DrController {
         vppDrService.deleteStrategy(id);
         return ApiResult.success();
     }
+
+    @GetMapping(value = "/invitation")
+    public ApiResult<CommonPage<DrInvitationVO>> pageInvitation(@RequestParam(required = false) Map<String, Object> params) {
+        return ApiResult.success(vppDrInvitationService.pageInvitation(params));
+    }
+
+    @GetMapping(value = "/invitation/{id}")
+    public ApiResult<DrInvitationVO> getInvitation(@PathVariable("id") Long id) {
+        return ApiResult.success(vppDrInvitationService.getInvitation(id));
+    }
+
+    @PostMapping(value = "/invitation")
+    public ApiResult<DrInvitationVO> createInvitation(@RequestBody DrInvitationRequest body) {
+        return ApiResult.success(vppDrInvitationService.createInvitation(body));
+    }
+
+    @PutMapping(value = "/invitation/{id}")
+    public ApiResult<Void> updateInvitation(@PathVariable("id") Long id, @RequestBody DrInvitationRequest body) {
+        vppDrInvitationService.updateInvitation(id, body);
+        return ApiResult.success();
+    }
+
+    @DeleteMapping(value = "/invitation/{id}")
+    public ApiResult<Void> deleteInvitation(@PathVariable("id") Long id) {
+        vppDrInvitationService.deleteInvitation(id);
+        return ApiResult.success();
+    }
+
+    @PostMapping(value = "/invitation/{id}/notify-sms")
+    public ApiResult<DrInvitationVO> notifyInvitationSms(@PathVariable("id") Long id,
+                                                         @RequestBody(required = false) DrInvitationNotifyRequest body) {
+        return ApiResult.success(vppDrInvitationService.notifySms(id, body));
+    }
+
+    @PostMapping(value = "/invitation/{id}/reply")
+    public ApiResult<DrInvitationVO> replyInvitation(@PathVariable("id") Long id,
+                                                       @RequestBody DrInvitationReplyRequest body) {
+        return ApiResult.success(vppDrInvitationService.replyInvitation(id, body));
+    }
 }

+ 43 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContract.java

@@ -4,8 +4,12 @@ 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 javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
 import java.io.Serializable;
 import java.math.BigDecimal;
 import java.time.LocalDate;
@@ -21,47 +25,85 @@ public class VppContract implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
+
     @TableField("contract_no")
+    @NotBlank(message = "合同编号不能为空!")
     private String contractNo;
+
     @TableField("customer_id")
+    @NotNull(message = "客户ID不能为空!")
     private Long customerId;
+
     @TableField("template_id")
+    @NotNull(message = "合同模板ID不能为空!")
     private Long templateId;
+
+    /**
+     * 1购售电 2需求响应合作 3聚合代理 4服务代理 5居民充电桩 6自有资产
+     */
     @TableField("contract_type")
+    @NotNull(message = "合同类型不能为空!")
     private Integer contractType;
+
     @TableField("contract_name")
+    @NotNull(message = "合同名称不能为空!")
     private String contractName;
+
+    /**
+     * 0草稿 1审核中 2已生效 3已到期 4已终止
+     */
+    @NotNull(message = "合同状态不能为空!")
     @TableField("contract_status")
     private Integer contractStatus;
+
     @TableField("sign_date")
     private LocalDate signDate;
+
     @TableField("effective_date")
+    @JsonFormat(pattern = "yyyy-MM-dd")
     private LocalDate effectiveDate;
+
     @TableField("expire_date")
+    @JsonFormat(pattern = "yyyy-MM-dd")
     private LocalDate expireDate;
+
     @TableField("file_url")
     private String fileUrl;
+
     @TableField("share_ratio")
     private BigDecimal shareRatio;
+
     @TableField("price_json")
     private String priceJson;
+
     @TableField("account_info_json")
     private String accountInfoJson;
+
+    @TableField("remark")
     private String remark;
+
     @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")
     private LocalDateTime deletedAt;
 }

+ 1 - 34
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContractAuditLog.java

@@ -1,34 +1 @@
-package com.usky.vpp.domain;

-

-import com.baomidou.mybatisplus.annotation.IdType;

-import com.baomidou.mybatisplus.annotation.TableField;

-import com.baomidou.mybatisplus.annotation.TableId;

-import com.baomidou.mybatisplus.annotation.TableName;

-import lombok.Data;

-import lombok.EqualsAndHashCode;

-import java.io.Serializable;

-import java.time.LocalDateTime;

-

-/**

- * vpp_contract_audit_log

- */

-@Data

-@EqualsAndHashCode(callSuper = false)

-@TableName("vpp_contract_audit_log")

-public class VppContractAuditLog implements Serializable {

-

-    private static final long serialVersionUID = 1L;

-

-    @TableId(value = "id", type = IdType.ASSIGN_ID)

-    private Long id;

-    @TableField("contract_id")

-    private Long contractId;

-    private Integer action;

-    private String opinion;

-    @TableField("tenant_id")

-    private Integer tenantId;

-    @TableField("create_time")

-    private LocalDateTime createTime;

-    @TableField("created_by")

-    private String createdBy;

-}

+package com.usky.vpp.domain;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;
import java.time.LocalDateTime;


/**
 * vpp_contract_audit_log

 */

@Data

@EqualsAndHashCode(callSuper = false)

@TableName("vpp_contract_audit_log")

public class VppContractAuditLog implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)

    private Long id;

    @TableField("contract_id")

    private Long contractId;

    private Integer action;

    private String opinion;

    @TableField("tenant_id")

    private Integer tenantId;

    @TableField("create_time")

    private LocalDateTime createTime;

    @TableField("created_by")

    private String createdBy;

}

+ 17 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContractTemplate.java

@@ -4,8 +4,12 @@ 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 javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
 import java.io.Serializable;
 import java.time.LocalDateTime;
 
@@ -19,26 +23,37 @@ public class VppContractTemplate implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("template_code")
+    @NotBlank(message = "模板编码不能为空!")
     private String templateCode;
     @TableField("template_name")
+    @NotBlank(message = "模板名称不能为空!")
     private String templateName;
+
+    /**
+     * 1、购售电合同 2、需求响应合作协议 3、聚合代理协议(2024 版) 4、虚拟电厂服务代理协议 5、居民个人充电桩协议
+     */
     @TableField("contract_type")
+    @NotNull(message = "合同类型不能为空!")
     private Integer contractType;
     private String version;
     @TableField("file_url")
+    @NotBlank(message = "文件路径不能为空!")
     private String fileUrl;
     @TableField("variables_json")
     private String variablesJson;
     @TableField("is_enabled")
+    @NotNull(message = "是否启用不能为空!")
     private Integer isEnabled;
     @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;
@@ -47,5 +62,6 @@ public class VppContractTemplate implements Serializable {
     @TableField("delete_flag")
     private Integer deleteFlag;
     @TableField("deleted_at")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime deletedAt;
 }

+ 112 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppCustomer.java

@@ -4,11 +4,15 @@ 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 javax.validation.constraints.NotNull;
 import java.io.Serializable;
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
+import java.time.LocalTime;
 
 /**
  * vpp_customer
@@ -20,30 +24,68 @@ public class VppCustomer implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("account_no")
     private String accountNo;
     @TableField("customer_name")
     private String customerName;
+
+    /**
+     * 1中高压 2低压商用 3居民充电桩 4自有资产
+     */
     @TableField("customer_type")
     private Integer customerType;
     @TableField("power_company")
     private String powerCompany;
     @TableField("contract_capacity")
     private BigDecimal contractCapacity;
+
+    /**
+     * 信用状态
+     * 1正常 2关注 3不良
+     */
     @TableField("credit_status")
     private Integer creditStatus;
+
+    /**
+     * 签约状态
+     * 1待签约 2已签约 3履约中 4待续约 5已续约 6已解约
+     */
     @TableField("lifecycle_status")
     private Integer lifecycleStatus;
+
+    /**
+     * 需求响应提前通知分钟数,默认30
+     */
     @TableField("dr_notify_minutes")
     private Integer drNotifyMinutes;
+
+    /**
+     * 登记上调能力 kW
+     */
     @TableField("dr_up_capacity_kw")
     private BigDecimal drUpCapacityKw;
+
+    /**
+     * 需求响应下调能力 kW
+     */
     @TableField("dr_down_capacity_kw")
     private BigDecimal drDownCapacityKw;
+
+    /**
+     * 省
+     */
     private String province;
+
+    /**
+     * 市
+     */
     private String city;
+
+    /**
+     * 区/县
+     */
     private String district;
     private String address;
     @TableField("business_license_url")
@@ -63,4 +105,73 @@ public class VppCustomer implements Serializable {
     private Integer deleteFlag;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
+
+    /**
+     * 是否虚拟电厂资源(0:否,1:是)
+     */
+    @NotNull(message = "是否虚拟电厂资源不能为空!")
+    @TableField("is_vpp_resource")
+    private Integer isVppResource;
+
+    /**
+     * 虚拟电厂分类(1:充换电,2:分布式三联供,3:楼宇空调,5:用户侧储能,6:数据中心,7:工业负荷,8:其他)
+     */
+    @TableField("vpp_category")
+    private String vppCategory;
+
+    /**
+     * 需求响应资源分类(1:工业负荷,2:商业负荷,3:储能,4:电动汽车,5:居民负荷,6:其他)
+     */
+    @TableField("dr_resource_type")
+    private String drResourceType;
+
+    /**
+     * 聚合代理用户虚拟电厂编码
+     */
+    @TableField("vpp_proxy_code")
+    private String vppProxyCode;
+
+    /**
+     * 所属行业(1:制造业,2:建筑业,3:批发和零售业,4:交通运输、仓储和邮政业,5:住宿和餐饮业,6:信息传输、软件和信息技术服务业,7:金融业,8:房地产业,9:租赁和商务服务业,10:科学研究和技术服务业,
+     * 11:水利、环境和公共设施管理业,12:居民服务、修理和其他服务业,13:教育,14:卫生和社会工作,15:文化、体育和娱乐业,16:公共管理、社会保障和社会组织,17:国际组织,18:农、林、牧、渔业,
+     * 19:采矿业,20:电力、热力、燃气及水生产和供应业,21:其他)
+     */
+    @TableField("industry")
+    private String industry;
+
+    /**
+     * 所属产业(1:第一产业,2:第二产业,3:第三产业)
+     */
+    @TableField("industry_sector")
+    private String industrySector;
+
+    /**
+     * 运行容量(kW)
+     */
+    private BigDecimal runningCapacity;
+
+    /**
+     * 生产经营开始时间
+     */
+    @JsonFormat(pattern = "HH:mm")
+    private LocalTime productionStartDate;
+
+    /**
+     * 生产经营结束时间
+     */
+    @JsonFormat(pattern = "HH:mm")
+    private LocalTime productionEndDate;
+
+    /**
+     * 用电地址
+     */
+    @TableField("power_address")
+    private String powerAddress;
+
+    /**
+     * 签约时间
+     */
+    @TableField("signing_date")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime signingDate;
 }

+ 18 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppCustomerAccess.java

@@ -4,8 +4,10 @@ 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;
@@ -20,7 +22,7 @@ public class VppCustomerAccess implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("apply_no")
     private String applyNo;
@@ -46,13 +48,28 @@ public class VppCustomerAccess implements Serializable {
     private LocalDateTime auditAt;
     @TableField("customer_id")
     private Long customerId;
+
+    @TableField(exist = false)
+    private LocalDateTime signingDate;
+
+    @TableField(exist = false)
+    private String contactName;
+
+    @TableField(exist = false)
+    private String contactPhone;
+
+    @TableField(exist = false)
+    private String contactEmail;
+
     @TableField("apply_at")
     private LocalDateTime applyAt;
     @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;

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

@@ -19,7 +19,7 @@ public class VppCustomerContact implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("customer_id")
     private Long customerId;

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

@@ -20,7 +20,7 @@ public class VppDevice implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("device_code")
     private String deviceCode;
@@ -28,7 +28,7 @@ public class VppDevice implements Serializable {
     private String deviceUuid;
     @TableField("device_name")
     private String deviceName;
-    /** 所属站点ID,关è�vpp_site.id */
+    /** 所属站点ID,关�vpp_site.id */
     @TableField("site_id")
     private Long siteId;
     @TableField("device_type")

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

@@ -19,7 +19,7 @@ public class VppDeviceControlLog implements Serializable {
 

     private static final long serialVersionUID = 1L;

 

-    @TableId(value = "id", type = IdType.ASSIGN_ID)

+    @TableId(value = "id", type = IdType.AUTO)

     private Long id;

     @TableField("device_id")

     private Long deviceId;


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

@@ -20,7 +20,7 @@ public class VppDrEvaluation implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("event_id")
     private Long eventId;

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

@@ -20,7 +20,7 @@ public class VppDrEvent implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("event_id")
     private String eventId;

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

@@ -20,7 +20,7 @@ public class VppDrExecution implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("event_id")
     private Long eventId;

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

@@ -0,0 +1,76 @@
+package com.usky.vpp.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * vpp_dr_invitation
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("vpp_dr_invitation")
+public class VppDrInvitation implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+    @TableField("invitation_no")
+    private String invitationNo;
+    @TableField("dr_event_id")
+    private Long drEventId;
+    @TableField("customer_id")
+    private Long customerId;
+    @TableField("execute_start_date")
+    private LocalDate executeStartDate;
+    @TableField("execute_end_date")
+    private LocalDate executeEndDate;
+    @TableField("reply_deadline")
+    private LocalDateTime replyDeadline;
+    @TableField("issue_time")
+    private LocalDateTime issueTime;
+    @TableField("transaction_type")
+    private Integer transactionType;
+    @TableField("response_type")
+    private Integer responseType;
+    @TableField("demand_capacity_kw")
+    private BigDecimal demandCapacityKw;
+    @TableField("declared_capacity_kw")
+    private BigDecimal declaredCapacityKw;
+    @TableField("reply_status")
+    private Integer replyStatus;
+    @TableField("response_status")
+    private Integer responseStatus;
+    @TableField("sms_notify_status")
+    private Integer smsNotifyStatus;
+    @TableField("sms_notify_at")
+    private LocalDateTime smsNotifyAt;
+    @TableField("sms_contact_id")
+    private Long smsContactId;
+    @TableField("participation_id")
+    private Long participationId;
+    private String remark;
+    @TableField("tenant_id")
+    private Integer tenantId;
+    @TableField("create_time")
+    private LocalDateTime createTime;
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+    @TableField("created_by")
+    private String createdBy;
+    @TableField("updated_by")
+    private String updatedBy;
+    @TableField("delete_flag")
+    private Integer deleteFlag;
+    @TableField("deleted_at")
+    private LocalDateTime deletedAt;
+}

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

@@ -20,7 +20,7 @@ public class VppDrParticipation implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("event_id")
     private Long eventId;

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

@@ -20,7 +20,7 @@ public class VppDrStrategy implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("strategy_code")
     private String strategyCode;

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

@@ -19,7 +19,7 @@ public class VppDrStrategyResource implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("strategy_id")
     private Long strategyId;

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

@@ -20,7 +20,7 @@ public class VppEnergyReadingMonthly implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("customer_id")
     private Long customerId;

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

@@ -21,7 +21,7 @@ public class VppEnergySummaryDaily implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("summary_date")
     private LocalDate summaryDate;

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

@@ -129,4 +129,4 @@ public class VppFileArchive implements Serializable {
      */
     @TableField("tenant_id")
     private Integer tenantId;
-}
+}

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

@@ -21,7 +21,7 @@ public class VppPaymentRecord implements Serializable {
 

     private static final long serialVersionUID = 1L;

 

-    @TableId(value = "id", type = IdType.ASSIGN_ID)

+    @TableId(value = "id", type = IdType.AUTO)

     private Long id;

     @TableField("bill_id")

     private Long billId;


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

@@ -19,7 +19,7 @@ public class VppRegistration implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("dn_id")
     private String dnId;

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

@@ -19,7 +19,7 @@ public class VppReportAuditLog implements Serializable {
 

     private static final long serialVersionUID = 1L;

 

-    @TableId(value = "id", type = IdType.ASSIGN_ID)

+    @TableId(value = "id", type = IdType.AUTO)

     private Long id;

     @TableField("record_id")

     private Long recordId;


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

@@ -19,7 +19,7 @@ public class VppReportData implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("record_id")
     private Long recordId;

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

@@ -19,7 +19,7 @@ public class VppReportLog implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("report_type")
     private String reportType;

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

@@ -19,7 +19,7 @@ public class VppReportRecord implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("task_id")
     private Long taskId;

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

@@ -19,7 +19,7 @@ public class VppReportTask implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("task_no")
     private String taskNo;

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

@@ -21,7 +21,7 @@ public class VppResourcePoint implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("resource_code")
     private String resourceCode;

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

@@ -21,7 +21,7 @@ public class VppSettlementBill implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("bill_no")
     private String billNo;

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

@@ -19,7 +19,7 @@ public class VppSettlementBillDetail implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("bill_id")
     private Long billId;

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

@@ -21,7 +21,7 @@ public class VppSite implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("site_code")
     private String siteCode;

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

@@ -21,7 +21,7 @@ public class VppSiteConfig implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     @TableField("site_id")
     private Long siteId;

+ 2 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/mapper/VppContractTemplateMapper.java

@@ -2,9 +2,11 @@ package com.usky.vpp.mapper;
 
 import com.usky.common.mybatis.core.CrudMapper;
 import com.usky.vpp.domain.VppContractTemplate;
+import org.springframework.stereotype.Repository;
 
 /**
  * vpp_contract_template Mapper
  */
+@Repository
 public interface VppContractTemplateMapper extends CrudMapper<VppContractTemplate> {
 }

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

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

+ 2 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/mapper/VppFileArchiveMapper.java

@@ -2,9 +2,11 @@ package com.usky.vpp.mapper;
 
 import com.usky.common.mybatis.core.CrudMapper;
 import com.usky.vpp.domain.VppFileArchive;
+import org.springframework.stereotype.Repository;
 
 /**
  * vpp_file_archive Mapper
  */
+@Repository
 public interface VppFileArchiveMapper extends CrudMapper<VppFileArchive> {
 }

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

@@ -0,0 +1,16 @@
+package com.usky.vpp.service;
+
+/**
+ * 阿里云短信发送
+ */
+public interface VppAliyunSmsService {
+
+    /**
+     * 发送模板短信
+     *
+     * @param phone        手机号
+     * @param templateCode 模板 CODE
+     * @param templateParam JSON 模板变量,如 {"meet":"夏季削峰","time":"2026-07-08 14:00:00","room":"某某公司"}
+     */
+    void sendTemplateSms(String phone, String templateCode, String templateParam);
+}

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

@@ -3,7 +3,6 @@ package com.usky.vpp.service;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.vpp.service.vo.VppFileArchiveRequestVO;
 import com.usky.vpp.service.vo.VppFileArchiveResponseVO;
-import org.springframework.http.ResponseEntity;
 
 import javax.servlet.http.HttpServletResponse;
 import java.util.List;
@@ -20,7 +19,7 @@ public interface VppArchiveService {
 
     Boolean create(VppFileArchiveRequestVO vo);
 
-    CommonPage<VppFileArchiveResponseVO> page(String archiveName, Integer archiveType, Long siteId, String bizType, Long bizId, Integer pageNum, Integer pageSize);
+    CommonPage<VppFileArchiveResponseVO> page(Long id, String archiveName, Integer archiveType, Long siteId, String bizType, Long bizId, Integer pageNum, Integer pageSize);
 
     Boolean upload(VppFileArchiveRequestVO vo);
 

+ 34 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppCapabilityEvalService.java

@@ -0,0 +1,34 @@
+package com.usky.vpp.service;
+
+import com.usky.common.core.bean.CommonPage;
+import com.usky.vpp.service.vo.CapabilityEvalSummaryVO;
+import com.usky.vpp.service.vo.CapabilityHistoryVO;
+import com.usky.vpp.service.vo.CapabilityLoadCurveVO;
+
+import java.util.Map;
+
+/**
+ * 可调能力评估业务接口
+ */
+public interface VppCapabilityEvalService {
+
+    /**
+     * 顶部汇总统计(总资源/负荷类/储能类/发电类 + 最大上下调能力)
+     *
+     * @param siteId 站点 ID,为空时统计全部站点
+     */
+    CapabilityEvalSummaryVO getSummary(Long siteId);
+
+    /**
+     * 可调能力曲线(基线负荷 vs 实际负荷)
+     *
+     * @param siteId 站点 ID,为空时统计全部站点
+     * @param date   统计日期 yyyy-MM-dd,默认当天
+     */
+    CapabilityLoadCurveVO getLoadCurve(Long siteId, String date);
+
+    /**
+     * 历史响应记录(分页)
+     */
+    CommonPage<CapabilityHistoryVO> pageHistory(Long siteId, Map<String, Object> params);
+}

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

@@ -1,7 +1,11 @@
 package com.usky.vpp.service;
 
 import com.usky.common.core.bean.CommonPage;
+import com.usky.vpp.service.vo.VppContractRequestVO;
+import com.usky.vpp.service.vo.VppContractResponseVO;
 
+import javax.servlet.http.HttpServletResponse;
+import java.time.LocalDate;
 import java.util.Map;
 
 /**
@@ -12,4 +16,40 @@ public interface VppContractService {
     default Object stub(String action, Map<String, Object> params) {
         return null;
     }
+
+    /**
+     * 新增合同
+     */
+    Boolean create(VppContractRequestVO vo);
+
+    /**
+     * 修改合同
+     */
+    Boolean update(VppContractRequestVO vo);
+
+    /**
+     * 分页查询合同
+     * @param id             主键ID(精确匹配)
+     * @param signDateStart  签约日期起始
+     * @param signDateEnd    签约日期截止
+     * @param templateId     模板ID
+     * @param contractType   合同类型
+     * @param customerId     客户ID
+     * @param contractStatus 合同状态
+     * @param pageNum        页码
+     * @param pageSize       页大小
+     */
+    CommonPage<VppContractResponseVO> page(Long id, LocalDate signDateStart, LocalDate signDateEnd,
+                                           Long templateId, Integer contractType, Long customerId,
+                                           Integer contractStatus, Integer pageNum, Integer pageSize);
+
+    /**
+     * 删除合同(软删除)
+     */
+    Boolean delete(Long id);
+
+    /**
+     * 下载合同文件
+     */
+    void download(Long id, HttpServletResponse response);
 }

+ 50 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppContractTemplateService.java

@@ -0,0 +1,50 @@
+package com.usky.vpp.service;
+
+import com.usky.common.core.bean.CommonPage;
+import com.usky.vpp.service.vo.VppContractTemplateRequestVO;
+import com.usky.vpp.service.vo.VppContractTemplateResponseVO;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * VppContractTemplateService 业务接口
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/4
+ */
+public interface VppContractTemplateService {
+
+    /**
+     * 新增合同模板
+     */
+    Boolean create(VppContractTemplateRequestVO vo);
+
+    /**
+     * 修改合同模板
+     */
+    Boolean update(VppContractTemplateRequestVO vo);
+
+    /**
+     * 分页查询合同模板
+     * @param id           主键ID(精确匹配)
+     * @param templateName 模板名称(模糊匹配)
+     * @param contractType 合同类型
+     * @param isEnabled    是否启用
+     * @param pageNum      页码
+     * @param pageSize     页大小
+     */
+    CommonPage<VppContractTemplateResponseVO> page(Long id, String templateName, Integer contractType, Integer isEnabled,
+                                                     Integer pageNum, Integer pageSize);
+
+    /**
+     * 删除合同模板(软删除)
+     */
+    Boolean delete(Long id);
+
+    /**
+     * 下载合同模板文件
+     * @param id       主键ID
+     * @param response HTTP响应
+     */
+    void download(Long id, HttpServletResponse response);
+}

+ 103 - 11
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppCustomerService.java

@@ -1,37 +1,129 @@
 package com.usky.vpp.service;
 
 import com.usky.common.core.bean.CommonPage;
-import com.usky.vpp.domain.VppCustomer;
-import com.usky.vpp.domain.VppCustomerAccess;
 import com.usky.vpp.domain.VppCustomerContact;
 import com.usky.vpp.service.vo.CustomerAccessAuditRequest;
-import com.usky.vpp.service.vo.CustomerAccessRequest;
+import com.usky.vpp.service.vo.CustomerAccessRequestVO;
+import com.usky.vpp.service.vo.CustomerAccessResponseVO;
 import com.usky.vpp.service.vo.CustomerContactRequest;
+import com.usky.vpp.service.vo.CustomerRequestVO;
+import com.usky.vpp.service.vo.CustomerResponseVO;
 
-import java.util.Map;
+import javax.servlet.http.HttpServletResponse;
+import java.time.LocalDateTime;
 
 /**
  * 客户管理业务接口
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/6
  */
 public interface VppCustomerService {
 
-    VppCustomerAccess submitAccess(CustomerAccessRequest request);
+    // ==================== 准入管理 ====================
 
-    CommonPage<VppCustomerAccess> pageAccess(Map<String, Object> params);
+    /**
+     * 提交准入申请
+     */
+    Boolean submitAccess(CustomerAccessRequestVO request);
 
-    void auditAccess(Long id, CustomerAccessAuditRequest request);
+    /**
+     * 分页查询准入申请
+     */
+    CommonPage<CustomerAccessResponseVO> pageAccess(Long id, Integer accessStatus, String customerName,
+                                                    LocalDateTime startTime, LocalDateTime endTime,
+                                                    Integer pageNum, Integer pageSize);
 
-    CommonPage<VppCustomer> pageCustomer(Map<String, Object> params);
+    /**
+     * 审核准入申请
+     */
+    void auditAccess(CustomerAccessAuditRequest request);
 
-    VppCustomer getCustomer(Long id);
+    /**
+     * 删除准入申请(软删除)
+     */
+    Boolean deleteAccess(Long id);
 
-    void updateCustomer(Long id, VppCustomer customer);
+    /**
+     * 导出准入申请数据(Excel)
+     *
+     * @param scope        导出范围:current / all / filtered
+     * @param fileName     文件名(不含扩展名)
+     * @param id           申请ID精确匹配(filtered 模式可用)
+     * @param accessStatus 准入状态(filtered 模式可用)
+     * @param customerName 客户名称模糊匹配(filtered 模式可用)
+     * @param startTime    申请时间起(filtered 模式可用)
+     * @param endTime      申请时间止(filtered 模式可用)
+     * @param current      当前页码(current 模式必传)
+     * @param size         页大小(current 模式必传)
+     */
+    void exportAccess(String scope, String fileName, Long id, Integer accessStatus,
+                      String customerName, LocalDateTime startTime, LocalDateTime endTime,
+                      Integer current, Integer size, HttpServletResponse response);
 
-    CommonPage<VppCustomerContact> listContact(Long customerId, Map<String, Object> params);
+    // ==================== 客户管理 ====================
 
+    /**
+     * 直接新增客户(跳过准入流程)
+     */
+    Boolean createCustomer(CustomerRequestVO vo);
+
+    /**
+     * 分页查询客户(含关联合同)
+     */
+    CommonPage<CustomerResponseVO> pageCustomer(Long id, String customerName, String accountNo,
+                                                Integer lifecycleStatus, Integer customerType,
+                                                Integer pageNum, Integer pageSize);
+
+    /**
+     * 查询客户详情(含关联合同)
+     */
+    CustomerResponseVO getCustomer(Long id);
+
+    /**
+     * 更新客户信息
+     */
+    Boolean updateCustomer(CustomerRequestVO vo);
+
+    /**
+     * 删除客户(软删除)
+     */
+    Boolean deleteCustomer(Long id);
+
+    /**
+     * 导出客户数据(Excel)
+     *
+     * @param scope          导出范围:current / all / filtered
+     * @param fileName       文件名(不含扩展名)
+     * @param customerName   客户名称(模糊匹配,filtered 模式可用)
+     * @param lifecycleStatus 签约状态(filtered 模式可用)
+     * @param current        当前页码(current 模式必传)
+     * @param size           页大小(current 模式必传)
+     */
+    void exportCustomers(String scope, String fileName, String customerName,
+                         Integer lifecycleStatus, Integer current, Integer size,
+                         HttpServletResponse response);
+
+    // ==================== 联系人管理 ====================
+
+    /**
+     * 分页查询联系人
+     */
+    CommonPage<VppCustomerContact> listContact(Long customerId, Integer pageNum, Integer pageSize);
+
+    /**
+     * 新增联系人
+     */
     VppCustomerContact addContact(Long customerId, CustomerContactRequest request);
 
+    /**
+     * 更新联系人
+     */
     void updateContact(Long customerId, Long contactId, CustomerContactRequest request);
 
+    /**
+     * 删除联系人
+     */
     void deleteContact(Long customerId, Long contactId);
 }

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

@@ -0,0 +1,29 @@
+package com.usky.vpp.service;
+
+import com.usky.common.core.bean.CommonPage;
+import com.usky.vpp.service.vo.DrInvitationNotifyRequest;
+import com.usky.vpp.service.vo.DrInvitationReplyRequest;
+import com.usky.vpp.service.vo.DrInvitationRequest;
+import com.usky.vpp.service.vo.DrInvitationVO;
+
+import java.util.Map;
+
+/**
+ * 需求响应邀约业务接口
+ */
+public interface VppDrInvitationService {
+
+    CommonPage<DrInvitationVO> pageInvitation(Map<String, Object> params);
+
+    DrInvitationVO getInvitation(Long id);
+
+    DrInvitationVO createInvitation(DrInvitationRequest request);
+
+    void updateInvitation(Long id, DrInvitationRequest request);
+
+    void deleteInvitation(Long id);
+
+    DrInvitationVO notifySms(Long id, DrInvitationNotifyRequest request);
+
+    DrInvitationVO replyInvitation(Long id, DrInvitationReplyRequest request);
+}

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

@@ -5,9 +5,11 @@ import com.usky.vpp.domain.VppDrEvent;
 import com.usky.vpp.domain.VppDrEvaluation;
 import com.usky.vpp.domain.VppDrStrategy;
 import com.usky.vpp.service.vo.DrClearingRequest;
+import com.usky.vpp.service.vo.DrEventRequest;
 import com.usky.vpp.service.vo.DrInterveneRequest;
 import com.usky.vpp.service.vo.DrParticipateRequest;
 import com.usky.vpp.service.vo.DrStrategyRequest;
+import com.usky.vpp.service.vo.DrStrategyVO;
 
 import java.util.Map;
 
@@ -20,6 +22,12 @@ public interface VppDrService {
 
     VppDrEvent getEvent(Long id);
 
+    VppDrEvent createEvent(DrEventRequest request);
+
+    void updateEvent(Long id, DrEventRequest request);
+
+    void deleteEvent(Long id);
+
     Object assessCapability(Long eventId);
 
     void participate(Long eventId, DrParticipateRequest request);
@@ -38,6 +46,8 @@ public interface VppDrService {
 
     CommonPage<VppDrStrategy> pageStrategy(Map<String, Object> params);
 
+    DrStrategyVO getStrategy(Long id);
+
     VppDrStrategy createStrategy(DrStrategyRequest request);
 
     void updateStrategy(Long id, DrStrategyRequest request);

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

@@ -0,0 +1,31 @@
+package com.usky.vpp.service;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * VPP 时序数据(TDengine)查询服务
+ */
+public interface VppTsdbQueryService {
+
+    /**
+     * 批量查询设备指标历史曲线
+     *
+     * @return deviceUuid -> metric -> time -> value
+     */
+    Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> queryDeviceMetricHistory(
+            List<String> deviceUuids,
+            LocalDateTime startTime,
+            LocalDateTime endTime,
+            List<String> metrics);
+
+    /**
+     * 批量查询设备最新指标值
+     *
+     * @return deviceUuid -> metric -> value
+     */
+    Map<String, Map<String, BigDecimal>> queryLatestMetrics(List<String> deviceUuids, List<String> metrics);
+}

+ 87 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppAliyunSmsServiceImpl.java

@@ -0,0 +1,87 @@
+package com.usky.vpp.service.impl;
+
+import com.aliyuncs.DefaultAcsClient;
+import com.aliyuncs.IAcsClient;
+import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
+import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
+import com.aliyuncs.profile.DefaultProfile;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.vpp.config.VppSmsProperties;
+import com.usky.vpp.service.VppAliyunSmsService;
+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.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+/**
+ * 阿里云短信服务实现
+ */
+@Service
+public class VppAliyunSmsServiceImpl implements VppAliyunSmsService {
+
+    private static final Logger log = LoggerFactory.getLogger(VppAliyunSmsServiceImpl.class);
+
+    @Value("${sms.ali.region-id:}")
+    private String regionId;
+
+    @Value("${sms.ali.access-key-id:}")
+    private String accessKeyId;
+
+    @Value("${sms.ali.access-key-secret:}")
+    private String accessKeySecret;
+
+    @Autowired
+    private VppSmsProperties smsProperties;
+
+    @Override
+    public void sendTemplateSms(String phone, String templateCode, String templateParam) {
+        if (!Boolean.TRUE.equals(smsProperties.getEnabled())) {
+            throw new BusinessException("短信服务未启用");
+        }
+        if (!StringUtils.hasText(phone)) {
+            throw new BusinessException("手机号不能为空");
+        }
+        if (!StringUtils.hasText(templateCode)) {
+            throw new BusinessException("短信模板不能为空");
+        }
+        if (!StringUtils.hasText(smsProperties.getSignName())) {
+            throw new BusinessException("短信签名未配置,请设置 vpp.sms.sign-name");
+        }
+        ensureCredentialsConfigured();
+
+        try {
+            DefaultProfile profile = DefaultProfile.getProfile(regionId.trim(), accessKeyId.trim(), accessKeySecret.trim());
+            IAcsClient client = new DefaultAcsClient(profile);
+
+            SendSmsRequest request = new SendSmsRequest();
+            request.setPhoneNumbers(phone.trim());
+            request.setSignName(smsProperties.getSignName().trim());
+            request.setTemplateCode(templateCode.trim());
+            if (StringUtils.hasText(templateParam)) {
+                request.setTemplateParam(templateParam);
+            }
+
+            SendSmsResponse response = client.getAcsResponse(request);
+            if (response == null || !"OK".equalsIgnoreCase(response.getCode())) {
+                String message = response != null ? response.getMessage() : "无响应";
+                log.warn("阿里云短信发送失败 phone={} template={} code={} message={}",
+                        phone, templateCode, response != null ? response.getCode() : null, message);
+                throw new BusinessException("短信发送失败: " + message);
+            }
+            log.info("阿里云短信发送成功 phone={} template={} bizId={}", phone, templateCode, response.getBizId());
+        } catch (BusinessException ex) {
+            throw ex;
+        } catch (Exception ex) {
+            log.warn("阿里云短信发送异常 phone={} template={}: {}", phone, templateCode, ex.getMessage());
+            throw new BusinessException("短信发送失败: " + ex.getMessage());
+        }
+    }
+
+    private void ensureCredentialsConfigured() {
+        if (!StringUtils.hasText(regionId) || !StringUtils.hasText(accessKeyId) || !StringUtils.hasText(accessKeySecret)) {
+            throw new BusinessException("阿里云短信密钥未配置,请在 Nacos 配置 sms.ali.region-id / access-key-id / access-key-secret");
+        }
+    }
+}

+ 110 - 27
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppArchiveServiceImpl.java

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ruoyi.file.domain.FilesUpload;
 import com.ruoyi.file.mapper.FilesMapper;
 import com.usky.common.core.bean.CommonPage;
+import com.usky.common.core.exception.BusinessException;
 import com.usky.common.security.utils.SecurityUtils;
 import com.usky.vpp.domain.VppFileArchive;
 import com.usky.vpp.mapper.VppFileArchiveMapper;
@@ -13,17 +14,19 @@ import com.usky.vpp.service.VppArchiveService;
 import com.usky.vpp.service.vo.VppFileArchiveRequestVO;
 import com.usky.vpp.service.vo.VppFileArchiveResponseVO;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
 import org.springframework.stereotype.Service;
 import org.springframework.web.client.RestTemplate;
 
 import javax.servlet.ServletOutputStream;
 import javax.servlet.http.HttpServletResponse;
-import java.io.ByteArrayOutputStream;
+import java.io.BufferedOutputStream;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
@@ -31,9 +34,16 @@ import java.nio.charset.StandardCharsets;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 
@@ -48,6 +58,37 @@ public class VppArchiveServiceImpl implements VppArchiveService {
     @Autowired
     private VppFileArchiveMapper vppFileArchiveMapper;
 
+    /**
+     * 连接池化的 RestTemplate,避免每次创建新连接
+     */
+    private final RestTemplate restTemplate;
+
+    /**
+     * 并行下载线程池
+     */
+    private static final ExecutorService DOWNLOAD_EXECUTOR = new ThreadPoolExecutor(
+            4, 8, 60L, TimeUnit.SECONDS,
+            new LinkedBlockingQueue<>(200),
+            r -> {
+                Thread t = new Thread(r, "archive-download-");
+                t.setDaemon(true);
+                return t;
+            },
+            new ThreadPoolExecutor.CallerRunsPolicy()
+    );
+
+    {
+        // 配置 HttpClient 连接池(最大连接数、超时时间)
+        CloseableHttpClient httpClient = HttpClientBuilder.create()
+                .setMaxConnTotal(50)
+                .setMaxConnPerRoute(20)
+                .build();
+        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
+        factory.setConnectTimeout(10_000);
+        factory.setReadTimeout(30_000);
+        this.restTemplate = new RestTemplate(factory);
+    }
+
     @Override
     public Boolean create(VppFileArchiveRequestVO vo) {
         String username = SecurityUtils.getUsername();
@@ -82,12 +123,13 @@ public class VppArchiveServiceImpl implements VppArchiveService {
     }
 
     @Override
-    public CommonPage<VppFileArchiveResponseVO> page(String archiveName, Integer archiveType, Long siteId, String bizType, Long bizId, Integer pageNum, Integer pageSize) {
+    public CommonPage<VppFileArchiveResponseVO> page(Long id, String archiveName, Integer archiveType, Long siteId, String bizType, Long bizId, Integer pageNum, Integer pageSize) {
         IPage<VppFileArchive> page = new Page<>(pageNum, pageSize);
 
         LambdaQueryWrapper<VppFileArchive> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.eq(VppFileArchive::getDeleteFlag, 0)
                 .eq(VppFileArchive::getTenantId, SecurityUtils.getTenantId())
+                .eq(id != null, VppFileArchive::getId, id)
                 .eq(siteId != null, VppFileArchive::getSiteId, siteId)
                 .eq(archiveType != null, VppFileArchive::getArchiveType, archiveType)
                 .eq(StringUtils.isNotBlank(bizType), VppFileArchive::getBizType, bizType)
@@ -126,7 +168,7 @@ public class VppArchiveServiceImpl implements VppArchiveService {
     @Override
     public Boolean upload(VppFileArchiveRequestVO vo) {
         if (vo.getId() == null) {
-            throw new RuntimeException("主键ID不能为空!");
+            throw new BusinessException("主键ID不能为空!");
         }
 
         LambdaQueryWrapper<VppFileArchive> queryWrapper = new LambdaQueryWrapper<>();
@@ -136,7 +178,7 @@ public class VppArchiveServiceImpl implements VppArchiveService {
         VppFileArchive existingRecord = vppFileArchiveMapper.selectOne(queryWrapper);
 
         if (existingRecord == null) {
-            throw new RuntimeException("档案记录不存在或已被删除!");
+            throw new BusinessException("档案记录不存在或已被删除!");
         }
 
         // 根据 name + url 关联 FilesUpload 表,同步更新文件信息
@@ -153,7 +195,7 @@ public class VppArchiveServiceImpl implements VppArchiveService {
                 existingRecord.setFileType(filesUpload.getType());
                 existingRecord.setFileSize(filesUpload.getSize());
             } else {
-                throw new RuntimeException("未找到匹配的文件记录,请确认文件名与路径是否正确!");
+                throw new BusinessException("未找到匹配的文件记录,请确认文件名与路径是否正确!");
             }
         }
 
@@ -223,6 +265,25 @@ public class VppArchiveServiceImpl implements VppArchiveService {
             return;
         }
 
+        // ========== 预解析文件名(去重处理,必须在并行下载之前完成)==========
+        List<DownloadTask> tasks = new ArrayList<>();
+        Set<String> usedNames = new HashSet<>();
+        for (VppFileArchive archive : archives) {
+            String fileUrl = archive.getFileUrl();
+            String fileName = archive.getFileName();
+            if (StringUtils.isBlank(fileUrl) || StringUtils.isBlank(fileName)) {
+                continue;
+            }
+            String zipEntryName = resolveDuplicateName(fileName, usedNames);
+            usedNames.add(zipEntryName);
+            tasks.add(new DownloadTask(zipEntryName, fileUrl));
+        }
+
+        if (tasks.isEmpty()) {
+            writeErrorResponse(response, HttpStatus.BAD_REQUEST.value(), "没有可下载的文件!");
+            return;
+        }
+
         // ========== 构造 ZIP 文件名 ==========
         String zipFileName = "电子档案_"
                 + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))
@@ -233,36 +294,43 @@ public class VppArchiveServiceImpl implements VppArchiveService {
         response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
                 "attachment; filename=" + encodeFileName(zipFileName));
 
-        // ========== 生成 ZIP 并写入响应流 ==========
-        RestTemplate restTemplate = new RestTemplate();
-        Set<String> usedNames = new HashSet<>();
+        // ========== 并行下载所有文件 ==========
+        List<CompletableFuture<DownloadTask>> futures = tasks.stream()
+                .map(task -> CompletableFuture.supplyAsync(() -> {
+                    try {
+                        byte[] bytes = restTemplate.getForObject(task.fileUrl, byte[].class);
+                        task.fileBytes = bytes;
+                    } catch (Exception e) {
+                        task.error = e.getMessage();
+                    }
+                    return task;
+                }, DOWNLOAD_EXECUTOR))
+                .collect(Collectors.toList());
 
+        // ========== 生成 ZIP 并写入响应流 ==========
         try (ServletOutputStream sos = response.getOutputStream();
-             ZipOutputStream zos = new ZipOutputStream(sos)) {
-
-            for (VppFileArchive archive : archives) {
-                String fileUrl = archive.getFileUrl();
-                String fileName = archive.getFileName();
-
-                if (StringUtils.isBlank(fileUrl) || StringUtils.isBlank(fileName)) {
-                    continue;
-                }
-
-                String zipEntryName = resolveDuplicateName(fileName, usedNames);
-                usedNames.add(zipEntryName);
+             BufferedOutputStream bos = new BufferedOutputStream(sos, 8192);
+             ZipOutputStream zos = new ZipOutputStream(bos)) {
 
+            for (CompletableFuture<DownloadTask> future : futures) {
+                DownloadTask task = future.join(); // 阻塞等待当前文件下载完成
                 try {
-                    byte[] fileBytes = restTemplate.getForObject(fileUrl, byte[].class);
-                    if (fileBytes != null && fileBytes.length > 0) {
-                        ZipEntry zipEntry = new ZipEntry(zipEntryName);
+                    if (task.error != null) {
+                        // 下载失败,写入错误信息
+                        ZipEntry errorEntry = new ZipEntry(task.zipEntryName + ".error.txt");
+                        zos.putNextEntry(errorEntry);
+                        zos.write(("下载失败: " + task.error).getBytes(StandardCharsets.UTF_8));
+                        zos.closeEntry();
+                    } else if (task.fileBytes != null && task.fileBytes.length > 0) {
+                        ZipEntry zipEntry = new ZipEntry(task.zipEntryName);
                         zos.putNextEntry(zipEntry);
-                        zos.write(fileBytes);
+                        zos.write(task.fileBytes);
                         zos.closeEntry();
                     }
                 } catch (Exception e) {
-                    ZipEntry errorEntry = new ZipEntry(zipEntryName + ".error.txt");
+                    ZipEntry errorEntry = new ZipEntry(task.zipEntryName + ".error.txt");
                     zos.putNextEntry(errorEntry);
-                    zos.write(("下载失败: " + e.getMessage()).getBytes(StandardCharsets.UTF_8));
+                    zos.write(("写入ZIP失败: " + e.getMessage()).getBytes(StandardCharsets.UTF_8));
                     zos.closeEntry();
                 }
             }
@@ -277,6 +345,21 @@ public class VppArchiveServiceImpl implements VppArchiveService {
         }
     }
 
+    /**
+     * 下载任务内部类,承载文件名、URL、下载结果
+     */
+    private static class DownloadTask {
+        final String zipEntryName;
+        final String fileUrl;
+        volatile byte[] fileBytes;
+        volatile String error;
+
+        DownloadTask(String zipEntryName, String fileUrl) {
+            this.zipEntryName = zipEntryName;
+            this.fileUrl = fileUrl;
+        }
+    }
+
     /**
      * 处理 ZIP 内重名文件,自动添加编号后缀
      * @param fileName  原始文件名

+ 353 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppCapabilityEvalServiceImpl.java

@@ -0,0 +1,353 @@
+package com.usky.vpp.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.vpp.domain.VppDevice;
+import com.usky.vpp.domain.VppDrEvaluation;
+import com.usky.vpp.domain.VppDrEvent;
+import com.usky.vpp.domain.VppDrParticipation;
+import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.domain.VppSite;
+import com.usky.vpp.mapper.VppDeviceMapper;
+import com.usky.vpp.mapper.VppDrEvaluationMapper;
+import com.usky.vpp.mapper.VppDrEventMapper;
+import com.usky.vpp.mapper.VppDrParticipationMapper;
+import com.usky.vpp.mapper.VppResourcePointMapper;
+import com.usky.vpp.mapper.VppSiteMapper;
+import com.usky.vpp.constant.VppTsdbConstants;
+import com.usky.vpp.service.VppCapabilityEvalService;
+import com.usky.vpp.service.VppTsdbQueryService;
+import com.usky.vpp.service.vo.CapabilityCategoryStatVO;
+import com.usky.vpp.service.vo.CapabilityEvalSummaryVO;
+import com.usky.vpp.service.vo.CapabilityHistoryVO;
+import com.usky.vpp.service.vo.CapabilityLoadCurveVO;
+import com.usky.vpp.service.vo.CapabilityLoadPointVO;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppCapabilityEvalHelper;
+import com.usky.vpp.util.VppPageHelper;
+import com.usky.vpp.util.VppSiteResourceHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+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.TreeMap;
+import java.util.stream.Collectors;
+
+@Service
+public class VppCapabilityEvalServiceImpl implements VppCapabilityEvalService {
+
+    private static final int PARTICIPATE_STATUS_ACCEPT = 1;
+    private static final int BASELINE_DAYS_AGO = 7;
+    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+    @Autowired
+    private VppResourcePointMapper resourcePointMapper;
+    @Autowired
+    private VppSiteMapper siteMapper;
+    @Autowired
+    private VppDeviceMapper deviceMapper;
+    @Autowired
+    private VppDrEventMapper eventMapper;
+    @Autowired
+    private VppDrParticipationMapper participationMapper;
+    @Autowired
+    private VppDrEvaluationMapper evaluationMapper;
+    @Autowired
+    private VppSiteResourceHelper siteResourceHelper;
+    @Autowired
+    private VppTsdbQueryService vppTsdbQueryService;
+
+    @Override
+    public CapabilityEvalSummaryVO getSummary(Long siteId) {
+        VppSite site = resolveSite(siteId);
+        List<VppResourcePoint> resources = listResources(siteId);
+        Map<Long, VppSite> siteMap = siteResourceHelper.loadSiteMap(resources);
+        List<VppDevice> devices = loadDevices(resources);
+
+        CapabilityEvalSummaryVO summary = new CapabilityEvalSummaryVO();
+        summary.setSiteId(siteId);
+        summary.setSiteName(site != null ? site.getSiteName() : "全部站点");
+        summary.setTotal(buildCategoryStat(VppCapabilityEvalHelper.CATEGORY_TOTAL, resources, siteMap));
+        summary.setLoad(buildCategoryStat(VppCapabilityEvalHelper.CATEGORY_LOAD, resources, siteMap));
+        summary.setStorage(buildCategoryStat(VppCapabilityEvalHelper.CATEGORY_STORAGE, resources, siteMap));
+        summary.setGeneration(buildCategoryStat(VppCapabilityEvalHelper.CATEGORY_GENERATION, resources, siteMap));
+
+        BigDecimal maxUp = BigDecimal.ZERO;
+        BigDecimal maxDown = BigDecimal.ZERO;
+        for (VppResourcePoint resource : resources) {
+            if (resource.getIsControl() == null || resource.getIsControl() != 1) {
+                continue;
+            }
+            maxUp = maxUp.add(resource.getMaxUpKw() != null ? resource.getMaxUpKw() : BigDecimal.ZERO);
+            maxDown = maxDown.add(resource.getMinDownKw() != null ? resource.getMinDownKw() : BigDecimal.ZERO);
+        }
+        applyRealtimeAdjustCapacity(resources, devices, summary, maxUp, maxDown);
+        return summary;
+    }
+
+    private void applyRealtimeAdjustCapacity(List<VppResourcePoint> resources,
+                                             List<VppDevice> devices,
+                                             CapabilityEvalSummaryVO summary,
+                                             BigDecimal fallbackUp,
+                                             BigDecimal fallbackDown) {
+        List<String> deviceUuids = resolveDeviceUuids(resources, devices);
+        if (deviceUuids.isEmpty()) {
+            summary.setMaxUpKw(fallbackUp);
+            summary.setMaxDownKw(fallbackDown);
+            return;
+        }
+
+        List<String> metrics = new ArrayList<>();
+        metrics.addAll(VppTsdbConstants.UP_CAPACITY_METRICS);
+        metrics.addAll(VppTsdbConstants.DOWN_CAPACITY_METRICS);
+        Map<String, Map<String, BigDecimal>> latest = vppTsdbQueryService.queryLatestMetrics(deviceUuids, metrics);
+
+        BigDecimal realtimeUp = VppCapabilityEvalHelper.sumLatestMetric(latest, VppTsdbConstants.UP_CAPACITY_METRICS);
+        BigDecimal realtimeDown = VppCapabilityEvalHelper.sumLatestMetric(latest, VppTsdbConstants.DOWN_CAPACITY_METRICS);
+        summary.setMaxUpKw(realtimeUp.compareTo(BigDecimal.ZERO) > 0 ? realtimeUp : fallbackUp);
+        summary.setMaxDownKw(realtimeDown.compareTo(BigDecimal.ZERO) > 0 ? realtimeDown : fallbackDown);
+    }
+
+    @Override
+    public CapabilityLoadCurveVO getLoadCurve(Long siteId, String date) {
+        VppSite site = resolveSite(siteId);
+        LocalDate statDate = parseDate(date);
+        List<VppResourcePoint> resources = listResources(siteId);
+        List<VppDevice> devices = loadDevices(resources);
+
+        CapabilityLoadCurveVO curve = new CapabilityLoadCurveVO();
+        curve.setSiteId(siteId);
+        curve.setSiteName(site != null ? site.getSiteName() : "全部站点");
+        curve.setDate(statDate.format(DATE_FMT));
+        LoadCurveBuildResult buildResult = buildLoadCurvePoints(resources, devices, siteId, statDate);
+        curve.setPoints(buildResult.points);
+        curve.setDataSource(buildResult.dataSource);
+        return curve;
+    }
+
+    private static class LoadCurveBuildResult {
+        private final List<CapabilityLoadPointVO> points;
+        private final String dataSource;
+
+        private LoadCurveBuildResult(List<CapabilityLoadPointVO> points, String dataSource) {
+            this.points = points;
+            this.dataSource = dataSource;
+        }
+    }
+
+    private LoadCurveBuildResult buildLoadCurvePoints(List<VppResourcePoint> resources,
+                                                      List<VppDevice> devices,
+                                                      Long siteId,
+                                                      LocalDate statDate) {
+        List<String> deviceUuids = resolveDeviceUuids(resources, devices);
+        if (!deviceUuids.isEmpty()) {
+            LocalDateTime dayStart = statDate.atStartOfDay();
+            LocalDateTime dayEnd = statDate.atTime(23, 59, 59);
+            LocalDate baselineDate = statDate.minusDays(BASELINE_DAYS_AGO);
+            LocalDateTime baselineStart = baselineDate.atStartOfDay();
+            LocalDateTime baselineEnd = baselineDate.atTime(23, 59, 59);
+
+            List<String> powerMetrics = new ArrayList<>(VppTsdbConstants.ACTIVE_POWER_METRICS);
+            Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> actualData =
+                    vppTsdbQueryService.queryDeviceMetricHistory(deviceUuids, dayStart, dayEnd, powerMetrics);
+            if (VppCapabilityEvalHelper.hasTsdbPowerData(actualData, powerMetrics)) {
+                Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> baselineData =
+                        vppTsdbQueryService.queryDeviceMetricHistory(deviceUuids, baselineStart, baselineEnd, powerMetrics);
+                return new LoadCurveBuildResult(
+                        VppCapabilityEvalHelper.buildLoadCurveFromTsdb(actualData, baselineData, statDate, powerMetrics),
+                        "tsdb");
+            }
+        }
+        return new LoadCurveBuildResult(
+                VppCapabilityEvalHelper.buildLoadCurve(resources, devices, siteId, statDate),
+                "mock");
+    }
+
+    @Override
+    public CommonPage<CapabilityHistoryVO> pageHistory(Long siteId, Map<String, Object> params) {
+        resolveSite(siteId);
+        Set<Long> resourceIds = listResources(siteId).stream()
+                .map(VppResourcePoint::getId)
+                .collect(Collectors.toSet());
+        if (resourceIds.isEmpty()) {
+            return emptyPage(params);
+        }
+
+        List<VppDrParticipation> participations = participationMapper.selectList(
+                new LambdaQueryWrapper<VppDrParticipation>()
+                        .in(VppDrParticipation::getResourceId, resourceIds)
+                        .eq(VppDrParticipation::getParticipateStatus, PARTICIPATE_STATUS_ACCEPT)
+                        .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        Set<Long> eventIds = participations.stream()
+                .map(VppDrParticipation::getEventId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        if (eventIds.isEmpty()) {
+            return emptyPage(params);
+        }
+
+        List<VppDrEvent> events = eventMapper.selectBatchIds(eventIds).stream()
+                .filter(e -> !VppAuditHelper.isDeleted(e.getDeleteFlag()))
+                .sorted(Comparator.comparing(VppDrEvent::getStartTime, Comparator.nullsLast(Comparator.reverseOrder())))
+                .collect(Collectors.toList());
+
+        Map<Long, VppDrEvaluation> evaluationMap = 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));
+
+        List<CapabilityHistoryVO> records = events.stream()
+                .map(event -> toHistoryVo(event, evaluationMap.get(event.getId())))
+                .collect(Collectors.toList());
+
+        long current = VppPageHelper.of(params).getCurrent();
+        long size = VppPageHelper.of(params).getSize();
+        int from = (int) Math.min((current - 1) * size, records.size());
+        int to = (int) Math.min(from + size, records.size());
+        List<CapabilityHistoryVO> pageRecords = from >= records.size()
+                ? Collections.emptyList() : records.subList(from, to);
+        return new CommonPage<>(pageRecords, (long) records.size(), current, size);
+    }
+
+    private CapabilityCategoryStatVO buildCategoryStat(String category,
+                                                       List<VppResourcePoint> resources,
+                                                       Map<Long, VppSite> siteMap) {
+        CapabilityCategoryStatVO stat = new CapabilityCategoryStatVO();
+        stat.setCategory(category);
+        stat.setCategoryLabel(VppCapabilityEvalHelper.categoryLabel(category));
+        stat.setCustomerCount(0);
+        stat.setCapacityKw(BigDecimal.ZERO);
+
+        Set<Long> customerIds = new HashSet<>();
+        for (VppResourcePoint resource : resources) {
+            if (!matchesCategory(category, resource.getResourceType())) {
+                continue;
+            }
+            BigDecimal cap = resource.getCapacityKw() != null ? resource.getCapacityKw() : BigDecimal.ZERO;
+            stat.setCapacityKw(stat.getCapacityKw().add(cap));
+            Long customerId = siteResourceHelper.resolveCustomerId(resource, siteMap);
+            if (customerId != null) {
+                customerIds.add(customerId);
+            }
+        }
+        stat.setCustomerCount(customerIds.size());
+        return stat;
+    }
+
+    private boolean matchesCategory(String category, String resourceType) {
+        if (VppCapabilityEvalHelper.CATEGORY_TOTAL.equals(category)) {
+            return true;
+        }
+        return category.equals(VppCapabilityEvalHelper.resolveCategory(resourceType));
+    }
+
+    private CapabilityHistoryVO toHistoryVo(VppDrEvent event, VppDrEvaluation evaluation) {
+        CapabilityHistoryVO vo = new CapabilityHistoryVO();
+        vo.setEventId(event.getId());
+        vo.setEventCode(event.getEventId());
+        vo.setEventName(event.getEventName());
+        vo.setEventType(event.getEventType());
+        vo.setEventTypeLabel(VppCapabilityEvalHelper.eventTypeLabel(event.getEventType()));
+        vo.setResponseType(event.getResponseType());
+        vo.setResponseTypeLabel(VppCapabilityEvalHelper.responseTypeLabel(event.getResponseType()));
+        vo.setStartTime(event.getStartTime());
+        vo.setEndTime(event.getEndTime());
+        vo.setTargetCapacityKw(event.getClearedCapacityKw() != null
+                ? event.getClearedCapacityKw() : event.getTargetCapacityKw());
+        if (evaluation != null) {
+            vo.setActualCapacityKw(evaluation.getActualCapacityKw());
+            vo.setQualifiedRate(evaluation.getQualifiedRate());
+        }
+        return vo;
+    }
+
+    private List<VppResourcePoint> listResources(Long siteId) {
+        LambdaQueryWrapper<VppResourcePoint> wrapper = new LambdaQueryWrapper<VppResourcePoint>()
+                .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+        if (siteId != null) {
+            wrapper.eq(VppResourcePoint::getSiteId, siteId);
+        }
+        return resourcePointMapper.selectList(wrapper);
+    }
+
+    private List<VppDevice> loadDevices(List<VppResourcePoint> resources) {
+        Set<Long> deviceIds = resources.stream()
+                .map(VppResourcePoint::getDeviceId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        if (deviceIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return deviceMapper.selectBatchIds(deviceIds).stream()
+                .filter(d -> !VppAuditHelper.isDeleted(d.getDeleteFlag()))
+                .collect(Collectors.toList());
+    }
+
+    private List<String> resolveDeviceUuids(List<VppResourcePoint> resources, List<VppDevice> devices) {
+        if (resources == null || resources.isEmpty() || devices == null || devices.isEmpty()) {
+            return Collections.emptyList();
+        }
+        Map<Long, VppDevice> deviceMap = devices.stream()
+                .collect(Collectors.toMap(VppDevice::getId, d -> d, (a, b) -> a));
+        Set<String> uuids = new LinkedHashSet<>();
+        for (VppResourcePoint resource : resources) {
+            if (resource.getDeviceId() == null) {
+                continue;
+            }
+            VppDevice device = deviceMap.get(resource.getDeviceId());
+            if (device == null || !StringUtils.hasText(device.getDeviceUuid())) {
+                continue;
+            }
+            if (device.getCommStatus() != null && device.getCommStatus() == 0) {
+                continue;
+            }
+            uuids.add(device.getDeviceUuid().trim());
+        }
+        return new ArrayList<>(uuids);
+    }
+
+    private VppSite resolveSite(Long siteId) {
+        if (siteId == null) {
+            return null;
+        }
+        VppSite site = siteMapper.selectById(siteId);
+        if (site == null || VppAuditHelper.isDeleted(site.getDeleteFlag())) {
+            throw new BusinessException("站点不存在");
+        }
+        return site;
+    }
+
+    private LocalDate parseDate(String date) {
+        if (!StringUtils.hasText(date)) {
+            return LocalDate.now();
+        }
+        try {
+            return LocalDate.parse(date.trim(), DATE_FMT);
+        } catch (DateTimeParseException ex) {
+            throw new BusinessException("日期格式无效,请使用 yyyy-MM-dd");
+        }
+    }
+
+    private CommonPage<CapabilityHistoryVO> emptyPage(Map<String, Object> params) {
+        long current = VppPageHelper.of(params).getCurrent();
+        long size = VppPageHelper.of(params).getSize();
+        return new CommonPage<>(new ArrayList<>(), 0L, current, size);
+    }
+}

+ 325 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppContractServiceImpl.java

@@ -1,11 +1,335 @@
 package com.usky.vpp.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+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.VppContract;
+import com.usky.vpp.domain.VppContractTemplate;
+import com.usky.vpp.domain.VppCustomer;
+import com.usky.vpp.mapper.VppContractMapper;
+import com.usky.vpp.mapper.VppContractTemplateMapper;
+import com.usky.vpp.mapper.VppCustomerMapper;
 import com.usky.vpp.service.VppContractService;
+import com.usky.vpp.service.vo.VppContractRequestVO;
+import com.usky.vpp.service.vo.VppContractResponseVO;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppFieldValidator;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
 import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+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;
 
 /**
- * VppContractService 默认实现(待按业务完善)
+ * VppContractService 默认实现
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/4
  */
 @Service
 public class VppContractServiceImpl implements VppContractService {
+
+    @Autowired
+    private VppContractMapper vppContractMapper;
+    @Autowired
+    private VppContractTemplateMapper vppContractTemplateMapper;
+    @Autowired
+    private VppCustomerMapper vppCustomerMapper;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    @Override
+    public Boolean create(VppContractRequestVO vo) {
+        // ========== 根据 templateId 查询模板编码并生成合同编号 ==========
+        VppContractTemplate template = getTemplateById(vo.getTemplateId());
+        String contractNo = VppFieldValidator.generateContractNo(template.getTemplateCode());
+
+        // ========== 合同编号全租户唯一性校验(极小概率冲突) ==========
+        checkContractNoUnique(contractNo, null);
+
+        VppContract contract = new VppContract();
+        contract.setContractNo(contractNo);
+        contract.setCustomerId(vo.getCustomerId());
+        contract.setTemplateId(vo.getTemplateId());
+        contract.setContractType(vo.getContractType());
+        contract.setContractName(vo.getContractName());
+        contract.setContractStatus(vo.getContractStatus());
+        contract.setSignDate(vo.getSignDate());
+        contract.setEffectiveDate(vo.getEffectiveDate());
+        contract.setExpireDate(vo.getExpireDate());
+        contract.setFileUrl(vo.getFileUrl());
+        contract.setShareRatio(vo.getShareRatio());
+        contract.setPriceJson(VppFieldValidator.validateJson(vo.getPriceJson(), "价格定义"));
+        contract.setAccountInfoJson(VppFieldValidator.validateJson(vo.getAccountInfoJson(), "账户信息"));
+        contract.setRemark(vo.getRemark());
+
+        VppAuditHelper.fillCreate(contract);
+
+        int insert = vppContractMapper.insert(contract);
+        return insert > 0;
+    }
+
+    @Override
+    public Boolean update(VppContractRequestVO vo) {
+        if (vo.getId() == null) {
+            throw new BusinessException("主键ID不能为空!");
+        }
+
+        LambdaQueryWrapper<VppContract> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(VppContract::getId, vo.getId())
+                .eq(VppContract::getDeleteFlag, 0);
+        VppContract existingRecord = vppContractMapper.selectOne(queryWrapper);
+
+        if (existingRecord == null) {
+            throw new BusinessException("合同不存在或已被删除!");
+        }
+
+        // 仅更新非空字段
+        if (vo.getCustomerId() != null) {
+            existingRecord.setCustomerId(vo.getCustomerId());
+        }
+        if (vo.getTemplateId() != null) {
+            existingRecord.setTemplateId(vo.getTemplateId());
+        }
+        if (vo.getContractType() != null) {
+            existingRecord.setContractType(vo.getContractType());
+        }
+        if (StringUtils.isNotBlank(vo.getContractName())) {
+            existingRecord.setContractName(vo.getContractName());
+        }
+        if (vo.getContractStatus() != null) {
+            existingRecord.setContractStatus(vo.getContractStatus());
+        }
+        if (vo.getSignDate() != null) {
+            existingRecord.setSignDate(vo.getSignDate());
+        }
+        if (vo.getEffectiveDate() != null) {
+            existingRecord.setEffectiveDate(vo.getEffectiveDate());
+        }
+        if (vo.getExpireDate() != null) {
+            existingRecord.setExpireDate(vo.getExpireDate());
+        }
+        if (StringUtils.isNotBlank(vo.getFileUrl())) {
+            existingRecord.setFileUrl(vo.getFileUrl());
+        }
+        if (vo.getShareRatio() != null) {
+            existingRecord.setShareRatio(vo.getShareRatio());
+        }
+        if (vo.getPriceJson() != null) {
+            existingRecord.setPriceJson(VppFieldValidator.validateJson(vo.getPriceJson(), "价格定义"));
+        }
+        if (vo.getAccountInfoJson() != null) {
+            existingRecord.setAccountInfoJson(VppFieldValidator.validateJson(vo.getAccountInfoJson(), "账户信息"));
+        }
+        if (vo.getRemark() != null) {
+            existingRecord.setRemark(vo.getRemark());
+        }
+
+        VppAuditHelper.fillUpdate(existingRecord);
+
+        int result = vppContractMapper.updateById(existingRecord);
+        return result > 0;
+    }
+
+    @Override
+    public CommonPage<VppContractResponseVO> page(Long id, LocalDate signDateStart, LocalDate signDateEnd,
+                                                   Long templateId, Integer contractType, Long customerId,
+                                                   Integer contractStatus, Integer pageNum, Integer pageSize) {
+        IPage<VppContract> page = new Page<>(pageNum, pageSize);
+
+        LambdaQueryWrapper<VppContract> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(VppContract::getDeleteFlag, 0)
+                .eq(id != null, VppContract::getId, id)
+                .ge(signDateStart != null, VppContract::getSignDate, signDateStart)
+                .le(signDateEnd != null, VppContract::getSignDate, signDateEnd)
+                .eq(templateId != null, VppContract::getTemplateId, templateId)
+                .eq(contractType != null, VppContract::getContractType, contractType)
+                .eq(customerId != null, VppContract::getCustomerId, customerId)
+                .eq(contractStatus != null, VppContract::getContractStatus, contractStatus)
+                .orderByDesc(VppContract::getCreateTime);
+        page = vppContractMapper.selectPage(page, queryWrapper);
+
+        List<VppContract> records = page.getRecords();
+        if (records == null || records.isEmpty()) {
+            return new CommonPage<>(Collections.emptyList(), page.getTotal(), pageSize, pageNum);
+        }
+
+        // ========== 批量反查客户名称和模板名称 ==========
+        Set<Long> customerIds = records.stream().map(VppContract::getCustomerId).filter(Objects::nonNull).collect(Collectors.toSet());
+        Set<Long> templateIds = records.stream().map(VppContract::getTemplateId).filter(Objects::nonNull).collect(Collectors.toSet());
+
+        final Map<Long, String> customerNameMap = customerIds.isEmpty() ? Collections.emptyMap()
+                : vppCustomerMapper.selectBatchIds(customerIds).stream()
+                        .collect(Collectors.toMap(VppCustomer::getId, VppCustomer::getCustomerName, (a, b) -> a));
+
+        final Map<Long, String> templateNameMap = templateIds.isEmpty() ? Collections.emptyMap()
+                : vppContractTemplateMapper.selectBatchIds(templateIds).stream()
+                        .collect(Collectors.toMap(VppContractTemplate::getId, VppContractTemplate::getTemplateName, (a, b) -> a));
+
+        // ========== 组装响应VO ==========
+        List<VppContractResponseVO> list = records.stream().map(entity -> {
+            VppContractResponseVO vo = new VppContractResponseVO();
+            vo.setId(entity.getId());
+            vo.setContractNo(entity.getContractNo());
+            vo.setCustomerId(entity.getCustomerId());
+            vo.setCustomerName(customerNameMap.get(entity.getCustomerId()));
+            vo.setTemplateId(entity.getTemplateId());
+            vo.setTemplateName(templateNameMap.get(entity.getTemplateId()));
+            vo.setContractType(entity.getContractType());
+            vo.setContractName(entity.getContractName());
+            vo.setContractStatus(entity.getContractStatus());
+            vo.setSignDate(entity.getSignDate());
+            vo.setEffectiveDate(entity.getEffectiveDate());
+            vo.setExpireDate(entity.getExpireDate());
+            vo.setFileUrl(entity.getFileUrl());
+            vo.setShareRatio(entity.getShareRatio());
+            vo.setPriceJson(entity.getPriceJson());
+            vo.setAccountInfoJson(entity.getAccountInfoJson());
+            vo.setRemark(entity.getRemark());
+            vo.setTenantId(entity.getTenantId());
+            vo.setCreateTime(entity.getCreateTime());
+            vo.setUpdateTime(entity.getUpdateTime());
+            vo.setCreatedBy(entity.getCreatedBy());
+            vo.setUpdatedBy(entity.getUpdatedBy());
+            return vo;
+        }).collect(Collectors.toList());
+
+        return new CommonPage<>(list, page.getTotal(), pageSize, pageNum);
+    }
+
+    @Override
+    public Boolean delete(Long id) {
+        LambdaQueryWrapper<VppContract> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(VppContract::getId, id)
+                .eq(VppContract::getDeleteFlag, 0);
+        VppContract contract = vppContractMapper.selectOne(queryWrapper);
+
+        if (contract == null) {
+            throw new BusinessException("合同不存在或已被删除!");
+        }
+
+        VppAuditHelper.fillSoftDelete(contract);
+
+        int i = vppContractMapper.updateById(contract);
+        return i > 0;
+    }
+
+    @Override
+    public void download(Long id, HttpServletResponse response) {
+        LambdaQueryWrapper<VppContract> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(VppContract::getId, id)
+                .eq(VppContract::getDeleteFlag, 0);
+        VppContract contract = vppContractMapper.selectOne(queryWrapper);
+
+        if (contract == null) {
+            writeErrorResponse(response, HttpStatus.BAD_REQUEST.value(), "合同不存在或已被删除!");
+            return;
+        }
+
+        String fileUrl = contract.getFileUrl();
+        String fileName = contract.getContractName();
+        if (StringUtils.isBlank(fileUrl)) {
+            writeErrorResponse(response, HttpStatus.BAD_REQUEST.value(), "合同文件URL为空,无法下载!");
+            return;
+        }
+
+        // ========== 设置响应头 ==========
+        String downloadFileName = fileName;
+        int dotIndex = fileUrl.lastIndexOf('.');
+        if (dotIndex > 0 && !fileName.contains(".")) {
+            downloadFileName = fileName + fileUrl.substring(dotIndex);
+        }
+        response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+        response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
+                "attachment; filename=" + encodeFileName(downloadFileName));
+
+        // ========== 下载文件并写入响应流 ==========
+        try {
+            byte[] fileBytes = restTemplate.getForObject(fileUrl, byte[].class);
+            if (fileBytes == null || fileBytes.length == 0) {
+                writeErrorResponse(response, HttpStatus.NOT_FOUND.value(), "合同文件内容为空!");
+                return;
+            }
+            ServletOutputStream sos = response.getOutputStream();
+            sos.write(fileBytes);
+            sos.flush();
+        } catch (Exception e) {
+            if (!response.isCommitted()) {
+                writeErrorResponse(response, HttpStatus.INTERNAL_SERVER_ERROR.value(),
+                        "下载合同文件失败: " + e.getMessage());
+            }
+        }
+    }
+
+    // ========== 内部辅助方法 ==========
+
+    /**
+     * 根据模板ID查询模板信息
+     */
+    private VppContractTemplate getTemplateById(Long templateId) {
+        if (templateId == null) {
+            throw new BusinessException("模板ID不能为空!");
+        }
+        LambdaQueryWrapper<VppContractTemplate> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppContractTemplate::getId, templateId)
+                .eq(VppContractTemplate::getDeleteFlag, 0);
+        VppContractTemplate template = vppContractTemplateMapper.selectOne(wrapper);
+        if (template == null) {
+            throw new BusinessException("合同模板不存在或已被删除!");
+        }
+        return template;
+    }
+
+    /**
+     * 合同编号全租户唯一性校验
+     */
+    private void checkContractNoUnique(String contractNo, Long excludeId) {
+        LambdaQueryWrapper<VppContract> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppContract::getContractNo, contractNo)
+                .eq(VppContract::getDeleteFlag, 0)
+                .ne(excludeId != null, VppContract::getId, excludeId);
+        Integer count = vppContractMapper.selectCount(wrapper);
+        if (count != null && count > 0) {
+            throw new BusinessException("合同编号【" + contractNo + "】已存在,请重新提交!");
+        }
+    }
+
+    private void writeErrorResponse(HttpServletResponse response, int status, String message) {
+        response.setStatus(status);
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        try {
+            response.getWriter().write("{\"code\":\"" + status + "\",\"msg\":\"" + message + "\"}");
+        } catch (IOException e) {
+            // 忽略写入异常
+        }
+    }
+
+    private String encodeFileName(String fileName) {
+        try {
+            return URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replace("+", "%20");
+        } catch (UnsupportedEncodingException e) {
+            return fileName;
+        }
+    }
 }

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

@@ -0,0 +1,296 @@
+package com.usky.vpp.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.vpp.domain.VppContractTemplate;
+import com.usky.vpp.mapper.VppContractTemplateMapper;
+import com.usky.vpp.service.VppContractTemplateService;
+import com.usky.vpp.service.vo.VppContractTemplateRequestVO;
+import com.usky.vpp.service.vo.VppContractTemplateResponseVO;
+import com.usky.vpp.util.VppFieldValidator;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * VppContractTemplateService 默认实现
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/4
+ */
+@Service
+public class VppContractTemplateServiceImpl implements VppContractTemplateService {
+
+    @Autowired
+    private VppContractTemplateMapper vppContractTemplateMapper;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    @Override
+    public Boolean create(VppContractTemplateRequestVO vo) {
+        String username = SecurityUtils.getUsername();
+        Integer tenantId = SecurityUtils.getTenantId();
+
+        // ========== templateCode 格式校验 ==========
+        VppFieldValidator.validateTemplateCode(vo.getTemplateCode());
+
+        // ========== templateCode 全租户唯一性校验 ==========
+        checkTemplateCodeUnique(vo.getTemplateCode(), null, tenantId);
+
+        VppContractTemplate template = new VppContractTemplate();
+        template.setTemplateCode(vo.getTemplateCode());
+        template.setTemplateName(vo.getTemplateName());
+        template.setContractType(vo.getContractType());
+        template.setVersion("V1.0");
+        template.setFileUrl(vo.getFileUrl());
+        template.setVariablesJson(VppFieldValidator.validateJson(vo.getVariablesJson(), "占位符变量定义"));
+        template.setIsEnabled(vo.getIsEnabled() != null ? vo.getIsEnabled() : 1);
+        template.setCreateTime(LocalDateTime.now());
+        template.setCreatedBy(username);
+        template.setTenantId(tenantId);
+        template.setDeleteFlag(0);
+
+        int insert = vppContractTemplateMapper.insert(template);
+        return insert > 0;
+    }
+
+    @Override
+    public Boolean update(VppContractTemplateRequestVO vo) {
+        if (vo.getId() == null) {
+            throw new BusinessException("主键ID不能为空!");
+        }
+
+        LambdaQueryWrapper<VppContractTemplate> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(VppContractTemplate::getId, vo.getId())
+                .eq(VppContractTemplate::getTenantId, SecurityUtils.getTenantId())
+                .eq(VppContractTemplate::getDeleteFlag, 0);
+        VppContractTemplate existingRecord = vppContractTemplateMapper.selectOne(queryWrapper);
+
+        if (existingRecord == null) {
+            throw new BusinessException("合同模板不存在或已被删除!");
+        }
+
+        // 仅更新非空字段,追踪是否仅变更 isEnabled
+        boolean hasNonEnabledChange = false;
+
+        if (StringUtils.isNotBlank(vo.getTemplateCode())) {
+            // ========== templateCode 格式校验 ==========
+            VppFieldValidator.validateTemplateCode(vo.getTemplateCode());
+
+            // ========== 仅当与现有值不同时做唯一性校验 ==========
+            if (!vo.getTemplateCode().equals(existingRecord.getTemplateCode())) {
+                checkTemplateCodeUnique(vo.getTemplateCode(), vo.getId(), SecurityUtils.getTenantId());
+            }
+
+            existingRecord.setTemplateCode(vo.getTemplateCode());
+            hasNonEnabledChange = true;
+        }
+        if (StringUtils.isNotBlank(vo.getTemplateName())) {
+            existingRecord.setTemplateName(vo.getTemplateName());
+            hasNonEnabledChange = true;
+        }
+        if (vo.getContractType() != null) {
+            existingRecord.setContractType(vo.getContractType());
+            hasNonEnabledChange = true;
+        }
+        if (StringUtils.isNotBlank(vo.getFileUrl())) {
+            existingRecord.setFileUrl(vo.getFileUrl());
+            hasNonEnabledChange = true;
+        }
+        if (vo.getVariablesJson() != null) {
+            existingRecord.setVariablesJson(VppFieldValidator.validateJson(vo.getVariablesJson(), "占位符变量定义"));
+            hasNonEnabledChange = true;
+        }
+        // isEnabled 单独变更不触发版本号累加
+        if (vo.getIsEnabled() != null) {
+            existingRecord.setIsEnabled(vo.getIsEnabled());
+        }
+
+        // 版本号自动累加:仅当非 isEnabled 字段有变更时 +0.1
+        if (hasNonEnabledChange) {
+            String currentVersion = existingRecord.getVersion();
+            if (StringUtils.isNotBlank(currentVersion) && currentVersion.startsWith("V")) {
+                try {
+                    double versionNum = Double.parseDouble(currentVersion.substring(1));
+                    versionNum += 0.1;
+                    existingRecord.setVersion("V" + String.format("%.1f", versionNum));
+                } catch (NumberFormatException e) {
+                    existingRecord.setVersion(currentVersion + ".1");
+                }
+            }
+        }
+
+        existingRecord.setUpdateTime(LocalDateTime.now());
+        existingRecord.setUpdatedBy(SecurityUtils.getUsername());
+
+        int result = vppContractTemplateMapper.updateById(existingRecord);
+        return result > 0;
+    }
+
+    @Override
+    public CommonPage<VppContractTemplateResponseVO> page(Long id, String templateName, Integer contractType,
+                                                          Integer isEnabled, Integer pageNum, Integer pageSize) {
+        IPage<VppContractTemplate> page = new Page<>(pageNum, pageSize);
+
+        LambdaQueryWrapper<VppContractTemplate> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(VppContractTemplate::getDeleteFlag, 0)
+                .eq(VppContractTemplate::getTenantId, SecurityUtils.getTenantId())
+                .eq(id != null, VppContractTemplate::getId, id)
+                .eq(contractType != null, VppContractTemplate::getContractType, contractType)
+                .eq(isEnabled != null, VppContractTemplate::getIsEnabled, isEnabled)
+                .like(StringUtils.isNotBlank(templateName), VppContractTemplate::getTemplateName, templateName)
+                .orderByDesc(VppContractTemplate::getCreateTime);
+        page = vppContractTemplateMapper.selectPage(page, queryWrapper);
+
+        List<VppContractTemplateResponseVO> list = page.getRecords().stream().map(entity -> {
+            VppContractTemplateResponseVO responseVO = new VppContractTemplateResponseVO();
+            responseVO.setId(entity.getId());
+            responseVO.setTemplateCode(entity.getTemplateCode());
+            responseVO.setTemplateName(entity.getTemplateName());
+            responseVO.setContractType(entity.getContractType());
+            responseVO.setVersion(entity.getVersion());
+            responseVO.setFileUrl(entity.getFileUrl());
+            responseVO.setVariablesJson(entity.getVariablesJson());
+            responseVO.setIsEnabled(entity.getIsEnabled());
+            responseVO.setCreatedBy(entity.getCreatedBy());
+            responseVO.setCreateTime(entity.getCreateTime());
+            responseVO.setUpdatedBy(entity.getUpdatedBy());
+            responseVO.setUpdateTime(entity.getUpdateTime());
+            responseVO.setTenantId(entity.getTenantId());
+            return responseVO;
+        }).collect(Collectors.toList());
+
+        return new CommonPage<>(list, page.getTotal(), pageSize, pageNum);
+    }
+
+    @Override
+    public Boolean delete(Long id) {
+        LambdaQueryWrapper<VppContractTemplate> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(VppContractTemplate::getId, id)
+                .eq(VppContractTemplate::getTenantId, SecurityUtils.getTenantId());
+        VppContractTemplate template = vppContractTemplateMapper.selectOne(queryWrapper);
+
+        if (template == null) {
+            throw new BusinessException("合同模板不存在!");
+        }
+
+        template.setDeleteFlag(1);
+        template.setIsEnabled(0);
+        template.setDeletedAt(LocalDateTime.now());
+        template.setUpdatedBy(SecurityUtils.getUsername());
+        int i = vppContractTemplateMapper.updateById(template);
+        return i > 0;
+    }
+
+    @Override
+    public void download(Long id, HttpServletResponse response) {
+        // ========== 查询模板记录 ==========
+        LambdaQueryWrapper<VppContractTemplate> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(VppContractTemplate::getId, id)
+                .eq(VppContractTemplate::getDeleteFlag, 0)
+                .eq(VppContractTemplate::getTenantId, SecurityUtils.getTenantId());
+        VppContractTemplate template = vppContractTemplateMapper.selectOne(queryWrapper);
+
+        if (template == null) {
+            writeErrorResponse(response, HttpStatus.BAD_REQUEST.value(), "合同模板不存在或已被删除!");
+            return;
+        }
+
+        String fileUrl = template.getFileUrl();
+        String fileName = template.getTemplateName();
+        if (StringUtils.isBlank(fileUrl)) {
+            writeErrorResponse(response, HttpStatus.BAD_REQUEST.value(), "模板文件URL为空,无法下载!");
+            return;
+        }
+
+        // ========== 设置响应头 ==========
+        // 根据 fileUrl 推断原始文件名(优先使用模板名称 + 扩展名)
+        String downloadFileName = fileName;
+        int dotIndex = fileUrl.lastIndexOf('.');
+        if (dotIndex > 0 && !fileName.contains(".")) {
+            downloadFileName = fileName + fileUrl.substring(dotIndex);
+        }
+        response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+        response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
+                "attachment; filename=" + encodeFileName(downloadFileName));
+
+        // ========== 下载文件并写入响应流 ==========
+        try {
+            byte[] fileBytes = restTemplate.getForObject(fileUrl, byte[].class);
+            if (fileBytes == null || fileBytes.length == 0) {
+                writeErrorResponse(response, HttpStatus.NOT_FOUND.value(), "模板文件内容为空!");
+                return;
+            }
+            ServletOutputStream sos = response.getOutputStream();
+            sos.write(fileBytes);
+            sos.flush();
+        } catch (Exception e) {
+            if (!response.isCommitted()) {
+                writeErrorResponse(response, HttpStatus.INTERNAL_SERVER_ERROR.value(),
+                        "下载合同模板失败: " + e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * 向响应中写入 JSON 错误信息
+     */
+    private void writeErrorResponse(HttpServletResponse response, int status, String message) {
+        response.setStatus(status);
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        try {
+            response.getWriter().write("{\"code\":\"" + status + "\",\"msg\":\"" + message + "\"}");
+        } catch (IOException e) {
+            // 忽略写入异常
+        }
+    }
+
+    /**
+     * 编码文件名,兼容各浏览器
+     */
+    private String encodeFileName(String fileName) {
+        try {
+            return URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replace("+", "%20");
+        } catch (UnsupportedEncodingException e) {
+            return fileName;
+        }
+    }
+
+    /**
+     * 全租户唯一性校验:检查 templateCode 是否已被占用
+     *
+     * @param templateCode 模板编码
+     * @param excludeId    排除的记录ID(更新时传入当前记录ID,创建时传 null)
+     * @throws BusinessException 编码已被占用时抛出
+     */
+    private void checkTemplateCodeUnique(String templateCode, Long excludeId, Integer tenantId) {
+        LambdaQueryWrapper<VppContractTemplate> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppContractTemplate::getTemplateCode, templateCode)
+                .eq(VppContractTemplate::getDeleteFlag, 0)
+                .eq(VppContractTemplate::getTenantId, tenantId)
+                .ne(excludeId != null, VppContractTemplate::getId, excludeId);
+        Integer count = vppContractTemplateMapper.selectCount(wrapper);
+        if (count != null && count > 0) {
+            throw new BusinessException("模板编码【" + templateCode + "】已存在,请使用其他编码!");
+        }
+    }
+}

+ 1051 - 81
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppCustomerServiceImpl.java

@@ -1,40 +1,67 @@
 package com.usky.vpp.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.common.core.exception.BusinessException;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.vpp.domain.VppContract;
 import com.usky.vpp.domain.VppCustomer;
 import com.usky.vpp.domain.VppCustomerAccess;
 import com.usky.vpp.domain.VppCustomerContact;
+import com.usky.vpp.mapper.VppContractMapper;
 import com.usky.vpp.mapper.VppCustomerAccessMapper;
 import com.usky.vpp.mapper.VppCustomerContactMapper;
 import com.usky.vpp.mapper.VppCustomerMapper;
 import com.usky.vpp.service.VppCustomerService;
 import com.usky.vpp.service.vo.CustomerAccessAuditRequest;
-import com.usky.vpp.service.vo.CustomerAccessRequest;
+import com.usky.vpp.service.vo.CustomerAccessRequestVO;
+import com.usky.vpp.service.vo.CustomerAccessResponseVO;
 import com.usky.vpp.service.vo.CustomerContactRequest;
+import com.usky.vpp.service.vo.CustomerRequestVO;
+import com.usky.vpp.service.vo.CustomerResponseVO;
 import com.usky.vpp.util.VppAuditHelper;
-import com.usky.vpp.util.VppPageHelper;
+import com.usky.vpp.util.VppFieldValidator;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
-import org.springframework.util.StringUtils;
 
+import javax.servlet.http.HttpServletResponse;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
+import java.util.ArrayList;
+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;
 
 /**
  * 客户管理业务实现
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/6
  */
+@Slf4j
 @Service
 public class VppCustomerServiceImpl implements VppCustomerService {
 
     private static final int ACCESS_PENDING = 0;
     private static final int ACCESS_PASSED = 1;
     private static final int ACCESS_REJECTED = 2;
-    private static final int LIFECYCLE_PENDING = 1;
-    private static final int LIFECYCLE_ADMITTED = 2;
+    private static final int LIFECYCLE_ADMITTED = 1;
 
     @Autowired
     private VppCustomerMapper customerMapper;
@@ -42,11 +69,26 @@ public class VppCustomerServiceImpl implements VppCustomerService {
     private VppCustomerAccessMapper accessMapper;
     @Autowired
     private VppCustomerContactMapper contactMapper;
+    @Autowired
+    private VppContractMapper contractMapper;
+
+    // ==================== 准入管理 ====================
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public VppCustomerAccess submitAccess(CustomerAccessRequest request) {
+    public Boolean submitAccess(CustomerAccessRequestVO request) {
         validateAccessRequest(request);
+
+        // 校验同租户下 accountNo 唯一
+        LambdaQueryWrapper<VppCustomerAccess> dupWrapper = new LambdaQueryWrapper<>();
+        dupWrapper.eq(VppCustomerAccess::getAccountNo, request.getAccountNo())
+                .eq(VppCustomerAccess::getTenantId, SecurityUtils.getTenantId())
+                .eq(VppCustomerAccess::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+        Integer accessCount = accessMapper.selectCount(dupWrapper);
+        if (accessCount != null && accessCount > 0) {
+            throw new BusinessException("该电力户号已存在准入申请,请勿重复提交!");
+        }
+
         VppCustomerAccess access = new VppCustomerAccess();
         access.setApplyNo(VppAuditHelper.nextApplyNo());
         access.setAccountNo(request.getAccountNo());
@@ -58,112 +100,797 @@ public class VppCustomerServiceImpl implements VppCustomerService {
         access.setCreditStatus(request.getCreditStatus());
         access.setAccessStatus(ACCESS_PENDING);
         access.setApplyAt(LocalDateTime.now());
+        // 暂存联系人信息(审核通过时写入联系人表)
+        access.setContactName(request.getContactName());
+        access.setContactPhone(request.getContactPhone());
+        access.setContactEmail(request.getContactEmail());
+        access.setSigningDate(request.getSigningDate());
         VppAuditHelper.fillCreate(access);
-        accessMapper.insert(access);
-        return access;
+
+        int insert = accessMapper.insert(access);
+        return insert > 0;
     }
 
     @Override
-    public CommonPage<VppCustomerAccess> pageAccess(Map<String, Object> params) {
-        Page<VppCustomerAccess> page = VppPageHelper.of(params);
+    public CommonPage<CustomerAccessResponseVO> pageAccess(Long id, Integer accessStatus, String customerName,
+                                                           LocalDateTime startTime, LocalDateTime endTime,
+                                                           Integer pageNum, Integer pageSize) {
+        IPage<VppCustomerAccess> page = new Page<>(pageNum, pageSize);
+
         LambdaQueryWrapper<VppCustomerAccess> wrapper = new LambdaQueryWrapper<VppCustomerAccess>()
+                .eq(VppCustomerAccess::getTenantId, SecurityUtils.getTenantId())
                 .eq(VppCustomerAccess::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(id != null, VppCustomerAccess::getId, id)
+                .eq(accessStatus != null, VppCustomerAccess::getAccessStatus, accessStatus)
+                .like(StringUtils.isNotBlank(customerName), VppCustomerAccess::getCustomerName, customerName)
+                .ge(startTime != null, VppCustomerAccess::getApplyAt, startTime)
+                .le(endTime != null, VppCustomerAccess::getApplyAt, endTime)
                 .orderByDesc(VppCustomerAccess::getApplyAt);
-        if (params != null && params.get("accessStatus") != null) {
-            wrapper.eq(VppCustomerAccess::getAccessStatus, Integer.parseInt(params.get("accessStatus").toString()));
+
+        page = accessMapper.selectPage(page, wrapper);
+
+        List<VppCustomerAccess> records = page.getRecords();
+        if (records == null || records.isEmpty()) {
+            return new CommonPage<>(Collections.emptyList(), page.getTotal(), pageSize, pageNum);
         }
-        Page<VppCustomerAccess> result = accessMapper.selectPage(page, wrapper);
-        return toCommonPage(result);
+
+        List<CustomerAccessResponseVO> list = records.stream().map(entity -> {
+            CustomerAccessResponseVO vo = new CustomerAccessResponseVO();
+            BeanUtils.copyProperties(entity, vo);
+            return vo;
+        }).collect(Collectors.toList());
+
+        return new CommonPage<>(list, page.getTotal(), pageSize, pageNum);
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void auditAccess(Long id, CustomerAccessAuditRequest request) {
-        VppCustomerAccess access = accessMapper.selectById(id);
-        if (access == null || VppAuditHelper.isDeleted(access.getDeleteFlag())) {
-            throw new BusinessException("准入申请不存在");
+    public void auditAccess(CustomerAccessAuditRequest request) {
+        if (request.getId() == null) {
+            throw new BusinessException("准入申请ID不能为空!");
         }
-        if (access.getAccessStatus() != ACCESS_PENDING) {
-            throw new BusinessException("当前状态不允许审核");
+        if (request.getResult() == null) {
+            throw new BusinessException("审核结果不能为空!");
         }
-        if (request.getPassed() == null) {
-            throw new BusinessException("审核结果不能为空");
+        Long id = request.getId();
+        VppCustomerAccess access = findAccessById(id);
+
+        if (access.getAccessStatus() != ACCESS_PENDING) {
+            throw new BusinessException("当前状态不允许审核,仅待审核状态可操作!");
         }
-        access.setAuditOpinion(request.getAuditOpinion());
+
+        access.setAuditOpinion(request.getComment());
         access.setAuditAt(LocalDateTime.now());
-        if (Boolean.TRUE.equals(request.getPassed())) {
+
+        if (request.getResult() == ACCESS_PASSED) {
+            // 审核通过:校验同租户下 accountNo 和 customerName 唯一
+            Integer tenantId = SecurityUtils.getTenantId();
+            LambdaQueryWrapper<VppCustomer> dupWrapper = new LambdaQueryWrapper<>();
+            dupWrapper.eq(VppCustomer::getTenantId, tenantId)
+                    .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                    .and(w -> w.eq(VppCustomer::getAccountNo, access.getAccountNo())
+                            .or().eq(VppCustomer::getCustomerName, access.getCustomerName()));
+            Integer dupCount = customerMapper.selectCount(dupWrapper);
+            if (dupCount != null && dupCount > 0) {
+                throw new BusinessException("同租户下已存在相同电力户号或客户名称的客户!");
+            }
+
             access.setAccessStatus(ACCESS_PASSED);
             VppCustomer customer = buildCustomerFromAccess(access);
             VppAuditHelper.fillCreate(customer);
             customerMapper.insert(customer);
             access.setCustomerId(customer.getId());
+
+            // 审核通过时,同步创建主联系人
+            if (StringUtils.isNotBlank(access.getContactName())) {
+                VppCustomerContact contact = new VppCustomerContact();
+                contact.setCustomerId(customer.getId());
+                contact.setContactName(access.getContactName());
+                contact.setContactPhone(access.getContactPhone());
+                contact.setContactEmail(access.getContactEmail());
+                contact.setIsPrimary(1);
+                VppAuditHelper.fillCreate(contact);
+                contactMapper.insert(contact);
+            }
         } else {
             access.setAccessStatus(ACCESS_REJECTED);
         }
+
         VppAuditHelper.fillUpdate(access);
         accessMapper.updateById(access);
     }
 
     @Override
-    public CommonPage<VppCustomer> pageCustomer(Map<String, Object> params) {
-        Page<VppCustomer> page = VppPageHelper.of(params);
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean deleteAccess(Long id) {
+        VppCustomerAccess access = findAccessById(id);
+        if (access.getAccessStatus() == ACCESS_PENDING) {
+            throw new BusinessException("待审核的准入申请无法删除,请先审核或驳回后再操作!");
+        }
+        VppAuditHelper.fillSoftDelete(access);
+        int result = accessMapper.updateById(access);
+        return result > 0;
+    }
+
+    @Override
+    public void exportAccess(String scope, String fileName, Long id, Integer accessStatus,
+                             String customerName, LocalDateTime startTime, LocalDateTime endTime,
+                             Integer current, Integer size, HttpServletResponse response) {
+        List<VppCustomerAccess> accessList;
+
+        switch (scope) {
+            case "current":
+                if (current == null || size == null) {
+                    throw new BusinessException("current 模式下分页参数 current 和 size 不能为空!");
+                }
+                IPage<VppCustomerAccess> page = new Page<>(current, size);
+                LambdaQueryWrapper<VppCustomerAccess> currWrapper = new LambdaQueryWrapper<VppCustomerAccess>()
+                        .eq(VppCustomerAccess::getTenantId, SecurityUtils.getTenantId())
+                        .eq(VppCustomerAccess::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .orderByDesc(VppCustomerAccess::getApplyAt);
+                page = accessMapper.selectPage(page, currWrapper);
+                accessList = page.getRecords();
+                break;
+
+            case "all":
+                LambdaQueryWrapper<VppCustomerAccess> allWrapper = new LambdaQueryWrapper<VppCustomerAccess>()
+                        .eq(VppCustomerAccess::getTenantId, SecurityUtils.getTenantId())
+                        .eq(VppCustomerAccess::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .orderByDesc(VppCustomerAccess::getApplyAt);
+                accessList = accessMapper.selectList(allWrapper);
+                break;
+
+            case "filtered":
+                boolean hasFilter = id != null || accessStatus != null
+                        || StringUtils.isNotBlank(customerName)
+                        || startTime != null || endTime != null;
+                if (!hasFilter) {
+                    throw new BusinessException("filtered 模式下请至少提供一个筛选条件(id / accessStatus / customerName / startTime / endTime)!");
+                }
+                LambdaQueryWrapper<VppCustomerAccess> filteredWrapper = new LambdaQueryWrapper<VppCustomerAccess>()
+                        .eq(VppCustomerAccess::getTenantId, SecurityUtils.getTenantId())
+                        .eq(VppCustomerAccess::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .eq(id != null, VppCustomerAccess::getId, id)
+                        .eq(accessStatus != null, VppCustomerAccess::getAccessStatus, accessStatus)
+                        .like(StringUtils.isNotBlank(customerName), VppCustomerAccess::getCustomerName, customerName)
+                        .ge(startTime != null, VppCustomerAccess::getApplyAt, startTime)
+                        .le(endTime != null, VppCustomerAccess::getApplyAt, endTime)
+                        .orderByDesc(VppCustomerAccess::getApplyAt);
+                accessList = accessMapper.selectList(filteredWrapper);
+                break;
+
+            default:
+                throw new BusinessException("不支持的导出范围:" + scope + ",请使用 current / all / filtered");
+        }
+
+        if (accessList == null) {
+            accessList = Collections.emptyList();
+        }
+
+        // 生成 Excel
+        try (org.apache.poi.xssf.usermodel.XSSFWorkbook workbook = new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
+            org.apache.poi.xssf.usermodel.XSSFSheet sheet = workbook.createSheet("准入申请数据");
+
+            // 表头样式
+            org.apache.poi.xssf.usermodel.XSSFCellStyle headerStyle = workbook.createCellStyle();
+            headerStyle.setFillForegroundColor(org.apache.poi.ss.usermodel.IndexedColors.GREY_25_PERCENT.getIndex());
+            headerStyle.setFillPattern(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND);
+            org.apache.poi.xssf.usermodel.XSSFFont headerFont = workbook.createFont();
+            headerFont.setBold(true);
+            headerStyle.setFont(headerFont);
+
+            // 表头
+            String[] headers = {
+                    "序号", "申请编号", "电力户号", "客户名称", "客户类型",
+                    "供电公司", "用电容量(kW)", "信用状态", "审核状态",
+                    "审核意见", "营业执照",
+                    //"签约日期", "联系人姓名", "联系人电话", "联系人邮箱",
+                    "申请时间", "审核时间", "创建时间"
+            };
+            org.apache.poi.xssf.usermodel.XSSFRow headerRow = sheet.createRow(0);
+            for (int i = 0; i < headers.length; i++) {
+                org.apache.poi.xssf.usermodel.XSSFCell cell = headerRow.createCell(i);
+                cell.setCellValue(headers[i]);
+                cell.setCellStyle(headerStyle);
+            }
+
+            // 数据行
+            int rowIdx = 1;
+            for (VppCustomerAccess acc : accessList) {
+                org.apache.poi.xssf.usermodel.XSSFRow row = sheet.createRow(rowIdx++);
+                setCell(row, 0, rowIdx - 1);
+                setCell(row, 1, acc.getApplyNo());
+                setCell(row, 2, acc.getAccountNo());
+                setCell(row, 3, acc.getCustomerName());
+                setCell(row, 4, typeLabel(acc.getCustomerType(), "中高压", "低压商用", "居民充电桩", "自有资产"));
+                setCell(row, 5, acc.getPowerCompany());
+                setCell(row, 6, numberStr(acc.getContractCapacity()));
+                setCell(row, 7, typeLabel(acc.getCreditStatus(), "正常", "关注", "不良"));
+                setCell(row, 8, accessStatusLabel(acc.getAccessStatus()));
+                setCell(row, 9, acc.getAuditOpinion());
+                insertLicenseImage(workbook, sheet, row, 10, acc.getBusinessLicenseUrl());
+                // setCell(row, 11, dateStr(acc.getSigningDate()));
+                // setCell(row, 12, acc.getContactName());
+                // setCell(row, 13, acc.getContactPhone());
+                // setCell(row, 14, acc.getContactEmail());
+                setCell(row, 11, dateTimeStr(acc.getApplyAt()));
+                setCell(row, 12, dateTimeStr(acc.getAuditAt()));
+                setCell(row, 13, dateTimeStr(acc.getCreateTime()));
+            }
+
+            // 营业执照列宽度
+            sheet.setColumnWidth(10, 28 * 256);
+
+            // 自动调整列宽
+            for (int i = 0; i < headers.length; i++) {
+                if (i == 10) {
+                    continue;
+                }
+                sheet.autoSizeColumn(i);
+                int width = sheet.getColumnWidth(i);
+                if (width > 15000) {
+                    sheet.setColumnWidth(i, 15000);
+                }
+            }
+
+            // 写入响应
+            String encodedFileName = java.net.URLEncoder.encode(fileName + ".xlsx", StandardCharsets.UTF_8.name())
+                    .replace("+", "%20");
+            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+            response.setHeader("Content-Disposition", "attachment; filename=" + encodedFileName
+                    + "; filename*=UTF-8''" + encodedFileName);
+            workbook.write(response.getOutputStream());
+            response.getOutputStream().flush();
+
+        } catch (IOException e) {
+            throw new BusinessException("导出准入申请数据失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 客户管理 ====================
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean createCustomer(CustomerRequestVO vo) {
+        // 必填校验
+        if (StringUtils.isBlank(vo.getAccountNo())) {
+            throw new BusinessException("电力户号不能为空!");
+        }
+        if (StringUtils.isBlank(vo.getCustomerName())) {
+            throw new BusinessException("客户名称不能为空!");
+        }
+        if (vo.getCustomerType() == null) {
+            throw new BusinessException("客户类型不能为空!");
+        }
+        if (StringUtils.isBlank(vo.getPowerCompany())) {
+            throw new BusinessException("供电公司不能为空!");
+        }
+        if (vo.getContractCapacity() == null) {
+            throw new BusinessException("用电容量不能为空!");
+        }
+
+        // 同租户下 accountNo 和 customerName 唯一校验
+        Integer tenantId = SecurityUtils.getTenantId();
+        LambdaQueryWrapper<VppCustomer> dupWrapper = new LambdaQueryWrapper<>();
+        dupWrapper.eq(VppCustomer::getTenantId, tenantId)
+                .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .and(w -> w.eq(VppCustomer::getAccountNo, vo.getAccountNo())
+                        .or().eq(VppCustomer::getCustomerName, vo.getCustomerName()));
+        Integer dupCount = customerMapper.selectCount(dupWrapper);
+        if (dupCount != null && dupCount > 0) {
+            throw new BusinessException("同租户下已存在相同电力户号或客户名称的客户!");
+        }
+
+        // 构建客户
+        VppCustomer customer = new VppCustomer();
+        customer.setAccountNo(vo.getAccountNo());
+        customer.setCustomerName(vo.getCustomerName());
+        customer.setCustomerType(vo.getCustomerType());
+        customer.setPowerCompany(vo.getPowerCompany());
+        customer.setContractCapacity(vo.getContractCapacity());
+        customer.setCreditStatus(vo.getCreditStatus());
+        customer.setBusinessLicenseUrl(vo.getBusinessLicenseUrl());
+        customer.setLifecycleStatus(vo.getLifecycleStatus());
+        customer.setDrNotifyMinutes(vo.getDrNotifyMinutes());
+        customer.setDrUpCapacityKw(vo.getDrUpCapacityKw());
+        customer.setDrDownCapacityKw(vo.getDrDownCapacityKw());
+        customer.setProvince(vo.getProvince());
+        customer.setCity(vo.getCity());
+        customer.setDistrict(vo.getDistrict());
+        customer.setAddress(vo.getAddress());
+        customer.setRemark(vo.getRemark());
+        customer.setIsVppResource(vo.getIsVppResource());
+        customer.setVppCategory(vo.getVppCategory());
+        customer.setDrResourceType(vo.getDrResourceType());
+        customer.setVppProxyCode(vo.getVppProxyCode());
+        customer.setIndustry(vo.getIndustry());
+        customer.setIndustrySector(vo.getIndustrySector());
+        customer.setRunningCapacity(vo.getRunningCapacity());
+        customer.setProductionStartDate(vo.getProductionStartDate());
+        customer.setProductionEndDate(vo.getProductionEndDate());
+        customer.setPowerAddress(vo.getPowerAddress());
+        customer.setSigningDate(vo.getSigningDate());
+        VppAuditHelper.fillCreate(customer);
+        customerMapper.insert(customer);
+
+        // 若有联系人信息,创建主联系人
+        if (StringUtils.isNotBlank(vo.getContactName())) {
+            VppFieldValidator.validatePhone(vo.getContactPhone());
+            if (StringUtils.isNotBlank(vo.getContactEmail())) {
+                VppFieldValidator.validateEmail(vo.getContactEmail());
+            }
+            VppCustomerContact contact = new VppCustomerContact();
+            contact.setCustomerId(customer.getId());
+            contact.setContactName(vo.getContactName());
+            contact.setContactPhone(vo.getContactPhone());
+            contact.setContactEmail(vo.getContactEmail());
+            contact.setIsPrimary(1);
+            VppAuditHelper.fillCreate(contact);
+            contactMapper.insert(contact);
+        }
+
+        return true;
+    }
+
+    @Override
+    public CommonPage<CustomerResponseVO> pageCustomer(Long id, String customerName, String accountNo,
+                                                       Integer lifecycleStatus, Integer customerType,
+                                                       Integer pageNum, Integer pageSize) {
+        IPage<VppCustomer> page = new Page<>(pageNum, pageSize);
+
         LambdaQueryWrapper<VppCustomer> wrapper = new LambdaQueryWrapper<VppCustomer>()
+                .eq(VppCustomer::getTenantId, SecurityUtils.getTenantId())
                 .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(id != null, VppCustomer::getId, id)
+                .like(StringUtils.isNotBlank(customerName), VppCustomer::getCustomerName, customerName)
+                .eq(StringUtils.isNotBlank(accountNo), VppCustomer::getAccountNo, accountNo)
+                .eq(lifecycleStatus != null, VppCustomer::getLifecycleStatus, lifecycleStatus)
+                .eq(customerType != null, VppCustomer::getCustomerType, customerType)
                 .orderByDesc(VppCustomer::getCreateTime);
-        if (params != null) {
-            if (params.get("customerName") != null) {
-                wrapper.like(VppCustomer::getCustomerName, params.get("customerName").toString());
+
+        page = customerMapper.selectPage(page, wrapper);
+        List<VppCustomer> records = page.getRecords();
+
+        if (records == null || records.isEmpty()) {
+            return new CommonPage<>(Collections.emptyList(), page.getTotal(), pageSize, pageNum);
+        }
+
+        // 批量查询关联合同
+        final Set<Long> customerIds = records.stream()
+                .map(VppCustomer::getId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+
+        final Map<Long, List<VppContract>> contractMap;
+        if (!customerIds.isEmpty()) {
+            LambdaQueryWrapper<VppContract> contractWrapper = new LambdaQueryWrapper<>();
+            contractWrapper.in(VppContract::getCustomerId, customerIds)
+                    .eq(VppContract::getDeleteFlag, 0)
+                    .orderByDesc(VppContract::getCreateTime);
+            List<VppContract> allContracts = contractMapper.selectList(contractWrapper);
+            contractMap = allContracts.stream()
+                    .collect(Collectors.groupingBy(VppContract::getCustomerId));
+        } else {
+            contractMap = Collections.emptyMap();
+        }
+
+        // 批量查询关联联系人(取主联系人 is_primary=1)
+        final Map<Long, VppCustomerContact> contactMap;
+        if (!customerIds.isEmpty()) {
+            LambdaQueryWrapper<VppCustomerContact> contactWrapper = new LambdaQueryWrapper<>();
+            contactWrapper.in(VppCustomerContact::getCustomerId, customerIds)
+                    .eq(VppCustomerContact::getTenantId, SecurityUtils.getTenantId())
+                    .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                    .eq(VppCustomerContact::getIsPrimary, 1);
+            List<VppCustomerContact> allContacts = contactMapper.selectList(contactWrapper);
+            contactMap = allContacts.stream()
+                    .collect(Collectors.toMap(
+                            VppCustomerContact::getCustomerId,
+                            Function.identity(),
+                            (a, b) -> a));
+        } else {
+            contactMap = Collections.emptyMap();
+        }
+
+        // 组装响应VO
+        List<CustomerResponseVO> list = records.stream().map(entity -> {
+            CustomerResponseVO vo = new CustomerResponseVO();
+            BeanUtils.copyProperties(entity, vo);
+            // 设置关联合同
+            List<VppContract> contracts = contractMap.getOrDefault(entity.getId(), Collections.emptyList());
+            vo.setContracts(contracts.stream().map(this::toContractBrief).collect(Collectors.toList()));
+            // 设置关联联系人
+            VppCustomerContact contact = contactMap.get(entity.getId());
+            if (contact != null) {
+                vo.setContactName(contact.getContactName());
+                vo.setContactPhone(contact.getContactPhone());
+                vo.setContactEmail(contact.getContactEmail());
             }
-            if (params.get("accountNo") != null) {
-                wrapper.eq(VppCustomer::getAccountNo, params.get("accountNo").toString());
+            return vo;
+        }).collect(Collectors.toList());
+
+        return new CommonPage<>(list, page.getTotal(), pageSize, pageNum);
+    }
+
+    @Override
+    public CustomerResponseVO getCustomer(Long id) {
+        VppCustomer customer = findCustomerById(id);
+
+        CustomerResponseVO vo = new CustomerResponseVO();
+        BeanUtils.copyProperties(customer, vo);
+
+        // 关联查询合同
+        LambdaQueryWrapper<VppContract> contractWrapper = new LambdaQueryWrapper<>();
+        contractWrapper.eq(VppContract::getCustomerId, id)
+                .eq(VppContract::getDeleteFlag, 0)
+                .orderByDesc(VppContract::getCreateTime);
+        List<VppContract> contracts = contractMapper.selectList(contractWrapper);
+        vo.setContracts(contracts.stream().map(this::toContractBrief).collect(Collectors.toList()));
+
+        // 关联查询主联系人
+        LambdaQueryWrapper<VppCustomerContact> contactWrapper = new LambdaQueryWrapper<>();
+        contactWrapper.eq(VppCustomerContact::getCustomerId, id)
+                .eq(VppCustomerContact::getTenantId, SecurityUtils.getTenantId())
+                .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(VppCustomerContact::getIsPrimary, 1);
+        VppCustomerContact contact = contactMapper.selectOne(contactWrapper);
+        if (contact != null) {
+            vo.setContactName(contact.getContactName());
+            vo.setContactPhone(contact.getContactPhone());
+            vo.setContactEmail(contact.getContactEmail());
+        }
+
+        return vo;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean updateCustomer(CustomerRequestVO vo) {
+        if (vo.getId() == null) {
+            throw new BusinessException("客户ID不能为空!");
+        }
+        Long id = vo.getId();
+        VppCustomer existing = findCustomerById(id);
+
+        // 仅更新非空字段
+        if (StringUtils.isNotBlank(vo.getCustomerName())) {
+            if (!vo.getCustomerName().equals(existing.getCustomerName())) {
+                // 校验同租户下客户名称唯一
+                LambdaQueryWrapper<VppCustomer> uniqueWrapper = new LambdaQueryWrapper<>();
+                uniqueWrapper.eq(VppCustomer::getTenantId, SecurityUtils.getTenantId())
+                        .eq(VppCustomer::getCustomerName, vo.getCustomerName())
+                        .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+                Integer count = customerMapper.selectCount(uniqueWrapper);
+                if (count != null && count > 0) {
+                    throw new BusinessException("客户名称已存在,请更换!");
+                }
             }
-            if (params.get("lifecycleStatus") != null) {
-                wrapper.eq(VppCustomer::getLifecycleStatus, Integer.parseInt(params.get("lifecycleStatus").toString()));
+            existing.setCustomerName(vo.getCustomerName());
+        }
+        if (vo.getCustomerType() != null) {
+            existing.setCustomerType(vo.getCustomerType());
+        }
+        if (StringUtils.isNotBlank(vo.getPowerCompany())) {
+            existing.setPowerCompany(vo.getPowerCompany());
+        }
+        if (vo.getContractCapacity() != null) {
+            existing.setContractCapacity(vo.getContractCapacity());
+        }
+        if (vo.getCreditStatus() != null) {
+            existing.setCreditStatus(vo.getCreditStatus());
+        }
+        if (vo.getLifecycleStatus() != null) {
+            existing.setLifecycleStatus(vo.getLifecycleStatus());
+        }
+        if (vo.getDrNotifyMinutes() != null) {
+            existing.setDrNotifyMinutes(vo.getDrNotifyMinutes());
+        }
+        if (vo.getDrUpCapacityKw() != null) {
+            existing.setDrUpCapacityKw(vo.getDrUpCapacityKw());
+        }
+        if (vo.getDrDownCapacityKw() != null) {
+            existing.setDrDownCapacityKw(vo.getDrDownCapacityKw());
+        }
+        if (vo.getProvince() != null) {
+            existing.setProvince(vo.getProvince());
+        }
+        if (vo.getCity() != null) {
+            existing.setCity(vo.getCity());
+        }
+        if (vo.getDistrict() != null) {
+            existing.setDistrict(vo.getDistrict());
+        }
+        if (vo.getAddress() != null) {
+            existing.setAddress(vo.getAddress());
+        }
+        if (vo.getBusinessLicenseUrl() != null) {
+            existing.setBusinessLicenseUrl(vo.getBusinessLicenseUrl());
+        }
+        if (vo.getRemark() != null) {
+            existing.setRemark(vo.getRemark());
+        }
+        if (vo.getIsVppResource() != null) {
+            existing.setIsVppResource(vo.getIsVppResource());
+        }
+        if (vo.getVppCategory() != null) {
+            existing.setVppCategory(vo.getVppCategory());
+        }
+        if (vo.getDrResourceType() != null) {
+            existing.setDrResourceType(vo.getDrResourceType());
+        }
+        if (vo.getVppProxyCode() != null) {
+            existing.setVppProxyCode(vo.getVppProxyCode());
+        }
+        if (vo.getIndustry() != null) {
+            existing.setIndustry(vo.getIndustry());
+        }
+        if (vo.getIndustrySector() != null) {
+            existing.setIndustrySector(vo.getIndustrySector());
+        }
+        if (vo.getRunningCapacity() != null) {
+            existing.setRunningCapacity(vo.getRunningCapacity());
+        }
+        if (vo.getProductionStartDate() != null) {
+            existing.setProductionStartDate(vo.getProductionStartDate());
+        }
+        if (vo.getProductionEndDate() != null) {
+            existing.setProductionEndDate(vo.getProductionEndDate());
+        }
+        if (vo.getPowerAddress() != null) {
+            existing.setPowerAddress(vo.getPowerAddress());
+        }
+        if (vo.getSigningDate() != null) {
+            existing.setSigningDate(vo.getSigningDate());
+        }
+
+        // 同步维护主联系人:如果传了联系人姓名,则 upsert 主联系人
+        if (StringUtils.isNotBlank(vo.getContactName())) {
+            // 校验联系人信息
+            VppFieldValidator.validatePhone(vo.getContactPhone());
+            if (StringUtils.isNotBlank(vo.getContactEmail())) {
+                VppFieldValidator.validateEmail(vo.getContactEmail());
             }
-            if (params.get("customerType") != null) {
-                wrapper.eq(VppCustomer::getCustomerType, Integer.parseInt(params.get("customerType").toString()));
+
+            LambdaQueryWrapper<VppCustomerContact> contactWrapper = new LambdaQueryWrapper<>();
+            contactWrapper.eq(VppCustomerContact::getCustomerId, id)
+                    .eq(VppCustomerContact::getTenantId, SecurityUtils.getTenantId())
+                    .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                    .eq(VppCustomerContact::getIsPrimary, 1);
+            VppCustomerContact contact = contactMapper.selectOne(contactWrapper);
+
+            if (contact != null) {
+                // 更新已有主联系人
+                contact.setContactName(vo.getContactName());
+                contact.setContactPhone(vo.getContactPhone());
+                contact.setContactEmail(vo.getContactEmail());
+                VppAuditHelper.fillUpdate(contact);
+                contactMapper.updateById(contact);
+            } else {
+                // 新增主联系人
+                contact = new VppCustomerContact();
+                contact.setCustomerId(id);
+                contact.setContactName(vo.getContactName());
+                contact.setContactPhone(vo.getContactPhone());
+                contact.setContactEmail(vo.getContactEmail());
+                contact.setIsPrimary(1);
+                VppAuditHelper.fillCreate(contact);
+                contactMapper.insert(contact);
             }
         }
-        return toCommonPage(customerMapper.selectPage(page, wrapper));
+
+        VppAuditHelper.fillUpdate(existing);
+        int result = customerMapper.updateById(existing);
+        return result > 0;
     }
 
     @Override
-    public VppCustomer getCustomer(Long id) {
-        VppCustomer customer = customerMapper.selectById(id);
-        if (customer == null || VppAuditHelper.isDeleted(customer.getDeleteFlag())) {
-            throw new BusinessException("客户不存在");
-        }
-        return customer;
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean deleteCustomer(Long id) {
+        VppCustomer customer = findCustomerById(id);
+
+        VppAuditHelper.fillSoftDelete(customer);
+        int result = customerMapper.updateById(customer);
+        return result > 0;
     }
 
     @Override
-    @Transactional(rollbackFor = Exception.class)
-    public void updateCustomer(Long id, VppCustomer customer) {
-        VppCustomer existing = getCustomer(id);
-        customer.setId(existing.getId());
-        customer.setAccountNo(existing.getAccountNo());
-        customer.setCreateTime(existing.getCreateTime());
-        customer.setCreatedBy(existing.getCreatedBy());
-        VppAuditHelper.fillUpdate(customer);
-        customerMapper.updateById(customer);
+    public void exportCustomers(String scope, String fileName, String customerName,
+                                Integer lifecycleStatus, Integer current, Integer size,
+                                HttpServletResponse response) {
+        List<VppCustomer> customers;
+
+        switch (scope) {
+            case "current":
+                // current 模式:必须传分页参数
+                if (current == null || size == null) {
+                    throw new BusinessException("current 模式下分页参数 current 和 size 不能为空!");
+                }
+                IPage<VppCustomer> page = new Page<>(current, size);
+                LambdaQueryWrapper<VppCustomer> currWrapper = new LambdaQueryWrapper<VppCustomer>()
+                        .eq(VppCustomer::getTenantId, SecurityUtils.getTenantId())
+                        .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .orderByDesc(VppCustomer::getCreateTime);
+                page = customerMapper.selectPage(page, currWrapper);
+                customers = page.getRecords();
+                break;
+
+            case "all":
+                // all 模式:当前租户下所有未删除客户
+                LambdaQueryWrapper<VppCustomer> allWrapper = new LambdaQueryWrapper<VppCustomer>()
+                        .eq(VppCustomer::getTenantId, SecurityUtils.getTenantId())
+                        .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .orderByDesc(VppCustomer::getCreateTime);
+                customers = customerMapper.selectList(allWrapper);
+                break;
+
+            case "filtered":
+                // filtered 模式:必须至少传一个筛选条件
+                boolean hasFilter = StringUtils.isNotBlank(customerName) || lifecycleStatus != null;
+                if (!hasFilter) {
+                    throw new BusinessException("filtered 模式下请至少提供一个筛选条件(customerName / lifecycleStatus)!");
+                }
+                LambdaQueryWrapper<VppCustomer> filteredWrapper = new LambdaQueryWrapper<VppCustomer>()
+                        .eq(VppCustomer::getTenantId, SecurityUtils.getTenantId())
+                        .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .like(StringUtils.isNotBlank(customerName), VppCustomer::getCustomerName, customerName)
+                        .eq(lifecycleStatus != null, VppCustomer::getLifecycleStatus, lifecycleStatus)
+                        .orderByDesc(VppCustomer::getCreateTime);
+                customers = customerMapper.selectList(filteredWrapper);
+                break;
+
+            default:
+                throw new BusinessException("不支持的导出范围:" + scope + ",请使用 current / all / filtered");
+        }
+
+        if (customers == null) {
+            customers = Collections.emptyList();
+        }
+
+        // 批量查询关联合同
+        final Set<Long> customerIds = customers.stream()
+                .map(VppCustomer::getId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+
+        final Map<Long, List<VppContract>> contractMap;
+        if (!customerIds.isEmpty()) {
+            LambdaQueryWrapper<VppContract> contractWrapper = new LambdaQueryWrapper<>();
+            contractWrapper.in(VppContract::getCustomerId, customerIds)
+                    .eq(VppContract::getDeleteFlag, 0);
+            List<VppContract> allContracts = contractMapper.selectList(contractWrapper);
+            contractMap = allContracts.stream()
+                    .collect(Collectors.groupingBy(VppContract::getCustomerId));
+        } else {
+            contractMap = Collections.emptyMap();
+        }
+
+        // 生成 Excel
+        try (org.apache.poi.xssf.usermodel.XSSFWorkbook workbook = new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
+            org.apache.poi.xssf.usermodel.XSSFSheet sheet = workbook.createSheet("客户数据");
+
+            // 创建表头样式
+            org.apache.poi.xssf.usermodel.XSSFCellStyle headerStyle = workbook.createCellStyle();
+            headerStyle.setFillForegroundColor(org.apache.poi.ss.usermodel.IndexedColors.GREY_25_PERCENT.getIndex());
+            headerStyle.setFillPattern(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND);
+            org.apache.poi.xssf.usermodel.XSSFFont headerFont = workbook.createFont();
+            headerFont.setBold(true);
+            headerStyle.setFont(headerFont);
+
+            // 表头
+            String[] headers = {
+                    "序号", "电力户号", "客户名称", "客户类型", "供电公司",
+                    "签约容量(kW)", "信用状态", "签约状态",
+                    "省", "市", "区/县", "地址", "用电地址",
+                    "是否VPP资源", "VPP分类", "需求响应资源分类",
+                    "VPP代理编码", "所属行业", "所属产业", "运行容量(kW)",
+                    "生产经营开始", "生产经营结束", "合同编号列表",
+                    "营业执照", "签约日期", "备注", "创建时间"
+            };
+            org.apache.poi.xssf.usermodel.XSSFRow headerRow = sheet.createRow(0);
+            for (int i = 0; i < headers.length; i++) {
+                org.apache.poi.xssf.usermodel.XSSFCell cell = headerRow.createCell(i);
+                cell.setCellValue(headers[i]);
+                cell.setCellStyle(headerStyle);
+            }
+
+            // 数据行
+            int rowIdx = 1;
+            for (VppCustomer c : customers) {
+                org.apache.poi.xssf.usermodel.XSSFRow row = sheet.createRow(rowIdx++);
+                List<VppContract> contracts = contractMap.getOrDefault(c.getId(), Collections.emptyList());
+                String contractNos = contracts.stream()
+                        .map(VppContract::getContractNo)
+                        .collect(Collectors.joining("; "));
+
+                setCell(row, 0, rowIdx - 1);                       // 序号
+                setCell(row, 1, c.getAccountNo());
+                setCell(row, 2, c.getCustomerName());
+                setCell(row, 3, typeLabel(c.getCustomerType(), "中高压", "低压商用", "居民充电桩", "自有资产"));
+                setCell(row, 4, c.getPowerCompany());
+                setCell(row, 5, numberStr(c.getContractCapacity()));
+                setCell(row, 6, typeLabel(c.getCreditStatus(), "正常", "关注", "不良"));
+                setCell(row, 7, typeLabel(c.getLifecycleStatus(), "待签约", "已签约", "履约中", "待续约", "已续约", "已解约"));
+                setCell(row, 8, c.getProvince());
+                setCell(row, 9, c.getCity());
+                setCell(row, 10, c.getDistrict());
+                setCell(row, 11, c.getAddress());
+                setCell(row, 12, c.getPowerAddress());
+                setCell(row, 13, c.getIsVppResource() != null && c.getIsVppResource() == 1 ? "是" : "否");
+                setCell(row, 14, c.getVppCategory());
+                setCell(row, 15, c.getDrResourceType());
+                setCell(row, 16, c.getVppProxyCode());
+                setCell(row, 17, c.getIndustry());
+                setCell(row, 18, c.getIndustrySector());
+                setCell(row, 19, numberStr(c.getRunningCapacity()));
+                setCell(row, 20, timeStr(c.getProductionStartDate()));
+                setCell(row, 21, timeStr(c.getProductionEndDate()));
+                setCell(row, 22, contractNos);
+                insertLicenseImage(workbook, sheet, row, 23, c.getBusinessLicenseUrl());
+                setCell(row, 24, dateStr(c.getSigningDate()));
+                setCell(row, 25, c.getRemark());
+                setCell(row, 26, dateTimeStr(c.getCreateTime()));
+            }
+
+            // 设置营业执照列宽度(固定宽度以适配图片)
+            sheet.setColumnWidth(23, 28 * 256); // 约 28 字符宽
+
+            // 自动调整列宽
+            for (int i = 0; i < headers.length; i++) {
+                if (i == 23) {
+                    continue; // 营业执照列已手动设置宽度
+                }
+                sheet.autoSizeColumn(i);
+                // 限制最大宽度,避免营业执照URL列过宽
+                int width = sheet.getColumnWidth(i);
+                if (width > 15000) {
+                    sheet.setColumnWidth(i, 15000);
+                }
+            }
+
+            // 写入响应
+            String encodedFileName = java.net.URLEncoder.encode(fileName + ".xlsx", StandardCharsets.UTF_8.name())
+                    .replace("+", "%20");
+            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+            response.setHeader("Content-Disposition", "attachment; filename=" + encodedFileName
+                    + "; filename*=UTF-8''" + encodedFileName);
+            workbook.write(response.getOutputStream());
+            response.getOutputStream().flush();
+
+        } catch (IOException e) {
+            throw new BusinessException("导出客户数据失败: " + e.getMessage());
+        }
     }
 
+    // ==================== 联系人管理 ====================
+
     @Override
-    public CommonPage<VppCustomerContact> listContact(Long customerId, Map<String, Object> params) {
-        getCustomer(customerId);
-        Page<VppCustomerContact> page = VppPageHelper.of(params);
+    public CommonPage<VppCustomerContact> listContact(Long customerId, Integer pageNum, Integer pageSize) {
+        // 校验客户是否存在(租户内)
+        findCustomerById(customerId);
+
+        IPage<VppCustomerContact> page = new Page<>(pageNum, pageSize);
         LambdaQueryWrapper<VppCustomerContact> wrapper = new LambdaQueryWrapper<VppCustomerContact>()
                 .eq(VppCustomerContact::getCustomerId, customerId)
+                .eq(VppCustomerContact::getTenantId, SecurityUtils.getTenantId())
                 .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
-                .orderByDesc(VppCustomerContact::getIsPrimary);
-        return toCommonPage(contactMapper.selectPage(page, wrapper));
+                .orderByDesc(VppCustomerContact::getIsPrimary)
+                .orderByAsc(VppCustomerContact::getId);
+
+        page = contactMapper.selectPage(page, wrapper);
+
+        return new CommonPage<>(page.getRecords(), page.getTotal(), pageSize, pageNum);
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public VppCustomerContact addContact(Long customerId, CustomerContactRequest request) {
-        getCustomer(customerId);
+        // 校验客户是否存在(租户内)
+        findCustomerById(customerId);
+
         validateContactRequest(request);
+
         VppCustomerContact contact = new VppCustomerContact();
         contact.setCustomerId(customerId);
         contact.setContactName(request.getContactName());
@@ -181,6 +908,7 @@ public class VppCustomerServiceImpl implements VppCustomerService {
     public void updateContact(Long customerId, Long contactId, CustomerContactRequest request) {
         VppCustomerContact contact = getContact(customerId, contactId);
         validateContactRequest(request);
+
         contact.setContactName(request.getContactName());
         contact.setContactPhone(request.getContactPhone());
         contact.setContactEmail(request.getContactEmail());
@@ -188,6 +916,7 @@ public class VppCustomerServiceImpl implements VppCustomerService {
             contact.setIsPrimary(request.getIsPrimary());
         }
         contact.setPosition(request.getPosition());
+
         VppAuditHelper.fillUpdate(contact);
         contactMapper.updateById(contact);
     }
@@ -200,17 +929,7 @@ public class VppCustomerServiceImpl implements VppCustomerService {
         contactMapper.updateById(contact);
     }
 
-    private <T> CommonPage<T> toCommonPage(Page<T> page) {
-        return new CommonPage<>(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize());
-    }
-
-    private VppCustomerContact getContact(Long customerId, Long contactId) {
-        VppCustomerContact contact = contactMapper.selectById(contactId);
-        if (contact == null || VppAuditHelper.isDeleted(contact.getDeleteFlag()) || !customerId.equals(contact.getCustomerId())) {
-            throw new BusinessException("联系人不存在");
-        }
-        return contact;
-    }
+    // ==================== 内部辅助方法 ====================
 
     private VppCustomer buildCustomerFromAccess(VppCustomerAccess access) {
         VppCustomer customer = new VppCustomer();
@@ -223,33 +942,284 @@ public class VppCustomerServiceImpl implements VppCustomerService {
         customer.setBusinessLicenseUrl(access.getBusinessLicenseUrl());
         customer.setLifecycleStatus(LIFECYCLE_ADMITTED);
         customer.setDrNotifyMinutes(30);
+        customer.setSigningDate(access.getSigningDate());
         return customer;
     }
 
-    private void validateAccessRequest(CustomerAccessRequest request) {
-        if (request == null || !StringUtils.hasText(request.getAccountNo())) {
-            throw new BusinessException("电力户号不能为空");
+    private CustomerResponseVO.ContractBriefVO toContractBrief(VppContract contract) {
+        CustomerResponseVO.ContractBriefVO brief = new CustomerResponseVO.ContractBriefVO();
+        brief.setId(contract.getId());
+        brief.setContractNo(contract.getContractNo());
+        brief.setContractName(contract.getContractName());
+        brief.setContractType(contract.getContractType());
+        brief.setContractStatus(contract.getContractStatus());
+        brief.setSignDate(contract.getSignDate());
+        brief.setEffectiveDate(contract.getEffectiveDate());
+        brief.setExpireDate(contract.getExpireDate());
+        return brief;
+    }
+
+    /**
+     * 租户内查询客户(含删除检查)
+     */
+    private VppCustomer findCustomerById(Long id) {
+        LambdaQueryWrapper<VppCustomer> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppCustomer::getId, id)
+                .eq(VppCustomer::getTenantId, SecurityUtils.getTenantId());
+        VppCustomer customer = customerMapper.selectOne(wrapper);
+        if (customer == null || VppAuditHelper.isDeleted(customer.getDeleteFlag())) {
+            throw new BusinessException("客户不存在或已被删除!");
+        }
+        return customer;
+    }
+
+    /**
+     * 租户内查询准入申请(含删除检查)
+     */
+    private VppCustomerAccess findAccessById(Long id) {
+        LambdaQueryWrapper<VppCustomerAccess> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppCustomerAccess::getId, id)
+                .eq(VppCustomerAccess::getTenantId, SecurityUtils.getTenantId());
+        VppCustomerAccess access = accessMapper.selectOne(wrapper);
+        if (access == null || VppAuditHelper.isDeleted(access.getDeleteFlag())) {
+            throw new BusinessException("准入申请不存在或已被删除!");
+        }
+        return access;
+    }
+
+    private VppCustomerContact getContact(Long customerId, Long contactId) {
+        LambdaQueryWrapper<VppCustomerContact> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppCustomerContact::getId, contactId)
+                .eq(VppCustomerContact::getCustomerId, customerId)
+                .eq(VppCustomerContact::getTenantId, SecurityUtils.getTenantId());
+        VppCustomerContact contact = contactMapper.selectOne(wrapper);
+        if (contact == null || VppAuditHelper.isDeleted(contact.getDeleteFlag())) {
+            throw new BusinessException("联系人不存在或已被删除!");
+        }
+        return contact;
+    }
+
+    private void validateAccessRequest(CustomerAccessRequestVO request) {
+        if (request == null || StringUtils.isBlank(request.getAccountNo())) {
+            throw new BusinessException("电力户号不能为空!");
         }
-        if (!StringUtils.hasText(request.getCustomerName())) {
-            throw new BusinessException("客户名称不能为空");
+        if (StringUtils.isBlank(request.getCustomerName())) {
+            throw new BusinessException("客户名称不能为空");
         }
         if (request.getCustomerType() == null) {
-            throw new BusinessException("客户类型不能为空");
+            throw new BusinessException("客户类型不能为空");
         }
-        if (!StringUtils.hasText(request.getPowerCompany())) {
-            throw new BusinessException("供电公司不能为空");
+        if (StringUtils.isBlank(request.getPowerCompany())) {
+            throw new BusinessException("供电公司不能为空");
         }
         if (request.getContractCapacity() == null) {
-            throw new BusinessException("用电容量不能为空");
+            throw new BusinessException("用电容量不能为空");
         }
     }
 
     private void validateContactRequest(CustomerContactRequest request) {
-        if (request == null || !StringUtils.hasText(request.getContactName())) {
-            throw new BusinessException("联系人姓名不能为空");
+        if (request == null || StringUtils.isBlank(request.getContactName())) {
+            throw new BusinessException("联系人姓名不能为空!");
+        }
+        if (StringUtils.isBlank(request.getContactPhone())) {
+            throw new BusinessException("联系电话不能为空!");
+        }
+        VppFieldValidator.validatePhone(request.getContactPhone());
+        if (StringUtils.isNotBlank(request.getContactEmail())) {
+            VppFieldValidator.validateEmail(request.getContactEmail());
         }
-        if (!StringUtils.hasText(request.getContactPhone())) {
-            throw new BusinessException("联系电话不能为空");
+    }
+
+    // ==================== Excel 导出辅助方法 ====================
+
+    private static void setCell(org.apache.poi.xssf.usermodel.XSSFRow row, int col, Object value) {
+        if (value != null) {
+            row.createCell(col).setCellValue(String.valueOf(value));
+        } else {
+            row.createCell(col).setCellValue("");
         }
     }
+
+    /**
+     * 下载营业执照图片并嵌入到 Excel 单元格中
+     *
+     * @param workbook 当前工作簿
+     * @param sheet    当前工作表
+     * @param row      当前行
+     * @param col      目标列索引
+     * @param imageUrl 营业执照图片 URL,为空或无效时写入"无"
+     */
+    private void insertLicenseImage(org.apache.poi.xssf.usermodel.XSSFWorkbook workbook,
+                                    org.apache.poi.xssf.usermodel.XSSFSheet sheet,
+                                    org.apache.poi.xssf.usermodel.XSSFRow row,
+                                    int col, String imageUrl) {
+        if (org.apache.commons.lang3.StringUtils.isBlank(imageUrl)) {
+            row.createCell(col).setCellValue("无");
+            return;
+        }
+
+        log.info("开始下载营业执照图片, URL: {}", imageUrl);
+
+        // 如果是相对路径(以 / 开头),尝试补全为本地文件路径或跳过
+        if (imageUrl.startsWith("/")) {
+            log.warn("营业执照URL是相对路径,无法通过HTTP下载: {}", imageUrl);
+            row.createCell(col).setCellValue("无");
+            return;
+        }
+
+        // 校验 URL 格式
+        if (!imageUrl.startsWith("http://") && !imageUrl.startsWith("https://")) {
+            log.warn("营业执照URL格式不正确: {}", imageUrl);
+            row.createCell(col).setCellValue("无");
+            return;
+        }
+
+        byte[] imageBytes = null;
+        try {
+            imageBytes = downloadImage(imageUrl);
+            log.info("图片下载成功, 大小: {} bytes, URL: {}", imageBytes.length, imageUrl);
+        } catch (Exception e) {
+            log.error("下载营业执照图片失败, URL: {}, 错误: {}", imageUrl, e.getMessage(), e);
+            row.createCell(col).setCellValue("加载失败");
+            return;
+        }
+
+        if (imageBytes == null || imageBytes.length == 0) {
+            row.createCell(col).setCellValue("无");
+            return;
+        }
+
+        try {
+            int pictureType = detectImageType(imageBytes);
+            log.info("图片类型检测结果: {}, URL: {}", pictureType, imageUrl);
+            int pictureIdx = workbook.addPicture(imageBytes, pictureType);
+
+            org.apache.poi.xssf.usermodel.XSSFDrawing drawing = sheet.createDrawingPatriarch();
+            int emuPerPixel = org.apache.poi.util.Units.EMU_PER_PIXEL;
+            // 全参构造:dx1, dy1, dx2, dy2, col1, row1, col2, row2
+            // dx/dy 设 2 像素微边距,图片紧贴单元格边界
+            org.apache.poi.xssf.usermodel.XSSFClientAnchor anchor =
+                    new org.apache.poi.xssf.usermodel.XSSFClientAnchor(
+                            2 * emuPerPixel, 2 * emuPerPixel,   // dx1, dy1 左/上边距
+                            2 * emuPerPixel, 2 * emuPerPixel,   // dx2, dy2 右/下边距
+                            col, row.getRowNum(),                // col1, row1
+                            col + 1, row.getRowNum() + 1         // col2, row2
+                    );
+            anchor.setAnchorType(org.apache.poi.ss.usermodel.ClientAnchor.AnchorType.MOVE_AND_RESIZE);
+
+            drawing.createPicture(anchor, pictureIdx);
+
+            if (row.getHeight() < 2000) {
+                row.setHeight((short) 2000);
+            }
+        } catch (Exception e) {
+            log.error("营业执照图片插入Excel失败, URL: {}, 错误: {}", imageUrl, e.getMessage(), e);
+            row.createCell(col).setCellValue("图片插入失败");
+        }
+    }
+
+    /**
+     * 通过 HTTP 下载图片,返回字节数组
+     */
+    private static byte[] downloadImage(String imageUrl) throws IOException {
+        URL url = new URL(imageUrl);
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setConnectTimeout(10000);
+        conn.setReadTimeout(30000);
+        conn.setRequestMethod("GET");
+        conn.setDoInput(true);
+        conn.setInstanceFollowRedirects(true);
+        conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
+        conn.connect();
+
+        int responseCode = conn.getResponseCode();
+        if (responseCode != HttpURLConnection.HTTP_OK) {
+            conn.disconnect();
+            throw new IOException("下载图片失败,HTTP " + responseCode + ", URL: " + imageUrl);
+        }
+
+        String contentType = conn.getContentType();
+        if (contentType != null && !contentType.startsWith("image/")) {
+            conn.disconnect();
+            throw new IOException("返回的不是图片类型: " + contentType + ", URL: " + imageUrl);
+        }
+
+        try (InputStream is = conn.getInputStream();
+             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+            byte[] buffer = new byte[8192];
+            int len;
+            while ((len = is.read(buffer)) != -1) {
+                bos.write(buffer, 0, len);
+            }
+            return bos.toByteArray();
+        } finally {
+            conn.disconnect();
+        }
+    }
+
+    /**
+     * 根据图片字节前几个魔术字节判断图片类型
+     */
+    private static int detectImageType(byte[] bytes) {
+        if (bytes == null || bytes.length < 3) {
+            return org.apache.poi.ss.usermodel.Workbook.PICTURE_TYPE_JPEG;
+        }
+        // PNG: 89 50 4E 47
+        if (bytes[0] == (byte) 0x89 && bytes[1] == (byte) 0x50
+                && bytes[2] == (byte) 0x4E && bytes[3] == (byte) 0x47) {
+            return org.apache.poi.ss.usermodel.Workbook.PICTURE_TYPE_PNG;
+        }
+        // JPEG: FF D8 FF
+        if (bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xD8 && bytes[2] == (byte) 0xFF) {
+            return org.apache.poi.ss.usermodel.Workbook.PICTURE_TYPE_JPEG;
+        }
+        // GIF: 47 49 46
+        if (bytes[0] == (byte) 0x47 && bytes[1] == (byte) 0x49 && bytes[2] == (byte) 0x46) {
+            return org.apache.poi.ss.usermodel.Workbook.PICTURE_TYPE_JPEG; // POI 4.x 用 JPEG 兜底
+        }
+        // BMP: 42 4D
+        if (bytes[0] == (byte) 0x42 && bytes[1] == (byte) 0x4D) {
+            return org.apache.poi.ss.usermodel.Workbook.PICTURE_TYPE_JPEG; // POI 4.x 用 JPEG 兜底
+        }
+        return org.apache.poi.ss.usermodel.Workbook.PICTURE_TYPE_JPEG;
+    }
+
+    private static String typeLabel(Integer code, String... labels) {
+        if (code == null || code < 1 || code > labels.length) {
+            return "";
+        }
+        return labels[code - 1];
+    }
+
+    private static String accessStatusLabel(Integer code) {
+        if (code == null || code < 0 || code > 2) {
+            return "";
+        }
+        switch (code) {
+            case 0:
+                return "待审核";
+            case 1:
+                return "已通过";
+            case 2:
+                return "已驳回";
+            default:
+                return "";
+        }
+    }
+
+    private static String numberStr(Object value) {
+        return value == null ? "" : value.toString();
+    }
+
+    private static String timeStr(Object value) {
+        return value == null ? "" : value.toString();
+    }
+
+    private static String dateStr(Object value) {
+        return value == null ? "" : value.toString();
+    }
+
+    private static String dateTimeStr(Object value) {
+        return value == null ? "" : value.toString();
+    }
 }

+ 514 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrInvitationServiceImpl.java

@@ -0,0 +1,514 @@
+package com.usky.vpp.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.vpp.config.VppSmsProperties;
+import com.usky.vpp.domain.VppCustomer;
+import com.usky.vpp.domain.VppCustomerContact;
+import com.usky.vpp.domain.VppDrEvent;
+import com.usky.vpp.domain.VppDrInvitation;
+import com.usky.vpp.domain.VppDrParticipation;
+import com.usky.vpp.mapper.VppCustomerContactMapper;
+import com.usky.vpp.mapper.VppCustomerMapper;
+import com.usky.vpp.mapper.VppDrEventMapper;
+import com.usky.vpp.mapper.VppDrInvitationMapper;
+import com.usky.vpp.mapper.VppDrParticipationMapper;
+import com.usky.vpp.service.VppAliyunSmsService;
+import com.usky.vpp.service.VppDrInvitationService;
+import com.usky.vpp.service.VppUnIntegrationService;
+import com.usky.vpp.service.vo.DrInvitationNotifyRequest;
+import com.usky.vpp.service.vo.DrInvitationReplyRequest;
+import com.usky.vpp.service.vo.DrInvitationRequest;
+import com.usky.vpp.service.vo.DrInvitationVO;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppPageHelper;
+import com.usky.vpp.util.VppSmsTemplateHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Service
+public class VppDrInvitationServiceImpl implements VppDrInvitationService {
+
+    private static final Logger log = LoggerFactory.getLogger(VppDrInvitationServiceImpl.class);
+
+    private static final int REPLY_PENDING = 0;
+    private static final int REPLY_ACCEPT = 1;
+    private static final int REPLY_REJECT = 2;
+    private static final int REPLY_TIMEOUT = 3;
+
+    private static final int RESPONSE_PENDING = 0;
+    private static final int RESPONSE_DECLARED = 1;
+    private static final int RESPONSE_CANCELLED = 4;
+
+    private static final int SMS_NOT_SENT = 0;
+    private static final int SMS_SENT = 1;
+    private static final int SMS_FAILED = 2;
+
+    private static final int PARTICIPATE_ACCEPT = 1;
+
+    @Autowired
+    private VppDrInvitationMapper invitationMapper;
+    @Autowired
+    private VppDrEventMapper eventMapper;
+    @Autowired
+    private VppCustomerMapper customerMapper;
+    @Autowired
+    private VppCustomerContactMapper contactMapper;
+    @Autowired
+    private VppDrParticipationMapper participationMapper;
+    @Autowired
+    private VppUnIntegrationService unIntegrationService;
+    @Autowired
+    private VppAliyunSmsService aliyunSmsService;
+    @Autowired
+    private VppSmsProperties smsProperties;
+
+    @Override
+    public CommonPage<DrInvitationVO> pageInvitation(Map<String, Object> params) {
+        Page<VppDrInvitation> page = VppPageHelper.of(params);
+        LambdaQueryWrapper<VppDrInvitation> wrapper = new LambdaQueryWrapper<VppDrInvitation>()
+                .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .orderByDesc(VppDrInvitation::getIssueTime);
+
+        applyListFilters(wrapper, params);
+
+        Page<VppDrInvitation> result = invitationMapper.selectPage(page, wrapper);
+        List<DrInvitationVO> records = toVoList(result.getRecords());
+        return new CommonPage<>(records, result.getTotal(), result.getCurrent(), result.getSize());
+    }
+
+    @Override
+    public DrInvitationVO getInvitation(Long id) {
+        return toVo(requireInvitation(id));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DrInvitationVO createInvitation(DrInvitationRequest request) {
+        validateCreateRequest(request);
+
+        VppDrEvent event = requireEvent(request.getDrEventId());
+        VppCustomer customer = requireCustomer(request.getCustomerId());
+        ensureInvitationUnique(request.getDrEventId(), request.getCustomerId(), null);
+
+        VppDrInvitation invitation = new VppDrInvitation();
+        invitation.setInvitationNo(VppAuditHelper.nextInvitationNo());
+        invitation.setDrEventId(request.getDrEventId());
+        invitation.setCustomerId(request.getCustomerId());
+        invitation.setExecuteStartDate(request.getExecuteStartDate());
+        invitation.setExecuteEndDate(request.getExecuteEndDate());
+        invitation.setReplyDeadline(request.getReplyDeadline());
+        invitation.setIssueTime(LocalDateTime.now());
+        invitation.setTransactionType(request.getTransactionType() != null ? request.getTransactionType() : event.getEventType());
+        invitation.setResponseType(request.getResponseType() != null ? request.getResponseType() : event.getResponseType());
+        invitation.setDemandCapacityKw(request.getDemandCapacityKw());
+        invitation.setDeclaredCapacityKw(request.getDeclaredCapacityKw() != null ? request.getDeclaredCapacityKw() : BigDecimal.ZERO);
+        invitation.setReplyStatus(REPLY_PENDING);
+        invitation.setResponseStatus(RESPONSE_PENDING);
+        invitation.setSmsNotifyStatus(SMS_NOT_SENT);
+        invitation.setSmsContactId(resolveSmsContactId(request.getSmsContactId(), customer.getId()));
+        invitation.setRemark(request.getRemark());
+        VppAuditHelper.fillCreate(invitation);
+        invitationMapper.insert(invitation);
+        return toVo(invitation);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateInvitation(Long id, DrInvitationRequest request) {
+        VppDrInvitation invitation = requireInvitation(id);
+        if (invitation.getReplyStatus() != REPLY_PENDING || invitation.getResponseStatus() != RESPONSE_PENDING) {
+            throw new BusinessException("仅待回复状态的邀约可修改");
+        }
+        if (request == null) {
+            throw new BusinessException("请求不能为空");
+        }
+        if (request.getExecuteStartDate() == null || request.getExecuteEndDate() == null) {
+            throw new BusinessException("执行日期不能为空");
+        }
+        if (request.getReplyDeadline() == null) {
+            throw new BusinessException("邀约回复截止日期不能为空");
+        }
+        if (request.getDemandCapacityKw() == null || request.getDemandCapacityKw().compareTo(BigDecimal.ZERO) <= 0) {
+            throw new BusinessException("需求容量必须大于0");
+        }
+        validateDateRange(request.getExecuteStartDate(), request.getExecuteEndDate());
+
+        invitation.setExecuteStartDate(request.getExecuteStartDate());
+        invitation.setExecuteEndDate(request.getExecuteEndDate());
+        invitation.setReplyDeadline(request.getReplyDeadline());
+        invitation.setDemandCapacityKw(request.getDemandCapacityKw());
+        if (request.getDeclaredCapacityKw() != null) {
+            invitation.setDeclaredCapacityKw(request.getDeclaredCapacityKw());
+        }
+        if (request.getTransactionType() != null) {
+            invitation.setTransactionType(request.getTransactionType());
+        }
+        if (request.getResponseType() != null) {
+            invitation.setResponseType(request.getResponseType());
+        }
+        if (request.getSmsContactId() != null) {
+            requireContact(request.getSmsContactId(), invitation.getCustomerId());
+            invitation.setSmsContactId(request.getSmsContactId());
+        }
+        if (request.getRemark() != null) {
+            invitation.setRemark(request.getRemark());
+        }
+        VppAuditHelper.fillUpdate(invitation);
+        invitationMapper.updateById(invitation);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteInvitation(Long id) {
+        VppDrInvitation invitation = requireInvitation(id);
+        if (invitation.getReplyStatus() == REPLY_ACCEPT) {
+            throw new BusinessException("已参与的邀约不可删除");
+        }
+        if (invitation.getResponseStatus() != RESPONSE_PENDING) {
+            throw new BusinessException("仅待响应状态的邀约可删除");
+        }
+        if (invitation.getParticipationId() != null) {
+            throw new BusinessException("邀约已关联参与记录,不可删除");
+        }
+
+        VppAuditHelper.fillSoftDelete(invitation);
+        invitationMapper.updateById(invitation);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DrInvitationVO notifySms(Long id, DrInvitationNotifyRequest request) {
+        VppDrInvitation invitation = requireInvitation(id);
+        Long contactId = request != null && request.getContactId() != null
+                ? request.getContactId()
+                : invitation.getSmsContactId();
+        if (contactId == null) {
+            contactId = resolveSmsContactId(null, invitation.getCustomerId());
+            invitation.setSmsContactId(contactId);
+        }
+        VppCustomerContact contact = requireContact(contactId, invitation.getCustomerId());
+        if (!StringUtils.hasText(contact.getContactPhone())) {
+            throw new BusinessException("联系人手机号为空,无法发送短信");
+        }
+
+        VppDrEvent event = requireEvent(invitation.getDrEventId());
+        VppCustomer customer = requireCustomer(invitation.getCustomerId());
+        String templateCode = resolveTemplateCode(request);
+        String templateParam = VppSmsTemplateHelper.buildInvitationParam(invitation, event, customer, contact);
+
+        try {
+            aliyunSmsService.sendTemplateSms(contact.getContactPhone(), templateCode, templateParam);
+            invitation.setSmsNotifyStatus(SMS_SENT);
+            invitation.setSmsNotifyAt(LocalDateTime.now());
+        } catch (BusinessException ex) {
+            invitation.setSmsNotifyStatus(SMS_FAILED);
+            VppAuditHelper.fillUpdate(invitation);
+            invitationMapper.updateById(invitation);
+            throw ex;
+        } catch (Exception ex) {
+            log.warn("邀约短信发送失败 invitationId={}: {}", id, ex.getMessage());
+            invitation.setSmsNotifyStatus(SMS_FAILED);
+            VppAuditHelper.fillUpdate(invitation);
+            invitationMapper.updateById(invitation);
+            throw new BusinessException("短信发送失败: " + ex.getMessage());
+        }
+
+        VppAuditHelper.fillUpdate(invitation);
+        invitationMapper.updateById(invitation);
+        return toVo(invitation);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DrInvitationVO replyInvitation(Long id, DrInvitationReplyRequest request) {
+        VppDrInvitation invitation = requireInvitation(id);
+        if (invitation.getReplyStatus() != REPLY_PENDING) {
+            throw new BusinessException("当前邀约已回复");
+        }
+        if (invitation.getReplyDeadline() != null && LocalDateTime.now().isAfter(invitation.getReplyDeadline())) {
+            throw new BusinessException("已超过邀约回复截止时间");
+        }
+        if (request == null || request.getParticipate() == null) {
+            throw new BusinessException("回复结果不能为空");
+        }
+
+        if (Boolean.TRUE.equals(request.getParticipate())) {
+            BigDecimal declared = request.getDeclaredCapacityKw() != null
+                    ? request.getDeclaredCapacityKw()
+                    : invitation.getDeclaredCapacityKw();
+            if (declared == null || declared.compareTo(BigDecimal.ZERO) <= 0) {
+                throw new BusinessException("申报容量必须大于0");
+            }
+
+            VppDrParticipation participation = findOrCreateParticipation(invitation, declared);
+            invitation.setParticipationId(participation.getId());
+            invitation.setDeclaredCapacityKw(declared);
+            invitation.setReplyStatus(REPLY_ACCEPT);
+            invitation.setResponseStatus(RESPONSE_DECLARED);
+            syncParticipationToUn(invitation.getDrEventId(), true);
+        } else {
+            if (!StringUtils.hasText(request.getReason())) {
+                throw new BusinessException("拒绝原因不能为空");
+            }
+            invitation.setReplyStatus(REPLY_REJECT);
+            invitation.setResponseStatus(RESPONSE_CANCELLED);
+            invitation.setRemark(appendRemark(invitation.getRemark(), "拒绝原因: " + request.getReason()));
+            syncParticipationToUn(invitation.getDrEventId(), false);
+        }
+
+        VppAuditHelper.fillUpdate(invitation);
+        invitationMapper.updateById(invitation);
+        return toVo(invitation);
+    }
+
+    private void applyListFilters(LambdaQueryWrapper<VppDrInvitation> wrapper, Map<String, Object> params) {
+        if (params == null) {
+            return;
+        }
+        if (params.get("drEventId") != null) {
+            wrapper.eq(VppDrInvitation::getDrEventId, Long.parseLong(params.get("drEventId").toString()));
+        }
+        if (params.get("responseType") != null) {
+            wrapper.eq(VppDrInvitation::getResponseType, Integer.parseInt(params.get("responseType").toString()));
+        }
+        if (params.get("replyStatus") != null) {
+            wrapper.eq(VppDrInvitation::getReplyStatus, Integer.parseInt(params.get("replyStatus").toString()));
+        }
+        if (params.get("responseStatus") != null) {
+            wrapper.eq(VppDrInvitation::getResponseStatus, Integer.parseInt(params.get("responseStatus").toString()));
+        }
+        if (params.get("customerId") != null) {
+            wrapper.eq(VppDrInvitation::getCustomerId, Long.parseLong(params.get("customerId").toString()));
+        }
+        if (params.get("customerName") != null && StringUtils.hasText(params.get("customerName").toString())) {
+            List<Long> customerIds = customerMapper.selectList(new LambdaQueryWrapper<VppCustomer>()
+                            .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                            .like(VppCustomer::getCustomerName, params.get("customerName").toString()))
+                    .stream()
+                    .map(VppCustomer::getId)
+                    .collect(Collectors.toList());
+            if (customerIds.isEmpty()) {
+                wrapper.eq(VppDrInvitation::getCustomerId, -1L);
+            } else {
+                wrapper.in(VppDrInvitation::getCustomerId, customerIds);
+            }
+        }
+        if (params.get("issueDateStart") != null && StringUtils.hasText(params.get("issueDateStart").toString())) {
+            LocalDate start = LocalDate.parse(params.get("issueDateStart").toString());
+            wrapper.ge(VppDrInvitation::getIssueTime, start.atStartOfDay());
+        }
+        if (params.get("issueDateEnd") != null && StringUtils.hasText(params.get("issueDateEnd").toString())) {
+            LocalDate end = LocalDate.parse(params.get("issueDateEnd").toString());
+            wrapper.le(VppDrInvitation::getIssueTime, end.atTime(LocalTime.MAX));
+        }
+    }
+
+    private VppDrParticipation findOrCreateParticipation(VppDrInvitation invitation, BigDecimal declaredCapacityKw) {
+        VppDrParticipation existing = participationMapper.selectOne(new LambdaQueryWrapper<VppDrParticipation>()
+                .eq(VppDrParticipation::getEventId, invitation.getDrEventId())
+                .eq(VppDrParticipation::getCustomerId, invitation.getCustomerId())
+                .isNull(VppDrParticipation::getResourceId)
+                .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .last("LIMIT 1"));
+        if (existing != null) {
+            existing.setParticipateStatus(PARTICIPATE_ACCEPT);
+            existing.setDeclaredCapacityKw(declaredCapacityKw);
+            existing.setDeclaredAt(LocalDateTime.now());
+            VppAuditHelper.fillUpdate(existing);
+            participationMapper.updateById(existing);
+            return existing;
+        }
+
+        VppDrParticipation participation = new VppDrParticipation();
+        participation.setEventId(invitation.getDrEventId());
+        participation.setCustomerId(invitation.getCustomerId());
+        participation.setParticipateStatus(PARTICIPATE_ACCEPT);
+        participation.setDeclaredCapacityKw(declaredCapacityKw);
+        participation.setDeclaredAt(LocalDateTime.now());
+        VppAuditHelper.fillCreate(participation);
+        participationMapper.insert(participation);
+        return participation;
+    }
+
+    private void syncParticipationToUn(Long eventId, boolean participate) {
+        try {
+            unIntegrationService.submitParticipation(eventId, participate);
+        } catch (Exception ex) {
+            log.warn("同步 UN 邀约回复失败 eventId={}: {}", eventId, ex.getMessage());
+        }
+    }
+
+    private VppDrInvitation requireInvitation(Long id) {
+        VppDrInvitation invitation = invitationMapper.selectById(id);
+        if (invitation == null || VppAuditHelper.isDeleted(invitation.getDeleteFlag())) {
+            throw new BusinessException("邀约记录不存在");
+        }
+        return invitation;
+    }
+
+    private VppDrEvent requireEvent(Long id) {
+        VppDrEvent event = eventMapper.selectById(id);
+        if (event == null || VppAuditHelper.isDeleted(event.getDeleteFlag())) {
+            throw new BusinessException("需求响应事件不存在");
+        }
+        return event;
+    }
+
+    private VppCustomer requireCustomer(Long id) {
+        VppCustomer customer = customerMapper.selectById(id);
+        if (customer == null || VppAuditHelper.isDeleted(customer.getDeleteFlag())) {
+            throw new BusinessException("客户不存在");
+        }
+        return customer;
+    }
+
+    private VppCustomerContact requireContact(Long contactId, Long customerId) {
+        VppCustomerContact contact = contactMapper.selectById(contactId);
+        if (contact == null || VppAuditHelper.isDeleted(contact.getDeleteFlag())) {
+            throw new BusinessException("联系人不存在");
+        }
+        if (!Objects.equals(contact.getCustomerId(), customerId)) {
+            throw new BusinessException("联系人不属于该客户");
+        }
+        return contact;
+    }
+
+    private Long resolveSmsContactId(Long contactId, Long customerId) {
+        if (contactId != null) {
+            return requireContact(contactId, customerId).getId();
+        }
+        VppCustomerContact primary = contactMapper.selectOne(new LambdaQueryWrapper<VppCustomerContact>()
+                .eq(VppCustomerContact::getCustomerId, customerId)
+                .eq(VppCustomerContact::getIsPrimary, 1)
+                .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .last("LIMIT 1"));
+        if (primary != null) {
+            return primary.getId();
+        }
+        VppCustomerContact any = contactMapper.selectOne(new LambdaQueryWrapper<VppCustomerContact>()
+                .eq(VppCustomerContact::getCustomerId, customerId)
+                .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .last("LIMIT 1"));
+        return any != null ? any.getId() : null;
+    }
+
+    private void ensureInvitationUnique(Long drEventId, Long customerId, Long excludeId) {
+        LambdaQueryWrapper<VppDrInvitation> wrapper = new LambdaQueryWrapper<VppDrInvitation>()
+                .eq(VppDrInvitation::getDrEventId, drEventId)
+                .eq(VppDrInvitation::getCustomerId, customerId)
+                .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+        if (excludeId != null) {
+            wrapper.ne(VppDrInvitation::getId, excludeId);
+        }
+        if (invitationMapper.selectCount(wrapper) > 0) {
+            throw new BusinessException("该事件已存在该企业的邀约记录");
+        }
+    }
+
+    private void validateCreateRequest(DrInvitationRequest request) {
+        if (request == null) {
+            throw new BusinessException("请求不能为空");
+        }
+        if (request.getDrEventId() == null) {
+            throw new BusinessException("需求响应事件不能为空");
+        }
+        if (request.getCustomerId() == null) {
+            throw new BusinessException("企业不能为空");
+        }
+        if (request.getExecuteStartDate() == null || request.getExecuteEndDate() == null) {
+            throw new BusinessException("执行日期不能为空");
+        }
+        if (request.getReplyDeadline() == null) {
+            throw new BusinessException("邀约回复截止日期不能为空");
+        }
+        if (request.getDemandCapacityKw() == null || request.getDemandCapacityKw().compareTo(BigDecimal.ZERO) <= 0) {
+            throw new BusinessException("需求容量必须大于0");
+        }
+        validateDateRange(request.getExecuteStartDate(), request.getExecuteEndDate());
+    }
+
+    private void validateDateRange(LocalDate start, LocalDate end) {
+        if (start.isAfter(end)) {
+            throw new BusinessException("执行开始日期不能晚于结束日期");
+        }
+    }
+
+    private String appendRemark(String original, String addition) {
+        if (!StringUtils.hasText(original)) {
+            return addition;
+        }
+        return original + "; " + addition;
+    }
+
+    private List<DrInvitationVO> toVoList(List<VppDrInvitation> invitations) {
+        if (CollectionUtils.isEmpty(invitations)) {
+            return Collections.emptyList();
+        }
+        Set<Long> eventIds = invitations.stream().map(VppDrInvitation::getDrEventId).collect(Collectors.toSet());
+        Set<Long> customerIds = invitations.stream().map(VppDrInvitation::getCustomerId).collect(Collectors.toSet());
+
+        Map<Long, VppDrEvent> eventMap = eventMapper.selectBatchIds(eventIds).stream()
+                .collect(Collectors.toMap(VppDrEvent::getId, e -> e, (a, b) -> a));
+        Map<Long, VppCustomer> customerMap = customerMapper.selectBatchIds(customerIds).stream()
+                .collect(Collectors.toMap(VppCustomer::getId, c -> c, (a, b) -> a));
+
+        return invitations.stream()
+                .map(inv -> toVo(inv, eventMap.get(inv.getDrEventId()), customerMap.get(inv.getCustomerId()), null))
+                .collect(Collectors.toList());
+    }
+
+    private DrInvitationVO toVo(VppDrInvitation invitation) {
+        VppDrEvent event = eventMapper.selectById(invitation.getDrEventId());
+        VppCustomer customer = customerMapper.selectById(invitation.getCustomerId());
+        VppCustomerContact contact = invitation.getSmsContactId() != null
+                ? contactMapper.selectById(invitation.getSmsContactId())
+                : null;
+        return toVo(invitation, event, customer, contact);
+    }
+
+    private DrInvitationVO toVo(VppDrInvitation invitation, VppDrEvent event, VppCustomer customer, VppCustomerContact contact) {
+        DrInvitationVO vo = new DrInvitationVO();
+        BeanUtils.copyProperties(invitation, vo);
+        if (event != null) {
+            vo.setDrEventCode(event.getEventId());
+        }
+        if (customer != null) {
+            vo.setCustomerName(customer.getCustomerName());
+        }
+        if (contact != null) {
+            vo.setSmsContactName(contact.getContactName());
+            vo.setSmsContactPhone(contact.getContactPhone());
+        }
+        return vo;
+    }
+
+    private String resolveTemplateCode(DrInvitationNotifyRequest request) {
+        if (request != null && StringUtils.hasText(request.getTemplateCode())) {
+            return request.getTemplateCode().trim();
+        }
+        if (StringUtils.hasText(smsProperties.getInvitationTemplateCode())) {
+            return smsProperties.getInvitationTemplateCode().trim();
+        }
+        throw new BusinessException("短信模板未配置,请设置 vpp.sms.invitation-template-code 或在请求中传入 templateCode");
+    }
+}

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

@@ -21,9 +21,11 @@ import com.usky.vpp.mapper.VppResourcePointMapper;
 import com.usky.vpp.service.VppDrService;
 import com.usky.vpp.service.VppUnIntegrationService;
 import com.usky.vpp.service.vo.DrClearingRequest;
+import com.usky.vpp.service.vo.DrEventRequest;
 import com.usky.vpp.service.vo.DrInterveneRequest;
 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.util.VppAuditHelper;
 import com.usky.vpp.util.VppPageHelper;
 import com.usky.vpp.util.VppSiteResourceHelper;
@@ -114,6 +116,72 @@ public class VppDrServiceImpl implements VppDrService {
         return event;
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public VppDrEvent createEvent(DrEventRequest request) {
+        validateEventRequest(request);
+
+        String eventId = StringUtils.hasText(request.getEventId())
+                ? request.getEventId().trim()
+                : VppAuditHelper.nextEventId();
+        ensureEventIdUnique(eventId, null);
+
+        VppDrEvent event = new VppDrEvent();
+        event.setEventId(eventId);
+        event.setEventName(StringUtils.hasText(request.getEventName()) ? request.getEventName() : eventId);
+        event.setResponseType(request.getResponseType());
+        event.setEventType(request.getEventType());
+        event.setStartTime(request.getStartTime());
+        event.setEndTime(request.getEndTime());
+        event.setTargetCapacityKw(request.getTargetCapacityKw());
+        event.setClearedCapacityKw(request.getClearedCapacityKw());
+        event.setSubsidyPrice(request.getSubsidyPrice());
+        event.setEventStatus(EVENT_STATUS_PENDING);
+        VppAuditHelper.fillCreate(event);
+        eventMapper.insert(event);
+        return event;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateEvent(Long id, DrEventRequest request) {
+        VppDrEvent event = getEvent(id);
+        if (event.getEventStatus() != EVENT_STATUS_PENDING) {
+            throw new BusinessException("仅待参与状态的事件可修改");
+        }
+        if (hasEventRelatedData(id)) {
+            throw new BusinessException("事件已有关联参与记录,不可修改");
+        }
+
+        validateEventRequest(request);
+
+        event.setEventName(StringUtils.hasText(request.getEventName()) ? request.getEventName() : event.getEventId());
+        event.setResponseType(request.getResponseType());
+        event.setEventType(request.getEventType());
+        event.setStartTime(request.getStartTime());
+        event.setEndTime(request.getEndTime());
+        event.setTargetCapacityKw(request.getTargetCapacityKw());
+        event.setClearedCapacityKw(request.getClearedCapacityKw());
+        event.setSubsidyPrice(request.getSubsidyPrice());
+        VppAuditHelper.fillUpdate(event);
+        eventMapper.updateById(event);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteEvent(Long id) {
+        VppDrEvent event = getEvent(id);
+        if (event.getEventStatus() != EVENT_STATUS_PENDING && event.getEventStatus() != EVENT_STATUS_CANCELLED) {
+            throw new BusinessException("仅待参与或已取消状态的事件可删除");
+        }
+        if (hasEventRelatedData(id)) {
+            throw new BusinessException("事件已有关联业务数据,不可删除");
+        }
+
+        VppAuditHelper.fillSoftDelete(event);
+        eventMapper.updateById(event);
+    }
+
     @Override
     public Object assessCapability(Long eventId) {
         VppDrEvent event = getEvent(eventId);
@@ -567,6 +635,15 @@ public class VppDrServiceImpl implements VppDrService {
         return new CommonPage<>(result.getRecords(), result.getTotal(), result.getCurrent(), result.getSize());
     }
 
+    @Override
+    public DrStrategyVO getStrategy(Long id) {
+        VppDrStrategy strategy = strategyMapper.selectById(id);
+        if (strategy == null || VppAuditHelper.isDeleted(strategy.getDeleteFlag())) {
+            throw new BusinessException("策略不存在");
+        }
+        return toStrategyVo(strategy);
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public VppDrStrategy createStrategy(DrStrategyRequest request) {
@@ -684,6 +761,119 @@ public class VppDrServiceImpl implements VppDrService {
                 .eq(VppDrStrategyResource::getStrategyId, id));
     }
 
+    private void validateEventRequest(DrEventRequest request) {
+        if (request == null) {
+            throw new BusinessException("请求不能为空");
+        }
+        if (request.getResponseType() == null) {
+            throw new BusinessException("响应类型不能为空");
+        }
+        if (request.getResponseType() < 1 || request.getResponseType() > 3) {
+            throw new BusinessException("响应类型无效,取值 1日前 2日内 3秒级");
+        }
+        if (request.getEventType() != null && (request.getEventType() < 1 || request.getEventType() > 2)) {
+            throw new BusinessException("事件类型无效,取值 1削峰 2填谷");
+        }
+        if (request.getStartTime() == null) {
+            throw new BusinessException("响应开始时间不能为空");
+        }
+        if (request.getEndTime() == null) {
+            throw new BusinessException("响应结束时间不能为空");
+        }
+        if (request.getEndTime().isBefore(request.getStartTime())) {
+            throw new BusinessException("响应结束时间不能早于开始时间");
+        }
+        if (request.getTargetCapacityKw() == null || request.getTargetCapacityKw().compareTo(BigDecimal.ZERO) <= 0) {
+            throw new BusinessException("目标容量必须大于0");
+        }
+        if (request.getClearedCapacityKw() != null && request.getClearedCapacityKw().compareTo(BigDecimal.ZERO) < 0) {
+            throw new BusinessException("出清容量不能为负数");
+        }
+        if (request.getSubsidyPrice() != null && request.getSubsidyPrice().compareTo(BigDecimal.ZERO) < 0) {
+            throw new BusinessException("补贴标准不能为负数");
+        }
+    }
+
+    private void ensureEventIdUnique(String eventId, Long excludeId) {
+        LambdaQueryWrapper<VppDrEvent> wrapper = new LambdaQueryWrapper<VppDrEvent>()
+                .eq(VppDrEvent::getEventId, eventId)
+                .eq(VppDrEvent::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+        if (excludeId != null) {
+            wrapper.ne(VppDrEvent::getId, excludeId);
+        }
+        if (eventMapper.selectCount(wrapper) > 0) {
+            throw new BusinessException("事件ID已存在");
+        }
+    }
+
+    private boolean hasEventRelatedData(Long eventId) {
+        long participationCount = participationMapper.selectCount(
+                new LambdaQueryWrapper<VppDrParticipation>()
+                        .eq(VppDrParticipation::getEventId, eventId)
+                        .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        if (participationCount > 0) {
+            return true;
+        }
+        long executionCount = executionMapper.selectCount(
+                new LambdaQueryWrapper<VppDrExecution>()
+                        .eq(VppDrExecution::getEventId, eventId)
+                        .eq(VppDrExecution::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        if (executionCount > 0) {
+            return true;
+        }
+        return evaluationMapper.selectCount(
+                new LambdaQueryWrapper<VppDrEvaluation>()
+                        .eq(VppDrEvaluation::getEventId, eventId)
+                        .eq(VppDrEvaluation::getDeleteFlag, VppAuditHelper.NOT_DELETED)) > 0;
+    }
+
+    private DrStrategyVO toStrategyVo(VppDrStrategy strategy) {
+        DrStrategyVO vo = new DrStrategyVO();
+        vo.setId(strategy.getId());
+        vo.setStrategyCode(strategy.getStrategyCode());
+        vo.setStrategyName(strategy.getStrategyName());
+        vo.setResponseType(strategy.getResponseType());
+        vo.setAdjustStepKw(strategy.getAdjustStepKw());
+        vo.setExecuteMode(strategy.getExecuteMode());
+        vo.setStrategyConfig(strategy.getStrategyConfig());
+        vo.setIsDefault(strategy.getIsDefault());
+        vo.setIsEnabled(strategy.getIsEnabled());
+        vo.setTenantId(strategy.getTenantId());
+        vo.setCreateTime(strategy.getCreateTime());
+        vo.setCreatedBy(strategy.getCreatedBy());
+
+        List<VppDrStrategyResource> strategyResources = strategyResourceMapper.selectList(
+                new LambdaQueryWrapper<VppDrStrategyResource>()
+                        .eq(VppDrStrategyResource::getStrategyId, strategy.getId())
+                        .orderByAsc(VppDrStrategyResource::getPriority)
+        );
+        if (strategyResources.isEmpty()) {
+            vo.setResources(Collections.emptyList());
+            return vo;
+        }
+
+        Set<Long> resourceIds = strategyResources.stream()
+                .map(VppDrStrategyResource::getResourceId)
+                .collect(Collectors.toSet());
+        Map<Long, VppResourcePoint> resourceMap = resourcePointMapper.selectBatchIds(resourceIds).stream()
+                .filter(rp -> !VppAuditHelper.isDeleted(rp.getDeleteFlag()))
+                .collect(Collectors.toMap(VppResourcePoint::getId, rp -> rp, (a, b) -> a));
+
+        List<DrStrategyVO.DrStrategyResourceVO> resources = strategyResources.stream().map(sr -> {
+            DrStrategyVO.DrStrategyResourceVO item = new DrStrategyVO.DrStrategyResourceVO();
+            item.setResourceId(sr.getResourceId());
+            item.setPriority(sr.getPriority());
+            item.setMaxAdjustKw(sr.getMaxAdjustKw());
+            VppResourcePoint resourcePoint = resourceMap.get(sr.getResourceId());
+            if (resourcePoint != null) {
+                item.setResourceName(resourcePoint.getResourceName());
+            }
+            return item;
+        }).collect(Collectors.toList());
+        vo.setResources(resources);
+        return vo;
+    }
+
     private void validateStrategyRequest(DrStrategyRequest request) {
         if (request == null) {
             throw new BusinessException("请求不能为空");

+ 170 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppTsdbQueryServiceImpl.java

@@ -0,0 +1,170 @@
+package com.usky.vpp.service.impl;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.demo.RemoteTsdbProxyService;
+import com.usky.demo.domain.HistorysInnerRequestVO;
+import com.usky.demo.domain.HistorysInnerResultVO;
+import com.usky.demo.domain.LastInnerQueryVO;
+import com.usky.demo.domain.LastInnerResultVO;
+import com.usky.demo.domain.MetricVO;
+import com.usky.vpp.constant.VppTsdbConstants;
+import com.usky.vpp.service.VppTsdbQueryService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+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.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.temporal.ChronoField;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * TDengine 时序数据查询(通过 service-tsdb Feign,与 service-ems 一致)
+ */
+@Service
+public class VppTsdbQueryServiceImpl implements VppTsdbQueryService {
+
+    private static final Logger log = LoggerFactory.getLogger(VppTsdbQueryServiceImpl.class);
+
+    private static final DateTimeFormatter TSDB_FLEXIBLE_TIME_FORMAT = new DateTimeFormatterBuilder()
+            .appendPattern("yyyy-MM-dd HH:mm:ss")
+            .optionalStart()
+            .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
+            .optionalEnd()
+            .toFormatter();
+
+    @Autowired
+    private RemoteTsdbProxyService remoteTsdbProxyService;
+
+    @Override
+    public Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> queryDeviceMetricHistory(
+            List<String> deviceUuids,
+            LocalDateTime startTime,
+            LocalDateTime endTime,
+            List<String> metrics) {
+        if (CollectionUtils.isEmpty(deviceUuids) || startTime == null || endTime == null) {
+            return Collections.emptyMap();
+        }
+
+        HistorysInnerRequestVO request = new HistorysInnerRequestVO();
+        request.setDeviceuuid(new ArrayList<>(new LinkedHashSet<>(deviceUuids)));
+        request.setStartTime(startTime.format(DateTimeFormatter.ofPattern(VppTsdbConstants.TIME_FORMAT)));
+        request.setEndTime(endTime.format(DateTimeFormatter.ofPattern(VppTsdbConstants.TIME_FORMAT)));
+        if (!CollectionUtils.isEmpty(metrics)) {
+            request.setMetrics(new ArrayList<>(metrics));
+        }
+
+        try {
+            ApiResult<List<HistorysInnerResultVO>> result = remoteTsdbProxyService.queryHistoryDeviceData(request);
+            if (result == null || !result.isSuccess() || CollectionUtils.isEmpty(result.getData())) {
+                log.warn("TDengine 历史查询无数据, devices={}, start={}, end={}",
+                        deviceUuids.size(), request.getStartTime(), request.getEndTime());
+                return Collections.emptyMap();
+            }
+            return parseHistoryResult(result.getData());
+        } catch (Exception ex) {
+            log.error("TDengine 历史查询失败: {}", ex.getMessage(), ex);
+            return Collections.emptyMap();
+        }
+    }
+
+    @Override
+    public Map<String, Map<String, BigDecimal>> queryLatestMetrics(List<String> deviceUuids, List<String> metrics) {
+        if (CollectionUtils.isEmpty(deviceUuids)) {
+            return Collections.emptyMap();
+        }
+
+        LastInnerQueryVO request = new LastInnerQueryVO();
+        request.setDeviceuuid(new ArrayList<>(new LinkedHashSet<>(deviceUuids)));
+        if (!CollectionUtils.isEmpty(metrics)) {
+            request.setMetrics(new ArrayList<>(metrics));
+        }
+
+        try {
+            ApiResult<List<LastInnerResultVO>> result = remoteTsdbProxyService.queryLastDeviceData(request);
+            if (result == null || !result.isSuccess() || CollectionUtils.isEmpty(result.getData())) {
+                return Collections.emptyMap();
+            }
+            return parseLatestResult(result.getData());
+        } catch (Exception ex) {
+            log.error("TDengine 最新值查询失败: {}", ex.getMessage(), ex);
+            return Collections.emptyMap();
+        }
+    }
+
+    private Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> parseHistoryResult(
+            List<HistorysInnerResultVO> historyDataList) {
+        Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> deviceMetricMap = new HashMap<>();
+        for (HistorysInnerResultVO result : historyDataList) {
+            if (result == null || !StringUtils.hasText(result.getDeviceuuid()) || CollectionUtils.isEmpty(result.getMetrics())) {
+                continue;
+            }
+            String deviceUuid = result.getDeviceuuid();
+            Map<String, TreeMap<LocalDateTime, BigDecimal>> metricMap =
+                    deviceMetricMap.computeIfAbsent(deviceUuid, key -> new HashMap<>());
+
+            for (MetricVO metric : result.getMetrics()) {
+                if (metric == null || !StringUtils.hasText(metric.getMetric()) || CollectionUtils.isEmpty(metric.getMetricItems())) {
+                    continue;
+                }
+                TreeMap<LocalDateTime, BigDecimal> timeValueMap =
+                        metricMap.computeIfAbsent(metric.getMetric(), key -> new TreeMap<>());
+                for (Map<String, Object> point : metric.getMetricItems()) {
+                    if (point == null || point.get("timestamp") == null || point.get("value") == null) {
+                        continue;
+                    }
+                    try {
+                        LocalDateTime timestamp = parseTimestamp(point.get("timestamp").toString());
+                        BigDecimal value = new BigDecimal(point.get("value").toString());
+                        timeValueMap.put(timestamp, value);
+                    } catch (Exception ignored) {
+                        // skip invalid point
+                    }
+                }
+            }
+        }
+        return deviceMetricMap;
+    }
+
+    private Map<String, Map<String, BigDecimal>> parseLatestResult(List<LastInnerResultVO> latestList) {
+        Map<String, Map<String, BigDecimal>> result = new HashMap<>();
+        for (LastInnerResultVO item : latestList) {
+            if (item == null || !StringUtils.hasText(item.getDeviceuuid()) || item.getMetrics() == null) {
+                continue;
+            }
+            Map<String, BigDecimal> metricValues = new HashMap<>();
+            item.getMetrics().forEach((metric, value) -> {
+                if (value == null) {
+                    return;
+                }
+                try {
+                    metricValues.put(metric, new BigDecimal(value.toString()).setScale(4, RoundingMode.HALF_UP));
+                } catch (Exception ignored) {
+                    // skip invalid value
+                }
+            });
+            result.put(item.getDeviceuuid(), metricValues);
+        }
+        return result;
+    }
+
+    private LocalDateTime parseTimestamp(String timestampStr) {
+        String normalized = timestampStr.trim();
+        if (normalized.endsWith(".0")) {
+            normalized = normalized.substring(0, normalized.length() - 2);
+        }
+        return LocalDateTime.parse(normalized, TSDB_FLEXIBLE_TIME_FORMAT);
+    }
+}

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

@@ -0,0 +1,21 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 可调能力评估 - 分类统计(户数/容量)
+ */
+@Data
+public class CapabilityCategoryStatVO {
+
+    /** TOTAL / LOAD / STORAGE / GENERATION */
+    private String category;
+    /** 分类中文名 */
+    private String categoryLabel;
+    /** 户数(去重客户数) */
+    private Integer customerCount;
+    /** 装机容量 kW */
+    private BigDecimal capacityKw;
+}

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

@@ -0,0 +1,27 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 可调能力评估 - 顶部汇总卡片
+ */
+@Data
+public class CapabilityEvalSummaryVO {
+
+    private Long siteId;
+    private String siteName;
+    /** 总资源 */
+    private CapabilityCategoryStatVO total;
+    /** 负荷类 */
+    private CapabilityCategoryStatVO load;
+    /** 储能类 */
+    private CapabilityCategoryStatVO storage;
+    /** 发电类 */
+    private CapabilityCategoryStatVO generation;
+    /** 最大上调能力 kW */
+    private BigDecimal maxUpKw;
+    /** 最大下调能力 kW */
+    private BigDecimal maxDownKw;
+}

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

@@ -0,0 +1,31 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 可调能力评估 - 历史响应记录
+ */
+@Data
+public class CapabilityHistoryVO {
+
+    private Long eventId;
+    private String eventCode;
+    private String eventName;
+    /** 1削峰 2填谷 */
+    private Integer eventType;
+    private String eventTypeLabel;
+    /** 1日前 2日内 3秒级 */
+    private Integer responseType;
+    private String responseTypeLabel;
+    private LocalDateTime startTime;
+    private LocalDateTime endTime;
+    /** 目标/申报容量 kW */
+    private BigDecimal targetCapacityKw;
+    /** 实际响应容量 kW */
+    private BigDecimal actualCapacityKw;
+    /** 达标率 % */
+    private BigDecimal qualifiedRate;
+}

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

@@ -0,0 +1,20 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 可调能力曲线
+ */
+@Data
+public class CapabilityLoadCurveVO {
+
+    private Long siteId;
+    private String siteName;
+    /** 统计日期 yyyy-MM-dd */
+    private String date;
+    /** 数据来源:tsdb / mock */
+    private String dataSource;
+    private List<CapabilityLoadPointVO> points;
+}

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

@@ -0,0 +1,19 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 可调能力曲线 - 单时间点负荷
+ */
+@Data
+public class CapabilityLoadPointVO {
+
+    /** 时刻 HH:mm */
+    private String time;
+    /** 基线负荷 kW */
+    private BigDecimal baselineLoadKw;
+    /** 实际负荷 kW */
+    private BigDecimal actualLoadKw;
+}

+ 3 - 2
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CustomerAccessAuditRequest.java

@@ -8,6 +8,7 @@ import lombok.Data;
 @Data
 public class CustomerAccessAuditRequest {
 
-    private Boolean passed;
-    private String auditOpinion;
+    private Long id;
+    private Integer result;
+    private String comment;
 }

+ 73 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CustomerAccessRequestVO.java

@@ -0,0 +1,73 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 准入申请 请求VO
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/6
+ */
+@Data
+public class CustomerAccessRequestVO {
+
+    /**
+     * 电力户号
+     */
+    private String accountNo;
+
+    /**
+     * 客户名称
+     */
+    private String customerName;
+
+    /**
+     * 客户类型:1中高压 2低压商用 3居民充电桩 4自有资产
+     */
+    private Integer customerType;
+
+    /**
+     * 供电公司
+     */
+    private String powerCompany;
+
+    /**
+     * 签约容量
+     */
+    private BigDecimal contractCapacity;
+
+    /**
+     * 营业执照URL
+     */
+    private String businessLicenseUrl;
+
+    /**
+     * 信用状态:1正常 2关注 3不良
+     */
+    private Integer creditStatus;
+
+    /**
+     * 签约日期
+     */
+    private LocalDateTime signingDate;
+
+    /**
+     * 联系人姓名
+     */
+    private String contactName;
+
+    /**
+     * 联系电话
+     */
+    private String contactPhone;
+
+    /**
+     * 联系邮箱
+     */
+    private String contactEmail;
+}

+ 109 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CustomerAccessResponseVO.java

@@ -0,0 +1,109 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 准入申请 响应VO
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/6
+ */
+@Data
+public class CustomerAccessResponseVO {
+
+    private Long id;
+
+    /**
+     * 申请编号
+     */
+    private String applyNo;
+
+    /**
+     * 电力户号
+     */
+    private String accountNo;
+
+    /**
+     * 客户名称
+     */
+    private String customerName;
+
+    /**
+     * 客户类型:1中高压 2低压商用 3居民充电桩 4自有资产
+     */
+    private Integer customerType;
+
+    /**
+     * 供电公司
+     */
+    private String powerCompany;
+
+    /**
+     * 签约容量
+     */
+    private BigDecimal contractCapacity;
+
+    /**
+     * 营业执照URL
+     */
+    private String businessLicenseUrl;
+
+    /**
+     * 信用状态:1正常 2关注 3不良
+     */
+    private Integer creditStatus;
+
+    /**
+     * 准入状态:0待审核 1通过 2拒绝
+     */
+    private Integer accessStatus;
+
+    /**
+     * 审核意见
+     */
+    private String auditOpinion;
+
+    /**
+     * 审核时间
+     */
+    private LocalDateTime auditAt;
+
+    /**
+     * 关联客户ID(审核通过后生成)
+     */
+    private Long customerId;
+
+    /**
+     * 申请时间
+     */
+    private LocalDateTime applyAt;
+
+    /**
+     * 租户ID
+     */
+    private Integer tenantId;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 创建人
+     */
+    private String createdBy;
+
+    /**
+     * 更新人
+     */
+    private String updatedBy;
+}

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

@@ -0,0 +1,174 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+
+/**
+ * 客户管理 请求VO(用于更新客户信息)
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/6
+ */
+@Data
+public class CustomerRequestVO {
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 电力户号(新增时必填)
+     */
+    private String accountNo;
+
+    /**
+     * 客户名称
+     */
+    private String customerName;
+
+    /**
+     * 客户类型:1中高压 2低压商用 3居民充电桩 4自有资产
+     */
+    private Integer customerType;
+
+    /**
+     * 供电公司
+     */
+    private String powerCompany;
+
+    /**
+     * 签约容量
+     */
+    private BigDecimal contractCapacity;
+
+    /**
+     * 信用状态:1正常 2关注 3不良
+     */
+    private Integer creditStatus;
+
+    /**
+     * 签约状态:1待签约 2已签约 3履约中 4待续约 5已续约 6已解约
+     */
+    private Integer lifecycleStatus;
+
+    /**
+     * 需求响应提前通知分钟数,默认30
+     */
+    private Integer drNotifyMinutes;
+
+    /**
+     * 需求响应上调能力 kW
+     */
+    private BigDecimal drUpCapacityKw;
+
+    /**
+     * 需求响应下调能力 kW
+     */
+    private BigDecimal drDownCapacityKw;
+
+    /**
+     * 省
+     */
+    private String province;
+
+    /**
+     * 市
+     */
+    private String city;
+
+    /**
+     * 区/县
+     */
+    private String district;
+
+    /**
+     * 地址
+     */
+    private String address;
+
+    /**
+     * 营业执照URL
+     */
+    private String businessLicenseUrl;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 是否虚拟电厂资源:0否 1是
+     */
+    private Integer isVppResource;
+
+    /**
+     * 虚拟电厂分类:1充换电 2分布式三联供 3楼宇空调 5用户侧储能 6数据中心 7工业负荷 8其他
+     */
+    private String vppCategory;
+
+    /**
+     * 需求响应资源分类:1工业负荷 2商业负荷 3储能 4电动汽车 5居民负荷 6其他
+     */
+    private String drResourceType;
+
+    /**
+     * 聚合代理用户虚拟电厂编码
+     */
+    private String vppProxyCode;
+
+    /**
+     * 所属行业:1制造业 2建筑业 3批发和零售业 ... 21其他
+     */
+    private String industry;
+
+    /**
+     * 所属产业:1第一产业 2第二产业 3第三产业
+     */
+    private String industrySector;
+
+    /**
+     * 运行容量(kW)
+     */
+    private BigDecimal runningCapacity;
+
+    /**
+     * 生产经营开始时间
+     */
+    private LocalTime productionStartDate;
+
+    /**
+     * 生产经营结束时间
+     */
+    private LocalTime productionEndDate;
+
+    /**
+     * 用电地址
+     */
+    private String powerAddress;
+
+    /**
+     * 签约日期
+     */
+    private LocalDateTime signingDate;
+
+    /**
+     * 联系人姓名
+     */
+    private String contactName;
+
+    /**
+     * 联系电话
+     */
+    private String contactPhone;
+
+    /**
+     * 联系邮箱
+     */
+    private String contactEmail;
+}

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

@@ -0,0 +1,209 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.List;
+
+/**
+ * 客户管理 响应VO(含关联合同信息)
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/6
+ */
+@Data
+public class CustomerResponseVO {
+
+    // ========== 客户基础字段 ==========
+
+    private Long id;
+
+    private String accountNo;
+
+    private String customerName;
+
+    /**
+     * 客户类型:1中高压 2低压商用 3居民充电桩 4自有资产
+     */
+    private Integer customerType;
+
+    private String powerCompany;
+
+    private BigDecimal contractCapacity;
+
+    /**
+     * 信用状态:1正常 2关注 3不良
+     */
+    private Integer creditStatus;
+
+    /**
+     * 签约状态:1待签约 2已签约 3履约中 4待续约 5已续约 6已解约
+     */
+    private Integer lifecycleStatus;
+
+    /**
+     * 需求响应提前通知分钟数
+     */
+    private Integer drNotifyMinutes;
+
+    /**
+     * 需求响应上调能力 kW
+     */
+    private BigDecimal drUpCapacityKw;
+
+    /**
+     * 需求响应下调能力 kW
+     */
+    private BigDecimal drDownCapacityKw;
+
+    private String province;
+
+    private String city;
+
+    private String district;
+
+    private String address;
+
+    private String businessLicenseUrl;
+
+    private String remark;
+
+    /**
+     * 是否虚拟电厂资源:0否 1是
+     */
+    private Integer isVppResource;
+
+    /**
+     * 虚拟电厂分类
+     */
+    private String vppCategory;
+
+    /**
+     * 需求响应资源分类
+     */
+    private String drResourceType;
+
+    /**
+     * 聚合代理用户虚拟电厂编码
+     */
+    private String vppProxyCode;
+
+    /**
+     * 所属行业
+     */
+    private String industry;
+
+    /**
+     * 所属产业
+     */
+    private String industrySector;
+
+    /**
+     * 运行容量(kW)
+     */
+    private BigDecimal runningCapacity;
+
+    /**
+     * 生产经营开始时间
+     */
+    private LocalTime productionStartDate;
+
+    /**
+     * 生产经营结束时间
+     */
+    private LocalTime productionEndDate;
+
+    /**
+     * 用电地址
+     */
+    private String powerAddress;
+
+    /**
+     * 签约时间
+     */
+    private LocalDateTime signingDate;
+
+    // ========== 审计字段 ==========
+
+    private Integer tenantId;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+
+    private String createdBy;
+
+    private String updatedBy;
+
+    // ========== 关联合同信息 ==========
+
+    /**
+     * 关联合同列表
+     */
+    private List<ContractBriefVO> contracts;
+
+    // ========== 关联联系人信息 ==========
+
+    /**
+     * 主联系人姓名
+     */
+    private String contactName;
+
+    /**
+     * 主联系电话
+     */
+    private String contactPhone;
+
+    /**
+     * 主联系邮箱
+     */
+    private String contactEmail;
+
+    /**
+     * 合同简要信息(嵌套VO)
+     */
+    @Data
+    public static class ContractBriefVO {
+
+        private Long id;
+
+        /**
+         * 合同编号
+         */
+        private String contractNo;
+
+        /**
+         * 合同名称
+         */
+        private String contractName;
+
+        /**
+         * 合同类型:1购售电 2需求响应合作 3聚合代理 4服务代理 5居民充电桩 6自有资产
+         */
+        private Integer contractType;
+
+        /**
+         * 合同状态:0草稿 1审核中 2已生效 3已到期 4已终止
+         */
+        private Integer contractStatus;
+
+        /**
+         * 签约日期
+         */
+        private LocalDate signDate;
+
+        /**
+         * 生效日期
+         */
+        private LocalDate effectiveDate;
+
+        /**
+         * 到期日期
+         */
+        private LocalDate expireDate;
+    }
+}

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

@@ -0,0 +1,26 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 需求响应事件创建/更新请求(手工录入,非 UN 同步)
+ */
+@Data
+public class DrEventRequest {
+
+    /** 运管平台事件 ID,创建时可选,不传则自动生成 */
+    private String eventId;
+    private String eventName;
+    /** 1日前 2日内 3秒级 */
+    private Integer responseType;
+    /** 1削峰 2填谷 */
+    private Integer eventType;
+    private LocalDateTime startTime;
+    private LocalDateTime endTime;
+    private BigDecimal targetCapacityKw;
+    private BigDecimal clearedCapacityKw;
+    private BigDecimal subsidyPrice;
+}

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

@@ -0,0 +1,13 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+/**
+ * 邀约短信通知请求
+ */
+@Data
+public class DrInvitationNotifyRequest {
+
+    private Long contactId;
+    private String templateCode;
+}

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

@@ -0,0 +1,16 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 企业回复邀约请求
+ */
+@Data
+public class DrInvitationReplyRequest {
+
+    private Boolean participate;
+    private BigDecimal declaredCapacityKw;
+    private String reason;
+}

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

@@ -0,0 +1,28 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 需求响应邀约创建/更新请求
+ */
+@Data
+public class DrInvitationRequest {
+
+    private Long drEventId;
+    private Long customerId;
+    private LocalDate executeStartDate;
+    private LocalDate executeEndDate;
+    private LocalDateTime replyDeadline;
+    private BigDecimal demandCapacityKw;
+    private BigDecimal declaredCapacityKw;
+    /** 1削峰 2填谷 */
+    private Integer transactionType;
+    /** 1日前 2日内 3秒级 */
+    private Integer responseType;
+    private Long smsContactId;
+    private String remark;
+}

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

@@ -0,0 +1,43 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 需求响应邀约展示对象
+ */
+@Data
+public class DrInvitationVO {
+
+    private Long id;
+    private String invitationNo;
+    private Long drEventId;
+    private String drEventCode;
+    private Long customerId;
+    private String customerName;
+    private LocalDate executeStartDate;
+    private LocalDate executeEndDate;
+    private LocalDateTime replyDeadline;
+    private LocalDateTime issueTime;
+    private Integer transactionType;
+    private Integer responseType;
+    private BigDecimal demandCapacityKw;
+    private BigDecimal declaredCapacityKw;
+    private Integer replyStatus;
+    private Integer responseStatus;
+    private Integer smsNotifyStatus;
+    private LocalDateTime smsNotifyAt;
+    private Long smsContactId;
+    private String smsContactName;
+    private String smsContactPhone;
+    private Long participationId;
+    private String remark;
+    private Integer tenantId;
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+    private String createdBy;
+    private String updatedBy;
+}

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

@@ -1,5 +1,8 @@
 package com.usky.vpp.service.vo;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.JsonNode;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -16,11 +19,26 @@ public class DrStrategyRequest {
     private Integer responseType;
     private BigDecimal adjustStepKw;
     private Integer executeMode;
-    private String strategyConfig;
+    @JsonProperty("strategyConfig")
+    private JsonNode strategyConfigJson;
     private Integer isDefault;
     private Integer isEnabled;
     private List<DrStrategyResource> resources;
 
+    /**
+     * 策略扩展配置 JSON 字符串(兼容对象或字符串入参)
+     */
+    @JsonIgnore
+    public String getStrategyConfig() {
+        if (strategyConfigJson == null || strategyConfigJson.isNull()) {
+            return null;
+        }
+        if (strategyConfigJson.isTextual()) {
+            return strategyConfigJson.asText();
+        }
+        return strategyConfigJson.toString();
+    }
+
     @Data
     public static class DrStrategyResource {
         private Long resourceId;

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

@@ -0,0 +1,36 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 需求响应策略详情
+ */
+@Data
+public class DrStrategyVO {
+
+    private Long id;
+    private String strategyCode;
+    private String strategyName;
+    private Integer responseType;
+    private BigDecimal adjustStepKw;
+    private Integer executeMode;
+    private String strategyConfig;
+    private Integer isDefault;
+    private Integer isEnabled;
+    private Integer tenantId;
+    private LocalDateTime createTime;
+    private String createdBy;
+    private List<DrStrategyResourceVO> resources;
+
+    @Data
+    public static class DrStrategyResourceVO {
+        private Long resourceId;
+        private String resourceName;
+        private Integer priority;
+        private BigDecimal maxAdjustKw;
+    }
+}

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

@@ -0,0 +1,86 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+/**
+ * 合同 请求VO
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/4
+ */
+@Data
+public class VppContractRequestVO {
+
+    /**
+     * 主键(更新时必传)
+     */
+    private Long id;
+
+    /**
+     * 客户ID
+     */
+    private Long customerId;
+
+    /**
+     * 模板ID
+     */
+    private Long templateId;
+
+    /**
+     * 合同类型
+     */
+    private Integer contractType;
+
+    /**
+     * 合同名称
+     */
+    private String contractName;
+
+    /**
+     * 合同状态
+     */
+    private Integer contractStatus;
+
+    /**
+     * 签约日期
+     */
+    private LocalDate signDate;
+
+    /**
+     * 生效日期
+     */
+    private LocalDate effectiveDate;
+
+    /**
+     * 到期日期
+     */
+    private LocalDate expireDate;
+
+    /**
+     * 合同文件URL
+     */
+    private String fileUrl;
+
+    /**
+     * 分成比例
+     */
+    private BigDecimal shareRatio;
+
+    /**
+     * 价格定义(JSON)
+     */
+    private String priceJson;
+
+    /**
+     * 账户信息(JSON)
+     */
+    private String accountInfoJson;
+
+    /**
+     * 备注
+     */
+    private String remark;
+}

+ 124 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractResponseVO.java

@@ -0,0 +1,124 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 合同 响应VO
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/4
+ */
+@Data
+public class VppContractResponseVO {
+
+    private Long id;
+
+    /**
+     * 合同编号(自动生成)
+     */
+    private String contractNo;
+
+    /**
+     * 客户ID
+     */
+    private Long customerId;
+
+    /**
+     * 客户名称(关联查询)
+     */
+    private String customerName;
+
+    /**
+     * 模板ID
+     */
+    private Long templateId;
+
+    /**
+     * 模板名称(关联查询)
+     */
+    private String templateName;
+
+    /**
+     * 合同类型
+     */
+    private Integer contractType;
+
+    /**
+     * 合同名称
+     */
+    private String contractName;
+
+    /**
+     * 合同状态
+     */
+    private Integer contractStatus;
+
+    /**
+     * 签约日期
+     */
+    private LocalDate signDate;
+
+    /**
+     * 生效日期
+     */
+    private LocalDate effectiveDate;
+
+    /**
+     * 到期日期
+     */
+    private LocalDate expireDate;
+
+    /**
+     * 合同文件URL
+     */
+    private String fileUrl;
+
+    /**
+     * 分成比例
+     */
+    private BigDecimal shareRatio;
+
+    /**
+     * 价格定义(JSON)
+     */
+    private String priceJson;
+
+    /**
+     * 账户信息(JSON)
+     */
+    private String accountInfoJson;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 租户ID
+     */
+    private Integer tenantId;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 创建人
+     */
+    private String createdBy;
+
+    /**
+     * 更新人
+     */
+    private String updatedBy;
+}

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

@@ -0,0 +1,53 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+/**
+ * 合同模板 请求VO
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/4
+ */
+@Data
+public class VppContractTemplateRequestVO {
+
+    /**
+     * 主键(更新时必传)
+     */
+    private Long id;
+
+    /**
+     * 模板编码
+     */
+    private String templateCode;
+
+    /**
+     * 模板名称
+     */
+    private String templateName;
+
+    /**
+     * 合同类型
+     */
+    private Integer contractType;
+
+    /**
+     * 版本号
+     */
+    private String version;
+
+    /**
+     * 模板文件URL
+     */
+    private String fileUrl;
+
+    /**
+     * 占位符变量定义(JSON)
+     */
+    private String variablesJson;
+
+    /**
+     * 是否启用 0-禁用 1-启用
+     */
+    private Integer isEnabled;
+}

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

@@ -0,0 +1,83 @@
+package com.usky.vpp.service.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 合同模板 响应VO
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/4
+ */
+@Data
+public class VppContractTemplateResponseVO {
+
+    /**
+     * 主键
+     */
+    private Long id;
+
+    /**
+     * 模板编码
+     */
+    private String templateCode;
+
+    /**
+     * 模板名称
+     */
+    private String templateName;
+
+    /**
+     * 合同类型
+     */
+    private Integer contractType;
+
+    /**
+     * 版本号
+     */
+    private String version;
+
+    /**
+     * 模板文件URL
+     */
+    private String fileUrl;
+
+    /**
+     * 占位符变量定义(JSON)
+     */
+    private String variablesJson;
+
+    /**
+     * 是否启用 0-禁用 1-启用
+     */
+    private Integer isEnabled;
+
+    /**
+     * 创建人
+     */
+    private String createdBy;
+
+    /**
+     * 创建时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+
+    /**
+     * 更新人
+     */
+    private String updatedBy;
+
+    /**
+     * 更新时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updateTime;
+
+    /**
+     * 租户ID
+     */
+    private Integer tenantId;
+}

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

@@ -5,6 +5,7 @@ import org.springframework.util.StringUtils;
 
 import java.lang.reflect.Method;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.UUID;
 
 /**
@@ -68,6 +69,16 @@ public final class VppAuditHelper {
         return "ACC" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 4).toUpperCase();
     }
 
+    public static String nextEventId() {
+        return "EVE_" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now())
+                + UUID.randomUUID().toString().substring(0, 4).toUpperCase();
+    }
+
+    public static String nextInvitationNo() {
+        return "INV_" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now())
+                + UUID.randomUUID().toString().substring(0, 4).toUpperCase();
+    }
+
     private static String currentUsername() {
         try {
             return SecurityUtils.getUsername();

+ 364 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppCapabilityEvalHelper.java

@@ -0,0 +1,364 @@
+package com.usky.vpp.util;
+
+import com.usky.vpp.domain.VppDevice;
+import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.service.vo.CapabilityLoadPointVO;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * 可调能力评估辅助(TSDB 聚合 + 模拟数据兜底)
+ */
+public final class VppCapabilityEvalHelper {
+
+    public static final String CATEGORY_TOTAL = "TOTAL";
+    public static final String CATEGORY_LOAD = "LOAD";
+    public static final String CATEGORY_STORAGE = "STORAGE";
+    public static final String CATEGORY_GENERATION = "GENERATION";
+
+    private static final Set<String> LOAD_TYPES = new HashSet<>(Arrays.asList("IND_LOAD", "COM_BLDG", "EVCS"));
+    private static final Set<String> STORAGE_TYPES = Collections.singleton("ESS");
+    private static final Set<String> GENERATION_TYPES = Collections.singleton("PV");
+
+    private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("HH:mm");
+
+    private VppCapabilityEvalHelper() {
+    }
+
+    public static String categoryLabel(String category) {
+        if (category == null) {
+            return "未知";
+        }
+        switch (category) {
+            case CATEGORY_TOTAL:
+                return "总资源";
+            case CATEGORY_LOAD:
+                return "负荷类";
+            case CATEGORY_STORAGE:
+                return "储能类";
+            case CATEGORY_GENERATION:
+                return "发电类";
+            default:
+                return category;
+        }
+    }
+
+    public static String resolveCategory(String resourceType) {
+        if (resourceType == null) {
+            return CATEGORY_LOAD;
+        }
+        String type = resourceType.toUpperCase();
+        if (GENERATION_TYPES.contains(type)) {
+            return CATEGORY_GENERATION;
+        }
+        if (STORAGE_TYPES.contains(type)) {
+            return CATEGORY_STORAGE;
+        }
+        if (LOAD_TYPES.contains(type)) {
+            return CATEGORY_LOAD;
+        }
+        return CATEGORY_LOAD;
+    }
+
+    public static String eventTypeLabel(Integer eventType) {
+        if (eventType == null) {
+            return "未知";
+        }
+        return eventType == 1 ? "削峰" : eventType == 2 ? "填谷" : "未知";
+    }
+
+    public static String responseTypeLabel(Integer responseType) {
+        if (responseType == null) {
+            return "未知";
+        }
+        switch (responseType) {
+            case 1:
+                return "日前";
+            case 2:
+                return "日内";
+            case 3:
+                return "秒级";
+            default:
+                return "未知";
+        }
+    }
+
+    /**
+     * 基于 TDengine 历史数据构建负荷曲线(每 10 分钟一个点)
+     * <p>实际负荷取统计日实测;基线负荷取 7 日前同时段作为参考基线。</p>
+     */
+    public static List<CapabilityLoadPointVO> buildLoadCurveFromTsdb(
+            Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> actualData,
+            Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> baselineData,
+            LocalDate date,
+            List<String> powerMetrics) {
+        List<CapabilityLoadPointVO> points = new ArrayList<>();
+        for (int minute = 0; minute < 1440; minute += 10) {
+            LocalTime time = LocalTime.of(minute / 60, minute % 60);
+            LocalDateTime actualStart = date.atTime(time);
+            LocalDateTime actualEnd = actualStart.plusMinutes(10).minusSeconds(1);
+            LocalDateTime baselineStart = date.minusDays(7).atTime(time);
+            LocalDateTime baselineEnd = baselineStart.plusMinutes(10).minusSeconds(1);
+
+            BigDecimal actual = sumBucketPower(actualData, powerMetrics, actualStart, actualEnd);
+            BigDecimal baseline = sumBucketPower(baselineData, powerMetrics, baselineStart, baselineEnd);
+            if (baseline == null || baseline.compareTo(BigDecimal.ZERO) <= 0) {
+                baseline = actual;
+            }
+
+            CapabilityLoadPointVO point = new CapabilityLoadPointVO();
+            point.setTime(time.format(TIME_FMT));
+            point.setBaselineLoadKw(scale(baseline));
+            point.setActualLoadKw(scale(actual));
+            points.add(point);
+        }
+        return points;
+    }
+
+    /**
+     * 汇总设备最新上下调能力(TDengine 实时指标)
+     */
+    public static BigDecimal sumLatestMetric(Map<String, Map<String, BigDecimal>> latestData,
+                                             List<String> metricCandidates) {
+        if (latestData == null || latestData.isEmpty()) {
+            return BigDecimal.ZERO;
+        }
+        BigDecimal total = BigDecimal.ZERO;
+        for (Map<String, BigDecimal> metrics : latestData.values()) {
+            BigDecimal value = pickMetric(metrics, metricCandidates);
+            if (value != null) {
+                total = total.add(value);
+            }
+        }
+        return total.setScale(2, RoundingMode.HALF_UP);
+    }
+
+    public static boolean hasTsdbPowerData(Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> deviceMetricData,
+                                           List<String> powerMetrics) {
+        if (deviceMetricData == null || deviceMetricData.isEmpty()) {
+            return false;
+        }
+        for (Map<String, TreeMap<LocalDateTime, BigDecimal>> metricMap : deviceMetricData.values()) {
+            for (String metric : powerMetrics) {
+                TreeMap<LocalDateTime, BigDecimal> series = metricMap.get(metric);
+                if (series != null && !series.isEmpty()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 生成当日负荷曲线(每 10 分钟一个点,00:00 ~ 23:50)- 模拟兜底
+     */
+    public static List<CapabilityLoadPointVO> buildLoadCurve(List<VppResourcePoint> resources,
+                                                           List<VppDevice> devices,
+                                                           Long siteId,
+                                                           LocalDate date) {
+        List<CapabilityLoadPointVO> points = new ArrayList<>();
+        long seed = (siteId != null ? siteId : 0L) + date.toEpochDay();
+        for (int minute = 0; minute < 1440; minute += 10) {
+            LocalTime time = LocalTime.of(minute / 60, minute % 60);
+            double hourFactor = timeFactor(time);
+            BigDecimal baseline = aggregateLoad(resources, devices, seed, minute, hourFactor, 0);
+            BigDecimal actual = aggregateLoad(resources, devices, seed, minute, hourFactor, 1);
+            CapabilityLoadPointVO point = new CapabilityLoadPointVO();
+            point.setTime(time.format(TIME_FMT));
+            point.setBaselineLoadKw(baseline);
+            point.setActualLoadKw(actual);
+            points.add(point);
+        }
+        return points;
+    }
+
+    private static BigDecimal aggregateLoad(List<VppResourcePoint> resources,
+                                            List<VppDevice> devices,
+                                            long seed,
+                                            int minuteOfDay,
+                                            double hourFactor,
+                                            int seriesSalt) {
+        if (resources == null || resources.isEmpty()) {
+            double base = 800 * hourFactor + pseudoRandom(seed, minuteOfDay, seriesSalt) * 120;
+            return BigDecimal.valueOf(base).setScale(2, RoundingMode.HALF_UP);
+        }
+        BigDecimal total = BigDecimal.ZERO;
+        for (VppResourcePoint resource : resources) {
+            VppDevice device = findDevice(devices, resource.getDeviceId());
+            if (device != null && device.getCommStatus() != null && device.getCommStatus() == 0) {
+                continue;
+            }
+            BigDecimal cap = resource.getCapacityKw() != null ? resource.getCapacityKw() : BigDecimal.valueOf(100);
+            double typeFactor = typeLoadFactor(resource.getResourceType());
+            double noise = 0.85 + pseudoRandom(seed, resource.getId() != null ? resource.getId() : 0L, minuteOfDay, seriesSalt) * 0.3;
+            double value = cap.doubleValue() * hourFactor * typeFactor * noise;
+            if (CATEGORY_GENERATION.equals(resolveCategory(resource.getResourceType()))) {
+                value *= 0.15;
+            }
+            total = total.add(BigDecimal.valueOf(value));
+        }
+        if (seriesSalt == 1) {
+            total = total.multiply(BigDecimal.valueOf(1.02 + pseudoRandom(seed, 999, minuteOfDay, seriesSalt) * 0.08));
+        }
+        return total.setScale(2, RoundingMode.HALF_UP);
+    }
+
+    private static BigDecimal sumBucketPower(Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> deviceData,
+                                             List<String> metricCandidates,
+                                             LocalDateTime bucketStart,
+                                             LocalDateTime bucketEnd) {
+        if (deviceData == null || deviceData.isEmpty()) {
+            return BigDecimal.ZERO;
+        }
+        BigDecimal total = BigDecimal.ZERO;
+        for (Map<String, TreeMap<LocalDateTime, BigDecimal>> metricMap : deviceData.values()) {
+            TreeMap<LocalDateTime, BigDecimal> series = pickSeries(metricMap, metricCandidates);
+            if (series == null || series.isEmpty()) {
+                continue;
+            }
+            BigDecimal avg = averageInRange(series, bucketStart, bucketEnd);
+            if (avg != null) {
+                total = total.add(avg);
+            }
+        }
+        return total;
+    }
+
+    private static TreeMap<LocalDateTime, BigDecimal> pickSeries(Map<String, TreeMap<LocalDateTime, BigDecimal>> metricMap,
+                                                                 List<String> metricCandidates) {
+        if (metricMap == null || metricMap.isEmpty()) {
+            return null;
+        }
+        for (String metric : metricCandidates) {
+            TreeMap<LocalDateTime, BigDecimal> series = metricMap.get(metric);
+            if (series != null && !series.isEmpty()) {
+                return series;
+            }
+        }
+        for (Map.Entry<String, TreeMap<LocalDateTime, BigDecimal>> entry : metricMap.entrySet()) {
+            if (entry.getValue() != null && !entry.getValue().isEmpty()) {
+                return entry.getValue();
+            }
+        }
+        return null;
+    }
+
+    private static BigDecimal pickMetric(Map<String, BigDecimal> metrics, List<String> metricCandidates) {
+        if (metrics == null || metrics.isEmpty()) {
+            return null;
+        }
+        for (String metric : metricCandidates) {
+            BigDecimal value = metrics.get(metric);
+            if (value != null) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    private static BigDecimal averageInRange(TreeMap<LocalDateTime, BigDecimal> series,
+                                             LocalDateTime startInclusive,
+                                             LocalDateTime endInclusive) {
+        BigDecimal sum = BigDecimal.ZERO;
+        int count = 0;
+        for (Map.Entry<LocalDateTime, BigDecimal> entry : series.subMap(startInclusive, true, endInclusive, true).entrySet()) {
+            if (entry.getValue() != null) {
+                sum = sum.add(entry.getValue());
+                count++;
+            }
+        }
+        if (count == 0) {
+            Map.Entry<LocalDateTime, BigDecimal> floor = series.floorEntry(startInclusive);
+            if (floor != null && floor.getValue() != null) {
+                return floor.getValue();
+            }
+            return null;
+        }
+        return sum.divide(BigDecimal.valueOf(count), 4, RoundingMode.HALF_UP);
+    }
+
+    private static BigDecimal scale(BigDecimal value) {
+        if (value == null) {
+            return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+        }
+        return value.setScale(2, RoundingMode.HALF_UP);
+    }
+
+    private static VppDevice findDevice(List<VppDevice> devices, Long deviceId) {
+        if (devices == null || deviceId == null) {
+            return null;
+        }
+        return devices.stream().filter(d -> deviceId.equals(d.getId())).findFirst().orElse(null);
+    }
+
+    /**
+     * 日内负荷形态:夜间低、早晚峰、午间次峰
+     */
+    private static double timeFactor(LocalTime time) {
+        int hour = time.getHour();
+        int minute = time.getMinute();
+        double h = hour + minute / 60.0;
+        if (h < 6) {
+            return 0.45 + h / 6 * 0.15;
+        }
+        if (h < 9) {
+            return 0.6 + (h - 6) / 3 * 0.35;
+        }
+        if (h < 12) {
+            return 0.95 + (h - 9) / 3 * 0.05;
+        }
+        if (h < 14) {
+            return 0.85;
+        }
+        if (h < 17) {
+            return 0.9 + (h - 14) / 3 * 0.1;
+        }
+        if (h < 21) {
+            return 1.0 - (h - 17) / 4 * 0.35;
+        }
+        return 0.5 - (h - 21) / 3 * 0.15;
+    }
+
+    private static double typeLoadFactor(String resourceType) {
+        if (resourceType == null) {
+            return 0.7;
+        }
+        switch (resourceType.toUpperCase()) {
+            case "IND_LOAD":
+                return 0.75;
+            case "COM_BLDG":
+                return 0.65;
+            case "EVCS":
+                return 0.55;
+            case "ESS":
+                return 0.4;
+            case "PV":
+                return 1.0;
+            default:
+                return 0.7;
+        }
+    }
+
+    private static double pseudoRandom(long seed, long a, int b, int c) {
+        long mixed = seed * 31L + a * 17L + b * 13L + c * 7L;
+        return (mixed % 1000) / 1000.0;
+    }
+
+    private static double pseudoRandom(long seed, int minuteOfDay, int seriesSalt) {
+        long mixed = seed * 31L + minuteOfDay * 17L + seriesSalt * 13L;
+        return (mixed % 1000) / 1000.0;
+    }
+}

+ 141 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppFieldValidator.java

@@ -0,0 +1,141 @@
+package com.usky.vpp.util;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.common.core.exception.BusinessException;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.regex.Pattern;
+
+/**
+ * VPP 字段校验工具类
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/4
+ */
+public final class VppFieldValidator {
+
+    /**
+     * templateCode 校验正则:仅允许大写英文字母,长度 1~5
+     */
+    private static final Pattern TEMPLATE_CODE_PATTERN = Pattern.compile("^[A-Z]{1,5}$");
+
+    /**
+     * 中国大陆手机号正则:1 开头,第二位 3-9,共 11 位数字
+     */
+    private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
+
+    /**
+     * 邮箱正则
+     */
+    private static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$");
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    private VppFieldValidator() {
+        // 工具类禁止实例化
+    }
+
+    /**
+     * 校验并规范化模板编码 templateCode:
+     * <ul>
+     *   <li>不允许为空白</li>
+     *   <li>最大长度不超过 5 个字符</li>
+     *   <li>仅允许大写英文字母(A-Z)</li>
+     * </ul>
+     *
+     * @param templateCode 原始模板编码
+     * @throws BusinessException 校验不通过时抛出
+     */
+    public static void validateTemplateCode(String templateCode) {
+        if (StringUtils.isBlank(templateCode)) {
+            throw new BusinessException("模板编码不能为空!");
+        }
+        if (!TEMPLATE_CODE_PATTERN.matcher(templateCode).matches()) {
+            throw new BusinessException("模板编码格式错误:仅允许大写英文字母(A-Z),且长度不超过5个字符!");
+        }
+    }
+
+    /**
+     * 校验 JSON 格式字符串的合法性:
+     * <ul>
+     *   <li>null:视为合法,字段允许为空</li>
+     *   <li>空字符串:不允许,抛出异常</li>
+     *   <li>非空时:必须能解析为合法的 JSON 对象或数组</li>
+     * </ul>
+     *
+     * @param json      待校验的 JSON 字符串
+     * @param fieldName 字段名称(用于错误提示)
+     * @return 规范化后的值:null → null;合法 JSON → 原值
+     * @throws BusinessException 空字符串或 JSON 格式不合法时抛出
+     */
+    public static String validateJson(String json, String fieldName) {
+        if (json == null) {
+            return null;
+        }
+        if (json.trim().isEmpty()) {
+            throw new BusinessException(fieldName + "不能为空字符串!");
+        }
+        try {
+            JsonNode node = OBJECT_MAPPER.readTree(json);
+            if (!node.isObject() && !node.isArray()) {
+                throw new BusinessException(fieldName + "格式错误:仅支持JSON对象({})或数组([])格式!");
+            }
+        } catch (BusinessException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new BusinessException(fieldName + "格式错误:不是合法的JSON格式,请检查!");
+        }
+        return json;
+    }
+
+    /**
+     * 根据模板编码生成合同编号,格式:{templateCode}-{13位毫秒时间戳}
+     * <p>示例:templateCode=CG → CG-1783154814855</p>
+     *
+     * @param templateCode 模板编码
+     * @return 合同编号
+     */
+    public static String generateContractNo(String templateCode) {
+        return templateCode + "-" + System.currentTimeMillis();
+    }
+
+    /**
+     * 校验中国大陆手机号:
+     * <ul>
+     *   <li>不允许为空</li>
+     *   <li>1 开头,第二位 3-9,共 11 位数字</li>
+     * </ul>
+     *
+     * @param phone 手机号码
+     * @throws BusinessException 校验不通过时抛出
+     */
+    public static void validatePhone(String phone) {
+        if (StringUtils.isBlank(phone)) {
+            throw new BusinessException("手机号码不能为空!");
+        }
+        if (!PHONE_PATTERN.matcher(phone.trim()).matches()) {
+            throw new BusinessException("手机号码格式不正确!");
+        }
+    }
+
+    /**
+     * 校验邮箱格式:
+     * <ul>
+     *   <li>不允许为空</li>
+     *   <li>必须符合基本邮箱格式</li>
+     * </ul>
+     *
+     * @param email 邮箱地址
+     * @throws BusinessException 校验不通过时抛出
+     */
+    public static void validateEmail(String email) {
+        if (StringUtils.isBlank(email)) {
+            throw new BusinessException("邮箱不能为空!");
+        }
+        if (!EMAIL_PATTERN.matcher(email.trim()).matches()) {
+            throw new BusinessException("邮箱格式不正确!");
+        }
+    }
+}

+ 49 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppSmsTemplateHelper.java

@@ -0,0 +1,49 @@
+package com.usky.vpp.util;
+
+import com.usky.vpp.domain.VppCustomer;
+import com.usky.vpp.domain.VppCustomerContact;
+import com.usky.vpp.domain.VppDrEvent;
+import com.usky.vpp.domain.VppDrInvitation;
+
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 邀约短信模板参数构建
+ * <p>
+ * 阿里云模板变量需与控制台一致,默认使用:meet、time、room(SMS_465362899)
+ */
+public final class VppSmsTemplateHelper {
+
+    private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    private VppSmsTemplateHelper() {
+    }
+
+    public static String buildInvitationParam(VppDrInvitation invitation,
+                                            VppDrEvent event,
+                                            VppCustomer customer,
+                                            VppCustomerContact contact) {
+        String meet = event != null && event.getEventName() != null
+                ? event.getEventName()
+                : (event != null ? event.getEventId() : "");
+        String time = "";
+        if (event != null && event.getStartTime() != null) {
+            time = TIME_FORMAT.format(event.getStartTime());
+        } else if (invitation.getReplyDeadline() != null) {
+            time = TIME_FORMAT.format(invitation.getReplyDeadline());
+        }
+        String room = customer != null && customer.getCustomerName() != null
+                ? customer.getCustomerName()
+                : "";
+        return "{\"meet\":\"" + escapeJson(meet)
+                + "\",\"time\":\"" + escapeJson(time)
+                + "\",\"room\":\"" + escapeJson(room) + "\"}";
+    }
+
+    private static String escapeJson(String value) {
+        if (value == null) {
+            return "";
+        }
+        return value.replace("\\", "\\\\").replace("\"", "\\\"");
+    }
+}

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

@@ -31,3 +31,8 @@ vpp:
     token-ttl-minutes: 25
     auto-ack-clearing: false
     auto-register-on-startup: false
+  # 阿里云短信(密钥复用 Nacos 共享配置 sms.ali.*)
+  sms:
+    enabled: true
+    sign-name: 上海永天科技股份有限公司
+    invitation-template-code: SMS_465362899

+ 94 - 0
service-vpp/service-vpp-biz/src/main/resources/logback.xml

@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="60 seconds" debug="false">
+    <!-- 日志存放路径 -->
+    <property name="log.path" value="/var/log/uskycloud/service-vpp" />
+    <!-- 日志输出格式 -->
+    <property name="log.pattern" value="%d{MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{26}:%line: %msg%n" />
+    <!--    	<property name="log.pattern" value="%gray(%d{MM-dd HH:mm:ss.SSS}) %highlight(%-5level) &#45;&#45; [%gray(%thread)] %cyan(%logger{26}:%line): %msg%n" />-->
+
+
+    <property name="SQL_PACKAGE" value="com.usky.vpp.mapper"/>
+
+    <!-- 控制台输出 -->
+    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+    </appender>
+
+    <appender name="file_sql" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/sql.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/sql.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>3</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+    </appender>
+
+    <!-- 系统日志输出 -->
+    <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/info.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>3</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>INFO</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/error.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>ERROR</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- 系统模块日志级别控制  -->
+    <!--	<logger name="com.usky" level="info" />-->
+    <!-- Spring日志级别控制  -->
+    <!--	<logger name="org.springframework" level="warn" />-->
+
+    <logger name="${SQL_PACKAGE}" additivity="false" level="debug">
+        <appender-ref ref="console"/>
+        <appender-ref ref="file_sql"/>
+    </logger>
+
+    <!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="file_info" />
+        <appender-ref ref="file_error" />
+        <appender-ref ref="console" />
+    </root>
+</configuration>

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

@@ -0,0 +1,41 @@
+-- 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;

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

@@ -0,0 +1,284 @@
+-- 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

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

@@ -0,0 +1,362 @@
+-- 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;
+*/

+ 62 - 28
service-vpp/service-vpp-biz/src/main/resources/sql/vpp_schema.sql

@@ -2,7 +2,7 @@
 -- 来源: 虚拟电厂平台数据库设计文档 V1.0
 
 CREATE TABLE `vpp_customer` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `account_no` VARCHAR(32) NOT NULL COMMENT '电力户号,全局唯一',
     `customer_name` VARCHAR(200) NOT NULL COMMENT '客户名称',
     `customer_type` TINYINT NOT NULL COMMENT '1高压 2低压商用 3居民充电桩 4自有资产',
@@ -33,7 +33,7 @@ CREATE TABLE `vpp_customer` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户档案';
 
 CREATE TABLE `vpp_customer_access` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `apply_no` VARCHAR(32) NOT NULL COMMENT '申请编号',
     `account_no` VARCHAR(32) NOT NULL COMMENT '电力户号',
     `customer_name` VARCHAR(200) NOT NULL COMMENT '客户名称',
@@ -60,7 +60,7 @@ CREATE TABLE `vpp_customer_access` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='准入申请';
 
 CREATE TABLE `vpp_customer_contact` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `customer_id` BIGINT NOT NULL COMMENT '客户ID',
     `contact_name` VARCHAR(100) NOT NULL COMMENT '联系人姓名',
     `contact_phone` VARCHAR(20) NOT NULL COMMENT '联系电话',
@@ -79,7 +79,7 @@ CREATE TABLE `vpp_customer_contact` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户联系人';
 
 CREATE TABLE `vpp_contract` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `contract_no` VARCHAR(64) NOT NULL COMMENT '合同编号',
     `customer_id` BIGINT NOT NULL COMMENT '客户ID',
     `template_id` BIGINT NULL COMMENT '模板ID',
@@ -108,7 +108,7 @@ CREATE TABLE `vpp_contract` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同';
 
 CREATE TABLE `vpp_contract_template` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `template_code` VARCHAR(32) NOT NULL COMMENT '模板编码',
     `template_name` VARCHAR(200) NOT NULL COMMENT '模板名称',
     `contract_type` TINYINT NOT NULL COMMENT '合同类型',
@@ -128,7 +128,7 @@ CREATE TABLE `vpp_contract_template` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同模板';
 
 CREATE TABLE `vpp_contract_audit_log` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `contract_id` BIGINT NOT NULL COMMENT '合同ID',
     `action` TINYINT NOT NULL COMMENT '1提交 2通过 3驳回 4归档',
     `opinion` VARCHAR(500) NULL COMMENT '意见',
@@ -140,7 +140,7 @@ CREATE TABLE `vpp_contract_audit_log` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同审核流水';
 
 CREATE TABLE `vpp_resource_point` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `resource_code` VARCHAR(64) NOT NULL COMMENT '资源编号',
     `resource_name` VARCHAR(200) NOT NULL COMMENT '资源名称',
     `customer_id` BIGINT NOT NULL COMMENT '所属客户ID',
@@ -174,7 +174,7 @@ CREATE TABLE `vpp_resource_point` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='资源点';
 
 CREATE TABLE `vpp_resource_point_config` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `resource_id` BIGINT NOT NULL COMMENT '资源点ID',
     `collect_interval_sec` INT NOT NULL COMMENT '采集频率秒 60/300/900',
     `power_upper_limit` DECIMAL(12,4) NULL COMMENT '功率上限告警',
@@ -195,7 +195,7 @@ CREATE TABLE `vpp_resource_point_config` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='资源点运行参数';
 
 CREATE TABLE `vpp_device` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `device_code` VARCHAR(64) NOT NULL COMMENT '设备编号',
     `device_name` VARCHAR(200) NOT NULL COMMENT '设备名称',
     `resource_id` BIGINT NOT NULL COMMENT '所属资源点ID',
@@ -223,7 +223,7 @@ CREATE TABLE `vpp_device` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备';
 
 CREATE TABLE `vpp_device_control_log` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `device_id` BIGINT NOT NULL COMMENT '设备ID',
     `control_type` VARCHAR(32) NOT NULL COMMENT 'START/STOP/CONFIG/OTA',
     `control_params` JSON NULL COMMENT '控制参数',
@@ -237,7 +237,7 @@ CREATE TABLE `vpp_device_control_log` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备远程控制日志';
 
 CREATE TABLE `vpp_energy_reading_monthly` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `customer_id` BIGINT NOT NULL COMMENT '客户ID',
     `resource_id` BIGINT NULL COMMENT '资源点ID,可空表示户号级',
     `settle_year` SMALLINT NOT NULL COMMENT '结算年',
@@ -263,7 +263,7 @@ CREATE TABLE `vpp_energy_reading_monthly` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='月度电量核算';
 
 CREATE TABLE `vpp_energy_summary_daily` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `summary_date` DATE NOT NULL COMMENT '汇总日期',
     `resource_id` BIGINT NULL COMMENT '资源点ID,NULL表示全站',
     `resource_type` VARCHAR(16) NULL COMMENT '资源类型',
@@ -285,7 +285,7 @@ CREATE TABLE `vpp_energy_summary_daily` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='日度能源汇总';
 
 CREATE TABLE `vpp_settlement_bill` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `bill_no` VARCHAR(64) NOT NULL COMMENT '账单编号',
     `customer_id` BIGINT NOT NULL COMMENT '客户ID',
     `settle_year` SMALLINT NOT NULL COMMENT '结算年',
@@ -311,7 +311,7 @@ CREATE TABLE `vpp_settlement_bill` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='电费账单';
 
 CREATE TABLE `vpp_settlement_bill_detail` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `bill_id` BIGINT NOT NULL COMMENT '账单ID',
     `time_period` TINYINT NOT NULL COMMENT '1尖 2峰 3平 4谷',
     `energy_kwh` DECIMAL(18,4) NOT NULL COMMENT '时段电量',
@@ -323,7 +323,7 @@ CREATE TABLE `vpp_settlement_bill_detail` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='账单分时明细';
 
 CREATE TABLE `vpp_payment_record` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `bill_id` BIGINT NOT NULL COMMENT '账单ID',
     `payment_amount` DECIMAL(18,4) NOT NULL COMMENT '缴费金额',
     `payment_method` TINYINT NOT NULL COMMENT '1线下转账 2电费抵扣 3其他',
@@ -338,7 +338,7 @@ CREATE TABLE `vpp_payment_record` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='缴费记录';
 
 CREATE TABLE `vpp_report_task` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `task_no` VARCHAR(64) NOT NULL COMMENT '任务编号',
     `task_name` VARCHAR(200) NOT NULL COMMENT '任务名称',
     `report_org` TINYINT NOT NULL COMMENT '1交易中心 2能监局 3经信委 4国网上海',
@@ -361,7 +361,7 @@ CREATE TABLE `vpp_report_task` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='报送任务';
 
 CREATE TABLE `vpp_report_record` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `task_id` BIGINT NOT NULL COMMENT '任务ID',
     `record_no` VARCHAR(64) NOT NULL COMMENT '报送记录编号',
     `report_period` VARCHAR(32) NOT NULL COMMENT '报送周期标识 如2026-05',
@@ -386,7 +386,7 @@ CREATE TABLE `vpp_report_record` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='报送记录';
 
 CREATE TABLE `vpp_report_data` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `record_id` BIGINT NOT NULL COMMENT '报送记录ID',
     `form_data` JSON NOT NULL COMMENT '填报表单JSON',
     `validation_errors` JSON NULL COMMENT '校验异常项',
@@ -403,7 +403,7 @@ CREATE TABLE `vpp_report_data` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='报送填报数据';
 
 CREATE TABLE `vpp_report_audit_log` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `record_id` BIGINT NOT NULL COMMENT '报送记录ID',
     `audit_level` TINYINT NOT NULL COMMENT '1填报 2复核 3审批',
     `action` TINYINT NOT NULL COMMENT '1提交 2通过 3驳回',
@@ -417,7 +417,7 @@ CREATE TABLE `vpp_report_audit_log` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='报送审核流水';
 
 CREATE TABLE `vpp_dr_event` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `event_id` VARCHAR(64) NOT NULL COMMENT '运管平台事件ID',
     `event_name` VARCHAR(200) NULL COMMENT '事件名称',
     `response_type` TINYINT NOT NULL COMMENT '1日前 2日内 3秒级',
@@ -442,7 +442,7 @@ CREATE TABLE `vpp_dr_event` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='需求响应事件';
 
 CREATE TABLE `vpp_dr_participation` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `event_id` BIGINT NOT NULL COMMENT '事件ID',
     `customer_id` BIGINT NOT NULL COMMENT '客户ID',
     `resource_id` BIGINT NULL COMMENT '资源点ID',
@@ -462,8 +462,42 @@ CREATE TABLE `vpp_dr_participation` (
     UNIQUE KEY `uk_dr_part` (event_id, customer_id, resource_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='事件参与记录';
 
+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='需求响应企业邀约';
+
 CREATE TABLE `vpp_dr_strategy` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `strategy_code` VARCHAR(64) NOT NULL COMMENT '策略编码',
     `strategy_name` VARCHAR(200) NOT NULL COMMENT '策略名称',
     `response_type` TINYINT NOT NULL COMMENT '适用响应类型',
@@ -484,7 +518,7 @@ CREATE TABLE `vpp_dr_strategy` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='响应策略';
 
 CREATE TABLE `vpp_dr_strategy_resource` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `strategy_id` BIGINT NOT NULL COMMENT '策略ID',
     `resource_id` BIGINT NOT NULL COMMENT '资源点ID',
     `priority` TINYINT NOT NULL COMMENT '优先级序号',
@@ -496,7 +530,7 @@ CREATE TABLE `vpp_dr_strategy_resource` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='策略-资源关联';
 
 CREATE TABLE `vpp_dr_execution` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `event_id` BIGINT NOT NULL COMMENT '事件ID',
     `resource_id` BIGINT NOT NULL COMMENT '资源点ID',
     `target_kw` DECIMAL(12,4) NOT NULL COMMENT '目标调节量',
@@ -517,7 +551,7 @@ CREATE TABLE `vpp_dr_execution` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='响应执行明细';
 
 CREATE TABLE `vpp_dr_evaluation` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `event_id` BIGINT NOT NULL COMMENT '事件ID',
     `actual_capacity_kw` DECIMAL(12,4) NULL COMMENT '实际响应容量',
     `response_duration_min` INT NULL COMMENT '响应时长分钟',
@@ -537,7 +571,7 @@ CREATE TABLE `vpp_dr_evaluation` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='响应效果评估';
 
 CREATE TABLE `vpp_registration` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `dn_id` VARCHAR(64) NOT NULL COMMENT '虚拟电厂运营商ID',
     `registration_id` VARCHAR(128) NOT NULL COMMENT '运管平台registrationID',
     `reg_status` TINYINT NOT NULL COMMENT '0未注册 1已注册 2需重新注册',
@@ -555,7 +589,7 @@ CREATE TABLE `vpp_registration` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='运管平台注册信息';
 
 CREATE TABLE `vpp_report_log` (
-    `id` BIGINT NOT NULL COMMENT '主键',
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
     `report_type` VARCHAR(64) NOT NULL COMMENT 'MomentData/IntervalData/RegisterReport等',
     `request_id` VARCHAR(64) NULL COMMENT '请求ID',
     `report_status` TINYINT NOT NULL COMMENT '0失败 1成功',
@@ -569,7 +603,7 @@ CREATE TABLE `vpp_report_log` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='数据上报日志';
 
 CREATE TABLE vpp_file_archive (
-    id          BIGINT         NOT NULL COMMENT '主键',
+    id          BIGINT         NOT NULL AUTO_INCREMENT COMMENT '主键',
     file_name   VARCHAR(255)   NOT NULL COMMENT '原始文件名',
     file_url    VARCHAR(500)   NOT NULL COMMENT '存储路径',
     file_type   VARCHAR(32)    NOT NULL COMMENT '文件类型',