فهرست منبع

Merge branch 'feature/service-vpp-20260701' of http://47.111.81.118:3000/uskycloud/usky-modules into fyc-vpp

fuyuchuan 1 روز پیش
والد
کامیت
70356cd9f3
16فایلهای تغییر یافته به همراه591 افزوده شده و 137 حذف شده
  1. 2 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppDrEventStatus.java
  2. 1 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDrEvent.java
  3. 2 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/enums/VppUnEventPhase.java
  4. 13 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDrUnInvitationIngestService.java
  5. 5 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppUnDrSyncService.java
  6. 17 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppUnEventPhaseDetector.java
  7. 26 20
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrEventIngestServiceImpl.java
  8. 51 17
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrInvitationServiceImpl.java
  9. 54 54
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrServiceImpl.java
  10. 187 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrUnInvitationIngestServiceImpl.java
  11. 41 6
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppUnDrSyncServiceImpl.java
  12. 135 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppUnEventPhaseDetectorImpl.java
  13. 3 1
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppUnIntegrationServiceImpl.java
  14. 35 27
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppUnEventParser.java
  15. 18 5
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppUnMessageBuilder.java
  16. 1 0
      service-vpp/service-vpp-biz/src/main/resources/un-samples/create-cq-request.json

+ 2 - 2
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/constant/VppDrEventStatus.java

@@ -7,8 +7,8 @@ package com.usky.vpp.constant;
 public final class VppDrEventStatus {
 
     public static final int PENDING = 0;
-    public static final int DECLARED = 1;
-    public static final int DECLARE_COMPLETED = 2;
+    public static final int DECLARE_COMPLETED = 1;
+    public static final int AWARDED = 2;
     public static final int EXECUTING = 3;
     public static final int ENDED = 4;
     public static final int CANCELLED = 5;

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

@@ -60,7 +60,7 @@ public class VppDrEvent implements Serializable {
     /** 下浮系数(必填) */
     @TableField("floating_coefficient")
     private BigDecimal floatingCoefficient;
-    /** 0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消 */
+    /** 0待参与 1申报完成 2 已中标 3执行中 4已结束 5已取消 6末位分拆 */
     @TableField("event_status")
     private Integer eventStatus;
     @TableField("raw_payload")

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

@@ -9,9 +9,9 @@ public enum VppUnEventPhase {
     INVITATION,
     /** 申报结果反馈:filing=true,target.resources 含申报量 */
     DECLARE_FEEDBACK,
-    /** 末位分拆通知:filing=false,lastFiling=true,resources 为系统出清量 */
+    /** 末位分拆通知:系统出清量与本地出清容量不一致 */
     SPLIT_NOTICE,
-    /** 末位分拆申报结果反馈 */
+    /** 末位分拆申报结果反馈:各 account 上报量与站点资源点出清容量之和一致 */
     SPLIT_RESULT,
     /** 出清公示:target.resources 含最终出清量 */
     CLEARING_PUBLICITY,

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

@@ -0,0 +1,13 @@
+package com.usky.vpp.service;
+
+import com.usky.vpp.domain.VppDrEvent;
+
+import java.util.Map;
+
+/**
+ * UN 邀约报文入库(与 {@link VppDrInvitationService} 解耦,避免循环依赖)。
+ */
+public interface VppDrUnInvitationIngestService {
+
+    void syncInvitationsFromUnEvent(VppDrEvent event, Map<String, Object> eventMap);
+}

+ 5 - 2
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppUnDrSyncService.java

@@ -16,7 +16,10 @@ public interface VppUnDrSyncService {
     void syncOptContent(VppDrEvent event, List<Map<String, Object>> optList, boolean clearing);
 
     /**
-     * 解析 DistributeEventRequest.target.resources[],同步申报/出清量
+     * 解析 DistributeEventRequest.target.resources[],同步申报/出清量,并按站点更新邀约 response_status
+     *
+     * @param responseStatus 与 vpp_dr_event.event_status 一致,写入 vpp_dr_invitation.response_status
      */
-    void syncTargetResources(VppDrEvent event, List<Map<String, Object>> resources, boolean clearing);
+    void syncTargetResources(VppDrEvent event, List<Map<String, Object>> resources, boolean clearing,
+                             Integer responseStatus);
 }

+ 17 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppUnEventPhaseDetector.java

@@ -0,0 +1,17 @@
+package com.usky.vpp.service;
+
+import com.usky.vpp.enums.VppUnEventPhase;
+
+import java.util.Map;
+
+/**
+ * 运管平台事件阶段识别(含需数据库比对的末位分拆阶段)
+ */
+public interface VppUnEventPhaseDetector {
+
+    /**
+     * @param event     UN Event 报文节点
+     * @param drEventId 本地 vpp_dr_event.id,末位分拆比对出清容量时使用;可为 null
+     */
+    VppUnEventPhase detectPhase(Map<String, Object> event, Long drEventId);
+}

+ 26 - 20
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrEventIngestServiceImpl.java

@@ -12,7 +12,9 @@ import com.usky.vpp.mapper.VppDrEventMapper;
 import com.usky.vpp.mapper.VppDrParticipationMapper;
 import com.usky.vpp.service.VppDrEventIngestService;
 import com.usky.vpp.service.VppDrExecutionBootstrapService;
+import com.usky.vpp.service.VppDrUnInvitationIngestService;
 import com.usky.vpp.service.VppUnDrSyncService;
+import com.usky.vpp.service.VppUnEventPhaseDetector;
 import com.usky.vpp.util.VppAuditHelper;
 import com.usky.vpp.util.VppUnEventParser;
 import com.usky.vpp.util.VppUnPayloadHelper;
@@ -34,7 +36,8 @@ public class VppDrEventIngestServiceImpl implements VppDrEventIngestService {
     private static final Logger log = LoggerFactory.getLogger(VppDrEventIngestServiceImpl.class);
 
     private static final int EVENT_STATUS_PENDING = VppDrEventStatus.PENDING;
-    private static final int EVENT_STATUS_DECLARED = VppDrEventStatus.DECLARED;
+    private static final int EVENT_STATUS_DECLARE_COMPLETED = VppDrEventStatus.DECLARE_COMPLETED;
+    private static final int EVENT_STATUS_AWARDED = VppDrEventStatus.AWARDED;
     private static final int EVENT_STATUS_EXECUTING = VppDrEventStatus.EXECUTING;
     private static final int EVENT_STATUS_ENDED = VppDrEventStatus.ENDED;
     private static final int EVENT_STATUS_CANCELLED = VppDrEventStatus.CANCELLED;
@@ -49,6 +52,10 @@ public class VppDrEventIngestServiceImpl implements VppDrEventIngestService {
     @Autowired
     private VppDrExecutionBootstrapService executionBootstrapService;
     @Autowired
+    private VppDrUnInvitationIngestService unInvitationIngestService;
+    @Autowired
+    private VppUnEventPhaseDetector eventPhaseDetector;
+    @Autowired
     private ObjectMapper objectMapper;
 
     @Override
@@ -93,7 +100,7 @@ public class VppDrEventIngestServiceImpl implements VppDrEventIngestService {
             }
         }
         if (event.getEventStatus() == null || event.getEventStatus() == EVENT_STATUS_PENDING) {
-            event.setEventStatus(EVENT_STATUS_DECLARED);
+            event.setEventStatus(EVENT_STATUS_DECLARE_COMPLETED);
         }
         VppAuditHelper.fillUpdate(event);
         eventMapper.updateById(event);
@@ -128,7 +135,7 @@ public class VppDrEventIngestServiceImpl implements VppDrEventIngestService {
                 ? (List<Map<String, Object>>) body.get("list") : null;
         if (list != null && !list.isEmpty()) {
             unDrSyncService.syncOptContent(event, list, false);
-            event.setEventStatus(EVENT_STATUS_DECLARED);
+            event.setEventStatus(EVENT_STATUS_DECLARE_COMPLETED);
         }
         VppAuditHelper.fillUpdate(event);
         eventMapper.updateById(event);
@@ -141,7 +148,7 @@ public class VppDrEventIngestServiceImpl implements VppDrEventIngestService {
     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) {
+        if (event.getEventStatus() == null || event.getEventStatus() <= EVENT_STATUS_DECLARE_COMPLETED) {
             event.setEventStatus(EVENT_STATUS_EXECUTING);
         }
         executionBootstrapService.ensureExecutions(event);
@@ -163,8 +170,8 @@ public class VppDrEventIngestServiceImpl implements VppDrEventIngestService {
             return cancelEvent(findByPlatformEventId(platformEventId), platformEventId, rawBody, source);
         }
 
-        VppUnEventPhase phase = VppUnEventParser.detectPhase(eventMap);
         VppDrEvent event = findByPlatformEventId(platformEventId);
+        VppUnEventPhase phase = eventPhaseDetector.detectPhase(eventMap, event != null ? event.getId() : null);
         if (event == null) {
             event = new VppDrEvent();
             event.setEventId(platformEventId);
@@ -187,6 +194,9 @@ public class VppDrEventIngestServiceImpl implements VppDrEventIngestService {
         }
 
         applyPhase(event, eventMap, phase, platformStatus);
+        if (phase == VppUnEventPhase.INVITATION) {
+            unInvitationIngestService.syncInvitationsFromUnEvent(event, eventMap);
+        }
         VppAuditHelper.fillUpdate(event);
         eventMapper.updateById(event);
         return event;
@@ -199,25 +209,25 @@ public class VppDrEventIngestServiceImpl implements VppDrEventIngestService {
                 event.setEventStatus(EVENT_STATUS_PENDING);
                 break;
             case DECLARE_FEEDBACK:
-                event.setEventStatus(EVENT_STATUS_DECLARED);
-                unDrSyncService.syncTargetResources(event, resources, false);
+                event.setEventStatus(EVENT_STATUS_DECLARE_COMPLETED);
+                unDrSyncService.syncTargetResources(event, resources, false, EVENT_STATUS_DECLARE_COMPLETED);
                 break;
             case SPLIT_NOTICE:
                 event.setEventStatus(EVENT_STATUS_SPLITTING);
                 event.setTargetCapacityKw(VppUnEventParser.sumResourceLoadKw(resources, true));
-                deleteEventParticipations(event);
+//                deleteEventParticipations(event);
                 break;
             case SPLIT_RESULT:
-                event.setEventStatus(EVENT_STATUS_DECLARED);
-                unDrSyncService.syncTargetResources(event, resources, true);
+                event.setEventStatus(EVENT_STATUS_DECLARE_COMPLETED);
+                unDrSyncService.syncTargetResources(event, resources, true, EVENT_STATUS_DECLARE_COMPLETED);
                 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);
+                unDrSyncService.syncTargetResources(event, resources, true, EVENT_STATUS_AWARDED);
+                event.setEventStatus(EVENT_STATUS_AWARDED);
                 break;
             default:
                 break;
@@ -235,19 +245,15 @@ public class VppDrEventIngestServiceImpl implements VppDrEventIngestService {
         if (event == null || event.getId() == null) {
             return;
         }
-        List<VppDrParticipation> participations = participationMapper.selectList(
-                new LambdaQueryWrapper<VppDrParticipation>()
-                        .eq(VppDrParticipation::getEventId, event.getId())
-                        .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED));
-        for (VppDrParticipation participation : participations) {
-            VppAuditHelper.fillSoftDelete(participation);
-            participationMapper.updateById(participation);
-        }
+        participationMapper.delete(new LambdaQueryWrapper<VppDrParticipation>()
+                .eq(VppDrParticipation::getEventId, event.getId()));
     }
 
     private void applyBaseFields(VppDrEvent event, Map<String, Object> eventMap, Map<String, Object> descriptor) {
         String comment = VppUnPayloadHelper.getString(descriptor, "comment");
+        LocalDateTime replyDeadline = VppUnPayloadHelper.getDateTime(descriptor, "deadline");
         event.setEventName(StringUtils.hasText(comment) ? comment : event.getEventId());
+        event.setDeclareDeadline(replyDeadline);
         event.setResponseType(VppUnEventParser.mapResponseType(descriptor));
         event.setEventType(VppUnEventParser.mapEventType(descriptor));
 

+ 51 - 17
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrInvitationServiceImpl.java

@@ -11,11 +11,13 @@ import com.usky.vpp.domain.VppCustomerContact;
 import com.usky.vpp.domain.VppDrEvent;
 import com.usky.vpp.domain.VppDrInvitation;
 import com.usky.vpp.domain.VppDrParticipation;
+import com.usky.vpp.domain.VppResourcePoint;
 import com.usky.vpp.mapper.VppCustomerContactMapper;
 import com.usky.vpp.mapper.VppCustomerMapper;
 import com.usky.vpp.mapper.VppDrEventMapper;
 import com.usky.vpp.mapper.VppDrInvitationMapper;
 import com.usky.vpp.mapper.VppDrParticipationMapper;
+import com.usky.vpp.mapper.VppResourcePointMapper;
 import com.usky.vpp.service.VppAliyunSmsService;
 import com.usky.vpp.service.VppDrInvitationService;
 import com.usky.vpp.service.VppUnIntegrationService;
@@ -59,7 +61,7 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
 
     /** 与事件状态一致:0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消 */
     private static final int RESPONSE_PENDING = VppDrEventStatus.PENDING;
-    private static final int RESPONSE_DECLARED = VppDrEventStatus.DECLARED;
+    private static final int EVENT_STATUS_DECLARE_COMPLETED = VppDrEventStatus.DECLARE_COMPLETED;
     private static final int RESPONSE_CANCELLED = VppDrEventStatus.CANCELLED;
 
     private static final int SMS_NOT_SENT = 0;
@@ -79,6 +81,8 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
     @Autowired
     private VppDrParticipationMapper participationMapper;
     @Autowired
+    private VppResourcePointMapper resourcePointMapper;
+    @Autowired
     private VppUnIntegrationService unIntegrationService;
     @Autowired
     private VppAliyunSmsService aliyunSmsService;
@@ -111,7 +115,7 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
 
         VppDrEvent event = requireEvent(request.getDrEventId());
         VppCustomer customer = requireCustomer(request.getCustomerId());
-        ensureInvitationUnique(request.getDrEventId(), request.getCustomerId(), null);
+        ensureInvitationUnique(request, null);
 
         VppDrInvitation invitation = new VppDrInvitation();
         invitation.setInvitationNo(VppAuditHelper.nextInvitationNo());
@@ -149,9 +153,9 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
     @Transactional(rollbackFor = Exception.class)
     public void updateInvitation(Long id, DrInvitationRequest request) {
         VppDrInvitation invitation = requireInvitation(id);
-        if (invitation.getReplyStatus() != REPLY_PENDING || invitation.getResponseStatus() != RESPONSE_PENDING) {
-            throw new BusinessException("仅待回复状态的邀约可修改");
-        }
+//        if (invitation.getReplyStatus() != REPLY_PENDING || invitation.getResponseStatus() != RESPONSE_PENDING) {
+//            throw new BusinessException("仅待回复状态的邀约可修改");
+//        }
         if (request == null) {
             throw new BusinessException("请求不能为空");
         }
@@ -272,9 +276,9 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
     @Transactional(rollbackFor = Exception.class)
     public DrInvitationVO replyInvitation(Long id, DrInvitationReplyRequest request) {
         VppDrInvitation invitation = requireInvitation(id);
-        if (invitation.getReplyStatus() != REPLY_PENDING) {
-            throw new BusinessException("当前邀约已回复");
-        }
+//        if (invitation.getReplyStatus() != REPLY_PENDING) {
+//            throw new BusinessException("当前邀约已回复");
+//        }
         if (invitation.getReplyDeadline() != null && LocalDateTime.now().isAfter(invitation.getReplyDeadline())) {
             throw new BusinessException("已超过邀约回复截止时间");
         }
@@ -294,7 +298,7 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
             invitation.setParticipationId(participation.getId());
             invitation.setDeclaredCapacityKw(declared);
             invitation.setReplyStatus(REPLY_ACCEPT);
-            invitation.setResponseStatus(RESPONSE_DECLARED);
+            invitation.setResponseStatus(EVENT_STATUS_DECLARE_COMPLETED);
 //            syncParticipationToUn(invitation.getDrEventId(), true, Collections.singletonList(participation));
 
         } else {
@@ -368,13 +372,19 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
     }
 
     private VppDrParticipation findOrCreateParticipation(VppDrInvitation invitation, BigDecimal declaredCapacityKw) {
-        VppDrParticipation existing = participationMapper.selectOne(new LambdaQueryWrapper<VppDrParticipation>()
+        Long resourceId = resolveResourceIdBySiteId(invitation.getSiteId());
+        LambdaQueryWrapper<VppDrParticipation> wrapper = new LambdaQueryWrapper<VppDrParticipation>()
                 .eq(VppDrParticipation::getEventId, invitation.getDrEventId())
                 .eq(VppDrParticipation::getCustomerId, invitation.getCustomerId())
-                .isNull(VppDrParticipation::getResourceId)
-                .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
-                .last("LIMIT 1"));
+                .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+        if (resourceId != null) {
+            wrapper.eq(VppDrParticipation::getResourceId, resourceId);
+        } else {
+            wrapper.isNull(VppDrParticipation::getResourceId);
+        }
+        VppDrParticipation existing = participationMapper.selectOne(wrapper.last("LIMIT 1"));
         if (existing != null) {
+            existing.setResourceId(resourceId);
             existing.setParticipateStatus(PARTICIPATE_ACCEPT);
             existing.setDeclaredCapacityKw(declaredCapacityKw);
             existing.setDeclaredAt(LocalDateTime.now());
@@ -387,6 +397,7 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
         VppDrParticipation participation = new VppDrParticipation();
         participation.setEventId(invitation.getDrEventId());
         participation.setCustomerId(invitation.getCustomerId());
+        participation.setResourceId(resourceId);
         participation.setParticipateStatus(PARTICIPATE_ACCEPT);
         participation.setDeclaredCapacityKw(declaredCapacityKw);
         participation.setDeclaredAt(LocalDateTime.now());
@@ -399,6 +410,20 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
         return participation;
     }
 
+    private Long resolveResourceIdBySiteId(Long siteId) {
+        if (siteId == null) {
+            throw new BusinessException("邀约未关联站点,无法创建参与记录");
+        }
+        VppResourcePoint resourcePoint = resourcePointMapper.selectOne(new LambdaQueryWrapper<VppResourcePoint>()
+                .eq(VppResourcePoint::getSiteId, siteId)
+                .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .last("LIMIT 1"));
+        if (resourcePoint == null) {
+            throw new BusinessException("站点未配置资源点,无法参与");
+        }
+        return resourcePoint.getId();
+    }
+
     private void syncParticipationToUn(Long eventId, boolean participate, List<VppDrParticipation> participations) {
         try {
             unIntegrationService.submitParticipation(eventId, participate, participations);
@@ -461,16 +486,25 @@ public class VppDrInvitationServiceImpl implements VppDrInvitationService {
         return any != null ? any.getId() : null;
     }
 
-    private void ensureInvitationUnique(Long drEventId, Long customerId, Long excludeId) {
+    private void ensureInvitationUnique(DrInvitationRequest request, Long excludeId) {
+        if (request == null || request.getDrEventId() == null) {
+            return;
+        }
         LambdaQueryWrapper<VppDrInvitation> wrapper = new LambdaQueryWrapper<VppDrInvitation>()
-                .eq(VppDrInvitation::getDrEventId, drEventId)
-                .eq(VppDrInvitation::getCustomerId, customerId)
+                .eq(VppDrInvitation::getDrEventId, request.getDrEventId())
                 .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+        if (request.getSiteId() != null) {
+            wrapper.eq(VppDrInvitation::getSiteId, request.getSiteId());
+        } else if (request.getCustomerId() != null) {
+            wrapper.eq(VppDrInvitation::getCustomerId, request.getCustomerId());
+        }
         if (excludeId != null) {
             wrapper.ne(VppDrInvitation::getId, excludeId);
         }
         if (invitationMapper.selectCount(wrapper) > 0) {
-            throw new BusinessException("该事件已存在该企业的邀约记录");
+            throw new BusinessException(request.getSiteId() != null
+                    ? "该事件已存在该站点的邀约记录"
+                    : "该事件已存在该企业的邀约记录");
         }
     }
 

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

@@ -62,7 +62,7 @@ public class VppDrServiceImpl implements VppDrService {
     private static final Logger log = LoggerFactory.getLogger(VppDrServiceImpl.class);
 
     private static final int EVENT_STATUS_PENDING = VppDrEventStatus.PENDING;
-    private static final int EVENT_STATUS_DECLARED = VppDrEventStatus.DECLARED;
+    private static final int EVENT_STATUS_DECLARE_COMPLETED = VppDrEventStatus.DECLARE_COMPLETED;
     private static final int EVENT_STATUS_EXECUTING = VppDrEventStatus.EXECUTING;
     private static final int EVENT_STATUS_ENDED = VppDrEventStatus.ENDED;
     private static final int EVENT_STATUS_CANCELLED = VppDrEventStatus.CANCELLED;
@@ -218,9 +218,9 @@ public class VppDrServiceImpl implements VppDrService {
         if (request.getEventStatus() == null) {
             throw new BusinessException("事件状态不能为空");
         }
-        if (request.getEventStatus() != EVENT_STATUS_DECLARED) {
-            throw new BusinessException("仅允许将事件从待参与改为已申报");
-        }
+//        if (request.getEventStatus() != EVENT_STATUS_DECLARED) {
+//            throw new BusinessException("仅允许将事件从待参与改为已申报");
+//        }
 
         VppDrEvent event = requireEvent(request.getId());
         Integer tenantId = SecurityUtils.getTenantId();
@@ -231,11 +231,11 @@ public class VppDrServiceImpl implements VppDrService {
             throw new BusinessException("仅待参与状态的事件可改为已申报");
         }
 
-        event.setEventStatus(EVENT_STATUS_DECLARED);
+        event.setEventStatus(EVENT_STATUS_DECLARE_COMPLETED);
         VppAuditHelper.fillUpdate(event);
         eventMapper.updateById(event);
         for (VppDrInvitation invitation : loadEventInvitations(event.getId())) {
-            invitation.setResponseStatus(EVENT_STATUS_DECLARED);
+            invitation.setResponseStatus(EVENT_STATUS_DECLARE_COMPLETED);
             if (invitation.getTenantId() == null) {
                 invitation.setTenantId(event.getTenantId() != null ? event.getTenantId() : tenantId);
             }
@@ -311,24 +311,24 @@ public class VppDrServiceImpl implements VppDrService {
     public void participate(Long eventId, DrParticipateRequest request) {
         VppDrEvent event = requireEvent(eventId);
 
-        if (event.getEventStatus() != EVENT_STATUS_PENDING) {
-            throw new BusinessException("当前状态不允许参与");
-        }
+//        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)
-                        .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
-        );
-
-        if (!existingParticipations.isEmpty()) {
-            throw new BusinessException("已参与该事件");
-        }
+//        List<VppDrParticipation> existingParticipations = participationMapper.selectList(
+//                new LambdaQueryWrapper<VppDrParticipation>()
+//                        .eq(VppDrParticipation::getEventId, eventId)
+//                        .eq(VppDrParticipation::getParticipateStatus, PARTICIPATE_STATUS_ACCEPT)
+//                        .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+//        );
+//
+//        if (!existingParticipations.isEmpty()) {
+//            throw new BusinessException("已参与该事件");
+//        }
 
         if (Boolean.TRUE.equals(request.getParticipate())) {
             if (request.getResources() == null || request.getResources().isEmpty()) {
@@ -385,27 +385,27 @@ public class VppDrServiceImpl implements VppDrService {
 
             List<VppDrParticipation> savedParticipations = new ArrayList<>();
 
-            for (DrParticipateRequest.DrResourceDeclare resource : request.getResources()) {
-                VppResourcePoint rp = resourcePointMapper.selectById(resource.getResourceId());
-                VppDrParticipation participation = new VppDrParticipation();
-                participation.setEventId(eventId);
-                participation.setCustomerId(siteResourceHelper.resolveCustomerId(rp, siteMap));
-                participation.setResourceId(resource.getResourceId());
-                participation.setParticipateStatus(PARTICIPATE_STATUS_ACCEPT);
-                participation.setDeclaredCapacityKw(declaredByResourceId.get(resource.getResourceId()));
-                participation.setDeclaredAt(LocalDateTime.now());
-                VppDrParticipationHelper.refreshCompletionRate(participation);
-                VppAuditHelper.fillCreate(participation);
-                if (participation.getTenantId() == null && event.getTenantId() != null) {
-                    participation.setTenantId(event.getTenantId());
-                }
-                participationMapper.insert(participation);
-
-                savedParticipations.add(participation);
-
-            }
-
-            event.setEventStatus(EVENT_STATUS_DECLARED);
+//            for (DrParticipateRequest.DrResourceDeclare resource : request.getResources()) {
+//                VppResourcePoint rp = resourcePointMapper.selectById(resource.getResourceId());
+//                VppDrParticipation participation = new VppDrParticipation();
+//                participation.setEventId(eventId);
+//                participation.setCustomerId(siteResourceHelper.resolveCustomerId(rp, siteMap));
+//                participation.setResourceId(resource.getResourceId());
+//                participation.setParticipateStatus(PARTICIPATE_STATUS_ACCEPT);
+//                participation.setDeclaredCapacityKw(declaredByResourceId.get(resource.getResourceId()));
+//                participation.setDeclaredAt(LocalDateTime.now());
+//                VppDrParticipationHelper.refreshCompletionRate(participation);
+//                VppAuditHelper.fillCreate(participation);
+//                if (participation.getTenantId() == null && event.getTenantId() != null) {
+//                    participation.setTenantId(event.getTenantId());
+//                }
+//                participationMapper.insert(participation);
+//
+//                savedParticipations.add(participation);
+//
+//            }
+
+            event.setEventStatus(EVENT_STATUS_DECLARE_COMPLETED);
             VppAuditHelper.fillUpdate(event);
             eventMapper.updateById(event);
             syncParticipationToUn(eventId, request.getParticipate(), savedParticipations);
@@ -452,7 +452,7 @@ public class VppDrServiceImpl implements VppDrService {
             }
             if (participate) {
                 invitation.setReplyStatus(REPLY_ACCEPT);
-                invitation.setResponseStatus(EVENT_STATUS_DECLARED);
+                invitation.setResponseStatus(EVENT_STATUS_DECLARE_COMPLETED);
                 if (invitation.getParticipationId() == null && invitation.getCustomerId() != null) {
                     Long participationId = participationIdByCustomer.get(invitation.getCustomerId());
                     if (participationId != null) {
@@ -491,9 +491,9 @@ public class VppDrServiceImpl implements VppDrService {
     public void clearing(Long eventId, DrClearingRequest request) {
         VppDrEvent event = requireEvent(eventId);
 
-        if (event.getEventStatus() != EVENT_STATUS_DECLARED) {
-            throw new BusinessException("当前状态不允许出清分拆");
-        }
+//        if (event.getEventStatus() != EVENT_STATUS_DECLARED) {
+//            throw new BusinessException("当前状态不允许出清分拆");
+//        }
 
         if (request == null || request.getTotalClearedCapacityKw() == null) {
             throw new BusinessException("出清总容量不能为空");
@@ -503,14 +503,14 @@ public class VppDrServiceImpl implements VppDrService {
             throw new BusinessException("资源分拆列表不能为空");
         }
 
-        long existingExecCount = executionMapper.selectCount(
-                new LambdaQueryWrapper<VppDrExecution>()
-                        .eq(VppDrExecution::getEventId, eventId)
-                        .eq(VppDrExecution::getDeleteFlag, VppAuditHelper.NOT_DELETED)
-        );
-        if (existingExecCount > 0) {
-            throw new BusinessException("已出清,请勿重复操作");
-        }
+//        long existingExecCount = executionMapper.selectCount(
+//                new LambdaQueryWrapper<VppDrExecution>()
+//                        .eq(VppDrExecution::getEventId, eventId)
+//                        .eq(VppDrExecution::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+//        );
+//        if (existingExecCount > 0) {
+//            throw new BusinessException("已出清,请勿重复操作");
+//        }
 
         BigDecimal totalCleared = BigDecimal.ZERO;
         for (DrClearingRequest.DrResourceClearing resource : request.getResources()) {
@@ -549,7 +549,7 @@ public class VppDrServiceImpl implements VppDrService {
         }
 
         event.setClearedCapacityKw(request.getTotalClearedCapacityKw());
-        event.setEventStatus(EVENT_STATUS_EXECUTING);
+//        event.setEventStatus(EVENT_STATUS_EXECUTING);
         VppAuditHelper.fillUpdate(event);
         eventMapper.updateById(event);
 
@@ -761,7 +761,7 @@ public class VppDrServiceImpl implements VppDrService {
     @Transactional(rollbackFor = Exception.class)
     public void acknowledgeClearing(Long eventId) {
         VppDrEvent event = requireEvent(eventId);
-        if (event.getEventStatus() != EVENT_STATUS_DECLARED) {
+        if (event.getEventStatus() != EVENT_STATUS_DECLARE_COMPLETED) {
             throw new BusinessException("当前状态不允许确认出清公示");
         }
         unIntegrationService.acknowledgeClearing(eventId);

+ 187 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrUnInvitationIngestServiceImpl.java

@@ -0,0 +1,187 @@
+package com.usky.vpp.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.vpp.constant.VppDrEventStatus;
+import com.usky.vpp.domain.VppCustomerContact;
+import com.usky.vpp.domain.VppDrEvent;
+import com.usky.vpp.domain.VppDrInvitation;
+import com.usky.vpp.domain.VppSite;
+import com.usky.vpp.mapper.VppCustomerContactMapper;
+import com.usky.vpp.mapper.VppDrInvitationMapper;
+import com.usky.vpp.mapper.VppSiteMapper;
+import com.usky.vpp.service.VppDrUnInvitationIngestService;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppDrParticipationHelper;
+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 java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class VppDrUnInvitationIngestServiceImpl implements VppDrUnInvitationIngestService {
+
+    private static final Logger log = LoggerFactory.getLogger(VppDrUnInvitationIngestServiceImpl.class);
+
+    private static final int REPLY_PENDING = 0;
+    private static final int RESPONSE_PENDING = VppDrEventStatus.PENDING;
+    private static final int SMS_NOT_SENT = 0;
+
+    @Autowired
+    private VppDrInvitationMapper invitationMapper;
+    @Autowired
+    private VppSiteMapper siteMapper;
+    @Autowired
+    private VppCustomerContactMapper contactMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void syncInvitationsFromUnEvent(VppDrEvent event, Map<String, Object> eventMap) {
+        if (event == null || event.getId() == null || eventMap == null) {
+            return;
+        }
+        List<String> unResourceIds = VppUnEventParser.getTargetResourceIds(eventMap);
+        if (unResourceIds.isEmpty()) {
+            return;
+        }
+
+        Map<String, Object> descriptor = VppUnEventParser.getDescriptor(eventMap);
+        LocalDateTime[] period = VppUnEventParser.resolveActivePeriod(eventMap);
+        LocalDate executeStart = resolveExecuteDate(period[0], event.getStartTime());
+        LocalDate executeEnd = resolveExecuteDate(period[1], event.getEndTime());
+        if (executeStart == null) {
+            log.warn("UN 邀约缺少执行开始时间 eventId={}", event.getEventId());
+            return;
+        }
+        if (executeEnd == null) {
+            executeEnd = executeStart;
+        }
+        LocalDateTime replyDeadline = VppUnPayloadHelper.getDateTime(descriptor, "deadline");
+        if (replyDeadline == null && event.getStartTime() != null) {
+            replyDeadline = event.getStartTime();
+        }
+        if (replyDeadline == null) {
+            log.warn("UN 邀约缺少回复截止时间 eventId={}", event.getEventId());
+            return;
+        }
+
+        BigDecimal demandCapacityKw = resolveUnDemandCapacityKw(event, eventMap);
+        for (String unResourceId : unResourceIds) {
+            syncSingleUnInvitation(event, unResourceId, executeStart, executeEnd, replyDeadline, demandCapacityKw);
+        }
+    }
+
+    private void syncSingleUnInvitation(VppDrEvent event,
+                                        String resourceId,
+                                        LocalDate executeStart,
+                                        LocalDate executeEnd,
+                                        LocalDateTime replyDeadline,
+                                        BigDecimal demandCapacityKw) {
+        VppSite site = siteMapper.selectOne(new LambdaQueryWrapper<VppSite>()
+                .eq(VppSite::getAccountNo, resourceId.trim())
+                .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .last("LIMIT 1"));
+        if (site == null) {
+            log.warn("UN resourceID 未匹配站点 unResourceId={} eventId={}", resourceId, event.getEventId());
+            return;
+        }
+        if (site.getCustomerId() == null) {
+            log.warn("站点缺少 customerId siteId={} unResourceId={}", site.getId(), resourceId);
+            return;
+        }
+        if (findExistingInvitation(event.getId(), site.getId(), site.getCustomerId()) != null) {
+            log.debug("邀约已存在,跳过 eventPk={} siteId={}", event.getId(), site.getId());
+            return;
+        }
+
+        VppDrInvitation invitation = new VppDrInvitation();
+        invitation.setInvitationNo(VppAuditHelper.nextInvitationNo());
+        invitation.setDrEventId(event.getId());
+        invitation.setCustomerId(site.getCustomerId());
+        invitation.setSiteId(site.getId());
+        invitation.setExecuteStartDate(executeStart);
+        invitation.setExecuteEndDate(executeEnd);
+        invitation.setReplyDeadline(shiftOneHourEarlier(replyDeadline));
+        invitation.setIssueTime(shiftOneHourEarlier(LocalDateTime.now()));
+        invitation.setTransactionType(event.getEventType());
+        invitation.setResponseType(event.getResponseType());
+        invitation.setDemandCapacityKw(demandCapacityKw);
+        invitation.setDeclaredCapacityKw(BigDecimal.ZERO);
+        invitation.setReplyStatus(REPLY_PENDING);
+        invitation.setResponseStatus(RESPONSE_PENDING);
+        invitation.setSmsNotifyStatus(SMS_NOT_SENT);
+        invitation.setSmsContactId(resolveSmsContactId(site.getCustomerId()));
+        invitation.setEstimatedCompletionRate(VppDrParticipationHelper.calcEstimatedCompletionRate(null, null));
+        invitation.setResponseCompletionRate(VppDrParticipationHelper.calcResponseCompletionRate(null, null));
+        invitation.setRemark("UN邀约 resourceID=" + resourceId);
+        VppAuditHelper.fillCreate(invitation);
+        if (event.getTenantId() != null) {
+            invitation.setTenantId(event.getTenantId());
+        }
+        invitationMapper.insert(invitation);
+        log.info("UN 邀约已入库 eventId={} siteId={} unResourceId={}", event.getEventId(), site.getId(), resourceId);
+    }
+
+    private Long resolveSmsContactId(Long customerId) {
+        if (customerId == null) {
+            return null;
+        }
+        VppCustomerContact primary = contactMapper.selectOne(new LambdaQueryWrapper<VppCustomerContact>()
+                .eq(VppCustomerContact::getCustomerId, customerId)
+                .eq(VppCustomerContact::getIsPrimary, 1)
+                .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .last("LIMIT 1"));
+        if (primary != null) {
+            return primary.getId();
+        }
+        VppCustomerContact any = contactMapper.selectOne(new LambdaQueryWrapper<VppCustomerContact>()
+                .eq(VppCustomerContact::getCustomerId, customerId)
+                .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .last("LIMIT 1"));
+        return any != null ? any.getId() : null;
+    }
+
+    private LocalDate resolveExecuteDate(LocalDateTime fromPeriod, LocalDateTime fromEvent) {
+        if (fromPeriod != null) {
+            return fromPeriod.toLocalDate();
+        }
+        return fromEvent != null ? fromEvent.toLocalDate() : null;
+    }
+
+    /** UN 报文时间与平台展示/业务规则校正:向前(提前)1 小时。 */
+    private static LocalDateTime shiftOneHourEarlier(LocalDateTime time) {
+        return time != null ? time.minusHours(1) : null;
+    }
+
+    private BigDecimal resolveUnDemandCapacityKw(VppDrEvent event, Map<String, Object> eventMap) {
+        BigDecimal demand = VppUnEventParser.extractDemandChargeKw(eventMap);
+        if (demand == null || demand.compareTo(BigDecimal.ZERO) <= 0) {
+            demand = event.getTargetCapacityKw();
+        }
+        if (demand == null || demand.compareTo(BigDecimal.ZERO) <= 0) {
+            demand = BigDecimal.ONE;
+        }
+        return demand;
+    }
+
+    private VppDrInvitation findExistingInvitation(Long drEventId, Long siteId, Long customerId) {
+        LambdaQueryWrapper<VppDrInvitation> wrapper = new LambdaQueryWrapper<VppDrInvitation>()
+                .eq(VppDrInvitation::getDrEventId, drEventId)
+                .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .last("LIMIT 1");
+        if (siteId != null) {
+            wrapper.eq(VppDrInvitation::getSiteId, siteId);
+        } else {
+            wrapper.eq(VppDrInvitation::getCustomerId, customerId);
+        }
+        return invitationMapper.selectOne(wrapper);
+    }
+}

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

@@ -3,10 +3,12 @@ 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.VppDrInvitation;
 import com.usky.vpp.domain.VppDrParticipation;
 import com.usky.vpp.domain.VppResourcePoint;
 import com.usky.vpp.domain.VppSite;
 import com.usky.vpp.mapper.VppCustomerMapper;
+import com.usky.vpp.mapper.VppDrInvitationMapper;
 import com.usky.vpp.mapper.VppDrParticipationMapper;
 import com.usky.vpp.mapper.VppResourcePointMapper;
 import com.usky.vpp.mapper.VppSiteMapper;
@@ -37,6 +39,8 @@ public class VppUnDrSyncServiceImpl implements VppUnDrSyncService {
     private VppResourcePointMapper resourcePointMapper;
     @Autowired
     private VppDrParticipationMapper participationMapper;
+    @Autowired
+    private VppDrInvitationMapper invitationMapper;
 
     @Override
     public void syncOptContent(VppDrEvent event, List<Map<String, Object>> optList, boolean clearing) {
@@ -44,30 +48,34 @@ public class VppUnDrSyncServiceImpl implements VppUnDrSyncService {
             return;
         }
         for (Map<String, Object> item : optList) {
-            syncOne(event, item, clearing);
+            syncOne(event, item, clearing, null);
         }
     }
 
     @Override
-    public void syncTargetResources(VppDrEvent event, List<Map<String, Object>> resources, boolean clearing) {
+    public void syncTargetResources(VppDrEvent event, List<Map<String, Object>> resources, boolean clearing,
+                                    Integer responseStatus) {
         if (event == null || resources == null || resources.isEmpty()) {
             return;
         }
         for (Map<String, Object> resource : resources) {
-            syncOne(event, resource, clearing);
+            syncOne(event, resource, clearing, responseStatus);
         }
     }
 
-    private void syncOne(VppDrEvent event, Map<String, Object> item, boolean clearing) {
+    private void syncOne(VppDrEvent event, Map<String, Object> item, boolean clearing, Integer responseStatus) {
         String account = VppUnPayloadHelper.getString(item, "account");
         if (!StringUtils.hasText(account)) {
             return;
         }
         BigDecimal load = resolveLoadKw(item);
-        if (load == null) {
-            return;
+        if (load != null) {
+            syncParticipation(event, account, load, clearing);
         }
+        updateInvitationResponseStatus(event, account, responseStatus);
+    }
 
+    private void syncParticipation(VppDrEvent event, String account, BigDecimal load, boolean clearing) {
         VppCustomer customer = customerMapper.selectOne(new LambdaQueryWrapper<VppCustomer>()
                 .eq(VppCustomer::getAccountNo, account)
                 .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
@@ -122,6 +130,33 @@ public class VppUnDrSyncServiceImpl implements VppUnDrSyncService {
         }
     }
 
+    private void updateInvitationResponseStatus(VppDrEvent event, String account, Integer responseStatus) {
+        if (event == null || event.getId() == null || responseStatus == null || !StringUtils.hasText(account)) {
+            return;
+        }
+        VppSite site = siteMapper.selectOne(new LambdaQueryWrapper<VppSite>()
+                .eq(VppSite::getAccountNo, account.trim())
+                .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .last("LIMIT 1"));
+        if (site == null || site.getId() == null) {
+            return;
+        }
+        VppDrInvitation invitation = invitationMapper.selectOne(new LambdaQueryWrapper<VppDrInvitation>()
+                .eq(VppDrInvitation::getDrEventId, event.getId())
+                .eq(VppDrInvitation::getSiteId, site.getId())
+                .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .last("LIMIT 1"));
+        if (invitation == null) {
+            return;
+        }
+        invitation.setResponseStatus(responseStatus);
+        if(responseStatus == 2){
+            invitation.setIsWinningBid(1);
+        }
+        VppAuditHelper.fillUpdate(invitation);
+        invitationMapper.updateById(invitation);
+    }
+
     private BigDecimal resolveLoadKw(Map<String, Object> item) {
         BigDecimal load = VppUnPayloadHelper.getDecimal(item, "load");
         if (load != null) {

+ 135 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppUnEventPhaseDetectorImpl.java

@@ -0,0 +1,135 @@
+package com.usky.vpp.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.vpp.domain.VppDrParticipation;
+import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.domain.VppSite;
+import com.usky.vpp.enums.VppUnEventPhase;
+import com.usky.vpp.mapper.VppDrParticipationMapper;
+import com.usky.vpp.mapper.VppResourcePointMapper;
+import com.usky.vpp.mapper.VppSiteMapper;
+import com.usky.vpp.service.VppUnEventPhaseDetector;
+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.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+@Service
+public class VppUnEventPhaseDetectorImpl implements VppUnEventPhaseDetector {
+
+    private static final int CAPACITY_SCALE = 4;
+
+    @Autowired
+    private VppSiteMapper siteMapper;
+    @Autowired
+    private VppResourcePointMapper resourcePointMapper;
+    @Autowired
+    private VppDrParticipationMapper participationMapper;
+
+    @Override
+    public VppUnEventPhase detectPhase(Map<String, Object> event, Long drEventId) {
+        Map<String, Object> descriptor = VppUnEventParser.getDescriptor(event);
+        Boolean filing = VppUnPayloadHelper.getBoolean(descriptor, "filing");
+        Boolean lastFiling = VppUnPayloadHelper.getBoolean(descriptor, "lastFiling");
+        List<Map<String, Object>> resources = VppUnEventParser.getTargetResources(event);
+        boolean hasValues = VppUnEventParser.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)) {
+            return resolveSplitPhase(drEventId, resources);
+        }
+        if (hasValues) {
+            return VppUnEventPhase.CLEARING_PUBLICITY;
+        }
+        return VppUnEventPhase.UNKNOWN;
+    }
+
+    private VppUnEventPhase resolveSplitPhase(Long drEventId, List<Map<String, Object>> resources) {
+        if (drEventId == null || CollectionUtils.isEmpty(resources)) {
+            return VppUnEventPhase.SPLIT_NOTICE;
+        }
+        for (Map<String, Object> resource : resources) {
+            String account = VppUnPayloadHelper.getString(resource, "account");
+            if (!StringUtils.hasText(account)) {
+                return VppUnEventPhase.SPLIT_NOTICE;
+            }
+            BigDecimal reportedKw = VppUnEventParser.resolveResourceReportedKw(resource);
+            if (reportedKw == null) {
+                return VppUnEventPhase.SPLIT_NOTICE;
+            }
+            BigDecimal clearedSumKw = sumClearedCapacityKwByAccount(drEventId, account.trim());
+            if (!capacityEquals(reportedKw, clearedSumKw)) {
+                return VppUnEventPhase.SPLIT_NOTICE;
+            }
+        }
+        return VppUnEventPhase.SPLIT_RESULT;
+    }
+
+    private BigDecimal sumClearedCapacityKwByAccount(Long drEventId, String accountNo) {
+        VppSite site = siteMapper.selectOne(new LambdaQueryWrapper<VppSite>()
+                .eq(VppSite::getAccountNo, accountNo)
+                .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .last("LIMIT 1"));
+        if (site == null || site.getId() == null) {
+            return BigDecimal.ZERO;
+        }
+
+        List<VppResourcePoint> resourcePoints = resourcePointMapper.selectList(new LambdaQueryWrapper<VppResourcePoint>()
+                .eq(VppResourcePoint::getSiteId, site.getId())
+                .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        if (CollectionUtils.isEmpty(resourcePoints)) {
+            return BigDecimal.ZERO;
+        }
+
+        List<Long> resourceIds = resourcePoints.stream()
+                .map(VppResourcePoint::getId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+        if (resourceIds.isEmpty()) {
+            return BigDecimal.ZERO;
+        }
+
+        List<VppDrParticipation> participations = participationMapper.selectList(new LambdaQueryWrapper<VppDrParticipation>()
+                .eq(VppDrParticipation::getEventId, drEventId)
+                .in(VppDrParticipation::getResourceId, resourceIds)
+                .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        BigDecimal sum = BigDecimal.ZERO;
+        for (VppDrParticipation participation : participations) {
+            if (participation.getClearedCapacityKw() != null) {
+                sum = sum.add(participation.getClearedCapacityKw());
+            }
+        }
+        return sum;
+    }
+
+    private static boolean capacityEquals(BigDecimal reportedKw, BigDecimal clearedSumKw) {
+        BigDecimal reported = normalizeCapacity(reportedKw);
+        BigDecimal cleared = normalizeCapacity(clearedSumKw);
+        if (reported == null || cleared == null) {
+            return reported == null && cleared == null;
+        }
+        return reported.compareTo(cleared) == 0;
+    }
+
+    private static BigDecimal normalizeCapacity(BigDecimal value) {
+        if (value == null) {
+            return null;
+        }
+        return value.abs().setScale(CAPACITY_SCALE, RoundingMode.HALF_UP);
+    }
+}

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

@@ -234,7 +234,9 @@ public class VppUnIntegrationServiceImpl implements VppUnIntegrationService {
             if (load == null || load.compareTo(BigDecimal.ZERO) <= 0) {
                 continue;
             }
-            list.add(VppUnMessageBuilder.buildOptListItem(accountNo, load, "否"));
+            list.add(clearing
+                    ? VppUnMessageBuilder.buildCqListItem(accountNo, load)
+                    : VppUnMessageBuilder.buildOptListItem(accountNo, load, "否"));
         }
         return list;
     }

+ 35 - 27
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppUnEventParser.java

@@ -1,7 +1,6 @@
 package com.usky.vpp.util;
 
 import com.usky.vpp.constant.VppDrEventStatus;
-import com.usky.vpp.enums.VppUnEventPhase;
 import org.springframework.util.StringUtils;
 
 import java.math.BigDecimal;
@@ -56,6 +55,29 @@ public final class VppUnEventParser {
         return target instanceof Map ? (Map<String, Object>) target : Collections.emptyMap();
     }
 
+    @SuppressWarnings("unchecked")
+    public static List<String> getTargetResourceIds(Map<String, Object> event) {
+        Map<String, Object> target = getTarget(event);
+        Object resourceIds = target.get("resourceID");
+        if (resourceIds == null) {
+            resourceIds = target.get("resourceId");
+        }
+        if (!(resourceIds instanceof List)) {
+            return Collections.emptyList();
+        }
+        List<String> result = new ArrayList<>();
+        for (Object item : (List<?>) resourceIds) {
+            if (item == null) {
+                continue;
+            }
+            String value = String.valueOf(item).trim();
+            if (StringUtils.hasText(value)) {
+                result.add(value);
+            }
+        }
+        return result;
+    }
+
     @SuppressWarnings("unchecked")
     public static List<Map<String, Object>> getTargetResources(Map<String, Object> event) {
         Map<String, Object> target = getTarget(event);
@@ -72,31 +94,6 @@ public final class VppUnEventParser {
         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");
@@ -278,6 +275,17 @@ public final class VppUnEventParser {
     }
 
     @SuppressWarnings("unchecked")
+    /**
+     * 解析 target.resources 单条节点上报容量(values 最大值或 load 字段)
+     */
+    public static BigDecimal resolveResourceReportedKw(Map<String, Object> resource) {
+        BigDecimal load = maxAbsFromResourceValues(resource);
+        if (load == null) {
+            load = VppUnPayloadHelper.getDecimal(resource, "load");
+        }
+        return load;
+    }
+
     private static BigDecimal maxAbsFromResourceValues(Map<String, Object> resource) {
         Object values = resource.get("values");
         if (!(values instanceof List)) {
@@ -295,7 +303,7 @@ public final class VppUnEventParser {
         return max;
     }
 
-    private static boolean hasResourceValues(List<Map<String, Object>> resources) {
+    public static boolean hasResourceValues(List<Map<String, Object>> resources) {
         for (Map<String, Object> resource : resources) {
             if (maxAbsFromResourceValues(resource) != null) {
                 return true;

+ 18 - 5
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppUnMessageBuilder.java

@@ -65,17 +65,23 @@ public final class VppUnMessageBuilder {
         return req;
     }
 
+    private static final int CREATE_CQ_REQUEST_CODE = 6206;
+
     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);
+        Map<String, Object> req = new LinkedHashMap<>();
         req.put("optType", optType != null ? optType : "optIn");
+        req.put("eventID", eventId);
+        req.put("code", CREATE_CQ_REQUEST_CODE);
+        req.put("requestID", UUID.randomUUID().toString());
+        req.put("root", "CreateCqRequest");
         req.put("createdDateTime", CREATED_DATE_TIME.format(LocalDateTime.now()));
         req.put("priceDownCoeff", properties.getPriceDownCoeff());
-        if (list != null) {
-            req.put("list", list);
-        }
+        req.put("description", "");
+        req.put("dnID", properties.getDnId());
+        req.put("list", list != null ? list : new ArrayList<>());
+        req.put("version", 1);
         return req;
     }
 
@@ -286,6 +292,13 @@ public final class VppUnMessageBuilder {
         return StringUtils.hasText(id) ? id : fallback;
     }
 
+    public static Map<String, Object> buildCqListItem(String account, BigDecimal loadKw) {
+        Map<String, Object> item = new LinkedHashMap<>();
+        item.put("load", loadKw != null ? loadKw.stripTrailingZeros().toPlainString() : "0");
+        item.put("account", account);
+        return item;
+    }
+
     public static Map<String, Object> buildOptListItem(String account, BigDecimal loadKw, String needZk) {
         Map<String, Object> item = new LinkedHashMap<>();
         item.put("account", account);

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

@@ -6,6 +6,7 @@
   "root": "CreateCqRequest",
   "createdDateTime": "2025-11-27 13:34:00",
   "priceDownCoeff": "0.8",
+  "description": "",
   "dnID": "10010103000052",
   "list": [
     { "load": "10000", "account": "3101330499683" },