Просмотр исходного кода

service-vpp数据表增加租户ID字段、调整创建人、更改人字段类型

hanzhengyi 3 дней назад
Родитель
Сommit
2d9091ed1b
74 измененных файлов с 4126 добавлено и 288 удалено
  1. 45 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/client/VppUnClient.java
  2. 137 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/client/VppUnHttpExecutor.java
  3. 107 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/client/VppUnTokenHolder.java
  4. 44 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/config/VppUnProperties.java
  5. 22 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/config/VppUnRestTemplateConfig.java
  6. 5 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/un/UnDnController.java
  7. 61 23
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrController.java
  8. 35 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/UnIntegrationController.java
  9. 77 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/crypto/VppUnCryptoService.java
  10. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContract.java
  11. 34 34
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContractAuditLog.java
  12. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContractTemplate.java
  13. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppCustomer.java
  14. 4 4
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppCustomerAccess.java
  15. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppCustomerContact.java
  16. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDevice.java
  17. 40 38
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDeviceControlLog.java
  18. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrEvaluation.java
  19. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrEvent.java
  20. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrExecution.java
  21. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrParticipation.java
  22. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrStrategy.java
  23. 2 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrStrategyResource.java
  24. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppEnergyReadingMonthly.java
  25. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppEnergySummaryDaily.java
  26. 2 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppFileArchive.java
  27. 43 38
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppPaymentRecord.java
  28. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppRegistration.java
  29. 38 36
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppReportAuditLog.java
  30. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppReportData.java
  31. 2 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppReportLog.java
  32. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppReportRecord.java
  33. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppReportTask.java
  34. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppResourcePoint.java
  35. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppResourcePointConfig.java
  36. 4 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppSettlementBill.java
  37. 2 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppSettlementBillDetail.java
  38. 20 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/enums/VppUnEventPhase.java
  39. 47 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/job/VppUnBootstrapRunner.java
  40. 36 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/job/VppUnPollScheduler.java
  41. 2 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/mapper/VppDrEventMapper.java
  42. 37 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrEventIngestService.java
  43. 11 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrExecutionBootstrapService.java
  44. 36 5
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrService.java
  45. 2 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppUnDnService.java
  46. 22 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppUnDrSyncService.java
  47. 25 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppUnIntegrationService.java
  48. 11 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppUnReportService.java
  49. 311 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrEventIngestServiceImpl.java
  50. 77 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrExecutionBootstrapServiceImpl.java
  51. 731 4
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrServiceImpl.java
  52. 137 5
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppUnDnServiceImpl.java
  53. 119 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppUnDrSyncServiceImpl.java
  54. 262 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppUnIntegrationServiceImpl.java
  55. 112 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppUnReportServiceImpl.java
  56. 22 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrClearingRequest.java
  57. 14 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInterveneRequest.java
  58. 25 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrParticipateRequest.java
  59. 30 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrStrategyRequest.java
  60. 23 10
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppAuditHelper.java
  61. 318 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppUnEventParser.java
  62. 297 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppUnMessageBuilder.java
  63. 230 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppUnPayloadHelper.java
  64. 74 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/web/advice/VppUnDnCryptoRequestAdvice.java
  65. 49 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/web/advice/VppUnDnCryptoResponseAdvice.java
  66. 14 0
      service-vpp/service-vpp-biz/src/main/resources/bootstrap.yml
  67. 33 0
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_audit_field_unify_migration.sql
  68. 81 53
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_schema.sql
  69. 32 0
      service-vpp/service-vpp-biz/src/main/resources/sql/vpp_tenant_migration.sql
  70. 15 0
      service-vpp/service-vpp-biz/src/main/resources/un-samples/create-cq-request.json
  71. 20 0
      service-vpp/service-vpp-biz/src/main/resources/un-samples/create-event-response.json
  72. 14 0
      service-vpp/service-vpp-biz/src/main/resources/un-samples/create-opt-request.json
  73. 55 0
      service-vpp/service-vpp-biz/src/main/resources/un-samples/distribute-event-invitation.json
  74. 6 0
      service-vpp/service-vpp-biz/src/main/resources/un-samples/poll-request.json

+ 45 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/client/VppUnClient.java

@@ -0,0 +1,45 @@
+package com.usky.vpp.client;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.vpp.util.VppUnPayloadHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * UN 平台 JSON 客户端
+ */
+@Component
+public class VppUnClient {
+
+    @Autowired
+    private VppUnHttpExecutor httpExecutor;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    public Map<String, Object> post(String serviceName, Map<String, Object> body, boolean withToken) {
+        String raw = httpExecutor.postRaw(serviceName, body, withToken);
+        try {
+            Map<String, Object> response = objectMapper.readValue(raw, new TypeReference<Map<String, Object>>() {
+            });
+            assertSuccess(serviceName, response);
+            return response;
+        } catch (BusinessException ex) {
+            throw ex;
+        } catch (Exception ex) {
+            throw new BusinessException("UN 响应解析失败: " + serviceName);
+        }
+    }
+
+    private void assertSuccess(String serviceName, Map<String, Object> response) {
+        Integer code = VppUnPayloadHelper.getInteger(response, "code");
+        if (code != null && code >= 400) {
+            Object description = response.get("description");
+            throw new BusinessException("运管平台 " + serviceName + " 返回错误: " + description);
+        }
+    }
+}

+ 137 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/client/VppUnHttpExecutor.java

@@ -0,0 +1,137 @@
+package com.usky.vpp.client;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.vpp.config.VppUnProperties;
+import com.usky.vpp.crypto.VppUnCryptoService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.HttpStatusCodeException;
+import org.springframework.web.client.ResourceAccessException;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.Map;
+
+/**
+ * 向 UN 发送 HTTP 请求(可选国密加密 / Token 认证)
+ */
+@Component
+public class VppUnHttpExecutor {
+
+    private static final Logger log = LoggerFactory.getLogger(VppUnHttpExecutor.class);
+
+    @Autowired
+    private VppUnProperties properties;
+
+    @Autowired
+    private VppUnCryptoService cryptoService;
+
+    @Autowired
+    @Qualifier("vppUnRestTemplate")
+    private RestTemplate restTemplate;
+
+    @Lazy
+    @Autowired
+    private VppUnTokenHolder tokenHolder;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    public String postRaw(String serviceName, Map<String, Object> body, boolean withToken) {
+        try {
+            return executePost(serviceName, body, withToken);
+        } catch (BusinessException ex) {
+            if (withToken && isTokenError(ex)) {
+                log.info("Token 失效,刷新后重试 {}", serviceName);
+                tokenHolder.invalidate();
+                tokenHolder.getToken();
+                return executePost(serviceName, body, withToken);
+            }
+            throw ex;
+        }
+    }
+
+    private String executePost(String serviceName, Map<String, Object> body, boolean withToken) {
+        if (!properties.isOutboundActive()) {
+            throw new BusinessException("运管平台 outbound 未启用或未配置 baseUrl");
+        }
+        try {
+            String plainJson = objectMapper.writeValueAsString(body);
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            headers.setAccept(java.util.Collections.singletonList(MediaType.APPLICATION_JSON));
+
+            String requestBody = plainJson;
+            if (cryptoService.isActive()) {
+                requestBody = cryptoService.encryptRequest(plainJson);
+                headers.set("X-Sign", cryptoService.signRequest(requestBody));
+            }
+            if (withToken) {
+                headers.set(properties.getTokenHeader(), properties.getTokenPrefix() + tokenHolder.getToken());
+            }
+
+            String url = normalizeUrl(properties.getBaseUrl()) + "/" + serviceName;
+            log.debug("UN 请求 {} crypto={} token={}", url, cryptoService.isActive(), withToken);
+
+            ResponseEntity<String> response = restTemplate.exchange(
+                    url, HttpMethod.POST, new HttpEntity<>(requestBody, headers), String.class);
+            return decodeResponseBody(response.getBody(), response.getHeaders().getFirst("X-Sign"));
+        } catch (HttpStatusCodeException ex) {
+            String message = ex.getResponseBodyAsString();
+            log.warn("UN 请求 {} 失败 status={} body={}", serviceName, ex.getRawStatusCode(), message);
+            if (withToken && isTokenErrorMessage(message)) {
+                tokenHolder.invalidate();
+            }
+            throw new BusinessException("运管平台请求失败: " + serviceName + " " + ex.getRawStatusCode()
+                    + (StringUtils.hasText(message) ? " " + message : ""));
+        } catch (ResourceAccessException ex) {
+            throw new BusinessException("运管平台连接失败: " + ex.getMessage());
+        } catch (BusinessException ex) {
+            throw ex;
+        } catch (Exception ex) {
+            throw new BusinessException("运管平台请求异常: " + ex.getMessage());
+        }
+    }
+
+    private boolean isTokenError(BusinessException ex) {
+        return isTokenErrorMessage(ex.getMessage());
+    }
+
+    private boolean isTokenErrorMessage(String message) {
+        return message != null && (message.contains("TokenExpiredException")
+                || message.contains("JWTVerificationException")
+                || message.contains("NoToken")
+                || message.contains("JWTDecodeException"));
+    }
+
+    private String decodeResponseBody(String body, String signHeader) {
+        if (!StringUtils.hasText(body)) {
+            return "{}";
+        }
+        String trimmed = body.trim();
+        if (cryptoService.isActive() && !trimmed.startsWith("{")) {
+            if (!cryptoService.verifyResponse(trimmed, signHeader)) {
+                throw new BusinessException("UN 响应验签失败");
+            }
+            return cryptoService.decryptResponse(trimmed);
+        }
+        return trimmed;
+    }
+
+    private static String normalizeUrl(String baseUrl) {
+        if (baseUrl.endsWith("/")) {
+            return baseUrl.substring(0, baseUrl.length() - 1);
+        }
+        return baseUrl;
+    }
+}

+ 107 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/client/VppUnTokenHolder.java

@@ -0,0 +1,107 @@
+package com.usky.vpp.client;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.vpp.config.VppUnProperties;
+import com.usky.vpp.util.VppUnMessageBuilder;
+import com.usky.vpp.util.VppUnPayloadHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.time.Instant;
+import java.util.Map;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * UN Token 缓存(TokenRequest 获取,默认约 30 分钟过期)
+ */
+@Component
+public class VppUnTokenHolder {
+
+    private static final Logger log = LoggerFactory.getLogger(VppUnTokenHolder.class);
+
+    private final ReentrantLock lock = new ReentrantLock();
+
+    @Autowired
+    private VppUnProperties properties;
+
+    @Autowired
+    private VppUnHttpExecutor httpExecutor;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    private volatile String token;
+    private volatile Instant expiresAt = Instant.EPOCH;
+
+    public String getToken() {
+        if (!needsRefresh()) {
+            return token;
+        }
+        lock.lock();
+        try {
+            if (!needsRefresh()) {
+                return token;
+            }
+            refreshTokenInternal();
+            return token;
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    public void invalidate() {
+        lock.lock();
+        try {
+            token = null;
+            expiresAt = Instant.EPOCH;
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    public void applyTokenResponse(Map<String, Object> response) {
+        String newToken = VppUnPayloadHelper.getString(response, "token");
+        if (!StringUtils.hasText(newToken)) {
+            throw new IllegalStateException("TokenResponse 缺少 token");
+        }
+        lock.lock();
+        try {
+            token = newToken;
+            int ttlMinutes = properties.getTokenTtlMinutes() != null ? properties.getTokenTtlMinutes() : 25;
+            expiresAt = Instant.now().plusSeconds(ttlMinutes * 60L);
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    private boolean needsRefresh() {
+        return !StringUtils.hasText(token) || Instant.now().isAfter(expiresAt);
+    }
+
+    private void refreshTokenInternal() {
+        Map<String, Object> request = VppUnMessageBuilder.buildTokenRequest(properties);
+        String responseBody = httpExecutor.postRaw("TokenRequest", request, false);
+        try {
+            Map<String, Object> response = objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {
+            });
+            Integer code = VppUnPayloadHelper.getInteger(response, "code");
+            if (code != null && code != 200) {
+                throw new IllegalStateException("TokenRequest 失败: " + response.get("description"));
+            }
+            String newToken = VppUnPayloadHelper.getString(response, "token");
+            if (!StringUtils.hasText(newToken)) {
+                throw new IllegalStateException("TokenResponse 缺少 token");
+            }
+            applyTokenResponse(response);
+            log.info("UN Token 已刷新");
+        } catch (IllegalStateException ex) {
+            throw ex;
+        } catch (Exception ex) {
+            throw new IllegalStateException("TokenResponse 解析失败", ex);
+        }
+    }
+}

+ 44 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/config/VppUnProperties.java

@@ -35,4 +35,48 @@ public class VppUnProperties {
 
     /** Poll 轮询间隔秒,默认 10 */
     private Integer pollIntervalSec = 10;
+
+    /** 是否启用 DN 主动调用 UN(Poll/申报/出清等) */
+    private Boolean outboundEnabled = false;
+
+    /** 是否启用 Poll 定时任务 */
+    private Boolean pollEnabled = false;
+
+    /** 是否启用 SM2/SM3 加解密(未配置密钥时自动降级为明文) */
+    private Boolean cryptoEnabled = false;
+
+    /** 申报价格下调系数,默认 0.8 */
+    private String priceDownCoeff = "0.8";
+
+    /** 已保存的 registrationID(首次注册后需持久化) */
+    private String registrationId;
+
+    /** Token 请求头,默认 Authorization */
+    private String tokenHeader = "Authorization";
+
+    /** Token 前缀,默认 Bearer  */
+    private String tokenPrefix = "Bearer ";
+
+    /** Token 有效期分钟(文档默认 30),用于本地缓存刷新 */
+    private Integer tokenTtlMinutes = 25;
+
+    /** 收到出清公示后是否自动向 UN 发送 CreateEventResponse */
+    private Boolean autoAckClearing = false;
+
+    /** 启动时自动注册 UN(需 outbound-enabled=true) */
+    private Boolean autoRegisterOnStartup = false;
+
+    /** HTTP 连接超时毫秒 */
+    private Integer connectTimeoutMs = 10000;
+
+    /** HTTP 读超时毫秒 */
+    private Integer readTimeoutMs = 30000;
+
+    public boolean isOutboundActive() {
+        return Boolean.TRUE.equals(outboundEnabled) && baseUrl != null && !baseUrl.trim().isEmpty();
+    }
+
+    public boolean isPollActive() {
+        return Boolean.TRUE.equals(pollEnabled) && isOutboundActive();
+    }
 }

+ 22 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/config/VppUnRestTemplateConfig.java

@@ -0,0 +1,22 @@
+package com.usky.vpp.config;
+
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+import java.time.Duration;
+
+@Configuration
+public class VppUnRestTemplateConfig {
+
+    @Bean
+    public RestTemplate vppUnRestTemplate(VppUnProperties properties, RestTemplateBuilder builder) {
+        int connectMs = properties.getConnectTimeoutMs() != null ? properties.getConnectTimeoutMs() : 10000;
+        int readMs = properties.getReadTimeoutMs() != null ? properties.getReadTimeoutMs() : 30000;
+        return builder
+                .setConnectTimeout(Duration.ofMillis(connectMs))
+                .setReadTimeout(Duration.ofMillis(readMs))
+                .build();
+    }
+}

+ 5 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/un/UnDnController.java

@@ -46,6 +46,11 @@ public class UnDnController {
         return vppUnDnService.poll(body);
     }
 
+    @PostMapping("/DistributeEventRequest")
+    public Map<String, Object> distributeEvent(@RequestBody(required = false) Map<String, Object> body) {
+        return vppUnDnService.distributeEvent(body);
+    }
+
     @PostMapping("/CreateOptRequest")
     public Map<String, Object> createOpt(@RequestBody(required = false) Map<String, Object> body) {
         return vppUnDnService.createOpt(body);

+ 61 - 23
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/DrController.java

@@ -1,13 +1,23 @@
 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.VppDrEvent;
+import com.usky.vpp.domain.VppDrEvaluation;
+import com.usky.vpp.domain.VppDrStrategy;
 import com.usky.vpp.service.VppDrService;
+import com.usky.vpp.service.vo.DrClearingRequest;
+import com.usky.vpp.service.vo.DrInterveneRequest;
+import com.usky.vpp.service.vo.DrParticipateRequest;
+import com.usky.vpp.service.vo.DrStrategyRequest;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.Map;
+
 /**
- * 虚拟电厂 - Dr 接口
- * 网关前缀: /prod-api/service-vpp
+ * 需求响应接口
+ * 网关前缀: /prod-api/service-vpp/dr
  */
 @RestController
 @RequestMapping("/dr")
@@ -17,51 +27,79 @@ public class DrController {
     private VppDrService vppDrService;
 
     @GetMapping(value = "/event")
-    public ApiResult<Object> pageEvent(@RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+    public ApiResult<CommonPage<VppDrEvent>> pageEvent(@RequestParam(required = false) Map<String, Object> params) {
+        return ApiResult.success(vppDrService.pageEvent(params));
     }
+
     @GetMapping(value = "/event/{id}")
-    public ApiResult<Object> getEvent(@PathVariable("id") Long id, @RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+    public ApiResult<VppDrEvent> getEvent(@PathVariable("id") Long id) {
+        return ApiResult.success(vppDrService.getEvent(id));
     }
-    @PostMapping(value = "/event/{id}/assess")
-    public ApiResult<Object> assess(@PathVariable("id") Long id, @RequestBody(required = false) Object body) {
-        return ApiResult.success(null);
+
+    @GetMapping(value = "/event/{id}/capability")
+    public ApiResult<Object> assessCapability(@PathVariable("id") Long id) {
+        return ApiResult.success(vppDrService.assessCapability(id));
     }
+
     @PostMapping(value = "/event/{id}/participate")
-    public ApiResult<Void> participate(@PathVariable("id") Long id, @RequestBody(required = false) Object body) {
+    public ApiResult<Void> participate(@PathVariable("id") Long id, @RequestBody DrParticipateRequest body) {
+        vppDrService.participate(id, body);
         return ApiResult.success();
     }
+
     @PostMapping(value = "/event/{id}/clearing")
-    public ApiResult<Void> clearing(@PathVariable("id") Long id, @RequestBody(required = false) Object body) {
+    public ApiResult<Void> clearing(@PathVariable("id") Long id, @RequestBody DrClearingRequest body) {
+        vppDrService.clearing(id, body);
         return ApiResult.success();
     }
-    @GetMapping(value = "/event/{id}/execution")
-    public ApiResult<Object> execution(@PathVariable("id") Long id, @RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+
+    @GetMapping(value = "/event/{id}/monitor")
+    public ApiResult<Object> monitorExecution(@PathVariable("id") Long id) {
+        return ApiResult.success(vppDrService.monitorExecution(id));
     }
+
     @PostMapping(value = "/event/{id}/intervene")
-    public ApiResult<Void> intervene(@PathVariable("id") Long id, @RequestBody(required = false) Object body) {
+    public ApiResult<Void> intervene(@PathVariable("id") Long id, @RequestBody DrInterveneRequest body) {
+        vppDrService.intervene(id, body);
+        return ApiResult.success();
+    }
+
+    @PostMapping(value = "/event/{id}/ack-clearing")
+    public ApiResult<Void> acknowledgeClearing(@PathVariable("id") Long id) {
+        vppDrService.acknowledgeClearing(id);
         return ApiResult.success();
     }
+
+    @PostMapping(value = "/event/{id}/complete")
+    public ApiResult<Void> completeEvent(@PathVariable("id") Long id) {
+        vppDrService.completeEvent(id);
+        return ApiResult.success();
+    }
+
     @GetMapping(value = "/event/{id}/evaluation")
-    public ApiResult<Object> evaluation(@PathVariable("id") Long id, @RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+    public ApiResult<VppDrEvaluation> getEvaluation(@PathVariable("id") Long id) {
+        return ApiResult.success(vppDrService.getEvaluation(id));
     }
+
     @GetMapping(value = "/strategy")
-    public ApiResult<Object> pageStrategy(@RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+    public ApiResult<CommonPage<VppDrStrategy>> pageStrategy(@RequestParam(required = false) Map<String, Object> params) {
+        return ApiResult.success(vppDrService.pageStrategy(params));
     }
+
     @PostMapping(value = "/strategy")
-    public ApiResult<Object> createStrategy(@RequestBody(required = false) Object body) {
-        return ApiResult.success(null);
+    public ApiResult<VppDrStrategy> createStrategy(@RequestBody DrStrategyRequest body) {
+        return ApiResult.success(vppDrService.createStrategy(body));
     }
+
     @PutMapping(value = "/strategy/{id}")
-    public ApiResult<Void> updateStrategy(@PathVariable("id") Long id, @RequestBody(required = false) Object body) {
+    public ApiResult<Void> updateStrategy(@PathVariable("id") Long id, @RequestBody DrStrategyRequest body) {
+        vppDrService.updateStrategy(id, body);
         return ApiResult.success();
     }
+
     @DeleteMapping(value = "/strategy/{id}")
     public ApiResult<Void> deleteStrategy(@PathVariable("id") Long id) {
+        vppDrService.deleteStrategy(id);
         return ApiResult.success();
     }
-}
+}

+ 35 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/UnIntegrationController.java

@@ -0,0 +1,35 @@
+package com.usky.vpp.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.vpp.service.VppUnIntegrationService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * 运管平台 UN 主动对接(Poll、注册、Token)
+ * 网关前缀: /prod-api/service-vpp/un
+ */
+@RestController
+@RequestMapping("/un")
+public class UnIntegrationController {
+
+    @Autowired
+    private VppUnIntegrationService integrationService;
+
+    @PostMapping("/token/refresh")
+    public ApiResult<Map<String, Object>> refreshToken() {
+        return ApiResult.success(integrationService.refreshToken());
+    }
+
+    @PostMapping("/register")
+    public ApiResult<Map<String, Object>> register() {
+        return ApiResult.success(integrationService.register());
+    }
+
+    @PostMapping("/poll")
+    public ApiResult<Map<String, Object>> poll() {
+        return ApiResult.success(integrationService.pollOnce());
+    }
+}

+ 77 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/crypto/VppUnCryptoService.java

@@ -0,0 +1,77 @@
+package com.usky.vpp.crypto;
+
+import cn.hutool.core.codec.Base64;
+import cn.hutool.crypto.SmUtil;
+import cn.hutool.crypto.asymmetric.KeyType;
+import cn.hutool.crypto.asymmetric.SM2;
+import com.usky.vpp.config.VppUnProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 运管平台 UN/DN 国密加解密与签名(SM2/SM3withSM2)
+ */
+@Service
+public class VppUnCryptoService {
+
+    private static final Logger log = LoggerFactory.getLogger(VppUnCryptoService.class);
+
+    @Autowired
+    private VppUnProperties properties;
+
+    public boolean isActive() {
+        return Boolean.TRUE.equals(properties.getCryptoEnabled())
+                && StringUtils.hasText(properties.getUnPublicKey())
+                && StringUtils.hasText(properties.getDnPrivateKey());
+    }
+
+    public String encryptRequest(String plainJson) {
+        SM2 sm2 = SmUtil.sm2(null, properties.getUnPublicKey());
+        return sm2.encryptBase64(plainJson, KeyType.PublicKey);
+    }
+
+    public String signRequest(String cipherBase64) {
+        SM2 sm2 = SmUtil.sm2(properties.getDnPrivateKey(), null);
+        byte[] sign = sm2.sign(cipherBase64.getBytes(StandardCharsets.UTF_8));
+        return Base64.encode(sign);
+    }
+
+    public String decryptResponse(String cipherBase64) {
+        SM2 sm2 = SmUtil.sm2(properties.getDnPrivateKey(), properties.getDnPublicKey());
+        return sm2.decryptStr(cipherBase64, KeyType.PrivateKey);
+    }
+
+    public boolean verifyResponse(String cipherBase64, String signBase64) {
+        return verifyInbound(cipherBase64, signBase64);
+    }
+
+    /** UN→DN 请求验签 */
+    public boolean verifyInbound(String cipherBase64, String signBase64) {
+        if (!StringUtils.hasText(signBase64)) {
+            log.warn("UN 请求缺少 X-Sign,跳过验签");
+            return true;
+        }
+        SM2 sm2 = SmUtil.sm2(null, properties.getUnPublicKey());
+        return sm2.verify(cipherBase64.getBytes(StandardCharsets.UTF_8), Base64.decode(signBase64));
+    }
+
+    /** UN→DN 请求解密 */
+    public String decryptInbound(String cipherBase64) {
+        return decryptResponse(cipherBase64);
+    }
+
+    /** DN→UN 响应加密 */
+    public String encryptOutbound(String plainJson) {
+        return encryptRequest(plainJson);
+    }
+
+    /** DN→UN 响应签名 */
+    public String signOutbound(String cipherBase64) {
+        return signRequest(cipherBase64);
+    }
+}

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

@@ -50,14 +50,16 @@ public class VppContract implements Serializable {
     @TableField("account_info_json")
     private String accountInfoJson;
     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 Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -1,34 +1,34 @@
-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("operator_id")
-    private Long operatorId;
-    @TableField("operator_name")
-    private String operatorName;
-    @TableField("operated_at")
-    private LocalDateTime operatedAt;
-}
+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;

+}


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

@@ -34,14 +34,16 @@ public class VppContractTemplate implements Serializable {
     private String variablesJson;
     @TableField("is_enabled")
     private Integer isEnabled;
+    @TableField("tenant_id")
+    private Integer tenantId;
     @TableField("create_time")
     private LocalDateTime createTime;
     @TableField("update_time")
     private LocalDateTime updateTime;
     @TableField("created_by")
-    private Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -49,14 +49,16 @@ public class VppCustomer implements Serializable {
     @TableField("business_license_url")
     private String businessLicenseUrl;
     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 Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -42,22 +42,22 @@ public class VppCustomerAccess implements Serializable {
     private Integer accessStatus;
     @TableField("audit_opinion")
     private String auditOpinion;
-    @TableField("audit_by")
-    private Long auditBy;
     @TableField("audit_at")
     private LocalDateTime auditAt;
     @TableField("customer_id")
     private Long customerId;
     @TableField("apply_at")
     private LocalDateTime applyAt;
+    @TableField("tenant_id")
+    private Integer tenantId;
     @TableField("create_time")
     private LocalDateTime createTime;
     @TableField("update_time")
     private LocalDateTime updateTime;
     @TableField("created_by")
-    private Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -32,14 +32,16 @@ public class VppCustomerContact implements Serializable {
     @TableField("is_primary")
     private Integer isPrimary;
     private String position;
+    @TableField("tenant_id")
+    private Integer tenantId;
     @TableField("create_time")
     private LocalDateTime createTime;
     @TableField("update_time")
     private LocalDateTime updateTime;
     @TableField("created_by")
-    private Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -45,14 +45,16 @@ public class VppDevice implements Serializable {
     @TableField("gateway_id")
     private String gatewayId;
     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 Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -1,38 +1,40 @@
-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_device_control_log
- */
-@Data
-@EqualsAndHashCode(callSuper = false)
-@TableName("vpp_device_control_log")
-public class VppDeviceControlLog implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
-    private Long id;
-    @TableField("device_id")
-    private Long deviceId;
-    @TableField("control_type")
-    private String controlType;
-    @TableField("control_params")
-    private String controlParams;
-    @TableField("control_result")
-    private Integer controlResult;
-    @TableField("result_message")
-    private String resultMessage;
-    @TableField("operator_id")
-    private Long operatorId;
-    @TableField("operated_at")
-    private LocalDateTime operatedAt;
-}
+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_device_control_log

+ */

+@Data

+@EqualsAndHashCode(callSuper = false)

+@TableName("vpp_device_control_log")

+public class VppDeviceControlLog implements Serializable {

+

+    private static final long serialVersionUID = 1L;

+

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

+    private Long id;

+    @TableField("device_id")

+    private Long deviceId;

+    @TableField("control_type")

+    private String controlType;

+    @TableField("control_params")

+    private String controlParams;

+    @TableField("control_result")

+    private Integer controlResult;

+    @TableField("result_message")

+    private String resultMessage;

+    @TableField("tenant_id")

+    private Integer tenantId;

+    @TableField("create_time")

+    private LocalDateTime createTime;

+    @TableField("created_by")

+    private String createdBy;

+}


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

@@ -36,14 +36,16 @@ public class VppDrEvaluation implements Serializable {
     private String reportFileUrl;
     @TableField("evaluated_at")
     private LocalDateTime evaluatedAt;
+    @TableField("tenant_id")
+    private Integer tenantId;
     @TableField("create_time")
     private LocalDateTime createTime;
     @TableField("update_time")
     private LocalDateTime updateTime;
     @TableField("created_by")
-    private Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -44,14 +44,16 @@ public class VppDrEvent implements Serializable {
     private Integer eventStatus;
     @TableField("raw_payload")
     private String rawPayload;
+    @TableField("tenant_id")
+    private Integer tenantId;
     @TableField("create_time")
     private LocalDateTime createTime;
     @TableField("update_time")
     private LocalDateTime updateTime;
     @TableField("created_by")
-    private Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -36,14 +36,16 @@ public class VppDrExecution implements Serializable {
     private LocalDateTime startedAt;
     @TableField("finished_at")
     private LocalDateTime finishedAt;
+    @TableField("tenant_id")
+    private Integer tenantId;
     @TableField("create_time")
     private LocalDateTime createTime;
     @TableField("update_time")
     private LocalDateTime updateTime;
     @TableField("created_by")
-    private Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -36,14 +36,16 @@ public class VppDrParticipation implements Serializable {
     private BigDecimal clearedCapacityKw;
     @TableField("declared_at")
     private LocalDateTime declaredAt;
+    @TableField("tenant_id")
+    private Integer tenantId;
     @TableField("create_time")
     private LocalDateTime createTime;
     @TableField("update_time")
     private LocalDateTime updateTime;
     @TableField("created_by")
-    private Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -38,14 +38,16 @@ public class VppDrStrategy implements Serializable {
     private Integer isDefault;
     @TableField("is_enabled")
     private Integer isEnabled;
+    @TableField("tenant_id")
+    private Integer tenantId;
     @TableField("create_time")
     private LocalDateTime createTime;
     @TableField("update_time")
     private LocalDateTime updateTime;
     @TableField("created_by")
-    private Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -28,4 +28,6 @@ public class VppDrStrategyResource implements Serializable {
     private Integer priority;
     @TableField("max_adjust_kw")
     private BigDecimal maxAdjustKw;
+    @TableField("tenant_id")
+    private Integer tenantId;
 }

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

@@ -46,14 +46,16 @@ public class VppEnergyReadingMonthly implements Serializable {
     private Integer calcStatus;
     @TableField("calc_at")
     private LocalDateTime calcAt;
+    @TableField("tenant_id")
+    private Integer tenantId;
     @TableField("create_time")
     private LocalDateTime createTime;
     @TableField("update_time")
     private LocalDateTime updateTime;
     @TableField("created_by")
-    private Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -39,14 +39,16 @@ public class VppEnergySummaryDaily implements Serializable {
     private BigDecimal maxPowerKw;
     @TableField("avg_power_kw")
     private BigDecimal avgPowerKw;
+    @TableField("tenant_id")
+    private Integer tenantId;
     @TableField("create_time")
     private LocalDateTime createTime;
     @TableField("update_time")
     private LocalDateTime updateTime;
     @TableField("created_by")
-    private Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -1,6 +1,7 @@
 package com.usky.vpp.domain;
 
 import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.fasterxml.jackson.annotation.JsonFormat;
@@ -117,5 +118,6 @@ public class VppFileArchive implements Serializable {
     /**
      * 租户id
      */
+    @TableField("tenant_id")
     private Integer tenantId;
 }

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

@@ -1,38 +1,43 @@
-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;
-
-/**
- * vpp_payment_record
- */
-@Data
-@EqualsAndHashCode(callSuper = false)
-@TableName("vpp_payment_record")
-public class VppPaymentRecord implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
-    private Long id;
-    @TableField("bill_id")
-    private Long billId;
-    @TableField("payment_amount")
-    private BigDecimal paymentAmount;
-    @TableField("payment_method")
-    private Integer paymentMethod;
-    @TableField("payment_date")
-    private LocalDate paymentDate;
-    @TableField("voucher_url")
-    private String voucherUrl;
-    private String remark;
-    @TableField("recorded_by")
-    private Long recordedBy;
-}
+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_payment_record

+ */

+@Data

+@EqualsAndHashCode(callSuper = false)

+@TableName("vpp_payment_record")

+public class VppPaymentRecord implements Serializable {

+

+    private static final long serialVersionUID = 1L;

+

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

+    private Long id;

+    @TableField("bill_id")

+    private Long billId;

+    @TableField("payment_amount")

+    private BigDecimal paymentAmount;

+    @TableField("payment_method")

+    private Integer paymentMethod;

+    @TableField("payment_date")

+    private LocalDate paymentDate;

+    @TableField("voucher_url")

+    private String voucherUrl;

+    private String remark;

+    @TableField("tenant_id")

+    private Integer tenantId;

+    @TableField("create_time")

+    private LocalDateTime createTime;

+    @TableField("created_by")

+    private String createdBy;

+}


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

@@ -31,14 +31,16 @@ public class VppRegistration implements Serializable {
     private LocalDateTime registeredAt;
     @TableField("meta_report_id")
     private String metaReportId;
+    @TableField("tenant_id")
+    private Integer tenantId;
     @TableField("create_time")
     private LocalDateTime createTime;
     @TableField("update_time")
     private LocalDateTime updateTime;
     @TableField("created_by")
-    private Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -1,36 +1,38 @@
-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_report_audit_log
- */
-@Data
-@EqualsAndHashCode(callSuper = false)
-@TableName("vpp_report_audit_log")
-public class VppReportAuditLog implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-
-    @TableId(value = "id", type = IdType.ASSIGN_ID)
-    private Long id;
-    @TableField("record_id")
-    private Long recordId;
-    @TableField("audit_level")
-    private Integer auditLevel;
-    private Integer action;
-    private String opinion;
-    @TableField("operator_id")
-    private Long operatorId;
-    @TableField("operator_ip")
-    private String operatorIp;
-    @TableField("operated_at")
-    private LocalDateTime operatedAt;
-}
+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_report_audit_log

+ */

+@Data

+@EqualsAndHashCode(callSuper = false)

+@TableName("vpp_report_audit_log")

+public class VppReportAuditLog implements Serializable {

+

+    private static final long serialVersionUID = 1L;

+

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

+    private Long id;

+    @TableField("record_id")

+    private Long recordId;

+    @TableField("audit_level")

+    private Integer auditLevel;

+    private Integer action;

+    private String opinion;

+    @TableField("operator_ip")

+    private String operatorIp;

+    @TableField("tenant_id")

+    private Integer tenantId;

+    @TableField("create_time")

+    private LocalDateTime createTime;

+    @TableField("created_by")

+    private String createdBy;

+}


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

@@ -29,14 +29,16 @@ public class VppReportData implements Serializable {
     private String validationErrors;
     @TableField("is_draft")
     private Integer isDraft;
+    @TableField("tenant_id")
+    private Integer tenantId;
     @TableField("create_time")
     private LocalDateTime createTime;
     @TableField("update_time")
     private LocalDateTime updateTime;
     @TableField("created_by")
-    private Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -35,4 +35,6 @@ public class VppReportLog implements Serializable {
     private Integer payloadSize;
     @TableField("reported_at")
     private LocalDateTime reportedAt;
+    @TableField("tenant_id")
+    private Integer tenantId;
 }

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

@@ -41,14 +41,16 @@ public class VppReportRecord implements Serializable {
     private LocalDateTime submittedAt;
     @TableField("archived_at")
     private LocalDateTime archivedAt;
+    @TableField("tenant_id")
+    private Integer tenantId;
     @TableField("create_time")
     private LocalDateTime createTime;
     @TableField("update_time")
     private LocalDateTime updateTime;
     @TableField("created_by")
-    private Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -38,14 +38,16 @@ public class VppReportTask implements Serializable {
     private Long assigneeId;
     @TableField("remind_days")
     private Integer remindDays;
+    @TableField("tenant_id")
+    private Integer tenantId;
     @TableField("create_time")
     private LocalDateTime createTime;
     @TableField("update_time")
     private LocalDateTime updateTime;
     @TableField("created_by")
-    private Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -53,14 +53,16 @@ public class VppResourcePoint implements Serializable {
     @TableField("un_resource_id")
     private String unResourceId;
     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 Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -38,14 +38,16 @@ public class VppResourcePointConfig implements Serializable {
     private Integer offlineTimeoutSec;
     @TableField("alarm_rule_json")
     private String alarmRuleJson;
+    @TableField("tenant_id")
+    private Integer tenantId;
     @TableField("create_time")
     private LocalDateTime createTime;
     @TableField("update_time")
     private LocalDateTime updateTime;
     @TableField("created_by")
-    private Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -45,14 +45,16 @@ public class VppSettlementBill implements Serializable {
     private String billFileUrl;
     @TableField("generated_at")
     private LocalDateTime generatedAt;
+    @TableField("tenant_id")
+    private Integer tenantId;
     @TableField("create_time")
     private LocalDateTime createTime;
     @TableField("update_time")
     private LocalDateTime updateTime;
     @TableField("created_by")
-    private Long createdBy;
+    private String createdBy;
     @TableField("updated_by")
-    private Long updatedBy;
+    private String updatedBy;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

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

@@ -29,4 +29,6 @@ public class VppSettlementBillDetail implements Serializable {
     private BigDecimal energyKwh;
     private BigDecimal price;
     private BigDecimal amount;
+    @TableField("tenant_id")
+    private Integer tenantId;
 }

+ 20 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/enums/VppUnEventPhase.java

@@ -0,0 +1,20 @@
+package com.usky.vpp.enums;
+
+/**
+ * 运管平台 DistributeEventRequest 业务阶段(见联调文档事件节点表)
+ */
+public enum VppUnEventPhase {
+
+    /** 邀约:filing=true,target.resources 为空 */
+    INVITATION,
+    /** 申报结果反馈:filing=true,target.resources 含申报量 */
+    DECLARE_FEEDBACK,
+    /** 末位分拆通知:filing=false,lastFiling=true,resources 为系统出清量 */
+    SPLIT_NOTICE,
+    /** 末位分拆申报结果反馈 */
+    SPLIT_RESULT,
+    /** 出清公示:target.resources 含最终出清量 */
+    CLEARING_PUBLICITY,
+    /** 无法识别阶段,按邀约处理 */
+    UNKNOWN
+}

+ 47 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/job/VppUnBootstrapRunner.java

@@ -0,0 +1,47 @@
+package com.usky.vpp.job;
+
+import com.usky.vpp.config.VppUnProperties;
+import com.usky.vpp.service.VppUnIntegrationService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+/**
+ * 启动时自动向 UN 注册(可选)
+ */
+@Component
+@ConditionalOnProperty(name = "vpp.un.auto-register-on-startup", havingValue = "true")
+public class VppUnBootstrapRunner implements ApplicationRunner {
+
+    private static final Logger log = LoggerFactory.getLogger(VppUnBootstrapRunner.class);
+
+    @Autowired
+    private VppUnProperties properties;
+
+    @Autowired
+    private VppUnIntegrationService integrationService;
+
+    @Override
+    public void run(ApplicationArguments args) {
+        if (!properties.isOutboundActive()) {
+            log.warn("auto-register-on-startup 已开启但 outbound 未激活,跳过");
+            return;
+        }
+        try {
+            integrationService.refreshToken();
+            if (!StringUtils.hasText(properties.getRegistrationId())) {
+                integrationService.register();
+                log.info("启动注册 UN 完成");
+            } else {
+                log.info("已有 registrationID={},跳过启动注册", properties.getRegistrationId());
+            }
+        } catch (Exception ex) {
+            log.warn("启动注册 UN 失败: {}", ex.getMessage());
+        }
+    }
+}

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

@@ -0,0 +1,36 @@
+package com.usky.vpp.job;
+
+import com.usky.vpp.config.VppUnProperties;
+import com.usky.vpp.service.VppUnIntegrationService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * 定时 Poll UN 拉取事件/反馈
+ */
+@Component
+public class VppUnPollScheduler {
+
+    private static final Logger log = LoggerFactory.getLogger(VppUnPollScheduler.class);
+
+    @Autowired
+    private VppUnProperties properties;
+
+    @Autowired
+    private VppUnIntegrationService integrationService;
+
+    @Scheduled(fixedDelayString = "#{${vpp.un.poll-interval-sec:10} * 1000}")
+    public void pollUn() {
+        if (!properties.isPollActive()) {
+            return;
+        }
+        try {
+            integrationService.pollOnce();
+        } catch (Exception ex) {
+            log.warn("UN Poll 失败: {}", ex.getMessage());
+        }
+    }
+}

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

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

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

@@ -0,0 +1,37 @@
+package com.usky.vpp.service;
+
+import com.usky.vpp.domain.VppDrEvent;
+
+import java.util.Map;
+
+/**
+ * 运管平台邀约/出清报文入库
+ */
+public interface VppDrEventIngestService {
+
+    /**
+     * 解析 DistributeEventRequest(Poll 拉取或 UN 推送),写入 vpp_dr_event
+     */
+    VppDrEvent ingestDistributeEvent(Map<String, Object> body, String source);
+
+    /**
+     * @deprecated 请使用 {@link #ingestDistributeEvent}
+     */
+    @Deprecated
+    VppDrEvent ingestInvitation(Map<String, Object> body, String source);
+
+    /**
+     * 处理 CreateCqRequest(DN→UN 分拆出清申报前的本地同步,或联调回显)
+     */
+    VppDrEvent ingestCreateCqRequest(Map<String, Object> body, String source);
+
+    /**
+     * 处理 CreateOptRequest:optOut 取消事件;optIn 同步 list 申报
+     */
+    VppDrEvent ingestCreateOptRequest(Map<String, Object> body, String source);
+
+    /**
+     * 出清公示确认后更新事件状态(配合 CreateEventResponse)
+     */
+    VppDrEvent acknowledgeClearingPublicity(String platformEventId, Map<String, Object> body, String source);
+}

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

@@ -0,0 +1,11 @@
+package com.usky.vpp.service;
+
+import com.usky.vpp.domain.VppDrEvent;
+
+/**
+ * 需求响应执行明细初始化(出清确认后创建 vpp_dr_execution)
+ */
+public interface VppDrExecutionBootstrapService {
+
+    void ensureExecutions(VppDrEvent event);
+}

+ 36 - 5
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrService.java

@@ -1,15 +1,46 @@
 package com.usky.vpp.service;
 
 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.vo.DrClearingRequest;
+import com.usky.vpp.service.vo.DrInterveneRequest;
+import com.usky.vpp.service.vo.DrParticipateRequest;
+import com.usky.vpp.service.vo.DrStrategyRequest;
 
 import java.util.Map;
 
 /**
- * VppDrService 业务接口
+ * 需求响应业务接口
  */
 public interface VppDrService {
 
-    default Object stub(String action, Map<String, Object> params) {
-        return null;
-    }
-}
+    CommonPage<VppDrEvent> pageEvent(Map<String, Object> params);
+
+    VppDrEvent getEvent(Long id);
+
+    Object assessCapability(Long eventId);
+
+    void participate(Long eventId, DrParticipateRequest request);
+
+    void clearing(Long eventId, DrClearingRequest request);
+
+    Object monitorExecution(Long eventId);
+
+    void intervene(Long eventId, DrInterveneRequest request);
+
+    void completeEvent(Long eventId);
+
+    void acknowledgeClearing(Long eventId);
+
+    VppDrEvaluation getEvaluation(Long eventId);
+
+    CommonPage<VppDrStrategy> pageStrategy(Map<String, Object> params);
+
+    VppDrStrategy createStrategy(DrStrategyRequest request);
+
+    void updateStrategy(Long id, DrStrategyRequest request);
+
+    void deleteStrategy(Long id);
+}

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

@@ -19,6 +19,8 @@ public interface VppUnDnService {
 
     Map<String, Object> poll(Map<String, Object> body);
 
+    Map<String, Object> distributeEvent(Map<String, Object> body);
+
     Map<String, Object> createOpt(Map<String, Object> body);
 
     Map<String, Object> createCq(Map<String, Object> body);

+ 22 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppUnDrSyncService.java

@@ -0,0 +1,22 @@
+package com.usky.vpp.service;
+
+import com.usky.vpp.domain.VppDrEvent;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 运管平台 Opt/Cq 报文与本地 DR 参与记录同步
+ */
+public interface VppUnDrSyncService {
+
+    /**
+     * 解析 CreateOptRequest / CreateCqRequest 中 list[],同步参与/出清记录
+     */
+    void syncOptContent(VppDrEvent event, List<Map<String, Object>> optList, boolean clearing);
+
+    /**
+     * 解析 DistributeEventRequest.target.resources[],同步申报/出清量
+     */
+    void syncTargetResources(VppDrEvent event, List<Map<String, Object>> resources, boolean clearing);
+}

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

@@ -0,0 +1,25 @@
+package com.usky.vpp.service;
+
+import com.usky.vpp.domain.VppDrEvent;
+
+import java.util.Map;
+
+/**
+ * DN 主动调用 UN(Poll、注册、申报、出清确认)
+ */
+public interface VppUnIntegrationService {
+
+    Map<String, Object> refreshToken();
+
+    Map<String, Object> register();
+
+    Map<String, Object> pollOnce();
+
+    Map<String, Object> submitParticipation(Long eventId, boolean participate);
+
+    Map<String, Object> submitClearing(Long eventId);
+
+    Map<String, Object> acknowledgeClearing(Long eventId);
+
+    void handlePollResponse(Map<String, Object> response);
+}

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

@@ -0,0 +1,11 @@
+package com.usky.vpp.service;
+
+import java.util.Map;
+
+/**
+ * UN Poll 返回 CreateReportRequest 时的报告应答与上报
+ */
+public interface VppUnReportService {
+
+    void handleCreateReportRequest(Map<String, Object> createReportRequest);
+}

+ 311 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrEventIngestServiceImpl.java

@@ -0,0 +1,311 @@
+package com.usky.vpp.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.vpp.domain.VppDrEvent;
+import com.usky.vpp.enums.VppUnEventPhase;
+import com.usky.vpp.mapper.VppDrEventMapper;
+import com.usky.vpp.service.VppDrEventIngestService;
+import com.usky.vpp.service.VppDrExecutionBootstrapService;
+import com.usky.vpp.service.VppUnDrSyncService;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppUnEventParser;
+import com.usky.vpp.util.VppUnPayloadHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class VppDrEventIngestServiceImpl implements VppDrEventIngestService {
+
+    private static final Logger log = LoggerFactory.getLogger(VppDrEventIngestServiceImpl.class);
+
+    private static final int EVENT_STATUS_PENDING = 0;
+    private static final int EVENT_STATUS_DECLARED = 1;
+    private static final int EVENT_STATUS_EXECUTING = 2;
+    private static final int EVENT_STATUS_ENDED = 3;
+    private static final int EVENT_STATUS_CANCELLED = 4;
+
+    @Autowired
+    private VppDrEventMapper eventMapper;
+    @Autowired
+    private VppUnDrSyncService unDrSyncService;
+    @Autowired
+    private VppDrExecutionBootstrapService executionBootstrapService;
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public VppDrEvent ingestDistributeEvent(Map<String, Object> body, String source) {
+        List<Map<String, Object>> events = VppUnEventParser.extractEvents(body);
+        if (events.isEmpty()) {
+            throw new IllegalArgumentException("DistributeEventRequest 缺少 events");
+        }
+        VppDrEvent last = null;
+        for (Map<String, Object> eventMap : events) {
+            last = ingestSingleEvent(eventMap, body, source);
+        }
+        return last;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public VppDrEvent ingestInvitation(Map<String, Object> body, String source) {
+        return ingestDistributeEvent(body, source);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public VppDrEvent ingestCreateCqRequest(Map<String, Object> body, String source) {
+        String platformEventId = VppUnPayloadHelper.getString(body, "eventID", "eventId");
+        if (!StringUtils.hasText(platformEventId)) {
+            throw new IllegalArgumentException("CreateCqRequest 缺少 eventID");
+        }
+        VppDrEvent event = requireEvent(platformEventId);
+        event.setRawPayload(toJson(body));
+
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> list = body.get("list") instanceof List
+                ? (List<Map<String, Object>>) body.get("list") : null;
+        if (list != null && !list.isEmpty()) {
+            unDrSyncService.syncOptContent(event, list, true);
+            BigDecimal total = sumListLoad(list);
+            if (total.compareTo(BigDecimal.ZERO) > 0) {
+                event.setClearedCapacityKw(total);
+            }
+        }
+        if (event.getEventStatus() == null || event.getEventStatus() == EVENT_STATUS_PENDING) {
+            event.setEventStatus(EVENT_STATUS_DECLARED);
+        }
+        VppAuditHelper.fillUpdate(event);
+        eventMapper.updateById(event);
+        log.info("[{}] 处理 CreateCqRequest eventId={}", source, platformEventId);
+        return event;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public VppDrEvent ingestCreateOptRequest(Map<String, Object> body, String source) {
+        String platformEventId = VppUnPayloadHelper.getString(body, "eventID", "eventId");
+        if (!StringUtils.hasText(platformEventId)) {
+            throw new IllegalArgumentException("CreateOptRequest 缺少 eventID");
+        }
+        VppDrEvent event = findByPlatformEventId(platformEventId);
+        if (event == null) {
+            throw new IllegalArgumentException("事件不存在: " + platformEventId);
+        }
+        event.setRawPayload(toJson(body));
+
+        String optType = VppUnPayloadHelper.getString(body, "optType");
+        if ("optOut".equalsIgnoreCase(optType)) {
+            event.setEventStatus(EVENT_STATUS_CANCELLED);
+            VppAuditHelper.fillUpdate(event);
+            eventMapper.updateById(event);
+            log.info("[{}] optOut 取消事件 eventId={}", source, platformEventId);
+            return event;
+        }
+
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> list = body.get("list") instanceof List
+                ? (List<Map<String, Object>>) body.get("list") : null;
+        if (list != null && !list.isEmpty()) {
+            unDrSyncService.syncOptContent(event, list, false);
+            event.setEventStatus(EVENT_STATUS_DECLARED);
+        }
+        VppAuditHelper.fillUpdate(event);
+        eventMapper.updateById(event);
+        log.info("[{}] 处理 CreateOptRequest eventId={}", source, platformEventId);
+        return event;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public VppDrEvent acknowledgeClearingPublicity(String platformEventId, Map<String, Object> body, String source) {
+        VppDrEvent event = requireEvent(platformEventId);
+        event.setRawPayload(toJson(body));
+        if (event.getEventStatus() == null || event.getEventStatus() <= EVENT_STATUS_DECLARED) {
+            event.setEventStatus(EVENT_STATUS_EXECUTING);
+        }
+        executionBootstrapService.ensureExecutions(event);
+        VppAuditHelper.fillUpdate(event);
+        eventMapper.updateById(event);
+        log.info("[{}] 出清公示已确认 eventId={}", source, platformEventId);
+        return event;
+    }
+
+    private VppDrEvent ingestSingleEvent(Map<String, Object> eventMap, Map<String, Object> rawBody, String source) {
+        Map<String, Object> descriptor = VppUnEventParser.getDescriptor(eventMap);
+        String platformEventId = VppUnEventParser.getEventId(eventMap);
+        if (!StringUtils.hasText(platformEventId)) {
+            throw new IllegalArgumentException("Event 缺少 descriptor.eventID");
+        }
+
+        Integer platformStatus = VppUnEventParser.mapPlatformStatus(descriptor);
+        if (platformStatus != null && platformStatus == EVENT_STATUS_CANCELLED) {
+            return cancelEvent(findByPlatformEventId(platformEventId), platformEventId, rawBody, source);
+        }
+
+        VppUnEventPhase phase = VppUnEventParser.detectPhase(eventMap);
+        VppDrEvent event = findByPlatformEventId(platformEventId);
+        if (event == null) {
+            event = new VppDrEvent();
+            event.setEventId(platformEventId);
+            event.setEventStatus(EVENT_STATUS_PENDING);
+            applyBaseFields(event, eventMap, descriptor);
+            event.setRawPayload(toJson(rawBody));
+            VppAuditHelper.fillCreate(event);
+            eventMapper.insert(event);
+            log.info("[{}] 新建事件 eventId={}, phase={}", source, platformEventId, phase);
+        } else {
+            applyBaseFields(event, eventMap, descriptor);
+            event.setRawPayload(toJson(rawBody));
+            VppAuditHelper.fillUpdate(event);
+            eventMapper.updateById(event);
+            log.info("[{}] 更新事件 eventId={}, phase={}", source, platformEventId, phase);
+        }
+
+        applyPhase(event, eventMap, phase, platformStatus);
+        VppAuditHelper.fillUpdate(event);
+        eventMapper.updateById(event);
+        return event;
+    }
+
+    private void applyPhase(VppDrEvent event, Map<String, Object> eventMap, VppUnEventPhase phase, Integer platformStatus) {
+        List<Map<String, Object>> resources = VppUnEventParser.getTargetResources(eventMap);
+        switch (phase) {
+            case INVITATION:
+                event.setEventStatus(EVENT_STATUS_PENDING);
+                break;
+            case DECLARE_FEEDBACK:
+                event.setEventStatus(EVENT_STATUS_DECLARED);
+                unDrSyncService.syncTargetResources(event, resources, false);
+                break;
+            case SPLIT_NOTICE:
+                event.setEventStatus(EVENT_STATUS_DECLARED);
+                unDrSyncService.syncTargetResources(event, resources, true);
+                break;
+            case SPLIT_RESULT:
+                event.setEventStatus(EVENT_STATUS_DECLARED);
+                unDrSyncService.syncTargetResources(event, resources, true);
+                break;
+            case CLEARING_PUBLICITY:
+                BigDecimal cleared = VppUnEventParser.sumResourceLoadKw(resources, true);
+                if (cleared.compareTo(BigDecimal.ZERO) > 0) {
+                    event.setClearedCapacityKw(cleared);
+                }
+                unDrSyncService.syncTargetResources(event, resources, true);
+                event.setEventStatus(EVENT_STATUS_DECLARED);
+                break;
+            default:
+                break;
+        }
+        if (platformStatus != null) {
+            if (platformStatus == EVENT_STATUS_EXECUTING) {
+                event.setEventStatus(EVENT_STATUS_EXECUTING);
+            } else if (platformStatus == EVENT_STATUS_ENDED) {
+                event.setEventStatus(EVENT_STATUS_ENDED);
+            }
+        }
+    }
+
+    private void applyBaseFields(VppDrEvent event, Map<String, Object> eventMap, Map<String, Object> descriptor) {
+        String comment = VppUnPayloadHelper.getString(descriptor, "comment");
+        event.setEventName(StringUtils.hasText(comment) ? comment : event.getEventId());
+        event.setResponseType(VppUnEventParser.mapResponseType(descriptor));
+        event.setEventType(VppUnEventParser.mapEventType(descriptor));
+
+        LocalDateTime[] period = VppUnEventParser.resolveActivePeriod(eventMap);
+        if (period[0] != null) {
+            event.setStartTime(period[0]);
+        } else if (event.getStartTime() == null) {
+            throw new IllegalArgumentException("Event 缺少 activePeriod.dtstart");
+        }
+        if (period[1] != null) {
+            event.setEndTime(period[1]);
+        } else if (event.getEndTime() == null && event.getStartTime() != null) {
+            event.setEndTime(event.getStartTime().plusHours(2));
+        }
+
+        BigDecimal demandKw = VppUnEventParser.extractDemandChargeKw(eventMap);
+        if (demandKw != null) {
+            event.setTargetCapacityKw(demandKw);
+        } else if (event.getTargetCapacityKw() == null) {
+            event.setTargetCapacityKw(BigDecimal.ZERO);
+        }
+
+        BigDecimal energyPrice = VppUnEventParser.extractEnergyPrice(eventMap);
+        if (energyPrice != null) {
+            event.setSubsidyPrice(energyPrice);
+        }
+    }
+
+    private VppDrEvent cancelEvent(VppDrEvent existing, String platformEventId, Map<String, Object> body, String source) {
+        if (existing == null) {
+            existing = new VppDrEvent();
+            existing.setEventId(platformEventId);
+            existing.setEventName(platformEventId);
+            existing.setResponseType(1);
+            existing.setStartTime(LocalDateTime.now());
+            existing.setEndTime(LocalDateTime.now());
+            existing.setTargetCapacityKw(BigDecimal.ZERO);
+            existing.setRawPayload(toJson(body));
+            existing.setEventStatus(EVENT_STATUS_CANCELLED);
+            VppAuditHelper.fillCreate(existing);
+            eventMapper.insert(existing);
+        } else {
+            existing.setEventStatus(EVENT_STATUS_CANCELLED);
+            existing.setRawPayload(toJson(body));
+            VppAuditHelper.fillUpdate(existing);
+            eventMapper.updateById(existing);
+        }
+        log.info("[{}] 取消事件 eventId={}", source, platformEventId);
+        return existing;
+    }
+
+    private BigDecimal sumListLoad(List<Map<String, Object>> list) {
+        BigDecimal total = BigDecimal.ZERO;
+        for (Map<String, Object> item : list) {
+            BigDecimal load = VppUnPayloadHelper.getDecimal(item, "load");
+            if (load != null) {
+                total = total.add(load.abs());
+            }
+        }
+        return total;
+    }
+
+    private VppDrEvent findByPlatformEventId(String platformEventId) {
+        return eventMapper.selectOne(new LambdaQueryWrapper<VppDrEvent>()
+                .eq(VppDrEvent::getEventId, platformEventId)
+                .isNull(VppDrEvent::getDeletedAt)
+                .last("LIMIT 1"));
+    }
+
+    private VppDrEvent requireEvent(String platformEventId) {
+        VppDrEvent event = findByPlatformEventId(platformEventId);
+        if (event == null) {
+            throw new IllegalArgumentException("事件不存在: " + platformEventId);
+        }
+        return event;
+    }
+
+    private String toJson(Map<String, Object> body) {
+        if (body == null) {
+            return null;
+        }
+        try {
+            return objectMapper.writeValueAsString(body);
+        } catch (JsonProcessingException ex) {
+            return String.valueOf(body);
+        }
+    }
+}

+ 77 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrExecutionBootstrapServiceImpl.java

@@ -0,0 +1,77 @@
+package com.usky.vpp.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.vpp.domain.VppDrEvent;
+import com.usky.vpp.domain.VppDrExecution;
+import com.usky.vpp.domain.VppDrParticipation;
+import com.usky.vpp.mapper.VppDrExecutionMapper;
+import com.usky.vpp.mapper.VppDrParticipationMapper;
+import com.usky.vpp.service.VppDrExecutionBootstrapService;
+import com.usky.vpp.util.VppAuditHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Service
+public class VppDrExecutionBootstrapServiceImpl implements VppDrExecutionBootstrapService {
+
+    private static final Logger log = LoggerFactory.getLogger(VppDrExecutionBootstrapServiceImpl.class);
+
+    private static final int PARTICIPATE_STATUS_ACCEPT = 1;
+    private static final int EXECUTE_STATUS_RUNNING = 1;
+
+    @Autowired
+    private VppDrParticipationMapper participationMapper;
+    @Autowired
+    private VppDrExecutionMapper executionMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void ensureExecutions(VppDrEvent event) {
+        if (event == null || event.getId() == null) {
+            return;
+        }
+        long existing = executionMapper.selectCount(new LambdaQueryWrapper<VppDrExecution>()
+                .eq(VppDrExecution::getEventId, event.getId())
+                .isNull(VppDrExecution::getDeletedAt));
+        if (existing > 0) {
+            return;
+        }
+
+        List<VppDrParticipation> participations = participationMapper.selectList(
+                new LambdaQueryWrapper<VppDrParticipation>()
+                        .eq(VppDrParticipation::getEventId, event.getId())
+                        .eq(VppDrParticipation::getParticipateStatus, PARTICIPATE_STATUS_ACCEPT)
+                        .isNull(VppDrParticipation::getDeletedAt));
+        LocalDateTime now = LocalDateTime.now();
+        int created = 0;
+        for (VppDrParticipation participation : participations) {
+            BigDecimal target = participation.getClearedCapacityKw();
+            if (target == null || target.compareTo(BigDecimal.ZERO) <= 0) {
+                target = participation.getDeclaredCapacityKw();
+            }
+            if (target == null || target.compareTo(BigDecimal.ZERO) <= 0 || participation.getResourceId() == null) {
+                continue;
+            }
+            VppDrExecution execution = new VppDrExecution();
+            execution.setEventId(event.getId());
+            execution.setResourceId(participation.getResourceId());
+            execution.setTargetKw(target);
+            execution.setActualKw(BigDecimal.ZERO);
+            execution.setExecuteStatus(EXECUTE_STATUS_RUNNING);
+            execution.setStartedAt(now);
+            VppAuditHelper.fillCreate(execution);
+            executionMapper.insert(execution);
+            created++;
+        }
+        if (created > 0) {
+            log.info("已为事件 {} 初始化 {} 条执行明细", event.getEventId(), created);
+        }
+    }
+}

+ 731 - 4
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrServiceImpl.java

@@ -1,11 +1,738 @@
 package com.usky.vpp.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.vpp.domain.VppDrEvaluation;
+import com.usky.vpp.domain.VppDrEvent;
+import com.usky.vpp.domain.VppDrExecution;
+import com.usky.vpp.domain.VppDrParticipation;
+import com.usky.vpp.domain.VppDrStrategy;
+import com.usky.vpp.domain.VppDrStrategyResource;
+import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.mapper.VppDrEvaluationMapper;
+import com.usky.vpp.mapper.VppDrEventMapper;
+import com.usky.vpp.mapper.VppDrExecutionMapper;
+import com.usky.vpp.mapper.VppDrParticipationMapper;
+import com.usky.vpp.mapper.VppDrStrategyMapper;
+import com.usky.vpp.mapper.VppDrStrategyResourceMapper;
+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.DrInterveneRequest;
+import com.usky.vpp.service.vo.DrParticipateRequest;
+import com.usky.vpp.service.vo.DrStrategyRequest;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppPageHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
 
-/**
- * VppDrService 默认实现(待按业务完善)
- */
 @Service
 public class VppDrServiceImpl implements VppDrService {
-}
+
+    private static final Logger log = LoggerFactory.getLogger(VppDrServiceImpl.class);
+
+    private static final int EVENT_STATUS_PENDING = 0;
+    private static final int EVENT_STATUS_DECLARED = 1;
+    private static final int EVENT_STATUS_EXECUTING = 2;
+    private static final int EVENT_STATUS_ENDED = 3;
+    private static final int EVENT_STATUS_CANCELLED = 4;
+
+    private static final int PARTICIPATE_STATUS_ACCEPT = 1;
+
+    private static final int EXECUTE_STATUS_PENDING = 0;
+    private static final int EXECUTE_STATUS_RUNNING = 1;
+    private static final int EXECUTE_STATUS_FINISHED = 2;
+
+    @Autowired
+    private VppDrEventMapper eventMapper;
+    @Autowired
+    private VppDrParticipationMapper participationMapper;
+    @Autowired
+    private VppDrStrategyMapper strategyMapper;
+    @Autowired
+    private VppDrStrategyResourceMapper strategyResourceMapper;
+    @Autowired
+    private VppDrExecutionMapper executionMapper;
+    @Autowired
+    private VppDrEvaluationMapper evaluationMapper;
+    @Autowired
+    private VppResourcePointMapper resourcePointMapper;
+    @Autowired
+    private VppUnIntegrationService unIntegrationService;
+
+    @Override
+    public CommonPage<VppDrEvent> pageEvent(Map<String, Object> params) {
+        Page<VppDrEvent> page = VppPageHelper.of(params);
+        LambdaQueryWrapper<VppDrEvent> wrapper = new LambdaQueryWrapper<VppDrEvent>()
+                .isNull(VppDrEvent::getDeletedAt)
+                .orderByDesc(VppDrEvent::getStartTime);
+
+        if (params != null) {
+            if (params.get("eventStatus") != null) {
+                wrapper.eq(VppDrEvent::getEventStatus, Integer.parseInt(params.get("eventStatus").toString()));
+            }
+            if (params.get("responseType") != null) {
+                wrapper.eq(VppDrEvent::getResponseType, Integer.parseInt(params.get("responseType").toString()));
+            }
+            if (params.get("eventType") != null) {
+                wrapper.eq(VppDrEvent::getEventType, Integer.parseInt(params.get("eventType").toString()));
+            }
+            if (params.get("startTime") != null) {
+                wrapper.ge(VppDrEvent::getStartTime, params.get("startTime").toString());
+            }
+            if (params.get("endTime") != null) {
+                wrapper.le(VppDrEvent::getStartTime, params.get("endTime").toString());
+            }
+        }
+
+        Page<VppDrEvent> result = eventMapper.selectPage(page, wrapper);
+        return new CommonPage<>(result.getRecords(), result.getTotal(), result.getCurrent(), result.getSize());
+    }
+
+    @Override
+    public VppDrEvent getEvent(Long id) {
+        VppDrEvent event = eventMapper.selectById(id);
+        if (event == null || event.getDeletedAt() != null) {
+            throw new BusinessException("需求响应事件不存在");
+        }
+        return event;
+    }
+
+    @Override
+    public Object assessCapability(Long eventId) {
+        VppDrEvent event = getEvent(eventId);
+
+        LambdaQueryWrapper<VppResourcePoint> resourceWrapper = new LambdaQueryWrapper<VppResourcePoint>()
+                .isNull(VppResourcePoint::getDeletedAt)
+                .eq(VppResourcePoint::getRunStatus, 1);
+
+        List<VppResourcePoint> resources = resourcePointMapper.selectList(resourceWrapper);
+
+        BigDecimal totalUpCapacity = BigDecimal.ZERO;
+        BigDecimal totalDownCapacity = BigDecimal.ZERO;
+        int avgResponseTime = 120;
+        BigDecimal avgRampSpeed = BigDecimal.valueOf(50);
+
+        List<Map<String, Object>> resourceList = new ArrayList<>();
+        for (VppResourcePoint resource : resources) {
+            Map<String, Object> item = new HashMap<>();
+            item.put("resourceId", resource.getId());
+            item.put("resourceName", resource.getResourceName());
+            item.put("upCapacityKw", resource.getAdjustableKw() != null ? resource.getAdjustableKw() : BigDecimal.ZERO);
+            item.put("downCapacityKw", resource.getAdjustableKw() != null ? resource.getAdjustableKw() : BigDecimal.ZERO);
+            item.put("responsePriority", resource.getResponsePriority());
+            item.put("available", resource.getRunStatus() == 1);
+            resourceList.add(item);
+
+            BigDecimal adj = resource.getAdjustableKw() != null ? resource.getAdjustableKw() : BigDecimal.ZERO;
+            totalUpCapacity = totalUpCapacity.add(adj);
+            totalDownCapacity = totalDownCapacity.add(adj);
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("eventId", event.getEventId());
+        result.put("targetCapacityKw", event.getTargetCapacityKw());
+        result.put("totalUpCapacityKw", totalUpCapacity);
+        result.put("totalDownCapacityKw", totalDownCapacity);
+        result.put("avgResponseTimeSec", avgResponseTime);
+        result.put("avgRampSpeedKwMin", avgRampSpeed);
+        result.put("sufficient", event.getTargetCapacityKw() == null
+                || totalUpCapacity.compareTo(event.getTargetCapacityKw()) >= 0);
+        result.put("resources", resourceList);
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void participate(Long eventId, DrParticipateRequest request) {
+        VppDrEvent event = getEvent(eventId);
+
+        if (event.getEventStatus() != EVENT_STATUS_PENDING) {
+            throw new BusinessException("当前状态不允许参与");
+        }
+
+        if (request == null || request.getParticipate() == null) {
+            throw new BusinessException("参与状态不能为空");
+        }
+
+        List<VppDrParticipation> existingParticipations = participationMapper.selectList(
+                new LambdaQueryWrapper<VppDrParticipation>()
+                        .eq(VppDrParticipation::getEventId, eventId)
+                        .eq(VppDrParticipation::getParticipateStatus, PARTICIPATE_STATUS_ACCEPT)
+                        .isNull(VppDrParticipation::getDeletedAt)
+        );
+
+        if (!existingParticipations.isEmpty()) {
+            throw new BusinessException("已参与该事件");
+        }
+
+        if (Boolean.TRUE.equals(request.getParticipate())) {
+            if (request.getDeclaredCapacityKw() == null || request.getDeclaredCapacityKw().compareTo(BigDecimal.ZERO) <= 0) {
+                throw new BusinessException("申报容量必须大于0");
+            }
+
+            if (request.getResources() == null || request.getResources().isEmpty()) {
+                throw new BusinessException("必须选择参与资源");
+            }
+
+            BigDecimal resourceDeclaredTotal = BigDecimal.ZERO;
+            for (DrParticipateRequest.DrResourceDeclare resource : request.getResources()) {
+                if (resource.getResourceId() == null) {
+                    throw new BusinessException("资源ID不能为空");
+                }
+                if (resource.getDeclaredCapacityKw() == null || resource.getDeclaredCapacityKw().compareTo(BigDecimal.ZERO) <= 0) {
+                    throw new BusinessException("资源申报容量必须大于0");
+                }
+                VppResourcePoint rp = resourcePointMapper.selectById(resource.getResourceId());
+                if (rp == null || rp.getDeletedAt() != null) {
+                    throw new BusinessException("资源点不存在");
+                }
+                if (rp.getRunStatus() != 1) {
+                    throw new BusinessException("资源点不在线,无法参与");
+                }
+                Long customerId = rp.getCustomerId();
+                if (customerId == null) {
+                    throw new BusinessException("资源点未关联客户");
+                }
+                resourceDeclaredTotal = resourceDeclaredTotal.add(resource.getDeclaredCapacityKw());
+            }
+
+            if (resourceDeclaredTotal.compareTo(request.getDeclaredCapacityKw()) != 0) {
+                throw new BusinessException("各资源申报容量之和与总申报容量不一致");
+            }
+
+            for (DrParticipateRequest.DrResourceDeclare resource : request.getResources()) {
+                VppResourcePoint rp = resourcePointMapper.selectById(resource.getResourceId());
+                VppDrParticipation participation = new VppDrParticipation();
+                participation.setEventId(eventId);
+                participation.setCustomerId(rp.getCustomerId());
+                participation.setResourceId(resource.getResourceId());
+                participation.setParticipateStatus(PARTICIPATE_STATUS_ACCEPT);
+                participation.setDeclaredCapacityKw(resource.getDeclaredCapacityKw());
+                participation.setDeclaredAt(LocalDateTime.now());
+                VppAuditHelper.fillCreate(participation);
+                participationMapper.insert(participation);
+            }
+
+            event.setEventStatus(EVENT_STATUS_DECLARED);
+            VppAuditHelper.fillUpdate(event);
+            eventMapper.updateById(event);
+        } else {
+            event.setEventStatus(EVENT_STATUS_CANCELLED);
+            VppAuditHelper.fillUpdate(event);
+            eventMapper.updateById(event);
+        }
+
+        syncParticipationToUn(eventId, request.getParticipate());
+    }
+
+    private void syncParticipationToUn(Long eventId, Boolean participate) {
+        try {
+            unIntegrationService.submitParticipation(eventId, Boolean.TRUE.equals(participate));
+        } catch (Exception ex) {
+            log.warn("同步 UN 参与申报失败 eventId={}: {}", eventId, ex.getMessage());
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void clearing(Long eventId, DrClearingRequest request) {
+        VppDrEvent event = getEvent(eventId);
+
+        if (event.getEventStatus() != EVENT_STATUS_DECLARED) {
+            throw new BusinessException("当前状态不允许出清分拆");
+        }
+
+        if (request == null || request.getTotalClearedCapacityKw() == null) {
+            throw new BusinessException("出清总容量不能为空");
+        }
+
+        if (request.getResources() == null || request.getResources().isEmpty()) {
+            throw new BusinessException("资源分拆列表不能为空");
+        }
+
+        long existingExecCount = executionMapper.selectCount(
+                new LambdaQueryWrapper<VppDrExecution>()
+                        .eq(VppDrExecution::getEventId, eventId)
+                        .isNull(VppDrExecution::getDeletedAt)
+        );
+        if (existingExecCount > 0) {
+            throw new BusinessException("已出清,请勿重复操作");
+        }
+
+        BigDecimal totalCleared = BigDecimal.ZERO;
+        for (DrClearingRequest.DrResourceClearing resource : request.getResources()) {
+            if (resource.getResourceId() == null) {
+                throw new BusinessException("资源ID不能为空");
+            }
+            if (resource.getClearedCapacityKw() == null || resource.getClearedCapacityKw().compareTo(BigDecimal.ZERO) <= 0) {
+                throw new BusinessException("出清容量必须大于0");
+            }
+            totalCleared = totalCleared.add(resource.getClearedCapacityKw());
+        }
+
+        if (totalCleared.compareTo(request.getTotalClearedCapacityKw()) != 0) {
+            throw new BusinessException("分拆容量之和与总出清容量不一致");
+        }
+
+        for (DrClearingRequest.DrResourceClearing resource : request.getResources()) {
+            LambdaQueryWrapper<VppDrParticipation> wrapper = new LambdaQueryWrapper<VppDrParticipation>()
+                    .eq(VppDrParticipation::getEventId, eventId)
+                    .eq(VppDrParticipation::getResourceId, resource.getResourceId())
+                    .eq(VppDrParticipation::getParticipateStatus, PARTICIPATE_STATUS_ACCEPT)
+                    .isNull(VppDrParticipation::getDeletedAt);
+
+            VppDrParticipation participation = participationMapper.selectOne(wrapper);
+            if (participation == null) {
+                throw new BusinessException("资源未参与申报,无法出清: " + resource.getResourceId());
+            }
+            if (participation.getDeclaredCapacityKw() != null
+                    && resource.getClearedCapacityKw().compareTo(participation.getDeclaredCapacityKw()) > 0) {
+                throw new BusinessException("出清容量不能超过申报容量");
+            }
+            participation.setClearedCapacityKw(resource.getClearedCapacityKw());
+            VppAuditHelper.fillUpdate(participation);
+            participationMapper.updateById(participation);
+        }
+
+        event.setClearedCapacityKw(request.getTotalClearedCapacityKw());
+        event.setEventStatus(EVENT_STATUS_EXECUTING);
+        VppAuditHelper.fillUpdate(event);
+        eventMapper.updateById(event);
+
+        for (DrClearingRequest.DrResourceClearing resource : request.getResources()) {
+            VppDrExecution execution = new VppDrExecution();
+            execution.setEventId(eventId);
+            execution.setResourceId(resource.getResourceId());
+            execution.setTargetKw(resource.getClearedCapacityKw());
+            execution.setActualKw(BigDecimal.ZERO);
+            execution.setExecuteStatus(EXECUTE_STATUS_RUNNING);
+            execution.setStartedAt(LocalDateTime.now());
+            VppAuditHelper.fillCreate(execution);
+            executionMapper.insert(execution);
+        }
+
+        syncClearingToUn(eventId);
+    }
+
+    private void syncClearingToUn(Long eventId) {
+        try {
+            unIntegrationService.submitClearing(eventId);
+        } catch (Exception ex) {
+            log.warn("同步 UN 出清分拆失败 eventId={}: {}", eventId, ex.getMessage());
+        }
+    }
+
+    @Override
+    public Object monitorExecution(Long eventId) {
+        VppDrEvent event = getEvent(eventId);
+
+        List<VppDrExecution> executions = executionMapper.selectList(
+                new LambdaQueryWrapper<VppDrExecution>()
+                        .eq(VppDrExecution::getEventId, eventId)
+                        .isNull(VppDrExecution::getDeletedAt)
+        );
+
+        BigDecimal totalTarget = BigDecimal.ZERO;
+        BigDecimal totalActual = BigDecimal.ZERO;
+
+        List<Map<String, Object>> resources = new ArrayList<>();
+        for (VppDrExecution exe : executions) {
+            VppResourcePoint rp = resourcePointMapper.selectById(exe.getResourceId());
+
+            Map<String, Object> item = new HashMap<>();
+            item.put("resourceId", exe.getResourceId());
+            item.put("resourceName", rp != null ? rp.getResourceName() : "");
+            item.put("targetKw", exe.getTargetKw());
+            item.put("actualKw", exe.getActualKw());
+            item.put("executeStatus", exe.getExecuteStatus());
+            resources.add(item);
+
+            totalTarget = totalTarget.add(exe.getTargetKw() != null ? exe.getTargetKw() : BigDecimal.ZERO);
+            totalActual = totalActual.add(exe.getActualKw() != null ? exe.getActualKw() : BigDecimal.ZERO);
+        }
+
+        int remainingMinutes = 0;
+        if (event.getEndTime() != null) {
+            remainingMinutes = (int) java.time.Duration.between(LocalDateTime.now(), event.getEndTime()).toMinutes();
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("eventId", event.getEventId());
+        result.put("targetCapacityKw", totalTarget.compareTo(BigDecimal.ZERO) > 0 ? totalTarget : event.getClearedCapacityKw());
+        result.put("actualCapacityKw", totalActual);
+        result.put("progressPercent", totalTarget.compareTo(BigDecimal.ZERO) > 0 ?
+                totalActual.multiply(BigDecimal.valueOf(100)).divide(totalTarget, 2, RoundingMode.HALF_UP) : BigDecimal.ZERO);
+        result.put("remainingMinutes", remainingMinutes > 0 ? remainingMinutes : 0);
+        result.put("resources", resources);
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void intervene(Long eventId, DrInterveneRequest request) {
+        VppDrEvent event = getEvent(eventId);
+
+        if (event.getEventStatus() != EVENT_STATUS_EXECUTING) {
+            throw new BusinessException("仅执行中的事件允许干预");
+        }
+
+        if (request == null || !StringUtils.hasText(request.getAction())) {
+            throw new BusinessException("干预动作不能为空");
+        }
+
+        String action = request.getAction();
+        List<VppDrExecution> executions = executionMapper.selectList(
+                new LambdaQueryWrapper<VppDrExecution>()
+                        .eq(VppDrExecution::getEventId, eventId)
+                        .isNull(VppDrExecution::getDeletedAt)
+        );
+        if (executions.isEmpty()) {
+            throw new BusinessException("暂无执行明细,无法干预");
+        }
+
+        if ("ADJUST".equals(action)) {
+            if (request.getStrategyId() == null) {
+                throw new BusinessException("切换策略时必须指定策略ID");
+            }
+            VppDrStrategy strategy = strategyMapper.selectById(request.getStrategyId());
+            if (strategy == null || strategy.getDeletedAt() != null) {
+                throw new BusinessException("策略不存在");
+            }
+            if (strategy.getIsEnabled() != null && strategy.getIsEnabled() == 0) {
+                throw new BusinessException("策略未启用");
+            }
+            applyStrategyTargets(event, executions, request.getStrategyId());
+        } else if ("PAUSE".equals(action)) {
+            for (VppDrExecution execution : executions) {
+                if (execution.getExecuteStatus() == EXECUTE_STATUS_RUNNING) {
+                    execution.setExecuteStatus(EXECUTE_STATUS_PENDING);
+                    VppAuditHelper.fillUpdate(execution);
+                    executionMapper.updateById(execution);
+                }
+            }
+        } else if ("RESUME".equals(action)) {
+            for (VppDrExecution execution : executions) {
+                if (execution.getExecuteStatus() == EXECUTE_STATUS_PENDING) {
+                    execution.setExecuteStatus(EXECUTE_STATUS_RUNNING);
+                    if (execution.getStartedAt() == null) {
+                        execution.setStartedAt(LocalDateTime.now());
+                    }
+                    VppAuditHelper.fillUpdate(execution);
+                    executionMapper.updateById(execution);
+                }
+            }
+        } else {
+            throw new BusinessException("无效的干预动作");
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void completeEvent(Long eventId) {
+        VppDrEvent event = getEvent(eventId);
+        if (event.getEventStatus() != EVENT_STATUS_EXECUTING) {
+            throw new BusinessException("仅执行中的事件可结束");
+        }
+
+        List<VppDrExecution> executions = executionMapper.selectList(
+                new LambdaQueryWrapper<VppDrExecution>()
+                        .eq(VppDrExecution::getEventId, eventId)
+                        .isNull(VppDrExecution::getDeletedAt)
+        );
+        if (executions.isEmpty()) {
+            throw new BusinessException("暂无执行明细,无法结束事件");
+        }
+
+        BigDecimal totalActual = BigDecimal.ZERO;
+        LocalDateTime now = LocalDateTime.now();
+        for (VppDrExecution execution : executions) {
+            BigDecimal actual = execution.getActualKw();
+            if (actual == null || actual.compareTo(BigDecimal.ZERO) == 0) {
+                actual = execution.getTargetKw() != null ? execution.getTargetKw() : BigDecimal.ZERO;
+                execution.setActualKw(actual);
+            }
+            totalActual = totalActual.add(actual);
+            execution.setExecuteStatus(EXECUTE_STATUS_FINISHED);
+            execution.setFinishedAt(now);
+            VppAuditHelper.fillUpdate(execution);
+            executionMapper.updateById(execution);
+        }
+
+        event.setEventStatus(EVENT_STATUS_ENDED);
+        VppAuditHelper.fillUpdate(event);
+        eventMapper.updateById(event);
+
+        VppDrEvaluation evaluation = evaluationMapper.selectOne(
+                new LambdaQueryWrapper<VppDrEvaluation>()
+                        .eq(VppDrEvaluation::getEventId, eventId)
+                        .isNull(VppDrEvaluation::getDeletedAt)
+        );
+        if (evaluation == null) {
+            evaluation = new VppDrEvaluation();
+            evaluation.setEventId(eventId);
+            evaluation.setActualCapacityKw(totalActual);
+            if (event.getStartTime() != null && event.getEndTime() != null) {
+                evaluation.setResponseDurationMin(
+                        (int) java.time.Duration.between(event.getStartTime(), event.getEndTime()).toMinutes());
+            }
+            if (event.getSubsidyPrice() != null) {
+                evaluation.setSubsidyAmount(totalActual.multiply(event.getSubsidyPrice()));
+            }
+            BigDecimal cleared = event.getClearedCapacityKw();
+            if (cleared != null && cleared.compareTo(BigDecimal.ZERO) > 0) {
+                evaluation.setQualifiedRate(totalActual.multiply(BigDecimal.valueOf(100))
+                        .divide(cleared, 2, RoundingMode.HALF_UP));
+            }
+            evaluation.setEvaluatedAt(now);
+            VppAuditHelper.fillCreate(evaluation);
+            evaluationMapper.insert(evaluation);
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void acknowledgeClearing(Long eventId) {
+        VppDrEvent event = getEvent(eventId);
+        if (event.getEventStatus() != EVENT_STATUS_DECLARED) {
+            throw new BusinessException("当前状态不允许确认出清公示");
+        }
+        unIntegrationService.acknowledgeClearing(eventId);
+    }
+
+    @Override
+    public VppDrEvaluation getEvaluation(Long eventId) {
+        VppDrEvent event = getEvent(eventId);
+
+        if (event.getEventStatus() != EVENT_STATUS_ENDED) {
+            throw new BusinessException("事件未结束,暂无评估结果");
+        }
+
+        LambdaQueryWrapper<VppDrEvaluation> wrapper = new LambdaQueryWrapper<VppDrEvaluation>()
+                .eq(VppDrEvaluation::getEventId, eventId)
+                .isNull(VppDrEvaluation::getDeletedAt);
+
+        VppDrEvaluation evaluation = evaluationMapper.selectOne(wrapper);
+        if (evaluation == null) {
+            throw new BusinessException("评估结果不存在");
+        }
+        return evaluation;
+    }
+
+    @Override
+    public CommonPage<VppDrStrategy> pageStrategy(Map<String, Object> params) {
+        Page<VppDrStrategy> page = VppPageHelper.of(params);
+        LambdaQueryWrapper<VppDrStrategy> wrapper = new LambdaQueryWrapper<VppDrStrategy>()
+                .isNull(VppDrStrategy::getDeletedAt)
+                .orderByDesc(VppDrStrategy::getCreateTime);
+
+        if (params != null) {
+            if (params.get("responseType") != null) {
+                wrapper.eq(VppDrStrategy::getResponseType, Integer.parseInt(params.get("responseType").toString()));
+            }
+            if (params.get("isEnabled") != null) {
+                wrapper.eq(VppDrStrategy::getIsEnabled, Integer.parseInt(params.get("isEnabled").toString()));
+            }
+            if (params.get("strategyName") != null) {
+                wrapper.like(VppDrStrategy::getStrategyName, params.get("strategyName").toString());
+            }
+        }
+
+        Page<VppDrStrategy> result = strategyMapper.selectPage(page, wrapper);
+        return new CommonPage<>(result.getRecords(), result.getTotal(), result.getCurrent(), result.getSize());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public VppDrStrategy createStrategy(DrStrategyRequest request) {
+        validateStrategyRequest(request);
+
+        VppDrStrategy strategy = new VppDrStrategy();
+        strategy.setStrategyCode(request.getStrategyCode());
+        strategy.setStrategyName(request.getStrategyName());
+        strategy.setResponseType(request.getResponseType());
+        strategy.setAdjustStepKw(request.getAdjustStepKw());
+        strategy.setExecuteMode(request.getExecuteMode() != null ? request.getExecuteMode() : 1);
+        strategy.setStrategyConfig(request.getStrategyConfig());
+        strategy.setIsDefault(request.getIsDefault() != null ? request.getIsDefault() : 0);
+        strategy.setIsEnabled(request.getIsEnabled() != null ? request.getIsEnabled() : 1);
+        VppAuditHelper.fillCreate(strategy);
+        strategyMapper.insert(strategy);
+
+        if (request.getIsDefault() != null && request.getIsDefault() == 1) {
+            LambdaQueryWrapper<VppDrStrategy> wrapper = new LambdaQueryWrapper<VppDrStrategy>()
+                    .ne(VppDrStrategy::getId, strategy.getId())
+                    .eq(VppDrStrategy::getIsDefault, 1)
+                    .isNull(VppDrStrategy::getDeletedAt);
+
+            List<VppDrStrategy> defaults = strategyMapper.selectList(wrapper);
+            for (VppDrStrategy def : defaults) {
+                def.setIsDefault(0);
+                VppAuditHelper.fillUpdate(def);
+                strategyMapper.updateById(def);
+            }
+        }
+
+        if (request.getResources() != null) {
+            for (DrStrategyRequest.DrStrategyResource resource : request.getResources()) {
+                VppDrStrategyResource sr = new VppDrStrategyResource();
+                sr.setStrategyId(strategy.getId());
+                sr.setResourceId(resource.getResourceId());
+                sr.setPriority(resource.getPriority() != null ? resource.getPriority() : 0);
+                sr.setMaxAdjustKw(resource.getMaxAdjustKw());
+                strategyResourceMapper.insert(sr);
+            }
+        }
+
+        return strategy;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateStrategy(Long id, DrStrategyRequest request) {
+        VppDrStrategy strategy = strategyMapper.selectById(id);
+        if (strategy == null || strategy.getDeletedAt() != null) {
+            throw new BusinessException("策略不存在");
+        }
+
+        validateStrategyRequest(request);
+
+        strategy.setStrategyCode(request.getStrategyCode());
+        strategy.setStrategyName(request.getStrategyName());
+        strategy.setResponseType(request.getResponseType());
+        strategy.setAdjustStepKw(request.getAdjustStepKw());
+        if (request.getExecuteMode() != null) {
+            strategy.setExecuteMode(request.getExecuteMode());
+        }
+        strategy.setStrategyConfig(request.getStrategyConfig());
+        if (request.getIsDefault() != null) {
+            strategy.setIsDefault(request.getIsDefault());
+        }
+        if (request.getIsEnabled() != null) {
+            strategy.setIsEnabled(request.getIsEnabled());
+        }
+
+        if (request.getIsDefault() != null && request.getIsDefault() == 1) {
+            LambdaQueryWrapper<VppDrStrategy> wrapper = new LambdaQueryWrapper<VppDrStrategy>()
+                    .ne(VppDrStrategy::getId, id)
+                    .eq(VppDrStrategy::getIsDefault, 1)
+                    .isNull(VppDrStrategy::getDeletedAt);
+
+            List<VppDrStrategy> defaults = strategyMapper.selectList(wrapper);
+            for (VppDrStrategy def : defaults) {
+                def.setIsDefault(0);
+                VppAuditHelper.fillUpdate(def);
+                strategyMapper.updateById(def);
+            }
+        }
+
+        VppAuditHelper.fillUpdate(strategy);
+        strategyMapper.updateById(strategy);
+
+        strategyResourceMapper.delete(new LambdaQueryWrapper<VppDrStrategyResource>()
+                .eq(VppDrStrategyResource::getStrategyId, id));
+
+        if (request.getResources() != null) {
+            for (DrStrategyRequest.DrStrategyResource resource : request.getResources()) {
+                VppDrStrategyResource sr = new VppDrStrategyResource();
+                sr.setStrategyId(id);
+                sr.setResourceId(resource.getResourceId());
+                sr.setPriority(resource.getPriority() != null ? resource.getPriority() : 0);
+                sr.setMaxAdjustKw(resource.getMaxAdjustKw());
+                strategyResourceMapper.insert(sr);
+            }
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteStrategy(Long id) {
+        VppDrStrategy strategy = strategyMapper.selectById(id);
+        if (strategy == null || strategy.getDeletedAt() != null) {
+            throw new BusinessException("策略不存在");
+        }
+
+        strategy.setDeletedAt(LocalDateTime.now());
+        VppAuditHelper.fillUpdate(strategy);
+        strategyMapper.updateById(strategy);
+
+        strategyResourceMapper.delete(new LambdaQueryWrapper<VppDrStrategyResource>()
+                .eq(VppDrStrategyResource::getStrategyId, id));
+    }
+
+    private void validateStrategyRequest(DrStrategyRequest request) {
+        if (request == null) {
+            throw new BusinessException("请求不能为空");
+        }
+        if (!StringUtils.hasText(request.getStrategyCode())) {
+            throw new BusinessException("策略编码不能为空");
+        }
+        if (!StringUtils.hasText(request.getStrategyName())) {
+            throw new BusinessException("策略名称不能为空");
+        }
+        if (request.getResponseType() == null) {
+            throw new BusinessException("响应类型不能为空");
+        }
+        if (request.getResources() != null) {
+            for (DrStrategyRequest.DrStrategyResource resource : request.getResources()) {
+                if (resource.getResourceId() == null) {
+                    throw new BusinessException("策略资源ID不能为空");
+                }
+                VppResourcePoint rp = resourcePointMapper.selectById(resource.getResourceId());
+                if (rp == null || rp.getDeletedAt() != null) {
+                    throw new BusinessException("策略关联的资源点不存在");
+                }
+            }
+        }
+    }
+
+    private void applyStrategyTargets(VppDrEvent event, List<VppDrExecution> executions, Long strategyId) {
+        List<VppDrStrategyResource> strategyResources = strategyResourceMapper.selectList(
+                new LambdaQueryWrapper<VppDrStrategyResource>()
+                        .eq(VppDrStrategyResource::getStrategyId, strategyId)
+                        .orderByAsc(VppDrStrategyResource::getPriority)
+        );
+        if (strategyResources.isEmpty()) {
+            throw new BusinessException("策略未配置资源");
+        }
+
+        Map<Long, VppDrExecution> executionByResource = executions.stream()
+                .collect(Collectors.toMap(VppDrExecution::getResourceId, e -> e, (a, b) -> a));
+
+        BigDecimal totalMaxAdjust = strategyResources.stream()
+                .map(sr -> sr.getMaxAdjustKw() != null ? sr.getMaxAdjustKw() : BigDecimal.ZERO)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+        BigDecimal totalTarget = event.getClearedCapacityKw() != null ? event.getClearedCapacityKw() : BigDecimal.ZERO;
+
+        for (VppDrStrategyResource strategyResource : strategyResources) {
+            VppDrExecution execution = executionByResource.get(strategyResource.getResourceId());
+            if (execution == null) {
+                continue;
+            }
+            BigDecimal maxAdjust = strategyResource.getMaxAdjustKw() != null ? strategyResource.getMaxAdjustKw() : BigDecimal.ZERO;
+            BigDecimal newTarget = totalMaxAdjust.compareTo(BigDecimal.ZERO) > 0
+                    ? totalTarget.multiply(maxAdjust).divide(totalMaxAdjust, 4, RoundingMode.HALF_UP)
+                    : BigDecimal.ZERO;
+            execution.setTargetKw(newTarget);
+            execution.setExecuteStatus(EXECUTE_STATUS_RUNNING);
+            if (execution.getStartedAt() == null) {
+                execution.setStartedAt(LocalDateTime.now());
+            }
+            VppAuditHelper.fillUpdate(execution);
+            executionMapper.updateById(execution);
+        }
+    }
+}

+ 137 - 5
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppUnDnServiceImpl.java

@@ -1,24 +1,36 @@
 package com.usky.vpp.service.impl;
 
 import com.usky.vpp.config.VppUnProperties;
+import com.usky.vpp.domain.VppDrEvent;
+import com.usky.vpp.service.VppDrEventIngestService;
 import com.usky.vpp.service.VppUnDnService;
+import com.usky.vpp.util.VppUnMessageBuilder;
+import com.usky.vpp.util.VppUnPayloadHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
 /**
- * 运管平台 UN/DN 对接默认实现
- * 加解密与国密签名待联调阶段完善
+ * 运管平台 UN/DN 对接实现(报文结构对齐联调 JSON 实例 20251127)
  */
 @Service
 public class VppUnDnServiceImpl implements VppUnDnService {
 
+    private static final Logger log = LoggerFactory.getLogger(VppUnDnServiceImpl.class);
+
     @Autowired
     private VppUnProperties unProperties;
 
+    @Autowired
+    private VppDrEventIngestService drEventIngestService;
+
     @Override
     public Map<String, Object> tokenRequest(Map<String, Object> body) {
         return ok("TokenResponse", body);
@@ -54,19 +66,123 @@ public class VppUnDnServiceImpl implements VppUnDnService {
         return ok("PollResponse", body);
     }
 
+    @Override
+    public Map<String, Object> distributeEvent(Map<String, Object> body) {
+        return ingestDistributeAndRespond("DistributeEventRequest", body, () ->
+                drEventIngestService.ingestDistributeEvent(body, "DistributeEventRequest"));
+    }
+
     @Override
     public Map<String, Object> createOpt(Map<String, Object> body) {
-        return ok("CreateOptResponse", body);
+        try {
+            VppDrEvent event = drEventIngestService.ingestCreateOptRequest(body, "CreateOptRequest");
+            Map<String, Object> resp = VppUnMessageBuilder.buildCreateOptResponse(body, unProperties);
+            enrichEventMeta(resp, event);
+            return resp;
+        } catch (IllegalArgumentException ex) {
+            log.warn("CreateOptRequest 处理失败: {}", ex.getMessage());
+            return error("CreateOptResponse", body, 400, ex.getMessage());
+        } catch (Exception ex) {
+            log.error("CreateOptRequest 处理失败", ex);
+            return error("CreateOptResponse", body, 500, "处理失败");
+        }
     }
 
     @Override
     public Map<String, Object> createCq(Map<String, Object> body) {
-        return ok("CreateCqResponse", body);
+        try {
+            VppDrEvent event = drEventIngestService.ingestCreateCqRequest(body, "CreateCqRequest");
+            Map<String, Object> resp = VppUnMessageBuilder.buildCreateCqResponse(body, unProperties);
+            enrichEventMeta(resp, event);
+            return resp;
+        } catch (IllegalArgumentException ex) {
+            log.warn("CreateCqRequest 处理失败: {}", ex.getMessage());
+            return error("CreateCqResponse", body, 400, ex.getMessage());
+        } catch (Exception ex) {
+            log.error("CreateCqRequest 处理失败", ex);
+            return error("CreateCqResponse", body, 500, "处理失败");
+        }
     }
 
     @Override
     public Map<String, Object> createEventResponse(Map<String, Object> body) {
-        return ok("CreateEventResponseResponse", body);
+        try {
+            String eventId = resolveEventIdForAck(body);
+            if (!StringUtils.hasText(eventId)) {
+                throw new IllegalArgumentException("CreateEventResponse 缺少 eventID");
+            }
+            VppDrEvent event = drEventIngestService.acknowledgeClearingPublicity(eventId, body, "CreateEventResponse");
+            String requestId = VppUnPayloadHelper.getString(body, "requestID");
+            String optType = resolveOptType(body);
+            Map<String, Object> resp = VppUnMessageBuilder.buildCreateEventResponse(
+                    eventId, requestId, optType, unProperties);
+            enrichEventMeta(resp, event);
+            return resp;
+        } catch (IllegalArgumentException ex) {
+            log.warn("CreateEventResponse 处理失败: {}", ex.getMessage());
+            return error("CreateEventResponse", body, 400, ex.getMessage());
+        } catch (Exception ex) {
+            log.error("CreateEventResponse 处理失败", ex);
+            return error("CreateEventResponse", body, 500, "处理失败");
+        }
+    }
+
+    private Map<String, Object> ingestDistributeAndRespond(String root, Map<String, Object> body, IngestAction action) {
+        try {
+            VppDrEvent event = action.run();
+            Map<String, Object> resp = ok(root, body);
+            enrichEventMeta(resp, event);
+            return resp;
+        } catch (IllegalArgumentException ex) {
+            log.warn("{} 报文解析失败: {}", root, ex.getMessage());
+            return error(root, body, 400, ex.getMessage());
+        } catch (Exception ex) {
+            log.error("{} 入库失败", root, ex);
+            return error(root, body, 500, "事件入库失败");
+        }
+    }
+
+    private void enrichEventMeta(Map<String, Object> resp, VppDrEvent event) {
+        if (event != null) {
+            resp.put("eventID", event.getEventId());
+            resp.put("internalId", event.getId());
+            resp.put("eventStatus", event.getEventStatus());
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private String resolveEventIdForAck(Map<String, Object> body) {
+        String eventId = VppUnPayloadHelper.getString(body, "eventID", "eventId");
+        if (StringUtils.hasText(eventId)) {
+            return eventId;
+        }
+        Object eventResponses = body.get("eventResponses");
+        if (eventResponses instanceof List && !((List<?>) eventResponses).isEmpty()) {
+            Object first = ((List<?>) eventResponses).get(0);
+            if (first instanceof Map) {
+                Object qualified = ((Map<String, Object>) first).get("qualifiedEventID");
+                if (qualified instanceof Map) {
+                    return VppUnPayloadHelper.getString((Map<String, Object>) qualified, "eventID", "eventId");
+                }
+            }
+        }
+        return null;
+    }
+
+    @SuppressWarnings("unchecked")
+    private String resolveOptType(Map<String, Object> body) {
+        String optType = VppUnPayloadHelper.getString(body, "optType");
+        if (StringUtils.hasText(optType)) {
+            return optType;
+        }
+        Object eventResponses = body.get("eventResponses");
+        if (eventResponses instanceof List && !((List<?>) eventResponses).isEmpty()) {
+            Object first = ((List<?>) eventResponses).get(0);
+            if (first instanceof Map) {
+                return VppUnPayloadHelper.getString((Map<String, Object>) first, "optType");
+            }
+        }
+        return "optIn";
     }
 
     private Map<String, Object> ok(String root, Map<String, Object> body) {
@@ -79,4 +195,20 @@ public class VppUnDnServiceImpl implements VppUnDnService {
         resp.put("dnID", unProperties.getDnId());
         return resp;
     }
+
+    private Map<String, Object> error(String root, Map<String, Object> body, int code, String description) {
+        Map<String, Object> resp = new HashMap<>();
+        resp.put("root", root);
+        resp.put("version", 1);
+        resp.put("code", code);
+        resp.put("description", description);
+        resp.put("requestID", body == null ? UUID.randomUUID().toString() : body.get("requestID"));
+        resp.put("dnID", unProperties.getDnId());
+        return resp;
+    }
+
+    @FunctionalInterface
+    private interface IngestAction {
+        VppDrEvent run();
+    }
 }

+ 119 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppUnDrSyncServiceImpl.java

@@ -0,0 +1,119 @@
+package com.usky.vpp.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.vpp.domain.VppCustomer;
+import com.usky.vpp.domain.VppDrEvent;
+import com.usky.vpp.domain.VppDrParticipation;
+import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.mapper.VppCustomerMapper;
+import com.usky.vpp.mapper.VppDrParticipationMapper;
+import com.usky.vpp.mapper.VppResourcePointMapper;
+import com.usky.vpp.service.VppUnDrSyncService;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppUnEventParser;
+import com.usky.vpp.util.VppUnPayloadHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class VppUnDrSyncServiceImpl implements VppUnDrSyncService {
+
+    private static final int PARTICIPATE_STATUS_ACCEPT = 1;
+
+    @Autowired
+    private VppCustomerMapper customerMapper;
+    @Autowired
+    private VppResourcePointMapper resourcePointMapper;
+    @Autowired
+    private VppDrParticipationMapper participationMapper;
+
+    @Override
+    public void syncOptContent(VppDrEvent event, List<Map<String, Object>> optList, boolean clearing) {
+        if (event == null || optList == null || optList.isEmpty()) {
+            return;
+        }
+        for (Map<String, Object> item : optList) {
+            syncOne(event, item, clearing);
+        }
+    }
+
+    @Override
+    public void syncTargetResources(VppDrEvent event, List<Map<String, Object>> resources, boolean clearing) {
+        if (event == null || resources == null || resources.isEmpty()) {
+            return;
+        }
+        for (Map<String, Object> resource : resources) {
+            syncOne(event, resource, clearing);
+        }
+    }
+
+    private void syncOne(VppDrEvent event, Map<String, Object> item, boolean clearing) {
+        String account = VppUnPayloadHelper.getString(item, "account");
+        if (!StringUtils.hasText(account)) {
+            return;
+        }
+        BigDecimal load = resolveLoadKw(item);
+        if (load == null) {
+            return;
+        }
+
+        VppCustomer customer = customerMapper.selectOne(new LambdaQueryWrapper<VppCustomer>()
+                .eq(VppCustomer::getAccountNo, account)
+                .isNull(VppCustomer::getDeletedAt)
+                .last("LIMIT 1"));
+        if (customer == null) {
+            return;
+        }
+
+        VppResourcePoint resourcePoint = resourcePointMapper.selectOne(new LambdaQueryWrapper<VppResourcePoint>()
+                .eq(VppResourcePoint::getCustomerId, customer.getId())
+                .isNull(VppResourcePoint::getDeletedAt)
+                .last("LIMIT 1"));
+
+        Long resourceId = resourcePoint != null ? resourcePoint.getId() : null;
+        LambdaQueryWrapper<VppDrParticipation> wrapper = new LambdaQueryWrapper<VppDrParticipation>()
+                .eq(VppDrParticipation::getEventId, event.getId())
+                .eq(VppDrParticipation::getCustomerId, customer.getId())
+                .isNull(VppDrParticipation::getDeletedAt);
+        if (resourceId != null) {
+            wrapper.eq(VppDrParticipation::getResourceId, resourceId);
+        }
+
+        VppDrParticipation participation = participationMapper.selectOne(wrapper);
+        if (participation == null) {
+            participation = new VppDrParticipation();
+            participation.setEventId(event.getId());
+            participation.setCustomerId(customer.getId());
+            participation.setResourceId(resourceId);
+            participation.setParticipateStatus(PARTICIPATE_STATUS_ACCEPT);
+            participation.setDeclaredAt(LocalDateTime.now());
+            VppAuditHelper.fillCreate(participation);
+        }
+
+        if (clearing) {
+            participation.setClearedCapacityKw(load.abs());
+        } else {
+            participation.setDeclaredCapacityKw(load.abs());
+        }
+        VppAuditHelper.fillUpdate(participation);
+        if (participation.getId() == null) {
+            participationMapper.insert(participation);
+        } else {
+            participationMapper.updateById(participation);
+        }
+    }
+
+    private BigDecimal resolveLoadKw(Map<String, Object> item) {
+        BigDecimal load = VppUnPayloadHelper.getDecimal(item, "load");
+        if (load != null) {
+            return load;
+        }
+        return VppUnEventParser.sumResourceLoadKw(java.util.Collections.singletonList(item), true);
+    }
+}

+ 262 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppUnIntegrationServiceImpl.java

@@ -0,0 +1,262 @@
+package com.usky.vpp.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.vpp.client.VppUnClient;
+import com.usky.vpp.client.VppUnTokenHolder;
+import com.usky.vpp.config.VppUnProperties;
+import com.usky.vpp.domain.VppCustomer;
+import com.usky.vpp.domain.VppDrEvent;
+import com.usky.vpp.domain.VppDrParticipation;
+import com.usky.vpp.domain.VppRegistration;
+import com.usky.vpp.mapper.VppCustomerMapper;
+import com.usky.vpp.mapper.VppDrEventMapper;
+import com.usky.vpp.mapper.VppDrParticipationMapper;
+import com.usky.vpp.mapper.VppRegistrationMapper;
+import com.usky.vpp.service.VppDrEventIngestService;
+import com.usky.vpp.service.VppUnIntegrationService;
+import com.usky.vpp.service.VppUnReportService;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppUnMessageBuilder;
+import com.usky.vpp.util.VppUnPayloadHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import javax.annotation.PostConstruct;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class VppUnIntegrationServiceImpl implements VppUnIntegrationService {
+
+    private static final Logger log = LoggerFactory.getLogger(VppUnIntegrationServiceImpl.class);
+
+    private static final int PARTICIPATE_STATUS_ACCEPT = 1;
+
+    @Autowired
+    private VppUnProperties properties;
+    @Autowired
+    private VppUnClient unClient;
+    @Autowired
+    private VppUnTokenHolder tokenHolder;
+    @Autowired
+    private VppDrEventMapper eventMapper;
+    @Autowired
+    private VppDrParticipationMapper participationMapper;
+    @Autowired
+    private VppCustomerMapper customerMapper;
+    @Autowired
+    private VppRegistrationMapper registrationMapper;
+    @Autowired
+    private VppDrEventIngestService drEventIngestService;
+    @Autowired
+    private VppUnReportService reportService;
+
+    @PostConstruct
+    public void loadRegistrationFromDb() {
+        if (!StringUtils.hasText(properties.getDnId()) || StringUtils.hasText(properties.getRegistrationId())) {
+            return;
+        }
+        VppRegistration registration = registrationMapper.selectOne(new LambdaQueryWrapper<VppRegistration>()
+                .eq(VppRegistration::getDnId, properties.getDnId())
+                .isNull(VppRegistration::getDeletedAt)
+                .orderByDesc(VppRegistration::getRegisteredAt)
+                .last("LIMIT 1"));
+        if (registration != null && StringUtils.hasText(registration.getRegistrationId())) {
+            properties.setRegistrationId(registration.getRegistrationId());
+            log.info("已加载 registrationID={}", registration.getRegistrationId());
+        }
+    }
+
+    @Override
+    public Map<String, Object> refreshToken() {
+        ensureOutboundEnabled();
+        tokenHolder.invalidate();
+        Map<String, Object> response = unClient.post("TokenRequest", VppUnMessageBuilder.buildTokenRequest(properties), false);
+        tokenHolder.applyTokenResponse(response);
+        return response;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> register() {
+        ensureOutboundEnabled();
+        Map<String, Object> request = VppUnMessageBuilder.buildCreateRegistrationRequest(properties);
+        Map<String, Object> response = unClient.post("CreateRegistrationRequest", request, true);
+        persistRegistration(response);
+        return response;
+    }
+
+    @Override
+    public Map<String, Object> pollOnce() {
+        ensureOutboundEnabled();
+        Map<String, Object> response = unClient.post("Poll", VppUnMessageBuilder.buildPollRequest(properties), true);
+        handlePollResponse(response);
+        return response;
+    }
+
+    @Override
+    public Map<String, Object> submitParticipation(Long eventId, boolean participate) {
+        if (!properties.isOutboundActive()) {
+            return null;
+        }
+        VppDrEvent event = requireEvent(eventId);
+        String optType = participate ? "optIn" : "optOut";
+        List<Map<String, Object>> list = participate ? buildListFromParticipations(eventId, false) : null;
+        Map<String, Object> request = VppUnMessageBuilder.buildCreateOptRequest(
+                event.getEventId(), optType, list, properties);
+        Map<String, Object> response = unClient.post("CreateOptRequest", request, true);
+        log.info("已向 UN 提交 CreateOptRequest eventId={} optType={}", event.getEventId(), optType);
+        return response;
+    }
+
+    @Override
+    public Map<String, Object> submitClearing(Long eventId) {
+        if (!properties.isOutboundActive()) {
+            return null;
+        }
+        VppDrEvent event = requireEvent(eventId);
+        List<Map<String, Object>> list = buildListFromParticipations(eventId, true);
+        if (list.isEmpty()) {
+            throw new BusinessException("无出清数据可上报 UN");
+        }
+        Map<String, Object> request = VppUnMessageBuilder.buildCreateCqRequest(
+                event.getEventId(), "optIn", list, properties);
+        Map<String, Object> response = unClient.post("CreateCqRequest", request, true);
+        log.info("已向 UN 提交 CreateCqRequest eventId={}", event.getEventId());
+        return response;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> acknowledgeClearing(Long eventId) {
+        if (!properties.isOutboundActive()) {
+            return null;
+        }
+        VppDrEvent event = requireEvent(eventId);
+        Map<String, Object> request = VppUnMessageBuilder.buildCreateEventResponse(
+                event.getEventId(), null, "optIn", properties);
+        Map<String, Object> response = unClient.post("CreateEventResponse", request, true);
+        drEventIngestService.acknowledgeClearingPublicity(event.getEventId(), request, "Outbound-CreateEventResponse");
+        log.info("已向 UN 确认出清公示 eventId={}", event.getEventId());
+        return response;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void handlePollResponse(Map<String, Object> response) {
+        if (response == null || response.isEmpty()) {
+            return;
+        }
+        String root = VppUnPayloadHelper.getString(response, "root");
+        if ("DistributeEventRequest".equals(root) || response.containsKey("events")) {
+            VppDrEvent event = drEventIngestService.ingestDistributeEvent(response, "Poll-Outbound");
+            if (Boolean.TRUE.equals(properties.getAutoAckClearing()) && event != null) {
+                maybeAutoAckClearing(event);
+            }
+            return;
+        }
+        if ("ReregistrationRequest".equals(root)) {
+            log.info("UN 要求重新注册");
+            String registrationId = VppUnPayloadHelper.getString(response, "registrationID", "registrationId");
+            if (StringUtils.hasText(registrationId)) {
+                properties.setRegistrationId(registrationId);
+            }
+            register();
+            return;
+        }
+        if ("CreateReportRequest".equals(root)) {
+            try {
+                reportService.handleCreateReportRequest(response);
+            } catch (Exception ex) {
+                log.warn("处理 CreateReportRequest 失败: {}", ex.getMessage());
+            }
+        }
+    }
+
+    private void maybeAutoAckClearing(VppDrEvent event) {
+        if (event.getEventStatus() != null && event.getEventStatus() == 1
+                && event.getClearedCapacityKw() != null
+                && event.getClearedCapacityKw().compareTo(BigDecimal.ZERO) > 0) {
+            try {
+                acknowledgeClearing(event.getId());
+            } catch (Exception ex) {
+                log.warn("自动确认出清公示失败 eventId={}: {}", event.getEventId(), ex.getMessage());
+            }
+        }
+    }
+
+    private List<Map<String, Object>> buildListFromParticipations(Long eventId, boolean clearing) {
+        List<VppDrParticipation> participations = participationMapper.selectList(
+                new LambdaQueryWrapper<VppDrParticipation>()
+                        .eq(VppDrParticipation::getEventId, eventId)
+                        .eq(VppDrParticipation::getParticipateStatus, PARTICIPATE_STATUS_ACCEPT)
+                        .isNull(VppDrParticipation::getDeletedAt));
+        List<Map<String, Object>> list = new ArrayList<>();
+        for (VppDrParticipation participation : participations) {
+            VppCustomer customer = customerMapper.selectById(participation.getCustomerId());
+            if (customer == null || !StringUtils.hasText(customer.getAccountNo())) {
+                log.warn("参与记录缺少电力户号 customerId={}", participation.getCustomerId());
+                continue;
+            }
+            BigDecimal load = clearing ? participation.getClearedCapacityKw() : participation.getDeclaredCapacityKw();
+            if (load == null || load.compareTo(BigDecimal.ZERO) <= 0) {
+                continue;
+            }
+            list.add(VppUnMessageBuilder.buildOptListItem(customer.getAccountNo(), load, "否"));
+        }
+        return list;
+    }
+
+    private void persistRegistration(Map<String, Object> response) {
+        String registrationId = VppUnPayloadHelper.getString(response, "registrationID", "registrationId");
+        if (!StringUtils.hasText(registrationId)) {
+            return;
+        }
+        properties.setRegistrationId(registrationId);
+
+        VppRegistration existing = registrationMapper.selectOne(new LambdaQueryWrapper<VppRegistration>()
+                .eq(VppRegistration::getDnId, properties.getDnId())
+                .isNull(VppRegistration::getDeletedAt)
+                .last("LIMIT 1"));
+        if (existing == null) {
+            existing = new VppRegistration();
+            existing.setDnId(properties.getDnId());
+            existing.setRegistrationId(registrationId);
+            existing.setRegStatus(1);
+            existing.setRegisteredAt(LocalDateTime.now());
+            VppAuditHelper.fillCreate(existing);
+            registrationMapper.insert(existing);
+        } else {
+            existing.setRegistrationId(registrationId);
+            existing.setRegisteredAt(LocalDateTime.now());
+            VppAuditHelper.fillUpdate(existing);
+            registrationMapper.updateById(existing);
+        }
+    }
+
+    private VppDrEvent requireEvent(Long eventId) {
+        VppDrEvent event = eventMapper.selectById(eventId);
+        if (event == null || event.getDeletedAt() != null) {
+            throw new BusinessException("需求响应事件不存在");
+        }
+        if (!StringUtils.hasText(event.getEventId())) {
+            throw new BusinessException("事件缺少运管平台 eventID");
+        }
+        return event;
+    }
+
+    private void ensureOutboundEnabled() {
+        if (!properties.isOutboundActive()) {
+            throw new BusinessException("运管平台 outbound 未启用,请配置 vpp.un.baseUrl 与 outbound-enabled");
+        }
+    }
+}

+ 112 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppUnReportServiceImpl.java

@@ -0,0 +1,112 @@
+package com.usky.vpp.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.vpp.client.VppUnClient;
+import com.usky.vpp.config.VppUnProperties;
+import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.mapper.VppResourcePointMapper;
+import com.usky.vpp.service.VppUnReportService;
+import com.usky.vpp.util.VppUnMessageBuilder;
+import com.usky.vpp.util.VppUnPayloadHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class VppUnReportServiceImpl implements VppUnReportService {
+
+    private static final Logger log = LoggerFactory.getLogger(VppUnReportServiceImpl.class);
+
+    @Autowired
+    private VppUnProperties properties;
+    @Autowired
+    private VppUnClient unClient;
+    @Autowired
+    private VppResourcePointMapper resourcePointMapper;
+
+    @Override
+    public void handleCreateReportRequest(Map<String, Object> createReportRequest) {
+        if (!properties.isOutboundActive()) {
+            return;
+        }
+        String reportRequestId = resolveReportRequestId(createReportRequest);
+        log.info("处理 UN CreateReportRequest reportRequestID={}", reportRequestId);
+
+        Map<String, Object> ack = VppUnMessageBuilder.buildCreateReportResponse(createReportRequest, properties);
+        unClient.post("CreateReportResponse", ack, true);
+
+        if (!StringUtils.hasText(reportRequestId)) {
+            return;
+        }
+        switch (reportRequestId) {
+            case "MetaDataReport":
+                submitRegisterReport();
+                break;
+            case "IntervalDataReport":
+                submitIntervalDataReport(createReportRequest);
+                break;
+            case "MomentDataReport":
+                submitMomentDataReport(createReportRequest);
+                break;
+            default:
+                log.info("暂不支持的报告类型: {}", reportRequestId);
+                break;
+        }
+    }
+
+    private void submitRegisterReport() {
+        List<VppResourcePoint> resources = resourcePointMapper.selectList(
+                new LambdaQueryWrapper<VppResourcePoint>()
+                        .isNull(VppResourcePoint::getDeletedAt)
+                        .orderByAsc(VppResourcePoint::getId));
+        Map<String, Object> request = VppUnMessageBuilder.buildRegisterReportRequest(resources, properties);
+        unClient.post("RegisterReportRequest", request, true);
+        log.info("已提交 RegisterReportRequest");
+    }
+
+    private void submitIntervalDataReport(Map<String, Object> createReportRequest) {
+        List<VppResourcePoint> resources = resourcePointMapper.selectList(
+                new LambdaQueryWrapper<VppResourcePoint>()
+                        .isNull(VppResourcePoint::getDeletedAt)
+                        .eq(VppResourcePoint::getRunStatus, 1)
+                        .orderByAsc(VppResourcePoint::getId));
+        Map<String, Object> request = VppUnMessageBuilder.buildIntervalDataReportRequest(
+                createReportRequest, resources, properties);
+        unClient.post("IntervalDataReportRequest", request, true);
+        log.info("已提交 IntervalDataReportRequest");
+    }
+
+    private void submitMomentDataReport(Map<String, Object> createReportRequest) {
+        List<VppResourcePoint> resources = resourcePointMapper.selectList(
+                new LambdaQueryWrapper<VppResourcePoint>()
+                        .isNull(VppResourcePoint::getDeletedAt)
+                        .eq(VppResourcePoint::getRunStatus, 1)
+                        .orderByAsc(VppResourcePoint::getId));
+        Map<String, Object> request = VppUnMessageBuilder.buildMomentDataReportRequest(
+                createReportRequest, resources, properties);
+        unClient.post("MomentDataReportRequest", request, true);
+        log.info("已提交 MomentDataReportRequest");
+    }
+
+    @SuppressWarnings("unchecked")
+    private String resolveReportRequestId(Map<String, Object> request) {
+        String id = VppUnPayloadHelper.getString(request, "reportRequestID", "reportRequestId");
+        if (StringUtils.hasText(id)) {
+            return id;
+        }
+        Object reportRequest = request.get("reportRequest");
+        if (reportRequest instanceof List && !((List<?>) reportRequest).isEmpty()) {
+            Object first = ((List<?>) reportRequest).get(0);
+            if (first instanceof Map) {
+                return VppUnPayloadHelper.getString((Map<String, Object>) first,
+                        "reportRequestID", "reportRequestId");
+            }
+        }
+        return null;
+    }
+}

+ 22 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrClearingRequest.java

@@ -0,0 +1,22 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 需求响应出清分拆申报请求
+ */
+@Data
+public class DrClearingRequest {
+
+    private BigDecimal totalClearedCapacityKw;
+    private List<DrResourceClearing> resources;
+
+    @Data
+    public static class DrResourceClearing {
+        private Long resourceId;
+        private BigDecimal clearedCapacityKw;
+    }
+}

+ 14 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DrInterveneRequest.java

@@ -0,0 +1,14 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+/**
+ * 需求响应手动干预请求
+ */
+@Data
+public class DrInterveneRequest {
+
+    private String action;
+    private Long strategyId;
+    private String remark;
+}

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

@@ -0,0 +1,25 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 需求响应参与/拒绝请求
+ */
+@Data
+public class DrParticipateRequest {
+
+    private Boolean participate;
+    private Long strategyId;
+    private BigDecimal declaredCapacityKw;
+    private String reason;
+    private List<DrResourceDeclare> resources;
+
+    @Data
+    public static class DrResourceDeclare {
+        private Long resourceId;
+        private BigDecimal declaredCapacityKw;
+    }
+}

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

@@ -0,0 +1,30 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 需求响应策略创建/更新请求
+ */
+@Data
+public class DrStrategyRequest {
+
+    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 List<DrStrategyResource> resources;
+
+    @Data
+    public static class DrStrategyResource {
+        private Long resourceId;
+        private Integer priority;
+        private BigDecimal maxAdjustKw;
+    }
+}

+ 23 - 10
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppAuditHelper.java

@@ -1,6 +1,7 @@
 package com.usky.vpp.util;
 
 import com.usky.common.security.utils.SecurityUtils;
+import org.springframework.util.StringUtils;
 
 import java.lang.reflect.Method;
 import java.time.LocalDateTime;
@@ -18,18 +19,22 @@ public final class VppAuditHelper {
         LocalDateTime now = LocalDateTime.now();
         invokeSetter(entity, "setCreateTime", now);
         invokeSetter(entity, "setUpdateTime", now);
-        long userId = currentUserId();
-        if (userId > 0) {
-            invokeSetter(entity, "setCreatedBy", userId);
-            invokeSetter(entity, "setUpdatedBy", userId);
+        String auditUser = currentUsername();
+        if (StringUtils.hasText(auditUser)) {
+            invokeSetter(entity, "setCreatedBy", auditUser);
+            invokeSetter(entity, "setUpdatedBy", auditUser);
+        }
+        Integer tenantId = currentTenantId();
+        if (tenantId != null) {
+            invokeSetter(entity, "setTenantId", tenantId);
         }
     }
 
     public static void fillUpdate(Object entity) {
         invokeSetter(entity, "setUpdateTime", LocalDateTime.now());
-        long userId = currentUserId();
-        if (userId > 0) {
-            invokeSetter(entity, "setUpdatedBy", userId);
+        String auditUser = currentUsername();
+        if (StringUtils.hasText(auditUser)) {
+            invokeSetter(entity, "setUpdatedBy", auditUser);
         }
     }
 
@@ -37,11 +42,19 @@ public final class VppAuditHelper {
         return "ACC" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 4).toUpperCase();
     }
 
-    private static long currentUserId() {
+    private static String currentUsername() {
+        try {
+            return SecurityUtils.getUsername();
+        } catch (Exception ex) {
+            return null;
+        }
+    }
+
+    private static Integer currentTenantId() {
         try {
-            return SecurityUtils.getUserId();
+            return SecurityUtils.getTenantId();
         } catch (Exception ex) {
-            return 0L;
+            return null;
         }
     }
 

+ 318 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppUnEventParser.java

@@ -0,0 +1,318 @@
+package com.usky.vpp.util;
+
+import com.usky.vpp.enums.VppUnEventPhase;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 解析运管平台 DistributeEventRequest / Event 结构(DLT 1867 联调 JSON 实例)
+ */
+public final class VppUnEventParser {
+
+    private VppUnEventParser() {
+    }
+
+    @SuppressWarnings("unchecked")
+    public static List<Map<String, Object>> extractEvents(Map<String, Object> body) {
+        if (body == null) {
+            return Collections.emptyList();
+        }
+        Object events = body.get("events");
+        if (events instanceof List) {
+            return (List<Map<String, Object>>) events;
+        }
+        if (body.containsKey("descriptor") || body.containsKey("activePeriod")) {
+            return Collections.singletonList(body);
+        }
+        return Collections.emptyList();
+    }
+
+    @SuppressWarnings("unchecked")
+    public static Map<String, Object> getDescriptor(Map<String, Object> event) {
+        if (event == null) {
+            return Collections.emptyMap();
+        }
+        Object descriptor = event.get("descriptor");
+        if (descriptor instanceof Map) {
+            return (Map<String, Object>) descriptor;
+        }
+        return event;
+    }
+
+    @SuppressWarnings("unchecked")
+    public static Map<String, Object> getTarget(Map<String, Object> event) {
+        if (event == null) {
+            return Collections.emptyMap();
+        }
+        Object target = event.get("target");
+        return target instanceof Map ? (Map<String, Object>) target : Collections.emptyMap();
+    }
+
+    @SuppressWarnings("unchecked")
+    public static List<Map<String, Object>> getTargetResources(Map<String, Object> event) {
+        Map<String, Object> target = getTarget(event);
+        Object resources = target.get("resources");
+        if (!(resources instanceof List)) {
+            return Collections.emptyList();
+        }
+        List<Map<String, Object>> list = new ArrayList<>();
+        for (Object item : (List<?>) resources) {
+            if (item instanceof Map) {
+                list.add((Map<String, Object>) item);
+            }
+        }
+        return list;
+    }
+
+    public static VppUnEventPhase detectPhase(Map<String, Object> event) {
+        Map<String, Object> descriptor = getDescriptor(event);
+        Boolean filing = VppUnPayloadHelper.getBoolean(descriptor, "filing");
+        Boolean lastFiling = VppUnPayloadHelper.getBoolean(descriptor, "lastFiling");
+        List<Map<String, Object>> resources = getTargetResources(event);
+        boolean hasValues = hasResourceValues(resources);
+
+        if (Boolean.TRUE.equals(filing) && !hasValues) {
+            return VppUnEventPhase.INVITATION;
+        }
+        if (Boolean.TRUE.equals(filing) && hasValues) {
+            return VppUnEventPhase.DECLARE_FEEDBACK;
+        }
+        if (Boolean.FALSE.equals(filing) && Boolean.TRUE.equals(lastFiling) && hasValues) {
+            return VppUnEventPhase.SPLIT_RESULT;
+        }
+        if (Boolean.FALSE.equals(filing) && Boolean.TRUE.equals(lastFiling)) {
+            return VppUnEventPhase.SPLIT_NOTICE;
+        }
+        if (hasValues) {
+            return VppUnEventPhase.CLEARING_PUBLICITY;
+        }
+        return VppUnEventPhase.UNKNOWN;
+    }
+
+    public static String getEventId(Map<String, Object> event) {
+        Map<String, Object> descriptor = getDescriptor(event);
+        return VppUnPayloadHelper.getString(descriptor, "eventID", "eventId", "event_id");
+    }
+
+    public static Integer mapResponseType(Map<String, Object> descriptor) {
+        Object notification = VppUnPayloadHelper.getRaw(descriptor, "notification");
+        Integer mapped = VppUnPayloadHelper.parseResponseType(notification);
+        if (mapped != null) {
+            return mapped;
+        }
+        return 1;
+    }
+
+    public static Integer mapEventType(Map<String, Object> descriptor) {
+        Object control = VppUnPayloadHelper.getRaw(descriptor, "control");
+        Integer mapped = VppUnPayloadHelper.parseEventType(control);
+        if (mapped != null) {
+            return mapped;
+        }
+        String text = control == null ? "" : String.valueOf(control);
+        if (text.contains("fillValley")) {
+            return 2;
+        }
+        return 1;
+    }
+
+    public static Integer mapPlatformStatus(Map<String, Object> descriptor) {
+        String status = VppUnPayloadHelper.getString(descriptor, "status");
+        if (!StringUtils.hasText(status)) {
+            return null;
+        }
+        switch (status.trim().toLowerCase()) {
+            case "cancelled":
+                return 4;
+            case "completed":
+                return 3;
+            case "active":
+                return 2;
+            case "far":
+            case "near":
+            case "none":
+            default:
+                return 0;
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public static LocalDateTime[] resolveActivePeriod(Map<String, Object> event) {
+        Object periods = event.get("activePeriod");
+        if (!(periods instanceof List) || ((List<?>) periods).isEmpty()) {
+            return new LocalDateTime[]{null, null};
+        }
+        Object first = ((List<?>) periods).get(0);
+        if (!(first instanceof Map)) {
+            return new LocalDateTime[]{null, null};
+        }
+        Map<String, Object> period = (Map<String, Object>) first;
+        LocalDateTime start = VppUnPayloadHelper.getDateTime(period, "dtstart", "startTime");
+        LocalDateTime end = VppUnPayloadHelper.getDateTime(period, "dtend", "endTime");
+        return new LocalDateTime[]{start, end};
+    }
+
+    @SuppressWarnings("unchecked")
+    public static BigDecimal extractDemandChargeKw(Map<String, Object> event) {
+        BigDecimal maxAbs = null;
+        Object signals = event.get("signals");
+        if (!(signals instanceof Map)) {
+            return null;
+        }
+        Object signal = ((Map<String, Object>) signals).get("signal");
+        List<?> signalList;
+        if (signal instanceof List) {
+            signalList = (List<?>) signal;
+        } else if (signal instanceof Map) {
+            signalList = Collections.singletonList(signal);
+        } else {
+            return null;
+        }
+        for (Object sigObj : signalList) {
+            if (!(sigObj instanceof Map)) {
+                continue;
+            }
+            Map<String, Object> sig = (Map<String, Object>) sigObj;
+            String signalName = VppUnPayloadHelper.getString(sig, "signalName");
+            if (!"DEMAND_CHARGE".equalsIgnoreCase(signalName)) {
+                continue;
+            }
+            maxAbs = maxAbs(maxAbs, maxAbsFromIntervals(sig));
+        }
+        return maxAbs;
+    }
+
+    @SuppressWarnings("unchecked")
+    public static BigDecimal extractEnergyPrice(Map<String, Object> event) {
+        Object signals = event.get("signals");
+        if (!(signals instanceof Map)) {
+            return null;
+        }
+        Object signal = ((Map<String, Object>) signals).get("signal");
+        List<?> signalList;
+        if (signal instanceof List) {
+            signalList = (List<?>) signal;
+        } else if (signal instanceof Map) {
+            signalList = Collections.singletonList(signal);
+        } else {
+            return null;
+        }
+        BigDecimal sum = BigDecimal.ZERO;
+        int count = 0;
+        for (Object sigObj : signalList) {
+            if (!(sigObj instanceof Map)) {
+                continue;
+            }
+            Map<String, Object> sig = (Map<String, Object>) sigObj;
+            String signalName = VppUnPayloadHelper.getString(sig, "signalName");
+            if (!"ENERGY_PRICE".equalsIgnoreCase(signalName)) {
+                continue;
+            }
+            List<BigDecimal> values = valuesFromIntervals(sig);
+            for (BigDecimal value : values) {
+                sum = sum.add(value);
+                count++;
+            }
+        }
+        if (count == 0) {
+            return null;
+        }
+        return sum.divide(BigDecimal.valueOf(count), 4, RoundingMode.HALF_UP);
+    }
+
+    public static BigDecimal sumResourceLoadKw(List<Map<String, Object>> resources, boolean absolute) {
+        BigDecimal total = BigDecimal.ZERO;
+        for (Map<String, Object> resource : resources) {
+            BigDecimal load = maxAbsFromResourceValues(resource);
+            if (load == null) {
+                load = VppUnPayloadHelper.getDecimal(resource, "load");
+            }
+            if (load == null) {
+                continue;
+            }
+            total = total.add(absolute ? load.abs() : load);
+        }
+        return total;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static List<BigDecimal> valuesFromIntervals(Map<String, Object> signal) {
+        Object intervals = signal.get("intervals");
+        if (!(intervals instanceof Map)) {
+            return Collections.emptyList();
+        }
+        Object irregular = ((Map<String, Object>) intervals).get("irregular");
+        if (!(irregular instanceof Map)) {
+            return Collections.emptyList();
+        }
+        Object values = ((Map<String, Object>) irregular).get("values");
+        if (!(values instanceof List)) {
+            return Collections.emptyList();
+        }
+        List<BigDecimal> result = new ArrayList<>();
+        for (Object item : (List<?>) values) {
+            if (item instanceof Map) {
+                BigDecimal value = VppUnPayloadHelper.getDecimal((Map<String, Object>) item, "value");
+                if (value != null) {
+                    result.add(value);
+                }
+            }
+        }
+        return result;
+    }
+
+    private static BigDecimal maxAbsFromIntervals(Map<String, Object> signal) {
+        BigDecimal max = null;
+        for (BigDecimal value : valuesFromIntervals(signal)) {
+            max = maxAbs(max, value.abs());
+        }
+        return max;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static BigDecimal maxAbsFromResourceValues(Map<String, Object> resource) {
+        Object values = resource.get("values");
+        if (!(values instanceof List)) {
+            return null;
+        }
+        BigDecimal max = null;
+        for (Object item : (List<?>) values) {
+            if (item instanceof Map) {
+                BigDecimal value = VppUnPayloadHelper.getDecimal((Map<String, Object>) item, "value");
+                if (value != null) {
+                    max = maxAbs(max, value.abs());
+                }
+            }
+        }
+        return max;
+    }
+
+    private static boolean hasResourceValues(List<Map<String, Object>> resources) {
+        for (Map<String, Object> resource : resources) {
+            if (maxAbsFromResourceValues(resource) != null) {
+                return true;
+            }
+            if (VppUnPayloadHelper.getDecimal(resource, "load") != null) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static BigDecimal maxAbs(BigDecimal current, BigDecimal candidate) {
+        if (candidate == null) {
+            return current;
+        }
+        if (current == null || candidate.compareTo(current) > 0) {
+            return candidate;
+        }
+        return current;
+    }
+}

+ 297 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppUnMessageBuilder.java

@@ -0,0 +1,297 @@
+package com.usky.vpp.util;
+
+import com.usky.vpp.config.VppUnProperties;
+import com.usky.vpp.domain.VppResourcePoint;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * 构造运管平台标准响应报文(联调 JSON 实例)
+ */
+public final class VppUnMessageBuilder {
+
+    private static final DateTimeFormatter CREATED_DATE_TIME =
+            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    private VppUnMessageBuilder() {
+    }
+
+    public static Map<String, Object> buildTokenRequest(VppUnProperties properties) {
+        return buildBaseRequest("TokenRequest", properties);
+    }
+
+    public static Map<String, Object> buildPollRequest(VppUnProperties properties) {
+        return buildBaseRequest("Poll", properties);
+    }
+
+    public static Map<String, Object> buildCreateRegistrationRequest(VppUnProperties properties) {
+        Map<String, Object> req = buildBaseRequest("CreateRegistrationRequest", properties);
+        req.put("dnName", properties.getDnName());
+        if (properties.getRegistrationId() != null) {
+            req.put("registrationID", properties.getRegistrationId());
+        }
+        req.put("reportOnly", false);
+        req.put("pullMode", true);
+        req.put("signature", false);
+        req.put("transport", "REST");
+        req.put("transportAddress", properties.getTransportAddress());
+        return req;
+    }
+
+    public static Map<String, Object> buildCreateOptRequest(String eventId, String optType,
+                                                            List<Map<String, Object>> list,
+                                                            VppUnProperties properties) {
+        Map<String, Object> req = buildBaseRequest("CreateOptRequest", properties);
+        req.put("eventID", eventId);
+        req.put("optType", optType);
+        req.put("createdDateTime", CREATED_DATE_TIME.format(LocalDateTime.now()));
+        req.put("priceDownCoeff", properties.getPriceDownCoeff());
+        if (list != null) {
+            req.put("list", list);
+        }
+        return req;
+    }
+
+    public static Map<String, Object> buildCreateCqRequest(String eventId, String optType,
+                                                           List<Map<String, Object>> list,
+                                                           VppUnProperties properties) {
+        Map<String, Object> req = buildBaseRequest("CreateCqRequest", properties);
+        req.put("eventID", eventId);
+        req.put("optType", optType != null ? optType : "optIn");
+        req.put("createdDateTime", CREATED_DATE_TIME.format(LocalDateTime.now()));
+        req.put("priceDownCoeff", properties.getPriceDownCoeff());
+        if (list != null) {
+            req.put("list", list);
+        }
+        return req;
+    }
+
+    public static Map<String, Object> buildCreateReportResponse(Map<String, Object> request, VppUnProperties properties) {
+        Map<String, Object> resp = new LinkedHashMap<>();
+        if (request != null) {
+            resp.put("requestID", request.get("requestID"));
+        }
+        resp.put("root", "CreateReportResponse");
+        resp.put("version", 1);
+        resp.put("code", 200);
+        resp.put("description", "ok");
+        if (resp.get("requestID") == null) {
+            resp.put("requestID", UUID.randomUUID().toString());
+        }
+        resp.put("dnID", properties.getDnId());
+        return resp;
+    }
+
+    public static Map<String, Object> buildRegisterReportRequest(List<VppResourcePoint> resources,
+                                                                 VppUnProperties properties) {
+        Map<String, Object> req = buildBaseRequest("RegisterReportRequest", properties);
+        req.put("reportRequestID", "MetaDataReport");
+        List<Map<String, Object>> reports = new ArrayList<>();
+        int rid = 0;
+        if (resources != null) {
+            for (VppResourcePoint resource : resources) {
+                reports.add(buildMetadataReportItem(resource, rid++, properties));
+            }
+        }
+        if (reports.isEmpty()) {
+            reports.add(buildDefaultVppMetadataReport(properties));
+        }
+        req.put("report", reports);
+        return req;
+    }
+
+    public static Map<String, Object> buildIntervalDataReportRequest(Map<String, Object> createReportRequest,
+                                                                     List<VppResourcePoint> resources,
+                                                                     VppUnProperties properties) {
+        Map<String, Object> req = buildBaseRequest("IntervalDataReportRequest", properties);
+        req.put("reportRequestID", resolveReportRequestIdFromPoll(createReportRequest, "IntervalDataReport"));
+        req.put("createdDateTime", CREATED_DATE_TIME.format(LocalDateTime.now()));
+        req.put("pointData", buildPointData(resources, properties));
+        return req;
+    }
+
+    public static Map<String, Object> buildMomentDataReportRequest(Map<String, Object> createReportRequest,
+                                                                   List<VppResourcePoint> resources,
+                                                                   VppUnProperties properties) {
+        Map<String, Object> req = buildBaseRequest("MomentDataReportRequest", properties);
+        req.put("reportRequestID", resolveReportRequestIdFromPoll(createReportRequest, "MomentDataReport"));
+        req.put("createdDateTime", CREATED_DATE_TIME.format(LocalDateTime.now()));
+        req.put("pointData", buildPointData(resources, properties));
+        return req;
+    }
+
+    private static Map<String, Object> buildDefaultVppMetadataReport(VppUnProperties properties) {
+        Map<String, Object> report = new LinkedHashMap<>();
+        report.put("createdDateTime", CREATED_DATE_TIME.format(LocalDateTime.now()));
+        Map<String, Object> description = new LinkedHashMap<>();
+        description.put("rID", 0);
+        Map<String, Object> metric = new LinkedHashMap<>();
+        metric.put("metricName", "AP");
+        metric.put("multiplier", "k");
+        metric.put("symbol", "W");
+        description.put("metric", metric);
+        Map<String, Object> dataSource = new LinkedHashMap<>();
+        dataSource.put("resourceID", java.util.Collections.singletonList(properties.getDnId()));
+        description.put("reportDataSource", dataSource);
+        description.put("readingType", "Summed");
+        report.put("reportDescription", description);
+        return report;
+    }
+
+    private static Map<String, Object> buildMetadataReportItem(VppResourcePoint resource, int rid,
+                                                               VppUnProperties properties) {
+        Map<String, Object> report = new LinkedHashMap<>();
+        report.put("createdDateTime", CREATED_DATE_TIME.format(LocalDateTime.now()));
+        Map<String, Object> description = new LinkedHashMap<>();
+        description.put("rID", rid);
+        Map<String, Object> metric = new LinkedHashMap<>();
+        metric.put("metricName", "AP");
+        metric.put("multiplier", "k");
+        metric.put("symbol", "W");
+        description.put("metric", metric);
+        Map<String, Object> dataSource = new LinkedHashMap<>();
+        String resourceId = StringUtils.hasText(resource.getUnResourceId())
+                ? resource.getUnResourceId()
+                : (StringUtils.hasText(resource.getResourceCode()) ? resource.getResourceCode() : properties.getDnId());
+        dataSource.put("resourceID", java.util.Collections.singletonList(resourceId));
+        description.put("reportDataSource", dataSource);
+        description.put("readingType", "Summed");
+        report.put("reportDescription", description);
+        return report;
+    }
+
+    private static List<Map<String, Object>> buildPointData(List<VppResourcePoint> resources,
+                                                            VppUnProperties properties) {
+        List<Map<String, Object>> pointData = new ArrayList<>();
+        if (resources == null || resources.isEmpty()) {
+            Map<String, Object> point = new LinkedHashMap<>();
+            point.put("resourceID", properties.getDnId());
+            point.put("value", "0");
+            point.put("timestamp", CREATED_DATE_TIME.format(LocalDateTime.now()));
+            pointData.add(point);
+            return pointData;
+        }
+        String timestamp = CREATED_DATE_TIME.format(LocalDateTime.now());
+        for (VppResourcePoint resource : resources) {
+            Map<String, Object> point = new LinkedHashMap<>();
+            String resourceId = StringUtils.hasText(resource.getUnResourceId())
+                    ? resource.getUnResourceId()
+                    : (StringUtils.hasText(resource.getResourceCode()) ? resource.getResourceCode() : properties.getDnId());
+            point.put("resourceID", resourceId);
+            BigDecimal value = resource.getAdjustableKw() != null ? resource.getAdjustableKw() : BigDecimal.ZERO;
+            point.put("value", value.stripTrailingZeros().toPlainString());
+            point.put("timestamp", timestamp);
+            pointData.add(point);
+        }
+        return pointData;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static String resolveReportRequestIdFromPoll(Map<String, Object> createReportRequest, String fallback) {
+        if (createReportRequest == null) {
+            return fallback;
+        }
+        Object reportRequest = createReportRequest.get("reportRequest");
+        if (reportRequest instanceof List && !((List<?>) reportRequest).isEmpty()) {
+            Object first = ((List<?>) reportRequest).get(0);
+            if (first instanceof Map) {
+                String id = VppUnPayloadHelper.getString((Map<String, Object>) first,
+                        "reportRequestID", "reportRequestId");
+                if (StringUtils.hasText(id)) {
+                    return id;
+                }
+            }
+        }
+        String id = VppUnPayloadHelper.getString(createReportRequest, "reportRequestID", "reportRequestId");
+        return StringUtils.hasText(id) ? id : fallback;
+    }
+
+    public static Map<String, Object> buildOptListItem(String account, BigDecimal loadKw, String needZk) {
+        Map<String, Object> item = new LinkedHashMap<>();
+        item.put("account", account);
+        item.put("load", loadKw != null ? loadKw.stripTrailingZeros().toPlainString() : "0");
+        if (StringUtils.hasText(needZk)) {
+            item.put("needzk", needZk);
+        }
+        return item;
+    }
+
+    private static Map<String, Object> buildBaseRequest(String root, VppUnProperties properties) {
+        Map<String, Object> req = new LinkedHashMap<>();
+        req.put("root", root);
+        req.put("version", 1);
+        req.put("requestID", UUID.randomUUID().toString());
+        req.put("dnID", properties.getDnId());
+        return req;
+    }
+
+    public static Map<String, Object> buildCreateOptResponse(Map<String, Object> request, VppUnProperties properties) {
+        Map<String, Object> resp = copyRequestAsResponse(request, "CreateOptResponse");
+        fillCommon(resp, properties);
+        resp.put("code", 200);
+        resp.put("description", "ok");
+        return resp;
+    }
+
+    public static Map<String, Object> buildCreateCqResponse(Map<String, Object> request, VppUnProperties properties) {
+        Map<String, Object> resp = copyRequestAsResponse(request, "CreateCqResponse");
+        fillCommon(resp, properties);
+        resp.put("code", 200);
+        resp.put("description", "ok");
+        return resp;
+    }
+
+    public static Map<String, Object> buildCreateEventResponse(String eventId, String requestId,
+                                                               String optType, VppUnProperties properties) {
+        Map<String, Object> resp = new LinkedHashMap<>();
+        resp.put("root", "CreateEventResponse");
+        resp.put("version", 1);
+        resp.put("code", 200);
+        resp.put("description", "ok");
+        resp.put("requestID", requestId != null ? requestId : UUID.randomUUID().toString());
+        resp.put("dnID", properties.getDnId());
+
+        Map<String, Object> eventResponse = new LinkedHashMap<>();
+        eventResponse.put("optType", optType != null ? optType : "optIn");
+        eventResponse.put("code", 200);
+        eventResponse.put("description", "ok");
+        eventResponse.put("requestID", UUID.randomUUID().toString());
+
+        Map<String, Object> qualifiedEventId = new HashMap<>();
+        qualifiedEventId.put("eventID", eventId);
+        qualifiedEventId.put("modificationNumber", 0);
+        eventResponse.put("qualifiedEventID", qualifiedEventId);
+
+        List<Map<String, Object>> eventResponses = new ArrayList<>();
+        eventResponses.add(eventResponse);
+        resp.put("eventResponses", eventResponses);
+        return resp;
+    }
+
+    private static Map<String, Object> copyRequestAsResponse(Map<String, Object> request, String root) {
+        Map<String, Object> resp = new LinkedHashMap<>();
+        if (request != null) {
+            resp.putAll(request);
+        }
+        resp.put("root", root);
+        resp.put("version", request != null && request.get("version") != null ? request.get("version") : 1);
+        return resp;
+    }
+
+    private static void fillCommon(Map<String, Object> resp, VppUnProperties properties) {
+        if (resp.get("requestID") == null) {
+            resp.put("requestID", UUID.randomUUID().toString());
+        }
+        resp.put("dnID", properties.getDnId());
+    }
+}

+ 230 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppUnPayloadHelper.java

@@ -0,0 +1,230 @@
+package com.usky.vpp.util;
+
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Map;
+
+/**
+ * 运管平台 UN 报文解析辅助(兼容多种字段命名与嵌套结构)
+ */
+public final class VppUnPayloadHelper {
+
+    private static final DateTimeFormatter[] DATE_TIME_FORMATTERS = {
+            DateTimeFormatter.ISO_LOCAL_DATE_TIME,
+            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
+            DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"),
+            DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")
+    };
+
+    private VppUnPayloadHelper() {
+    }
+
+    public static Map<String, Object> unwrapInvitationPayload(Map<String, Object> body) {
+        return unwrap(body, "event", "eventInfo", "eventData", "invitation", "data");
+    }
+
+    public static Map<String, Object> unwrapOptPayload(Map<String, Object> body) {
+        Map<String, Object> nested = unwrap(body, "opt", "optInfo", "optRequest", "event", "eventInfo", "data");
+        return nested != null ? nested : body;
+    }
+
+    public static Map<String, Object> unwrapClearingPayload(Map<String, Object> body) {
+        return unwrap(body, "cq", "cqInfo", "cqRequest", "clearing", "clearingInfo", "event", "data");
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Map<String, Object> unwrap(Map<String, Object> body, String... nestedKeys) {
+        if (body == null || body.isEmpty()) {
+            return body;
+        }
+        for (String key : nestedKeys) {
+            Object nested = body.get(key);
+            if (nested instanceof Map) {
+                return (Map<String, Object>) nested;
+            }
+        }
+        return body;
+    }
+
+    public static boolean hasInvitationFields(Map<String, Object> payload) {
+        if (payload == null) {
+            return false;
+        }
+        if (payload.containsKey("events") || payload.containsKey("descriptor")) {
+            return true;
+        }
+        return getDateTime(payload, "startTime", "start_time", "dtstart", "beginTime") != null
+                || getDecimal(payload, "targetCapacityKw", "target_capacity_kw", "targetKw", "capacity", "targetCapacity") != null;
+    }
+
+    public static Boolean getBoolean(Map<String, Object> map, String... keys) {
+        Object value = getValue(map, keys);
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof Boolean) {
+            return (Boolean) value;
+        }
+        String text = String.valueOf(value).trim().toLowerCase();
+        if ("true".equals(text) || "1".equals(text)) {
+            return true;
+        }
+        if ("false".equals(text) || "0".equals(text)) {
+            return false;
+        }
+        return null;
+    }
+
+    public static String getString(Map<String, Object> map, String... keys) {
+        Object value = getValue(map, keys);
+        if (value == null) {
+            return null;
+        }
+        String text = String.valueOf(value).trim();
+        return StringUtils.hasText(text) ? text : null;
+    }
+
+    public static Integer getInteger(Map<String, Object> map, String... keys) {
+        Object value = getValue(map, keys);
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof Number) {
+            return ((Number) value).intValue();
+        }
+        String text = String.valueOf(value).trim();
+        if (!StringUtils.hasText(text)) {
+            return null;
+        }
+        try {
+            return Integer.parseInt(text);
+        } catch (NumberFormatException ex) {
+            return null;
+        }
+    }
+
+    public static Object getRaw(Map<String, Object> map, String... keys) {
+        return getValue(map, keys);
+    }
+
+    public static BigDecimal getDecimal(Map<String, Object> map, String... keys) {
+        Object value = getValue(map, keys);
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof BigDecimal) {
+            return (BigDecimal) value;
+        }
+        if (value instanceof Number) {
+            return BigDecimal.valueOf(((Number) value).doubleValue());
+        }
+        String text = String.valueOf(value).trim();
+        if (!StringUtils.hasText(text)) {
+            return null;
+        }
+        try {
+            return new BigDecimal(text);
+        } catch (NumberFormatException ex) {
+            return null;
+        }
+    }
+
+    public static LocalDateTime getDateTime(Map<String, Object> map, String... keys) {
+        Object value = getValue(map, keys);
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof LocalDateTime) {
+            return (LocalDateTime) value;
+        }
+        if (value instanceof Number) {
+            long epoch = ((Number) value).longValue();
+            if (String.valueOf(epoch).length() <= 10) {
+                epoch *= 1000;
+            }
+            return LocalDateTime.ofInstant(Instant.ofEpochMilli(epoch), ZoneId.systemDefault());
+        }
+        String text = String.valueOf(value).trim();
+        if (!StringUtils.hasText(text)) {
+            return null;
+        }
+        for (DateTimeFormatter formatter : DATE_TIME_FORMATTERS) {
+            try {
+                return LocalDateTime.parse(text, formatter);
+            } catch (DateTimeParseException ignored) {
+                // try next pattern
+            }
+        }
+        return null;
+    }
+
+    public static Integer parseResponseType(Object raw) {
+        if (raw == null) {
+            return null;
+        }
+        if (raw instanceof Number) {
+            int value = ((Number) raw).intValue();
+            return value >= 1 && value <= 3 ? value : null;
+        }
+        String text = String.valueOf(raw).trim().toUpperCase();
+        if (text.contains("日前") || "DAY_AHEAD".equals(text) || "1".equals(text)) {
+            return 1;
+        }
+        if (text.contains("日内") || "INTRADAY".equals(text) || "2".equals(text)) {
+            return 2;
+        }
+        if (text.contains("秒") || "REALTIME".equals(text) || "3".equals(text)) {
+            return 3;
+        }
+        return null;
+    }
+
+    public static Integer parseEventType(Object raw) {
+        if (raw == null) {
+            return null;
+        }
+        if (raw instanceof Number) {
+            int value = ((Number) raw).intValue();
+            return value >= 1 && value <= 2 ? value : null;
+        }
+        String text = String.valueOf(raw).trim().toUpperCase();
+        if (text.contains("削峰") || "PEAK".equals(text) || "1".equals(text)) {
+            return 1;
+        }
+        if (text.contains("填谷") || "VALLEY".equals(text) || "FILLVALLEY".equals(text) || "2".equals(text)) {
+            return 2;
+        }
+        return null;
+    }
+
+    public static boolean isCancelled(Map<String, Object> payload) {
+        Integer status = getInteger(payload, "eventStatus", "event_status", "status");
+        if (status != null && status == 4) {
+            return true;
+        }
+        String text = getString(payload, "eventStatus", "event_status", "status", "eventState");
+        if (!StringUtils.hasText(text)) {
+            return false;
+        }
+        text = text.trim().toUpperCase();
+        return text.contains("取消") || "CANCELLED".equals(text) || "CANCELED".equals(text) || "4".equals(text);
+    }
+
+    private static Object getValue(Map<String, Object> map, String... keys) {
+        if (map == null) {
+            return null;
+        }
+        for (String key : keys) {
+            if (map.containsKey(key) && map.get(key) != null) {
+                return map.get(key);
+            }
+        }
+        return null;
+    }
+}

+ 74 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/web/advice/VppUnDnCryptoRequestAdvice.java

@@ -0,0 +1,74 @@
+package com.usky.vpp.web.advice;
+
+import com.usky.common.core.exception.BusinessException;
+import com.usky.vpp.controller.un.UnDnController;
+import com.usky.vpp.crypto.VppUnCryptoService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.MethodParameter;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.util.StreamUtils;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * UN 调用 DN 被动接口时,解密国密请求体
+ */
+@ControllerAdvice(assignableTypes = UnDnController.class)
+public class VppUnDnCryptoRequestAdvice extends RequestBodyAdviceAdapter {
+
+    @Autowired
+    private VppUnCryptoService cryptoService;
+
+    @Override
+    public boolean supports(MethodParameter methodParameter, Type targetType,
+                            Class<? extends HttpMessageConverter<?>> converterType) {
+        return cryptoService.isActive();
+    }
+
+    @Override
+    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
+                                           Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
+        byte[] bodyBytes = StreamUtils.copyToByteArray(inputMessage.getBody());
+        if (bodyBytes.length == 0) {
+            return inputMessage;
+        }
+        String body = new String(bodyBytes, StandardCharsets.UTF_8).trim();
+        if (body.startsWith("{")) {
+            return new DecodedHttpInputMessage(inputMessage.getHeaders(), bodyBytes);
+        }
+        String sign = inputMessage.getHeaders().getFirst("X-Sign");
+        if (!cryptoService.verifyInbound(body, sign)) {
+            throw new BusinessException("UN 请求验签失败");
+        }
+        String plain = cryptoService.decryptInbound(body);
+        return new DecodedHttpInputMessage(inputMessage.getHeaders(), plain.getBytes(StandardCharsets.UTF_8));
+    }
+
+    private static class DecodedHttpInputMessage implements HttpInputMessage {
+        private final HttpHeaders headers;
+        private final byte[] body;
+
+        private DecodedHttpInputMessage(HttpHeaders headers, byte[] body) {
+            this.headers = headers;
+            this.body = body;
+        }
+
+        @Override
+        public InputStream getBody() {
+            return new ByteArrayInputStream(body);
+        }
+
+        @Override
+        public HttpHeaders getHeaders() {
+            return headers;
+        }
+    }
+}

+ 49 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/web/advice/VppUnDnCryptoResponseAdvice.java

@@ -0,0 +1,49 @@
+package com.usky.vpp.web.advice;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.vpp.controller.un.UnDnController;
+import com.usky.vpp.crypto.VppUnCryptoService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.MethodParameter;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
+
+/**
+ * DN 响应 UN 被动调用时,可选国密加密响应体
+ */
+@ControllerAdvice(assignableTypes = UnDnController.class)
+public class VppUnDnCryptoResponseAdvice implements ResponseBodyAdvice<Object> {
+
+    @Autowired
+    private VppUnCryptoService cryptoService;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @Override
+    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
+        return cryptoService.isActive();
+    }
+
+    @Override
+    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
+                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
+                                  ServerHttpRequest request, ServerHttpResponse response) {
+        if (body == null) {
+            return null;
+        }
+        try {
+            String plainJson = body instanceof String ? (String) body : objectMapper.writeValueAsString(body);
+            String cipher = cryptoService.encryptOutbound(plainJson);
+            response.getHeaders().set("X-Sign", cryptoService.signOutbound(cipher));
+            response.getHeaders().setContentType(MediaType.TEXT_PLAIN);
+            return cipher;
+        } catch (Exception ex) {
+            return body;
+        }
+    }
+}

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

@@ -17,3 +17,17 @@ spring:
         file-extension: yml
         shared-configs:
           - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
+
+# 运管平台 UN 对接(也可在 Nacos 中配置 vpp.un.*)
+vpp:
+  un:
+    outbound-enabled: false
+    poll-enabled: false
+    crypto-enabled: false
+    poll-interval-sec: 10
+    price-down-coeff: "0.8"
+    token-header: Authorization
+    token-prefix: "Bearer "
+    token-ttl-minutes: 25
+    auto-ack-clearing: false
+    auto-register-on-startup: false

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

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

+ 81 - 53
service-vpp/service-vpp-biz/src/main/resources/sql/vpp_schema.sql

@@ -19,10 +19,11 @@ CREATE TABLE `vpp_customer` (
     `address` VARCHAR(500) NULL COMMENT '详细地址',
     `business_license_url` VARCHAR(500) NULL COMMENT '营业执照附件',
     `remark` VARCHAR(500) NULL COMMENT '备注',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_customer_account_no` (account_no),
@@ -42,14 +43,14 @@ CREATE TABLE `vpp_customer_access` (
     `credit_status` TINYINT NULL COMMENT '信用状况',
     `access_status` TINYINT NOT NULL COMMENT '0待审核 1通过 2驳回',
     `audit_opinion` VARCHAR(500) NULL COMMENT '审核意见',
-    `audit_by` BIGINT NULL COMMENT '审核人',
     `audit_at` DATETIME(3) NULL COMMENT '审核时间',
     `customer_id` BIGINT NULL COMMENT '通过后关联客户ID',
     `apply_at` DATETIME(3) NOT NULL COMMENT '申请时间',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_access_apply_no` (apply_no),
@@ -64,10 +65,11 @@ CREATE TABLE `vpp_customer_contact` (
     `contact_email` VARCHAR(100) NULL COMMENT '邮箱',
     `is_primary` TINYINT NOT NULL COMMENT '是否主联系人 0否 1是',
     `position` VARCHAR(50) NULL COMMENT '职务',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     KEY `idx_contact_customer` (customer_id)
@@ -89,10 +91,11 @@ CREATE TABLE `vpp_contract` (
     `price_json` JSON NULL COMMENT '电价条款JSON',
     `account_info_json` JSON NULL COMMENT '分成账户信息',
     `remark` VARCHAR(500) NULL COMMENT '备注',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_contract_no` (contract_no),
@@ -109,10 +112,11 @@ CREATE TABLE `vpp_contract_template` (
     `file_url` VARCHAR(500) NOT NULL COMMENT '模板文件URL',
     `variables_json` JSON NULL COMMENT '占位符变量定义',
     `is_enabled` TINYINT NOT NULL COMMENT '是否启用',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_template_code` (template_code)
@@ -123,11 +127,11 @@ CREATE TABLE `vpp_contract_audit_log` (
     `contract_id` BIGINT NOT NULL COMMENT '合同ID',
     `action` TINYINT NOT NULL COMMENT '1提交 2通过 3驳回 4归档',
     `opinion` VARCHAR(500) NULL COMMENT '意见',
-    `operator_id` BIGINT NOT NULL COMMENT '操作人',
-    `operator_name` VARCHAR(100) NULL COMMENT '操作人姓名',
-    `operated_at` DATETIME(3) NOT NULL COMMENT '操作时间',
+    `tenant_id` INT NULL COMMENT '租户ID',
+    `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
     PRIMARY KEY (`id`),
-    KEY `idx_contract_audit_contract` (contract_id, operated_at)
+    KEY `idx_contract_audit_contract` (contract_id, create_time)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同审核流水';
 
 CREATE TABLE `vpp_resource_point` (
@@ -151,10 +155,11 @@ CREATE TABLE `vpp_resource_point` (
     `response_priority` TINYINT NULL COMMENT '响应优先级1-10',
     `un_resource_id` VARCHAR(64) NULL COMMENT '运管平台分路资源ID',
     `remark` VARCHAR(500) NULL COMMENT '备注',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_resource_code` (resource_code),
@@ -172,10 +177,11 @@ CREATE TABLE `vpp_resource_point_config` (
     `soc_lower_limit` DECIMAL(5,2) NULL COMMENT 'SOC下限%',
     `offline_timeout_sec` INT NULL COMMENT '离线超时秒',
     `alarm_rule_json` JSON NULL COMMENT '扩展告警规则',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_config_resource` (resource_id)
@@ -196,10 +202,11 @@ CREATE TABLE `vpp_device` (
     `last_online_at` DATETIME(3) NULL COMMENT '最后在线时间',
     `gateway_id` VARCHAR(64) NULL COMMENT 'IoT网关标识',
     `remark` VARCHAR(500) NULL COMMENT '备注',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_device_code` (device_code),
@@ -214,10 +221,11 @@ CREATE TABLE `vpp_device_control_log` (
     `control_params` JSON NULL COMMENT '控制参数',
     `control_result` TINYINT NOT NULL COMMENT '0失败 1成功 2执行中',
     `result_message` VARCHAR(500) NULL COMMENT '结果说明',
-    `operator_id` BIGINT NOT NULL COMMENT '操作人',
-    `operated_at` DATETIME(3) NOT NULL COMMENT '操作时间',
+    `tenant_id` INT NULL COMMENT '租户ID',
+    `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
     PRIMARY KEY (`id`),
-    KEY `idx_control_device` (device_id, operated_at)
+    KEY `idx_control_device` (device_id, create_time)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备远程控制日志';
 
 CREATE TABLE `vpp_energy_reading_monthly` (
@@ -234,10 +242,11 @@ CREATE TABLE `vpp_energy_reading_monthly` (
     `gen_energy_kwh` DECIMAL(18,4) NULL COMMENT '发电量 kWh',
     `calc_status` TINYINT NOT NULL COMMENT '0待核算 1已核算 2异常',
     `calc_at` DATETIME(3) NULL COMMENT '核算时间',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_energy_monthly` (customer_id, resource_id, settle_year, settle_month),
@@ -254,10 +263,11 @@ CREATE TABLE `vpp_energy_summary_daily` (
     `green_ratio` DECIMAL(5,2) NULL COMMENT '绿电消纳比例%',
     `max_power_kw` DECIMAL(12,4) NULL COMMENT '日最大有功功率',
     `avg_power_kw` DECIMAL(12,4) NULL COMMENT '日平均有功功率',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_summary_daily` (summary_date, resource_id),
@@ -277,10 +287,11 @@ CREATE TABLE `vpp_settlement_bill` (
     `due_date` DATE NULL COMMENT '缴费截止日期',
     `bill_file_url` VARCHAR(500) NULL COMMENT '账单PDF',
     `generated_at` DATETIME(3) NULL COMMENT '生成时间',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_bill_no` (bill_no),
@@ -295,6 +306,7 @@ CREATE TABLE `vpp_settlement_bill_detail` (
     `energy_kwh` DECIMAL(18,4) NOT NULL COMMENT '时段电量',
     `price` DECIMAL(10,4) NOT NULL COMMENT '时段电价',
     `amount` DECIMAL(18,4) NOT NULL COMMENT '时段金额',
+    `tenant_id` INT NULL COMMENT '租户ID',
     PRIMARY KEY (`id`),
     KEY `idx_bill_detail_bill` (bill_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='账单分时明细';
@@ -307,7 +319,9 @@ CREATE TABLE `vpp_payment_record` (
     `payment_date` DATE NOT NULL COMMENT '缴费日期',
     `voucher_url` VARCHAR(500) NULL COMMENT '缴费凭证',
     `remark` VARCHAR(500) NULL COMMENT '对账备注',
-    `recorded_by` BIGINT NOT NULL COMMENT '录入人',
+    `tenant_id` INT NULL COMMENT '租户ID',
+    `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
     PRIMARY KEY (`id`),
     KEY `idx_payment_bill` (bill_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='缴费记录';
@@ -323,10 +337,11 @@ CREATE TABLE `vpp_report_task` (
     `task_status` TINYINT NOT NULL COMMENT '0待填报 1待审核 2报送中 3已完成 4已逾期',
     `assignee_id` BIGINT NULL COMMENT '填报人',
     `remind_days` INT NULL COMMENT '提前提醒天数',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_report_task_no` (task_no),
@@ -345,10 +360,11 @@ CREATE TABLE `vpp_report_record` (
     `retry_count` TINYINT NOT NULL COMMENT '重试次数,默认0',
     `submitted_at` DATETIME(3) NULL COMMENT '报送时间',
     `archived_at` DATETIME(3) NULL COMMENT '归档时间',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_report_record_no` (record_no),
@@ -362,10 +378,11 @@ CREATE TABLE `vpp_report_data` (
     `form_data` JSON NOT NULL COMMENT '填报表单JSON',
     `validation_errors` JSON NULL COMMENT '校验异常项',
     `is_draft` TINYINT NOT NULL COMMENT '是否草稿',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_report_data_record` (record_id)
@@ -377,11 +394,12 @@ CREATE TABLE `vpp_report_audit_log` (
     `audit_level` TINYINT NOT NULL COMMENT '1填报 2复核 3审批',
     `action` TINYINT NOT NULL COMMENT '1提交 2通过 3驳回',
     `opinion` VARCHAR(500) NULL COMMENT '意见',
-    `operator_id` BIGINT NOT NULL COMMENT '操作人',
     `operator_ip` VARCHAR(64) NULL COMMENT '操作IP',
-    `operated_at` DATETIME(3) NOT NULL COMMENT '操作时间',
+    `tenant_id` INT NULL COMMENT '租户ID',
+    `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
     PRIMARY KEY (`id`),
-    KEY `idx_report_audit_record` (record_id, operated_at)
+    KEY `idx_report_audit_record` (record_id, create_time)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='报送审核流水';
 
 CREATE TABLE `vpp_dr_event` (
@@ -397,10 +415,11 @@ CREATE TABLE `vpp_dr_event` (
     `subsidy_price` DECIMAL(10,4) NULL COMMENT '补贴标准 元/kWh',
     `event_status` TINYINT NOT NULL COMMENT '0待参与 1已申报 2执行中 3已结束 4已取消',
     `raw_payload` JSON NULL COMMENT '原始邀约报文',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_dr_event_id` (event_id),
@@ -416,10 +435,11 @@ CREATE TABLE `vpp_dr_participation` (
     `declared_capacity_kw` DECIMAL(12,4) NULL COMMENT '申报容量',
     `cleared_capacity_kw` DECIMAL(12,4) NULL COMMENT '出清容量',
     `declared_at` DATETIME(3) NULL COMMENT '申报时间',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     KEY `idx_dr_part_event` (event_id),
@@ -436,10 +456,11 @@ CREATE TABLE `vpp_dr_strategy` (
     `strategy_config` JSON NULL COMMENT '策略扩展配置',
     `is_default` TINYINT NOT NULL COMMENT '是否默认策略',
     `is_enabled` TINYINT NOT NULL COMMENT '是否启用',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_dr_strategy_code` (strategy_code)
@@ -451,6 +472,7 @@ CREATE TABLE `vpp_dr_strategy_resource` (
     `resource_id` BIGINT NOT NULL COMMENT '资源点ID',
     `priority` TINYINT NOT NULL COMMENT '优先级序号',
     `max_adjust_kw` DECIMAL(12,4) NULL COMMENT '最大调节量',
+    `tenant_id` INT NULL COMMENT '租户ID',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_strategy_resource` (strategy_id, resource_id),
     KEY `idx_strategy_resource_priority` (strategy_id, priority)
@@ -465,10 +487,11 @@ CREATE TABLE `vpp_dr_execution` (
     `execute_status` TINYINT NOT NULL COMMENT '0待执行 1执行中 2完成 3异常',
     `started_at` DATETIME(3) NULL COMMENT '开始时间',
     `finished_at` DATETIME(3) NULL COMMENT '结束时间',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     KEY `idx_dr_exec_event` (event_id),
@@ -484,10 +507,11 @@ CREATE TABLE `vpp_dr_evaluation` (
     `qualified_rate` DECIMAL(5,2) NULL COMMENT '执行合格率%',
     `report_file_url` VARCHAR(500) NULL COMMENT '评估报告URL',
     `evaluated_at` DATETIME(3) NULL COMMENT '评估时间',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_dr_eval_event` (event_id)
@@ -500,10 +524,11 @@ CREATE TABLE `vpp_registration` (
     `reg_status` TINYINT NOT NULL COMMENT '0未注册 1已注册 2需重新注册',
     `registered_at` DATETIME(3) NULL COMMENT '注册时间',
     `meta_report_id` VARCHAR(128) NULL COMMENT '元数据报告ID',
+    `tenant_id` INT NULL COMMENT '租户ID',
     `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    `created_by` BIGINT NULL COMMENT '创建人',
-    `updated_by` BIGINT NULL COMMENT '更新人',
+    `created_by` VARCHAR(30) NULL COMMENT '创建人',
+    `updated_by` VARCHAR(30) NULL COMMENT '更新人',
     `deleted_at` DATETIME(3) NULL COMMENT '软删除时间',
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_vpp_dn_id` (dn_id)
@@ -518,6 +543,7 @@ CREATE TABLE `vpp_report_log` (
     `error_message` VARCHAR(500) NULL COMMENT '错误信息',
     `payload_size` INT NULL COMMENT '报文大小字节',
     `reported_at` DATETIME(3) NOT NULL COMMENT '上报时间',
+    `tenant_id` INT NULL COMMENT '租户ID',
     PRIMARY KEY (`id`),
     KEY `idx_vpp_report_type_time` (report_type, reported_at)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='数据上报日志';
@@ -531,9 +557,11 @@ CREATE TABLE vpp_file_archive (
     biz_type    VARCHAR(32)    NOT NULL COMMENT '业务类型',
     biz_id      BIGINT         NOT NULL COMMENT '业务主键',
     remark      VARCHAR(500)   NULL COMMENT '备注',
+    tenant_id   INT            NULL COMMENT '租户ID',
     create_time DATETIME(3)    NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
     update_time DATETIME(3)    NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
-    created_by  BIGINT         NULL COMMENT '上传人',
+    created_by  VARCHAR(30)    NULL COMMENT '上传人',
+    updated_by  VARCHAR(30)    NULL COMMENT '更新人',
     deleted_at  DATETIME(3)    NULL COMMENT '软删除时间',
     PRIMARY KEY (id),
     KEY idx_file_archive_biz (biz_type, biz_id)

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

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

+ 15 - 0
service-vpp/service-vpp-biz/src/main/resources/un-samples/create-cq-request.json

@@ -0,0 +1,15 @@
+{
+  "optType": "optIn",
+  "eventID": "YY#1100_10010103000052",
+  "code": 6206,
+  "requestID": "111",
+  "root": "CreateCqRequest",
+  "createdDateTime": "2025-11-27 13:34:00",
+  "priceDownCoeff": "0.8",
+  "dnID": "10010103000052",
+  "list": [
+    { "load": "10000", "account": "3101330499683" },
+    { "load": "90000", "account": "3100149000967" }
+  ],
+  "version": 1
+}

+ 20 - 0
service-vpp/service-vpp-biz/src/main/resources/un-samples/create-event-response.json

@@ -0,0 +1,20 @@
+{
+  "root": "CreateEventResponse",
+  "version": 1,
+  "code": 200,
+  "description": "ok",
+  "requestID": "a42bb01d-2395-4038-a95c-7691a9273fc4",
+  "dnID": "10010103000002",
+  "eventResponses": [
+    {
+      "optType": "optIn",
+      "code": 200,
+      "description": "ok",
+      "requestID": "9ab66db1-8fd4-4b03-8fd0-c97005a7e33f",
+      "qualifiedEventID": {
+        "eventID": "EVE_10001911221018",
+        "modificationNumber": 0
+      }
+    }
+  ]
+}

+ 14 - 0
service-vpp/service-vpp-biz/src/main/resources/un-samples/create-opt-request.json

@@ -0,0 +1,14 @@
+{
+  "optType": "optIn",
+  "eventID": "YY#1100_10010103000052",
+  "requestID": "111",
+  "root": "CreateOptRequest",
+  "createdDateTime": "2025-11-27 13:34:00",
+  "priceDownCoeff": "0.8",
+  "dnID": "10010103000052",
+  "list": [
+    { "load": "10000", "account": "3101330499683", "needzk": "是" },
+    { "load": "90000", "account": "3100149000967", "needzk": "否" }
+  ],
+  "version": 1
+}

+ 55 - 0
service-vpp/service-vpp-biz/src/main/resources/un-samples/distribute-event-invitation.json

@@ -0,0 +1,55 @@
+{
+  "root": "DistributeEventRequest",
+  "version": 1,
+  "requestID": "a18b7934-43fe-4cae-bafc-5d185dd21e34",
+  "dnID": "10010103000052",
+  "events": [
+    {
+      "responseRequired": "always",
+      "descriptor": {
+        "eventID": "YY#1100_10010103000052",
+        "status": "far",
+        "shape": "switchType",
+        "control": "peakClipping",
+        "notification": "mediumterm",
+        "tradeType": "YY",
+        "comment": "this is real event.",
+        "filing": true,
+        "deadline": "2025-11-28 12:59:00",
+        "lastFiling": null
+      },
+      "activePeriod": [
+        {
+          "dtstart": "2025-11-28T13:00:00",
+          "dtend": "2025-11-28T18:00:00"
+        }
+      ],
+      "signals": {
+        "signal": [
+          {
+            "signalName": "ENERGY_PRICE",
+            "signalType": "price",
+            "intervals": {
+              "irregular": {
+                "values": [{ "value": 6, "timestamp": "2025-11-28T13:15:00" }]
+              }
+            }
+          },
+          {
+            "signalName": "DEMAND_CHARGE",
+            "signalType": "delta",
+            "intervals": {
+              "irregular": {
+                "values": [{ "value": 400, "timestamp": "2025-11-28T13:15:00" }]
+              }
+            }
+          }
+        ]
+      },
+      "target": {
+        "resourceID": ["3100139002062", "3100061516683"],
+        "resources": []
+      }
+    }
+  ]
+}

+ 6 - 0
service-vpp/service-vpp-biz/src/main/resources/un-samples/poll-request.json

@@ -0,0 +1,6 @@
+{
+  "root": "Poll",
+  "version": 1,
+  "requestID": "bf992657-d1b1-4634-badd-2be6f1275e4f",
+  "dnID": "10010103000002"
+}