Sfoglia il codice sorgente

新增或优化站点、站点运行参数和资源点三个表结构,同时开发资源管理-数据看板、资源管理-资源概览、资源管理-站点管理和资源管理-资源点管理四个界面的接口

james 19 ore fa
parent
commit
4fd6f2e7f7
39 ha cambiato i file con 2212 aggiunte e 126 eliminazioni
  1. 95 31
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ResourceController.java
  2. 134 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/SiteController.java
  3. 27 4
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/VppDeviceController.java
  4. 5 2
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppDevice.java
  5. 17 22
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppResourcePoint.java
  6. 63 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppSite.java
  7. 6 5
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppSiteConfig.java
  8. 0 10
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/mapper/VppResourcePointConfigMapper.java
  9. 10 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/mapper/VppSiteConfigMapper.java
  10. 10 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/mapper/VppSiteMapper.java
  11. 16 4
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDeviceService.java
  12. 23 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppResourceBoardService.java
  13. 28 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppResourceOverviewService.java
  14. 31 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppResourcePointService.java
  15. 46 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppSiteService.java
  16. 183 3
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDeviceServiceImpl.java
  17. 24 12
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDrServiceImpl.java
  18. 217 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResourceBoardServiceImpl.java
  19. 124 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResourceOverviewServiceImpl.java
  20. 259 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResourcePointServiceImpl.java
  21. 320 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppSiteServiceImpl.java
  22. 14 3
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppUnDrSyncServiceImpl.java
  23. 24 18
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppUnReportServiceImpl.java
  24. 19 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/AlarmLevelStatVO.java
  25. 25 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/DeviceRequest.java
  26. 19 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/EnergyTodayVO.java
  27. 29 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResourceBoardDetailVO.java
  28. 39 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResourceBoardItemVO.java
  29. 32 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResourcePointListVO.java
  30. 25 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResourcePointRequest.java
  31. 22 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResourceTypeStatVO.java
  32. 31 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/SiteConfigDetailVO.java
  33. 22 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/SiteConfigRequest.java
  34. 34 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/SiteListVO.java
  35. 28 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/SiteRequest.java
  36. 12 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppAuditHelper.java
  37. 127 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppResourceMockHelper.java
  38. 48 0
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppSiteResourceHelper.java
  39. 24 12
      service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppUnMessageBuilder.java

+ 95 - 31
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ResourceController.java

@@ -1,55 +1,119 @@
 package com.usky.vpp.controller.web;
 
 import com.usky.common.core.bean.ApiResult;
-import com.usky.vpp.service.VppResourceService;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.service.VppResourceBoardService;
+import com.usky.vpp.service.VppResourceOverviewService;
+import com.usky.vpp.service.VppResourcePointService;
+import com.usky.vpp.service.vo.AlarmLevelStatVO;
+import com.usky.vpp.service.vo.EnergyTodayVO;
+import com.usky.vpp.service.vo.ResourceBoardDetailVO;
+import com.usky.vpp.service.vo.ResourceBoardItemVO;
+import com.usky.vpp.service.vo.ResourcePointListVO;
+import com.usky.vpp.service.vo.ResourcePointRequest;
+import com.usky.vpp.service.vo.ResourceTypeStatVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.List;
+import java.util.Map;
+
 /**
- * 虚拟电厂 - Resource 接口
- * 网关前缀: /prod-api/service-vpp
+ * 资源管理接口
+ * <p>网关前缀: /prod-api/service-vpp/resource</p>
  */
 @RestController
 @RequestMapping("/resource")
 public class ResourceController {
 
     @Autowired
-    private VppResourceService vppResourceService;
+    private VppResourcePointService vppResourcePointService;
+    @Autowired
+    private VppResourceBoardService vppResourceBoardService;
+    @Autowired
+    private VppResourceOverviewService vppResourceOverviewService;
 
-    @GetMapping
-    public ApiResult<Object> pageResource() {
-        return ApiResult.success(null);
+    /**
+     * 资源看板 - 实时运行数据列表(模拟数据)
+     * <p>展示所有资源点的实时功率、电压、电流、储能SOC、充电桩充电功率;
+     * 支持按 resourceType(资源类型)、province/city/district(区域)、status(normal/abnormal)筛选;
+     * abnormal=true 的资源需前端红色高亮。</p>
+     *
+     * @param params resourceType, province, city, district, status, pageNum, pageSize
+     */
+    @GetMapping("/board")
+    public ApiResult<CommonPage<ResourceBoardItemVO>> resourceBoard(@RequestParam(required = false) Map<String, Object> params) {
+        return ApiResult.success(vppResourceBoardService.pageBoard(params));
     }
-    @GetMapping(value = "/board")
-    public ApiResult<Object> resourceBoard(@RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+
+    /**
+     * 资源看板 - 资源点运行详情(异常资源点击查看)
+     */
+    @GetMapping("/board/{resourceId}")
+    public ApiResult<ResourceBoardDetailVO> resourceBoardDetail(@PathVariable("resourceId") Long resourceId) {
+        return ApiResult.success(vppResourceBoardService.getBoardDetail(resourceId));
     }
-    @PostMapping
-    public ApiResult<Object> createResource() {
-        return ApiResult.success(null);
+
+    /**
+     * 资源概览 - 按资源类型统计
+     * <p>统计各类型资源总数、总装机容量、总可调容量(is_control=1)。</p>
+     */
+    @GetMapping("/overview/type-stats")
+    public ApiResult<List<ResourceTypeStatVO>> resourceOverviewTypeStats() {
+        return ApiResult.success(vppResourceOverviewService.getTypeStats());
     }
-    @GetMapping(value = "/{id}")
-    public ApiResult<Object> getResource(@PathVariable("id") Long id, @RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+
+    /**
+     * 资源概览 - 今日电能概况
+     * <p>总发电量、总用电量、绿电消纳比例(模拟数据)。</p>
+     */
+    @GetMapping("/overview/energy-today")
+    public ApiResult<EnergyTodayVO> resourceOverviewEnergyToday() {
+        return ApiResult.success(vppResourceOverviewService.getEnergyToday());
     }
-    @PutMapping(value = "/{id}")
-    public ApiResult<Void> updateResource(@PathVariable("id") Long id, @RequestBody(required = false) Object body) {
-        return ApiResult.success();
+
+    /**
+     * 资源概览 - 实时告警统计
+     * <p>按紧急/重要/一般统计告警数量,含跳转告警列表路径(模拟数据)。</p>
+     */
+    @GetMapping("/overview/alarm-stats")
+    public ApiResult<List<AlarmLevelStatVO>> resourceOverviewAlarmStats() {
+        return ApiResult.success(vppResourceOverviewService.getAlarmStats());
     }
-    @DeleteMapping(value = "/{id}")
-    public ApiResult<Void> deleteResource(@PathVariable("id") Long id) {
-        return ApiResult.success();
+
+    /**
+     * 资源点管理 - 分页列表
+     * <p>支持按 siteName(站点名称)、deviceName(设备名称)、resourceType、siteId 搜索。</p>
+     */
+    @GetMapping
+    public ApiResult<CommonPage<ResourcePointListVO>> pageResource(@RequestParam(required = false) Map<String, Object> params) {
+        return ApiResult.success(vppResourcePointService.pageResourcePointList(params));
     }
-    @GetMapping(value = "/overview")
-    public ApiResult<Object> overview(@RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+
+    /**
+     * 资源点管理 - 详情
+     */
+    @GetMapping("/{id}")
+    public ApiResult<VppResourcePoint> getResource(@PathVariable("id") Long id) {
+        return ApiResult.success(vppResourcePointService.getResourcePoint(id));
     }
-    @GetMapping(value = "/{resourceId}/device")
-    public ApiResult<Object> listDevice(@PathVariable("resourceId") Long resourceId, @RequestParam(required = false) java.util.Map<String, Object> params) {
-        return ApiResult.success(null);
+
+    /**
+     * 资源点管理 - 编辑
+     */
+    @PutMapping("/{id}")
+    public ApiResult<Void> updateResource(@PathVariable("id") Long id, @RequestBody ResourcePointRequest body) {
+        vppResourcePointService.updateResourcePoint(id, body);
+        return ApiResult.success();
     }
-    @PostMapping(value = "/{resourceId}/device")
-    public ApiResult<Object> addDevice(@PathVariable("resourceId") Long resourceId, @RequestBody(required = false) Object body) {
-        return ApiResult.success(null);
+
+    /**
+     * 资源点管理 - 删除(软删除)
+     */
+    @DeleteMapping("/{id}")
+    public ApiResult<Void> deleteResource(@PathVariable("id") Long id) {
+        vppResourcePointService.deleteResourcePoint(id);
+        return ApiResult.success();
     }
 }

+ 134 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/SiteController.java

@@ -0,0 +1,134 @@
+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.VppDevice;
+import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.domain.VppSite;
+import com.usky.vpp.service.VppDeviceService;
+import com.usky.vpp.service.VppResourcePointService;
+import com.usky.vpp.service.VppSiteService;
+import com.usky.vpp.service.vo.DeviceRequest;
+import com.usky.vpp.service.vo.ResourcePointRequest;
+import com.usky.vpp.service.vo.SiteConfigDetailVO;
+import com.usky.vpp.service.vo.SiteConfigRequest;
+import com.usky.vpp.service.vo.SiteListVO;
+import com.usky.vpp.service.vo.SiteRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * 站点管理接口
+ * <p>网关前缀: /prod-api/service-vpp/site</p>
+ */
+@RestController
+@RequestMapping("/site")
+public class SiteController {
+
+    @Autowired
+    private VppSiteService vppSiteService;
+    @Autowired
+    private VppResourcePointService vppResourcePointService;
+    @Autowired
+    private VppDeviceService vppDeviceService;
+
+    /**
+     * 站点管理 - 分页列表
+     * <p>支持按 siteName(站点名称)、province/city/district(区域)、resourceType(站点下资源类型)筛选。</p>
+     */
+    @GetMapping
+    public ApiResult<CommonPage<SiteListVO>> pageSite(@RequestParam(required = false) Map<String, Object> params) {
+        return ApiResult.success(vppSiteService.pageSiteList(params));
+    }
+
+    /**
+     * 站点管理 - 新增
+     */
+    @PostMapping
+    public ApiResult<VppSite> createSite(@RequestBody SiteRequest body) {
+        return ApiResult.success(vppSiteService.createSite(body));
+    }
+
+    /**
+     * 站点管理 - 详情
+     */
+    @GetMapping("/{id}")
+    public ApiResult<VppSite> getSite(@PathVariable("id") Long id) {
+        return ApiResult.success(vppSiteService.getSite(id));
+    }
+
+    /**
+     * 站点管理 - 编辑
+     */
+    @PutMapping("/{id}")
+    public ApiResult<Void> updateSite(@PathVariable("id") Long id, @RequestBody SiteRequest body) {
+        vppSiteService.updateSite(id, body);
+        return ApiResult.success();
+    }
+
+    /**
+     * 站点管理 - 删除(软删除)
+     */
+    @DeleteMapping("/{id}")
+    public ApiResult<Void> deleteSite(@PathVariable("id") Long id) {
+        vppSiteService.deleteSite(id);
+        return ApiResult.success();
+    }
+
+    /**
+     * 站点参数配置 - 查询
+     * <p>包含采集频率、功率/SOC告警阈值、离线超时、响应优先级等。</p>
+     */
+    @GetMapping("/{siteId}/config")
+    public ApiResult<SiteConfigDetailVO> getSiteConfig(@PathVariable("siteId") Long siteId) {
+        return ApiResult.success(vppSiteService.getSiteConfigDetail(siteId));
+    }
+
+    /**
+     * 站点参数配置 - 保存
+     * <p>配置采集频率、告警阈值;responsePriority 写入站点响应优先级。</p>
+     */
+    @PutMapping("/{siteId}/config")
+    public ApiResult<SiteConfigDetailVO> saveSiteConfig(@PathVariable("siteId") Long siteId,
+                                                        @RequestBody SiteConfigRequest body) {
+        return ApiResult.success(vppSiteService.saveSiteConfigDetail(siteId, body));
+    }
+
+    /**
+     * 站点下设备列表
+     */
+    @GetMapping("/{siteId}/device")
+    public ApiResult<CommonPage<VppDevice>> listSiteDevice(@PathVariable("siteId") Long siteId,
+                                                           @RequestParam(required = false) Map<String, Object> params) {
+        return ApiResult.success(vppDeviceService.listBySiteId(siteId, params));
+    }
+
+    /**
+     * 站点下新增设备
+     */
+    @PostMapping("/{siteId}/device")
+    public ApiResult<VppDevice> addSiteDevice(@PathVariable("siteId") Long siteId,
+                                              @RequestBody DeviceRequest body) {
+        return ApiResult.success(vppDeviceService.createDevice(siteId, body));
+    }
+
+    /**
+     * 站点下资源点列表
+     */
+    @GetMapping("/{siteId}/resource")
+    public ApiResult<CommonPage<VppResourcePoint>> listSiteResource(@PathVariable("siteId") Long siteId,
+                                                                    @RequestParam(required = false) Map<String, Object> params) {
+        return ApiResult.success(vppResourcePointService.listBySiteId(siteId, params));
+    }
+
+    /**
+     * 站点下新增资源点
+     */
+    @PostMapping("/{siteId}/resource")
+    public ApiResult<VppResourcePoint> addSiteResource(@PathVariable("siteId") Long siteId,
+                                                       @RequestBody ResourcePointRequest body) {
+        return ApiResult.success(vppResourcePointService.createResourcePoint(siteId, body));
+    }
+}

+ 27 - 4
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/VppDeviceController.java

@@ -1,12 +1,17 @@
 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.VppDevice;
 import com.usky.vpp.service.VppDeviceService;
+import com.usky.vpp.service.vo.DeviceRequest;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.Map;
+
 /**
- * 虚拟电厂 - VppDevice 接口
+ * 虚拟电厂 - 设备接口
  * 网关前缀: /prod-api/service-vpp
  */
 @RestController
@@ -16,12 +21,30 @@ public class VppDeviceController {
     @Autowired
     private VppDeviceService vppDeviceService;
 
-    @PutMapping(value = "/{id}")
-    public ApiResult<Void> updateDevice(@PathVariable("id") Long id, @RequestBody(required = false) Object body) {
+    @GetMapping
+    public ApiResult<CommonPage<VppDevice>> pageDevice(@RequestParam(required = false) Map<String, Object> params) {
+        return ApiResult.success(vppDeviceService.pageDevice(params));
+    }
+
+    @GetMapping("/uuid/{deviceUuid}")
+    public ApiResult<VppDevice> getByDeviceUuid(@PathVariable("deviceUuid") String deviceUuid) {
+        return ApiResult.success(vppDeviceService.getByDeviceUuid(deviceUuid));
+    }
+
+    @GetMapping("/{id}")
+    public ApiResult<VppDevice> getDevice(@PathVariable("id") Long id) {
+        return ApiResult.success(vppDeviceService.getDevice(id));
+    }
+
+    @PutMapping("/{id}")
+    public ApiResult<Void> updateDevice(@PathVariable("id") Long id, @RequestBody DeviceRequest body) {
+        vppDeviceService.updateDevice(id, body);
         return ApiResult.success();
     }
-    @DeleteMapping(value = "/{id}")
+
+    @DeleteMapping("/{id}")
     public ApiResult<Void> deleteDevice(@PathVariable("id") Long id) {
+        vppDeviceService.deleteDevice(id);
         return ApiResult.success();
     }
 }

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

@@ -24,10 +24,13 @@ public class VppDevice implements Serializable {
     private Long id;
     @TableField("device_code")
     private String deviceCode;
+    @TableField("device_uuid")
+    private String deviceUuid;
     @TableField("device_name")
     private String deviceName;
-    @TableField("resource_id")
-    private Long resourceId;
+    /** 所属站点ID,关联 vpp_site.id */
+    @TableField("site_id")
+    private Long siteId;
     @TableField("device_type")
     private String deviceType;
     private String manufacturer;

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

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
+
 import java.io.Serializable;
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
@@ -26,32 +27,26 @@ public class VppResourcePoint implements Serializable {
     private String resourceCode;
     @TableField("resource_name")
     private String resourceName;
-    @TableField("customer_id")
-    private Long customerId;
+    @TableField("site_id")
+    private Long siteId;
+    @TableField("device_id")
+    private Long deviceId;
     @TableField("resource_type")
     private String resourceType;
     @TableField("capacity_kw")
     private BigDecimal capacityKw;
-    @TableField("adjustable_kw")
-    private BigDecimal adjustableKw;
-    private String province;
-    private String city;
-    private String district;
-    private String address;
-    private BigDecimal longitude;
-    private BigDecimal latitude;
-    @TableField("owner_name")
-    private String ownerName;
-    @TableField("contact_name")
-    private String contactName;
-    @TableField("contact_phone")
-    private String contactPhone;
-    @TableField("run_status")
-    private Integer runStatus;
-    @TableField("response_priority")
-    private Integer responsePriority;
-    @TableField("un_resource_id")
-    private String unResourceId;
+    @TableField("is_control")
+    private Integer isControl;
+    @TableField("is_support_peak")
+    private Integer isSupportPeak;
+    @TableField("is_support_fm")
+    private Integer isSupportFm;
+    @TableField("response_min")
+    private Integer responseMin;
+    @TableField("max_up_kw")
+    private BigDecimal maxUpKw;
+    @TableField("min_down_kw")
+    private BigDecimal minDownKw;
     private String remark;
     @TableField("tenant_id")
     private Integer tenantId;

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

@@ -0,0 +1,63 @@
+package com.usky.vpp.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * vpp_site
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("vpp_site")
+public class VppSite implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    private Long id;
+    @TableField("site_code")
+    private String siteCode;
+    @TableField("site_name")
+    private String siteName;
+    @TableField("customer_id")
+    private Long customerId;
+    private String province;
+    private String city;
+    private String district;
+    private String address;
+    private BigDecimal longitude;
+    private BigDecimal latitude;
+    @TableField("owner_name")
+    private String ownerName;
+    @TableField("contact_name")
+    private String contactName;
+    @TableField("contact_phone")
+    private String contactPhone;
+    @TableField("response_priority")
+    private Integer responsePriority;
+    @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;
+    @TableField("updated_by")
+    private Long updatedBy;
+    @TableField("delete_flag")
+    private Integer deleteFlag;
+    @TableField("deleted_at")
+    private LocalDateTime deletedAt;
+}

+ 6 - 5
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppResourcePointConfig.java → service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppSiteConfig.java

@@ -6,24 +6,25 @@ import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
+
 import java.io.Serializable;
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
 /**
- * vpp_resource_point_config
+ * vpp_site_config
  */
 @Data
 @EqualsAndHashCode(callSuper = false)
-@TableName("vpp_resource_point_config")
-public class VppResourcePointConfig implements Serializable {
+@TableName("vpp_site_config")
+public class VppSiteConfig implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
     @TableId(value = "id", type = IdType.ASSIGN_ID)
     private Long id;
-    @TableField("resource_id")
-    private Long resourceId;
+    @TableField("site_id")
+    private Long siteId;
     @TableField("collect_interval_sec")
     private Integer collectIntervalSec;
     @TableField("power_upper_limit")

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

@@ -1,10 +0,0 @@
-package com.usky.vpp.mapper;
-
-import com.usky.common.mybatis.core.CrudMapper;
-import com.usky.vpp.domain.VppResourcePointConfig;
-
-/**
- * vpp_resource_point_config Mapper
- */
-public interface VppResourcePointConfigMapper extends CrudMapper<VppResourcePointConfig> {
-}

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

@@ -0,0 +1,10 @@
+package com.usky.vpp.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.vpp.domain.VppSiteConfig;
+
+/**
+ * vpp_site_config Mapper
+ */
+public interface VppSiteConfigMapper extends CrudMapper<VppSiteConfig> {
+}

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

@@ -0,0 +1,10 @@
+package com.usky.vpp.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.vpp.domain.VppSite;
+
+/**
+ * vpp_site Mapper
+ */
+public interface VppSiteMapper extends CrudMapper<VppSite> {
+}

+ 16 - 4
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppDeviceService.java

@@ -1,15 +1,27 @@
 package com.usky.vpp.service;
 
 import com.usky.common.core.bean.CommonPage;
+import com.usky.vpp.domain.VppDevice;
+import com.usky.vpp.service.vo.DeviceRequest;
 
 import java.util.Map;
 
 /**
- * VppDeviceService 业务接口
+ * 设备管理业务接口
  */
 public interface VppDeviceService {
 
-    default Object stub(String action, Map<String, Object> params) {
-        return null;
-    }
+    CommonPage<VppDevice> pageDevice(Map<String, Object> params);
+
+    CommonPage<VppDevice> listBySiteId(Long siteId, Map<String, Object> params);
+
+    VppDevice getDevice(Long id);
+
+    VppDevice getByDeviceUuid(String deviceUuid);
+
+    VppDevice createDevice(Long siteId, DeviceRequest request);
+
+    void updateDevice(Long id, DeviceRequest request);
+
+    void deleteDevice(Long id);
 }

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

@@ -0,0 +1,23 @@
+package com.usky.vpp.service;
+
+import com.usky.common.core.bean.CommonPage;
+import com.usky.vpp.service.vo.ResourceBoardDetailVO;
+import com.usky.vpp.service.vo.ResourceBoardItemVO;
+
+import java.util.Map;
+
+/**
+ * 资源看板业务接口
+ */
+public interface VppResourceBoardService {
+
+    /**
+     * 资源看板列表(模拟实时运行数据)
+     */
+    CommonPage<ResourceBoardItemVO> pageBoard(Map<String, Object> params);
+
+    /**
+     * 资源看板详情(异常资源点击查看)
+     */
+    ResourceBoardDetailVO getBoardDetail(Long resourceId);
+}

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

@@ -0,0 +1,28 @@
+package com.usky.vpp.service;
+
+import com.usky.vpp.service.vo.AlarmLevelStatVO;
+import com.usky.vpp.service.vo.EnergyTodayVO;
+import com.usky.vpp.service.vo.ResourceTypeStatVO;
+
+import java.util.List;
+
+/**
+ * 资源概览业务接口
+ */
+public interface VppResourceOverviewService {
+
+    /**
+     * 按资源类型统计总数、总装机容量、总可调容量(is_control=1)
+     */
+    List<ResourceTypeStatVO> getTypeStats();
+
+    /**
+     * 今日电能概况(总发电量、总用电量、绿电消纳比例,模拟数据)
+     */
+    EnergyTodayVO getEnergyToday();
+
+    /**
+     * 实时告警统计(紧急/重要/一般,含跳转路径,模拟数据)
+     */
+    List<AlarmLevelStatVO> getAlarmStats();
+}

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

@@ -0,0 +1,31 @@
+package com.usky.vpp.service;
+
+import com.usky.common.core.bean.CommonPage;
+import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.service.vo.ResourcePointListVO;
+import com.usky.vpp.service.vo.ResourcePointRequest;
+
+import java.util.Map;
+
+/**
+ * 资源点管理业务接口
+ */
+public interface VppResourcePointService {
+
+    CommonPage<VppResourcePoint> pageResourcePoint(Map<String, Object> params);
+
+    /**
+     * 资源点列表(含站点名称、设备名称,支持按站点/设备名称搜索)
+     */
+    CommonPage<ResourcePointListVO> pageResourcePointList(Map<String, Object> params);
+
+    CommonPage<VppResourcePoint> listBySiteId(Long siteId, Map<String, Object> params);
+
+    VppResourcePoint getResourcePoint(Long id);
+
+    VppResourcePoint createResourcePoint(Long siteId, ResourcePointRequest request);
+
+    void updateResourcePoint(Long id, ResourcePointRequest request);
+
+    void deleteResourcePoint(Long id);
+}

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

@@ -0,0 +1,46 @@
+package com.usky.vpp.service;
+
+import com.usky.common.core.bean.CommonPage;
+import com.usky.vpp.domain.VppSite;
+import com.usky.vpp.domain.VppSiteConfig;
+import com.usky.vpp.service.vo.SiteConfigDetailVO;
+import com.usky.vpp.service.vo.SiteConfigRequest;
+import com.usky.vpp.service.vo.SiteListVO;
+import com.usky.vpp.service.vo.SiteRequest;
+
+import java.util.Map;
+
+/**
+ * 站点管理业务接口
+ */
+public interface VppSiteService {
+
+    CommonPage<VppSite> pageSite(Map<String, Object> params);
+
+    /**
+     * 站点列表(含资源类型、资源数量,支持按区域/资源类型筛选)
+     */
+    CommonPage<SiteListVO> pageSiteList(Map<String, Object> params);
+
+    VppSite getSite(Long id);
+
+    VppSite createSite(SiteRequest request);
+
+    void updateSite(Long id, SiteRequest request);
+
+    void deleteSite(Long id);
+
+    VppSiteConfig getSiteConfig(Long siteId);
+
+    /**
+     * 站点参数配置详情(含响应优先级)
+     */
+    SiteConfigDetailVO getSiteConfigDetail(Long siteId);
+
+    VppSiteConfig saveSiteConfig(Long siteId, SiteConfigRequest request);
+
+    /**
+     * 保存站点参数配置并返回详情
+     */
+    SiteConfigDetailVO saveSiteConfigDetail(Long siteId, SiteConfigRequest request);
+}

+ 183 - 3
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppDeviceServiceImpl.java

@@ -1,11 +1,191 @@
 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.VppDevice;
+import com.usky.vpp.mapper.VppDeviceMapper;
 import com.usky.vpp.service.VppDeviceService;
+import com.usky.vpp.service.VppSiteService;
+import com.usky.vpp.service.vo.DeviceRequest;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppPageHelper;
+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.util.Map;
 
-/**
- * VppDeviceService 默认实现(待按业务完善)
- */
 @Service
 public class VppDeviceServiceImpl implements VppDeviceService {
+
+    private static final int COMM_OFFLINE = 0;
+    private static final int RUN_STOPPED = 0;
+
+    @Autowired
+    private VppDeviceMapper deviceMapper;
+    @Autowired
+    private VppSiteService siteService;
+
+    @Override
+    public CommonPage<VppDevice> pageDevice(Map<String, Object> params) {
+        Page<VppDevice> page = VppPageHelper.of(params);
+        return toCommonPage(deviceMapper.selectPage(page, buildQueryWrapper(params)));
+    }
+
+    @Override
+    public CommonPage<VppDevice> listBySiteId(Long siteId, Map<String, Object> params) {
+        siteService.getSite(siteId);
+        Page<VppDevice> page = VppPageHelper.of(params);
+        LambdaQueryWrapper<VppDevice> wrapper = buildQueryWrapper(params)
+                .eq(VppDevice::getSiteId, siteId);
+        return toCommonPage(deviceMapper.selectPage(page, wrapper));
+    }
+
+    @Override
+    public VppDevice getDevice(Long id) {
+        VppDevice device = deviceMapper.selectById(id);
+        if (device == null || VppAuditHelper.isDeleted(device.getDeleteFlag())) {
+            throw new BusinessException("设备不存在");
+        }
+        return device;
+    }
+
+    @Override
+    public VppDevice getByDeviceUuid(String deviceUuid) {
+        if (!StringUtils.hasText(deviceUuid)) {
+            throw new BusinessException("设备uuid不能为空");
+        }
+        VppDevice device = deviceMapper.selectOne(new LambdaQueryWrapper<VppDevice>()
+                .eq(VppDevice::getDeviceUuid, deviceUuid)
+                .eq(VppDevice::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .last("LIMIT 1"));
+        if (device == null) {
+            throw new BusinessException("设备不存在");
+        }
+        return device;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public VppDevice createDevice(Long siteId, DeviceRequest request) {
+        siteService.getSite(siteId);
+        validateDeviceRequest(request, null);
+        VppDevice device = new VppDevice();
+        device.setSiteId(siteId);
+        applyRequest(device, request);
+        if (device.getCommStatus() == null) {
+            device.setCommStatus(COMM_OFFLINE);
+        }
+        if (device.getRunStatus() == null) {
+            device.setRunStatus(RUN_STOPPED);
+        }
+        VppAuditHelper.fillCreate(device);
+        deviceMapper.insert(device);
+        return device;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateDevice(Long id, DeviceRequest request) {
+        VppDevice existing = getDevice(id);
+        validateDeviceRequest(request, id);
+        applyRequest(existing, request);
+        VppAuditHelper.fillUpdate(existing);
+        deviceMapper.updateById(existing);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteDevice(Long id) {
+        VppDevice device = getDevice(id);
+        VppAuditHelper.fillSoftDelete(device);
+        deviceMapper.updateById(device);
+    }
+
+    private LambdaQueryWrapper<VppDevice> buildQueryWrapper(Map<String, Object> params) {
+        LambdaQueryWrapper<VppDevice> wrapper = new LambdaQueryWrapper<VppDevice>()
+                .eq(VppDevice::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .orderByDesc(VppDevice::getCreateTime);
+        if (params == null) {
+            return wrapper;
+        }
+        if (params.get("siteId") != null) {
+            wrapper.eq(VppDevice::getSiteId, Long.parseLong(params.get("siteId").toString()));
+        }
+        if (params.get("deviceCode") != null) {
+            wrapper.eq(VppDevice::getDeviceCode, params.get("deviceCode").toString());
+        }
+        if (params.get("deviceUuid") != null) {
+            wrapper.eq(VppDevice::getDeviceUuid, params.get("deviceUuid").toString());
+        }
+        if (params.get("deviceName") != null) {
+            wrapper.like(VppDevice::getDeviceName, params.get("deviceName").toString());
+        }
+        if (params.get("deviceType") != null) {
+            wrapper.eq(VppDevice::getDeviceType, params.get("deviceType").toString());
+        }
+        if (params.get("commStatus") != null) {
+            wrapper.eq(VppDevice::getCommStatus, Integer.parseInt(params.get("commStatus").toString()));
+        }
+        if (params.get("runStatus") != null) {
+            wrapper.eq(VppDevice::getRunStatus, Integer.parseInt(params.get("runStatus").toString()));
+        }
+        return wrapper;
+    }
+
+    private void validateDeviceRequest(DeviceRequest request, Long excludeId) {
+        if (request == null || !StringUtils.hasText(request.getDeviceCode())) {
+            throw new BusinessException("设备编号不能为空");
+        }
+        if (!StringUtils.hasText(request.getDeviceUuid())) {
+            throw new BusinessException("设备uuid不能为空");
+        }
+        if (!StringUtils.hasText(request.getDeviceName())) {
+            throw new BusinessException("设备名称不能为空");
+        }
+        if (!StringUtils.hasText(request.getDeviceType())) {
+            throw new BusinessException("设备类型不能为空");
+        }
+        ensureUniqueField(VppDevice::getDeviceCode, request.getDeviceCode(), excludeId, "设备编号已存在");
+        ensureUniqueField(VppDevice::getDeviceUuid, request.getDeviceUuid(), excludeId, "设备uuid已存在");
+    }
+
+    private void ensureUniqueField(com.baomidou.mybatisplus.core.toolkit.support.SFunction<VppDevice, ?> field,
+                                   String value, Long excludeId, String message) {
+        LambdaQueryWrapper<VppDevice> wrapper = new LambdaQueryWrapper<VppDevice>()
+                .eq(field, value)
+                .eq(VppDevice::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+        if (excludeId != null) {
+            wrapper.ne(VppDevice::getId, excludeId);
+        }
+        if (deviceMapper.selectCount(wrapper) > 0) {
+            throw new BusinessException(message);
+        }
+    }
+
+    private void applyRequest(VppDevice device, DeviceRequest request) {
+        device.setDeviceCode(request.getDeviceCode());
+        device.setDeviceUuid(request.getDeviceUuid());
+        device.setDeviceName(request.getDeviceName());
+        device.setDeviceType(request.getDeviceType());
+        device.setManufacturer(request.getManufacturer());
+        device.setModel(request.getModel());
+        device.setRatedPowerKw(request.getRatedPowerKw());
+        if (request.getCommStatus() != null) {
+            device.setCommStatus(request.getCommStatus());
+        }
+        if (request.getRunStatus() != null) {
+            device.setRunStatus(request.getRunStatus());
+        }
+        device.setFirmwareVersion(request.getFirmwareVersion());
+        device.setGatewayId(request.getGatewayId());
+        device.setRemark(request.getRemark());
+    }
+
+    private CommonPage<VppDevice> toCommonPage(Page<VppDevice> page) {
+        return new CommonPage<>(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize());
+    }
 }

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

@@ -26,6 +26,7 @@ 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 com.usky.vpp.util.VppSiteResourceHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -71,6 +72,8 @@ public class VppDrServiceImpl implements VppDrService {
     @Autowired
     private VppResourcePointMapper resourcePointMapper;
     @Autowired
+    private VppSiteResourceHelper siteResourceHelper;
+    @Autowired
     private VppUnIntegrationService unIntegrationService;
 
     @Override
@@ -117,9 +120,10 @@ public class VppDrServiceImpl implements VppDrService {
 
         LambdaQueryWrapper<VppResourcePoint> resourceWrapper = new LambdaQueryWrapper<VppResourcePoint>()
                 .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
-                .eq(VppResourcePoint::getRunStatus, 1);
+                .eq(VppResourcePoint::getIsControl, 1);
 
         List<VppResourcePoint> resources = resourcePointMapper.selectList(resourceWrapper);
+        Map<Long, com.usky.vpp.domain.VppSite> siteMap = siteResourceHelper.loadSiteMap(resources);
 
         BigDecimal totalUpCapacity = BigDecimal.ZERO;
         BigDecimal totalDownCapacity = BigDecimal.ZERO;
@@ -128,18 +132,20 @@ public class VppDrServiceImpl implements VppDrService {
 
         List<Map<String, Object>> resourceList = new ArrayList<>();
         for (VppResourcePoint resource : resources) {
+            com.usky.vpp.domain.VppSite site = siteMap.get(resource.getSiteId());
             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);
+            item.put("upCapacityKw", resource.getMaxUpKw() != null ? resource.getMaxUpKw() : BigDecimal.ZERO);
+            item.put("downCapacityKw", resource.getMinDownKw() != null ? resource.getMinDownKw() : BigDecimal.ZERO);
+            item.put("responsePriority", site != null ? site.getResponsePriority() : null);
+            item.put("available", resource.getIsControl() != null && resource.getIsControl() == 1);
             resourceList.add(item);
 
-            BigDecimal adj = resource.getAdjustableKw() != null ? resource.getAdjustableKw() : BigDecimal.ZERO;
-            totalUpCapacity = totalUpCapacity.add(adj);
-            totalDownCapacity = totalDownCapacity.add(adj);
+            BigDecimal up = resource.getMaxUpKw() != null ? resource.getMaxUpKw() : BigDecimal.ZERO;
+            BigDecimal down = resource.getMinDownKw() != null ? resource.getMinDownKw() : BigDecimal.ZERO;
+            totalUpCapacity = totalUpCapacity.add(up);
+            totalDownCapacity = totalDownCapacity.add(down);
         }
 
         Map<String, Object> result = new HashMap<>();
@@ -188,6 +194,12 @@ public class VppDrServiceImpl implements VppDrService {
                 throw new BusinessException("必须选择参与资源");
             }
 
+            Map<Long, com.usky.vpp.domain.VppSite> siteMap = siteResourceHelper.loadSiteMap(
+                    request.getResources().stream()
+                            .map(r -> resourcePointMapper.selectById(r.getResourceId()))
+                            .filter(Objects::nonNull)
+                            .collect(Collectors.toList()));
+
             BigDecimal resourceDeclaredTotal = BigDecimal.ZERO;
             for (DrParticipateRequest.DrResourceDeclare resource : request.getResources()) {
                 if (resource.getResourceId() == null) {
@@ -200,10 +212,10 @@ public class VppDrServiceImpl implements VppDrService {
                 if (rp == null || VppAuditHelper.isDeleted(rp.getDeleteFlag())) {
                     throw new BusinessException("资源点不存在");
                 }
-                if (rp.getRunStatus() != 1) {
-                    throw new BusinessException("资源点不在线,无法参与");
+                if (rp.getIsControl() == null || rp.getIsControl() != 1) {
+                    throw new BusinessException("资源点不可控,无法参与");
                 }
-                Long customerId = rp.getCustomerId();
+                Long customerId = siteResourceHelper.resolveCustomerId(rp, siteMap);
                 if (customerId == null) {
                     throw new BusinessException("资源点未关联客户");
                 }
@@ -218,7 +230,7 @@ public class VppDrServiceImpl implements VppDrService {
                 VppResourcePoint rp = resourcePointMapper.selectById(resource.getResourceId());
                 VppDrParticipation participation = new VppDrParticipation();
                 participation.setEventId(eventId);
-                participation.setCustomerId(rp.getCustomerId());
+                participation.setCustomerId(siteResourceHelper.resolveCustomerId(rp, siteMap));
                 participation.setResourceId(resource.getResourceId());
                 participation.setParticipateStatus(PARTICIPATE_STATUS_ACCEPT);
                 participation.setDeclaredCapacityKw(resource.getDeclaredCapacityKw());

+ 217 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResourceBoardServiceImpl.java

@@ -0,0 +1,217 @@
+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.VppDevice;
+import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.domain.VppSite;
+import com.usky.vpp.mapper.VppDeviceMapper;
+import com.usky.vpp.mapper.VppResourcePointMapper;
+import com.usky.vpp.mapper.VppSiteMapper;
+import com.usky.vpp.service.VppResourceBoardService;
+import com.usky.vpp.service.vo.ResourceBoardDetailVO;
+import com.usky.vpp.service.vo.ResourceBoardItemVO;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppPageHelper;
+import com.usky.vpp.util.VppResourceMockHelper;
+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.*;
+import java.util.stream.Collectors;
+
+@Service
+public class VppResourceBoardServiceImpl implements VppResourceBoardService {
+
+    @Autowired
+    private VppResourcePointMapper resourcePointMapper;
+    @Autowired
+    private VppSiteMapper siteMapper;
+    @Autowired
+    private VppDeviceMapper deviceMapper;
+
+    @Override
+    public CommonPage<ResourceBoardItemVO> pageBoard(Map<String, Object> params) {
+        List<VppResourcePoint> all = loadFilteredResources(params);
+        List<ResourceBoardItemVO> items = buildBoardItems(all);
+        items = filterByStatus(items, params);
+
+        long pageNum = parseLong(params, "pageNum", 1L);
+        long pageSize = parseLong(params, "pageSize", 10L);
+        int from = (int) Math.min((pageNum - 1) * pageSize, items.size());
+        int to = (int) Math.min(from + pageSize, items.size());
+        List<ResourceBoardItemVO> pageRecords = from >= items.size()
+                ? Collections.emptyList() : items.subList(from, to);
+        return new CommonPage<>(pageRecords, (long) items.size(), pageNum, pageSize);
+    }
+
+    @Override
+    public ResourceBoardDetailVO getBoardDetail(Long resourceId) {
+        VppResourcePoint resource = resourcePointMapper.selectById(resourceId);
+        if (resource == null || VppAuditHelper.isDeleted(resource.getDeleteFlag())) {
+            throw new BusinessException("资源点不存在");
+        }
+        VppSite site = siteMapper.selectById(resource.getSiteId());
+        VppDevice device = loadDeviceForResource(resource);
+        ResourceBoardDetailVO detail = new ResourceBoardDetailVO();
+        fillBoardItem(detail, resource, site, device);
+        detail.setDeviceId(resource.getDeviceId());
+        detail.setDeviceName(device != null ? device.getDeviceName() : null);
+        detail.setDeviceCode(device != null ? device.getDeviceCode() : null);
+        detail.setIsControl(resource.getIsControl());
+        detail.setIsSupportPeak(resource.getIsSupportPeak());
+        detail.setIsSupportFm(resource.getIsSupportFm());
+        detail.setResponseMin(resource.getResponseMin());
+        if (site != null) {
+            detail.setSiteAddress(site.getAddress());
+            detail.setContactName(site.getContactName());
+            detail.setContactPhone(site.getContactPhone());
+        }
+        if (Boolean.TRUE.equals(detail.getAbnormal())) {
+            detail.setAbnormalReason(VppResourceMockHelper.mockAbnormalReason(resource, device));
+        }
+        detail.setDataTime(LocalDateTime.now());
+        return detail;
+    }
+
+    private List<VppResourcePoint> loadFilteredResources(Map<String, Object> params) {
+        LambdaQueryWrapper<VppResourcePoint> wrapper = new LambdaQueryWrapper<VppResourcePoint>()
+                .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .orderByDesc(VppResourcePoint::getCreateTime);
+        if (params != null) {
+            if (params.get("resourceType") != null) {
+                wrapper.eq(VppResourcePoint::getResourceType, params.get("resourceType").toString());
+            }
+            if (params.get("siteId") != null) {
+                wrapper.eq(VppResourcePoint::getSiteId, Long.parseLong(params.get("siteId").toString()));
+            }
+        }
+        List<VppResourcePoint> resources = resourcePointMapper.selectList(wrapper);
+        if (params == null) {
+            return resources;
+        }
+        Set<Long> siteIds = resolveRegionSiteIds(params);
+        if (siteIds != null) {
+            resources = resources.stream()
+                    .filter(r -> r.getSiteId() != null && siteIds.contains(r.getSiteId()))
+                    .collect(Collectors.toList());
+        }
+        return resources;
+    }
+
+    private Set<Long> resolveRegionSiteIds(Map<String, Object> params) {
+        boolean hasRegion = params.get("province") != null || params.get("city") != null || params.get("district") != null;
+        if (!hasRegion) {
+            return null;
+        }
+        LambdaQueryWrapper<VppSite> siteWrapper = new LambdaQueryWrapper<VppSite>()
+                .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+        if (params.get("province") != null) {
+            siteWrapper.eq(VppSite::getProvince, params.get("province").toString());
+        }
+        if (params.get("city") != null) {
+            siteWrapper.eq(VppSite::getCity, params.get("city").toString());
+        }
+        if (params.get("district") != null) {
+            siteWrapper.eq(VppSite::getDistrict, params.get("district").toString());
+        }
+        return siteMapper.selectList(siteWrapper).stream().map(VppSite::getId).collect(Collectors.toSet());
+    }
+
+    private List<ResourceBoardItemVO> buildBoardItems(List<VppResourcePoint> resources) {
+        if (resources.isEmpty()) {
+            return Collections.emptyList();
+        }
+        Set<Long> siteIds = resources.stream().map(VppResourcePoint::getSiteId).filter(Objects::nonNull).collect(Collectors.toSet());
+        Set<Long> deviceIds = resources.stream().map(VppResourcePoint::getDeviceId).filter(Objects::nonNull).collect(Collectors.toSet());
+        Map<Long, VppSite> siteMap = siteIds.isEmpty() ? Collections.emptyMap()
+                : siteMapper.selectBatchIds(siteIds).stream()
+                .filter(s -> !VppAuditHelper.isDeleted(s.getDeleteFlag()))
+                .collect(Collectors.toMap(VppSite::getId, s -> s, (a, b) -> a));
+        Map<Long, VppDevice> deviceMap = deviceIds.isEmpty() ? Collections.emptyMap()
+                : deviceMapper.selectBatchIds(deviceIds).stream()
+                .filter(d -> !VppAuditHelper.isDeleted(d.getDeleteFlag()))
+                .collect(Collectors.toMap(VppDevice::getId, d -> d, (a, b) -> a));
+
+        List<ResourceBoardItemVO> items = new ArrayList<>();
+        for (VppResourcePoint resource : resources) {
+            VppDevice device = resolveDeviceForResource(resource, deviceMap);
+            ResourceBoardItemVO item = new ResourceBoardItemVO();
+            fillBoardItem(item, resource, siteMap.get(resource.getSiteId()), device);
+            items.add(item);
+        }
+        return items;
+    }
+
+    private VppDevice loadDeviceForResource(VppResourcePoint resource) {
+        if (resource.getDeviceId() == null) {
+            return null;
+        }
+        VppDevice device = deviceMapper.selectById(resource.getDeviceId());
+        return matchesResourceSite(device, resource.getSiteId()) ? device : null;
+    }
+
+    private VppDevice resolveDeviceForResource(VppResourcePoint resource, Map<Long, VppDevice> deviceMap) {
+        if (resource.getDeviceId() == null) {
+            return null;
+        }
+        VppDevice device = deviceMap.get(resource.getDeviceId());
+        return matchesResourceSite(device, resource.getSiteId()) ? device : null;
+    }
+
+    private boolean matchesResourceSite(VppDevice device, Long siteId) {
+        return device != null && siteId != null && siteId.equals(device.getSiteId());
+    }
+
+    private void fillBoardItem(ResourceBoardItemVO item, VppResourcePoint resource, VppSite site, VppDevice device) {
+        item.setResourceId(resource.getId());
+        item.setResourceCode(resource.getResourceCode());
+        item.setResourceName(resource.getResourceName());
+        item.setResourceType(resource.getResourceType());
+        item.setResourceTypeLabel(VppResourceMockHelper.resourceTypeLabel(resource.getResourceType()));
+        item.setSiteId(resource.getSiteId());
+        item.setCapacityKw(resource.getCapacityKw());
+        if (site != null) {
+            item.setSiteName(site.getSiteName());
+            item.setProvince(site.getProvince());
+            item.setCity(site.getCity());
+            item.setDistrict(site.getDistrict());
+        }
+        boolean abnormal = VppResourceMockHelper.mockAbnormal(resource, device);
+        item.setAbnormal(abnormal);
+        item.setRunStatusLabel(VppResourceMockHelper.runStatusLabel(device, abnormal));
+        BigDecimal power = VppResourceMockHelper.mockRealtimePowerKw(resource, device);
+        BigDecimal voltage = VppResourceMockHelper.mockVoltageV(resource);
+        item.setRealtimePowerKw(power);
+        item.setVoltageV(voltage);
+        item.setCurrentA(VppResourceMockHelper.mockCurrentA(resource, power, voltage));
+        item.setSocPercent(VppResourceMockHelper.mockSocPercent(resource));
+        item.setEvcsChargePowerKw(VppResourceMockHelper.mockEvcsChargePowerKw(resource));
+    }
+
+    private List<ResourceBoardItemVO> filterByStatus(List<ResourceBoardItemVO> items, Map<String, Object> params) {
+        if (params == null || params.get("status") == null) {
+            return items;
+        }
+        String status = params.get("status").toString();
+        if ("abnormal".equalsIgnoreCase(status) || "1".equals(status)) {
+            return items.stream().filter(i -> Boolean.TRUE.equals(i.getAbnormal())).collect(Collectors.toList());
+        }
+        if ("normal".equalsIgnoreCase(status) || "0".equals(status)) {
+            return items.stream().filter(i -> !Boolean.TRUE.equals(i.getAbnormal())).collect(Collectors.toList());
+        }
+        return items;
+    }
+
+    private long parseLong(Map<String, Object> params, String key, long defaultValue) {
+        if (params == null || params.get(key) == null) {
+            return defaultValue;
+        }
+        return Long.parseLong(params.get(key).toString());
+    }
+}

+ 124 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResourceOverviewServiceImpl.java

@@ -0,0 +1,124 @@
+package com.usky.vpp.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.mapper.VppResourcePointMapper;
+import com.usky.vpp.service.VppResourceOverviewService;
+import com.usky.vpp.service.vo.AlarmLevelStatVO;
+import com.usky.vpp.service.vo.EnergyTodayVO;
+import com.usky.vpp.service.vo.ResourceTypeStatVO;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppResourceMockHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class VppResourceOverviewServiceImpl implements VppResourceOverviewService {
+
+    private static final List<String> RESOURCE_TYPES = Arrays.asList("PV", "ESS", "EVCS", "IND_LOAD", "COM_BLDG");
+
+    @Autowired
+    private VppResourcePointMapper resourcePointMapper;
+
+    @Override
+    public List<ResourceTypeStatVO> getTypeStats() {
+        return buildTypeStats(listActiveResources());
+    }
+
+    @Override
+    public EnergyTodayVO getEnergyToday() {
+        return buildEnergyTodayMock(listActiveResources());
+    }
+
+    @Override
+    public List<AlarmLevelStatVO> getAlarmStats() {
+        return buildAlarmStatsMock();
+    }
+
+    private List<VppResourcePoint> listActiveResources() {
+        return resourcePointMapper.selectList(
+                new LambdaQueryWrapper<VppResourcePoint>()
+                        .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+    }
+
+    private List<ResourceTypeStatVO> buildTypeStats(List<VppResourcePoint> resources) {
+        Map<String, ResourceTypeStatVO> statMap = new LinkedHashMap<>();
+        for (String type : RESOURCE_TYPES) {
+            ResourceTypeStatVO stat = new ResourceTypeStatVO();
+            stat.setResourceType(type);
+            stat.setResourceTypeLabel(VppResourceMockHelper.resourceTypeLabel(type));
+            stat.setTotalCount(0L);
+            stat.setTotalCapacityKw(BigDecimal.ZERO);
+            stat.setTotalAdjustableCapacityKw(BigDecimal.ZERO);
+            statMap.put(type, stat);
+        }
+        for (VppResourcePoint resource : resources) {
+            String type = resource.getResourceType() != null ? resource.getResourceType().toUpperCase() : "OTHER";
+            ResourceTypeStatVO stat = statMap.computeIfAbsent(type, key -> {
+                ResourceTypeStatVO s = new ResourceTypeStatVO();
+                s.setResourceType(key);
+                s.setResourceTypeLabel(VppResourceMockHelper.resourceTypeLabel(key));
+                s.setTotalCount(0L);
+                s.setTotalCapacityKw(BigDecimal.ZERO);
+                s.setTotalAdjustableCapacityKw(BigDecimal.ZERO);
+                return s;
+            });
+            stat.setTotalCount(stat.getTotalCount() + 1);
+            BigDecimal cap = resource.getCapacityKw() != null ? resource.getCapacityKw() : BigDecimal.ZERO;
+            stat.setTotalCapacityKw(stat.getTotalCapacityKw().add(cap));
+            if (resource.getIsControl() != null && resource.getIsControl() == 1) {
+                stat.setTotalAdjustableCapacityKw(stat.getTotalAdjustableCapacityKw().add(cap));
+            }
+        }
+        return new ArrayList<>(statMap.values());
+    }
+
+    private EnergyTodayVO buildEnergyTodayMock(List<VppResourcePoint> resources) {
+        BigDecimal pvCapacity = resources.stream()
+                .filter(r -> "PV".equalsIgnoreCase(r.getResourceType()))
+                .map(r -> r.getCapacityKw() != null ? r.getCapacityKw() : BigDecimal.ZERO)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+        BigDecimal loadCapacity = resources.stream()
+                .filter(r -> !"PV".equalsIgnoreCase(r.getResourceType()))
+                .map(r -> r.getCapacityKw() != null ? r.getCapacityKw() : BigDecimal.ZERO)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        EnergyTodayVO energy = new EnergyTodayVO();
+        energy.setTotalGenerationKwh(pvCapacity.multiply(BigDecimal.valueOf(4.2)).setScale(2, RoundingMode.HALF_UP));
+        energy.setTotalConsumptionKwh(loadCapacity.multiply(BigDecimal.valueOf(3.6)).setScale(2, RoundingMode.HALF_UP));
+        if (energy.getTotalConsumptionKwh().compareTo(BigDecimal.ZERO) > 0) {
+            BigDecimal ratio = energy.getTotalGenerationKwh()
+                    .multiply(BigDecimal.valueOf(100))
+                    .divide(energy.getTotalConsumptionKwh(), 2, RoundingMode.HALF_UP);
+            energy.setGreenEnergyRatioPercent(ratio.min(BigDecimal.valueOf(100)));
+        } else {
+            energy.setGreenEnergyRatioPercent(BigDecimal.ZERO);
+        }
+        return energy;
+    }
+
+    private List<AlarmLevelStatVO> buildAlarmStatsMock() {
+        List<AlarmLevelStatVO> stats = new ArrayList<>();
+        stats.add(buildAlarmStat(1, "紧急", 3L));
+        stats.add(buildAlarmStat(2, "重要", 8L));
+        stats.add(buildAlarmStat(3, "一般", 15L));
+        return stats;
+    }
+
+    private AlarmLevelStatVO buildAlarmStat(int level, String label, long count) {
+        AlarmLevelStatVO stat = new AlarmLevelStatVO();
+        stat.setAlarmLevel(level);
+        stat.setAlarmLevelLabel(label);
+        stat.setCount(count);
+        stat.setAlarmListPath("/alarm?alarmLevel=" + level);
+        return stat;
+    }
+}

+ 259 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppResourcePointServiceImpl.java

@@ -0,0 +1,259 @@
+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.VppDevice;
+import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.mapper.VppDeviceMapper;
+import com.usky.vpp.mapper.VppResourcePointMapper;
+import com.usky.vpp.service.VppResourcePointService;
+import com.usky.vpp.service.VppSiteService;
+import com.usky.vpp.domain.VppSite;
+import com.usky.vpp.mapper.VppSiteMapper;
+import com.usky.vpp.service.vo.ResourcePointListVO;
+import com.usky.vpp.service.vo.ResourcePointRequest;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppPageHelper;
+import com.usky.vpp.util.VppResourceMockHelper;
+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.util.List;
+import java.util.Map;
+
+@Service
+public class VppResourcePointServiceImpl implements VppResourcePointService {
+
+    @Autowired
+    private VppResourcePointMapper resourcePointMapper;
+    @Autowired
+    private VppSiteService siteService;
+    @Autowired
+    private VppDeviceMapper deviceMapper;
+    @Autowired
+    private VppSiteMapper siteMapper;
+
+    @Override
+    public CommonPage<VppResourcePoint> pageResourcePoint(Map<String, Object> params) {
+        Page<VppResourcePoint> page = VppPageHelper.of(params);
+        return toCommonPage(resourcePointMapper.selectPage(page, buildQueryWrapper(params)));
+    }
+
+    @Override
+    public CommonPage<ResourcePointListVO> pageResourcePointList(Map<String, Object> params) {
+        Page<VppResourcePoint> page = VppPageHelper.of(params);
+        LambdaQueryWrapper<VppResourcePoint> wrapper = buildQueryWrapper(params);
+        applyNameFilters(wrapper, params);
+        Page<VppResourcePoint> result = resourcePointMapper.selectPage(page, wrapper);
+        return toListVoPage(result);
+    }
+
+    @Override
+    public CommonPage<VppResourcePoint> listBySiteId(Long siteId, Map<String, Object> params) {
+        siteService.getSite(siteId);
+        Page<VppResourcePoint> page = VppPageHelper.of(params);
+        LambdaQueryWrapper<VppResourcePoint> wrapper = buildQueryWrapper(params)
+                .eq(VppResourcePoint::getSiteId, siteId);
+        return toCommonPage(resourcePointMapper.selectPage(page, wrapper));
+    }
+
+    @Override
+    public VppResourcePoint getResourcePoint(Long id) {
+        VppResourcePoint resource = resourcePointMapper.selectById(id);
+        if (resource == null || VppAuditHelper.isDeleted(resource.getDeleteFlag())) {
+            throw new BusinessException("资源点不存在");
+        }
+        return resource;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public VppResourcePoint createResourcePoint(Long siteId, ResourcePointRequest request) {
+        siteService.getSite(siteId);
+        validateRequest(request, null);
+        ensureDeviceBelongsToSite(request.getDeviceId(), siteId);
+        VppResourcePoint resource = new VppResourcePoint();
+        resource.setSiteId(siteId);
+        applyRequest(resource, request);
+        VppAuditHelper.fillCreate(resource);
+        resourcePointMapper.insert(resource);
+        return resource;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateResourcePoint(Long id, ResourcePointRequest request) {
+        VppResourcePoint existing = getResourcePoint(id);
+        validateRequest(request, id);
+        ensureDeviceBelongsToSite(request.getDeviceId(), existing.getSiteId());
+        applyRequest(existing, request);
+        VppAuditHelper.fillUpdate(existing);
+        resourcePointMapper.updateById(existing);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteResourcePoint(Long id) {
+        VppResourcePoint resource = getResourcePoint(id);
+        VppAuditHelper.fillSoftDelete(resource);
+        resourcePointMapper.updateById(resource);
+    }
+
+    private LambdaQueryWrapper<VppResourcePoint> buildQueryWrapper(Map<String, Object> params) {
+        LambdaQueryWrapper<VppResourcePoint> wrapper = new LambdaQueryWrapper<VppResourcePoint>()
+                .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .orderByDesc(VppResourcePoint::getCreateTime);
+        if (params == null) {
+            return wrapper;
+        }
+        if (params.get("siteId") != null) {
+            wrapper.eq(VppResourcePoint::getSiteId, Long.parseLong(params.get("siteId").toString()));
+        }
+        if (params.get("resourceType") != null) {
+            wrapper.eq(VppResourcePoint::getResourceType, params.get("resourceType").toString());
+        }
+        if (params.get("resourceName") != null) {
+            wrapper.like(VppResourcePoint::getResourceName, params.get("resourceName").toString());
+        }
+        if (params.get("isControl") != null) {
+            wrapper.eq(VppResourcePoint::getIsControl, Integer.parseInt(params.get("isControl").toString()));
+        }
+        return wrapper;
+    }
+
+    private void applyNameFilters(LambdaQueryWrapper<VppResourcePoint> wrapper, Map<String, Object> params) {
+        if (params == null) {
+            return;
+        }
+        if (params.get("siteName") != null && StringUtils.hasText(params.get("siteName").toString())) {
+            List<VppSite> sites = siteMapper.selectList(new LambdaQueryWrapper<VppSite>()
+                    .like(VppSite::getSiteName, params.get("siteName").toString())
+                    .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+            if (sites.isEmpty()) {
+                wrapper.eq(VppResourcePoint::getId, -1L);
+                return;
+            }
+            wrapper.in(VppResourcePoint::getSiteId,
+                    sites.stream().map(VppSite::getId).collect(java.util.stream.Collectors.toList()));
+        }
+        if (params.get("deviceName") != null && StringUtils.hasText(params.get("deviceName").toString())) {
+            List<VppDevice> devices = deviceMapper.selectList(new LambdaQueryWrapper<VppDevice>()
+                    .like(VppDevice::getDeviceName, params.get("deviceName").toString())
+                    .eq(VppDevice::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+            if (devices.isEmpty()) {
+                wrapper.eq(VppResourcePoint::getId, -1L);
+                return;
+            }
+            wrapper.in(VppResourcePoint::getDeviceId,
+                    devices.stream().map(VppDevice::getId).collect(java.util.stream.Collectors.toList()));
+        }
+    }
+
+    private CommonPage<ResourcePointListVO> toListVoPage(Page<VppResourcePoint> page) {
+        List<VppResourcePoint> records = page.getRecords();
+        if (records.isEmpty()) {
+            return new CommonPage<>(java.util.Collections.emptyList(), page.getTotal(), page.getCurrent(), page.getSize());
+        }
+        java.util.Set<Long> siteIds = records.stream().map(VppResourcePoint::getSiteId)
+                .filter(java.util.Objects::nonNull).collect(java.util.stream.Collectors.toSet());
+        java.util.Set<Long> deviceIds = records.stream().map(VppResourcePoint::getDeviceId)
+                .filter(java.util.Objects::nonNull).collect(java.util.stream.Collectors.toSet());
+        java.util.Map<Long, VppSite> siteMap = siteIds.isEmpty() ? java.util.Collections.emptyMap()
+                : siteMapper.selectBatchIds(siteIds).stream()
+                .collect(java.util.stream.Collectors.toMap(VppSite::getId, s -> s, (a, b) -> a));
+        java.util.Map<Long, VppDevice> deviceMap = deviceIds.isEmpty() ? java.util.Collections.emptyMap()
+                : deviceMapper.selectBatchIds(deviceIds).stream()
+                .collect(java.util.stream.Collectors.toMap(VppDevice::getId, d -> d, (a, b) -> a));
+
+        List<ResourcePointListVO> voList = new java.util.ArrayList<>();
+        for (VppResourcePoint resource : records) {
+            ResourcePointListVO vo = new ResourcePointListVO();
+            vo.setId(resource.getId());
+            vo.setResourceCode(resource.getResourceCode());
+            vo.setResourceName(resource.getResourceName());
+            vo.setSiteId(resource.getSiteId());
+            vo.setDeviceId(resource.getDeviceId());
+            vo.setResourceType(resource.getResourceType());
+            vo.setResourceTypeLabel(VppResourceMockHelper.resourceTypeLabel(resource.getResourceType()));
+            vo.setCapacityKw(resource.getCapacityKw());
+            vo.setIsControl(resource.getIsControl());
+            vo.setIsSupportPeak(resource.getIsSupportPeak());
+            vo.setIsSupportFm(resource.getIsSupportFm());
+            vo.setResponseMin(resource.getResponseMin());
+            vo.setMaxUpKw(resource.getMaxUpKw());
+            vo.setMinDownKw(resource.getMinDownKw());
+            vo.setRemark(resource.getRemark());
+            vo.setCreateTime(resource.getCreateTime());
+            VppSite site = siteMap.get(resource.getSiteId());
+            if (site != null) {
+                vo.setSiteName(site.getSiteName());
+            }
+            VppDevice device = deviceMap.get(resource.getDeviceId());
+            if (device != null) {
+                vo.setDeviceName(device.getDeviceName());
+            }
+            voList.add(vo);
+        }
+        return new CommonPage<>(voList, page.getTotal(), page.getCurrent(), page.getSize());
+    }
+
+    private void ensureDeviceBelongsToSite(Long deviceId, Long siteId) {
+        VppDevice device = deviceMapper.selectById(deviceId);
+        if (device == null || VppAuditHelper.isDeleted(device.getDeleteFlag())) {
+            throw new BusinessException("设备不存在");
+        }
+        if (!siteId.equals(device.getSiteId())) {
+            throw new BusinessException("设备不属于当前站点");
+        }
+    }
+
+    private void validateRequest(ResourcePointRequest request, Long excludeId) {
+        if (request == null || !StringUtils.hasText(request.getResourceCode())) {
+            throw new BusinessException("资源编号不能为空");
+        }
+        if (!StringUtils.hasText(request.getResourceName())) {
+            throw new BusinessException("资源名称不能为空");
+        }
+        if (request.getDeviceId() == null) {
+            throw new BusinessException("关联设备不能为空");
+        }
+        if (!StringUtils.hasText(request.getResourceType())) {
+            throw new BusinessException("资源类型不能为空");
+        }
+        if (request.getCapacityKw() == null) {
+            throw new BusinessException("装机容量不能为空");
+        }
+        LambdaQueryWrapper<VppResourcePoint> wrapper = new LambdaQueryWrapper<VppResourcePoint>()
+                .eq(VppResourcePoint::getResourceCode, request.getResourceCode())
+                .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+        if (excludeId != null) {
+            wrapper.ne(VppResourcePoint::getId, excludeId);
+        }
+        if (resourcePointMapper.selectCount(wrapper) > 0) {
+            throw new BusinessException("资源编号已存在");
+        }
+    }
+
+    private void applyRequest(VppResourcePoint resource, ResourcePointRequest request) {
+        resource.setResourceCode(request.getResourceCode());
+        resource.setResourceName(request.getResourceName());
+        resource.setDeviceId(request.getDeviceId());
+        resource.setResourceType(request.getResourceType());
+        resource.setCapacityKw(request.getCapacityKw());
+        resource.setIsControl(request.getIsControl() == null ? 0 : request.getIsControl());
+        resource.setIsSupportPeak(request.getIsSupportPeak() == null ? 0 : request.getIsSupportPeak());
+        resource.setIsSupportFm(request.getIsSupportFm() == null ? 0 : request.getIsSupportFm());
+        resource.setResponseMin(request.getResponseMin());
+        resource.setMaxUpKw(request.getMaxUpKw());
+        resource.setMinDownKw(request.getMinDownKw());
+        resource.setRemark(request.getRemark());
+    }
+
+    private CommonPage<VppResourcePoint> toCommonPage(Page<VppResourcePoint> page) {
+        return new CommonPage<>(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize());
+    }
+}

+ 320 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppSiteServiceImpl.java

@@ -0,0 +1,320 @@
+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.VppCustomer;
+import com.usky.vpp.domain.VppSite;
+import com.usky.vpp.domain.VppSiteConfig;
+import com.usky.vpp.mapper.VppCustomerMapper;
+import com.usky.vpp.mapper.VppSiteConfigMapper;
+import com.usky.vpp.mapper.VppSiteMapper;
+import com.usky.vpp.service.VppSiteService;
+import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.mapper.VppResourcePointMapper;
+import com.usky.vpp.service.vo.SiteConfigDetailVO;
+import com.usky.vpp.service.vo.SiteListVO;
+import com.usky.vpp.service.vo.SiteConfigRequest;
+import com.usky.vpp.service.vo.SiteRequest;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppPageHelper;
+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.util.*;
+import java.util.stream.Collectors;
+
+@Service
+public class VppSiteServiceImpl implements VppSiteService {
+
+    @Autowired
+    private VppSiteMapper siteMapper;
+    @Autowired
+    private VppSiteConfigMapper siteConfigMapper;
+    @Autowired
+    private VppCustomerMapper customerMapper;
+    @Autowired
+    private VppResourcePointMapper resourcePointMapper;
+
+    @Override
+    public CommonPage<VppSite> pageSite(Map<String, Object> params) {
+        Page<VppSite> page = VppPageHelper.of(params);
+        return toCommonPage(siteMapper.selectPage(page, buildSiteWrapper(params)));
+    }
+
+    @Override
+    public CommonPage<SiteListVO> pageSiteList(Map<String, Object> params) {
+        Page<VppSite> page = VppPageHelper.of(params);
+        LambdaQueryWrapper<VppSite> wrapper = buildSiteWrapper(params);
+        applyResourceTypeFilter(wrapper, params);
+        Page<VppSite> result = siteMapper.selectPage(page, wrapper);
+        return toSiteListPage(result);
+    }
+
+    @Override
+    public VppSite getSite(Long id) {
+        VppSite site = siteMapper.selectById(id);
+        if (site == null || VppAuditHelper.isDeleted(site.getDeleteFlag())) {
+            throw new BusinessException("站点不存在");
+        }
+        return site;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public VppSite createSite(SiteRequest request) {
+        validateSiteRequest(request, null);
+        ensureCustomerExists(request.getCustomerId());
+        VppSite site = new VppSite();
+        applySiteRequest(site, request);
+        VppAuditHelper.fillCreate(site);
+        siteMapper.insert(site);
+        return site;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateSite(Long id, SiteRequest request) {
+        VppSite existing = getSite(id);
+        validateSiteRequest(request, id);
+        ensureCustomerExists(request.getCustomerId());
+        applySiteRequest(existing, request);
+        VppAuditHelper.fillUpdate(existing);
+        siteMapper.updateById(existing);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteSite(Long id) {
+        VppSite site = getSite(id);
+        VppAuditHelper.fillSoftDelete(site);
+        siteMapper.updateById(site);
+    }
+
+    @Override
+    public VppSiteConfig getSiteConfig(Long siteId) {
+        getSite(siteId);
+        VppSiteConfig config = siteConfigMapper.selectOne(new LambdaQueryWrapper<VppSiteConfig>()
+                .eq(VppSiteConfig::getSiteId, siteId)
+                .eq(VppSiteConfig::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .last("LIMIT 1"));
+        if (config == null) {
+            throw new BusinessException("站点运行参数不存在");
+        }
+        return config;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public VppSiteConfig saveSiteConfig(Long siteId, SiteConfigRequest request) {
+        getSite(siteId);
+        if (request == null || request.getCollectIntervalSec() == null) {
+            throw new BusinessException("采集频率不能为空");
+        }
+        VppSiteConfig config = siteConfigMapper.selectOne(new LambdaQueryWrapper<VppSiteConfig>()
+                .eq(VppSiteConfig::getSiteId, siteId)
+                .eq(VppSiteConfig::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .last("LIMIT 1"));
+        if (config == null) {
+            config = new VppSiteConfig();
+            config.setSiteId(siteId);
+            applySiteConfig(config, request);
+            VppAuditHelper.fillCreate(config);
+            siteConfigMapper.insert(config);
+        } else {
+            applySiteConfig(config, request);
+            VppAuditHelper.fillUpdate(config);
+            siteConfigMapper.updateById(config);
+        }
+        return config;
+    }
+
+    @Override
+    public SiteConfigDetailVO getSiteConfigDetail(Long siteId) {
+        VppSite site = getSite(siteId);
+        VppSiteConfig config = siteConfigMapper.selectOne(new LambdaQueryWrapper<VppSiteConfig>()
+                .eq(VppSiteConfig::getSiteId, siteId)
+                .eq(VppSiteConfig::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .last("LIMIT 1"));
+        return toConfigDetail(site, config);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public SiteConfigDetailVO saveSiteConfigDetail(Long siteId, SiteConfigRequest request) {
+        saveSiteConfig(siteId, request);
+        if (request != null && request.getResponsePriority() != null) {
+            VppSite site = getSite(siteId);
+            site.setResponsePriority(request.getResponsePriority());
+            VppAuditHelper.fillUpdate(site);
+            siteMapper.updateById(site);
+        }
+        return getSiteConfigDetail(siteId);
+    }
+
+    private LambdaQueryWrapper<VppSite> buildSiteWrapper(Map<String, Object> params) {
+        LambdaQueryWrapper<VppSite> wrapper = new LambdaQueryWrapper<VppSite>()
+                .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .orderByDesc(VppSite::getCreateTime);
+        if (params != null) {
+            if (params.get("customerId") != null) {
+                wrapper.eq(VppSite::getCustomerId, Long.parseLong(params.get("customerId").toString()));
+            }
+            if (params.get("siteName") != null) {
+                wrapper.like(VppSite::getSiteName, params.get("siteName").toString());
+            }
+            if (params.get("siteCode") != null) {
+                wrapper.eq(VppSite::getSiteCode, params.get("siteCode").toString());
+            }
+            if (params.get("province") != null) {
+                wrapper.eq(VppSite::getProvince, params.get("province").toString());
+            }
+            if (params.get("city") != null) {
+                wrapper.eq(VppSite::getCity, params.get("city").toString());
+            }
+            if (params.get("district") != null) {
+                wrapper.eq(VppSite::getDistrict, params.get("district").toString());
+            }
+        }
+        return wrapper;
+    }
+
+    private void applyResourceTypeFilter(LambdaQueryWrapper<VppSite> wrapper, Map<String, Object> params) {
+        if (params == null || params.get("resourceType") == null) {
+            return;
+        }
+        List<VppResourcePoint> resources = resourcePointMapper.selectList(
+                new LambdaQueryWrapper<VppResourcePoint>()
+                        .eq(VppResourcePoint::getResourceType, params.get("resourceType").toString())
+                        .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        if (resources.isEmpty()) {
+            wrapper.eq(VppSite::getId, -1L);
+            return;
+        }
+        Set<Long> siteIds = resources.stream().map(VppResourcePoint::getSiteId).filter(Objects::nonNull).collect(Collectors.toSet());
+        wrapper.in(VppSite::getId, siteIds);
+    }
+
+    private CommonPage<SiteListVO> toSiteListPage(Page<VppSite> page) {
+        List<VppSite> sites = page.getRecords();
+        if (sites.isEmpty()) {
+            return new CommonPage<>(Collections.emptyList(), page.getTotal(), page.getCurrent(), page.getSize());
+        }
+        Set<Long> siteIds = sites.stream().map(VppSite::getId).collect(Collectors.toSet());
+        List<VppResourcePoint> allResources = resourcePointMapper.selectList(
+                new LambdaQueryWrapper<VppResourcePoint>()
+                        .in(VppResourcePoint::getSiteId, siteIds)
+                        .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED));
+        Map<Long, List<VppResourcePoint>> resourceBySite = allResources.stream()
+                .collect(Collectors.groupingBy(VppResourcePoint::getSiteId));
+
+        List<SiteListVO> voList = new ArrayList<>();
+        for (VppSite site : sites) {
+            SiteListVO vo = new SiteListVO();
+            vo.setId(site.getId());
+            vo.setSiteCode(site.getSiteCode());
+            vo.setSiteName(site.getSiteName());
+            vo.setCustomerId(site.getCustomerId());
+            vo.setProvince(site.getProvince());
+            vo.setCity(site.getCity());
+            vo.setDistrict(site.getDistrict());
+            vo.setAddress(site.getAddress());
+            vo.setLongitude(site.getLongitude());
+            vo.setLatitude(site.getLatitude());
+            vo.setOwnerName(site.getOwnerName());
+            vo.setContactName(site.getContactName());
+            vo.setContactPhone(site.getContactPhone());
+            vo.setResponsePriority(site.getResponsePriority());
+            vo.setCreateTime(site.getCreateTime());
+            List<VppResourcePoint> siteResources = resourceBySite.getOrDefault(site.getId(), Collections.emptyList());
+            vo.setResourceCount((long) siteResources.size());
+            vo.setResourceTypes(siteResources.stream()
+                    .map(VppResourcePoint::getResourceType)
+                    .filter(Objects::nonNull)
+                    .distinct()
+                    .collect(Collectors.toList()));
+            voList.add(vo);
+        }
+        return new CommonPage<>(voList, page.getTotal(), page.getCurrent(), page.getSize());
+    }
+
+    private SiteConfigDetailVO toConfigDetail(VppSite site, VppSiteConfig config) {
+        SiteConfigDetailVO detail = new SiteConfigDetailVO();
+        detail.setSiteId(site.getId());
+        detail.setSiteName(site.getSiteName());
+        detail.setResponsePriority(site.getResponsePriority());
+        if (config != null) {
+            detail.setCollectIntervalSec(config.getCollectIntervalSec());
+            detail.setPowerUpperLimit(config.getPowerUpperLimit());
+            detail.setPowerLowerLimit(config.getPowerLowerLimit());
+            detail.setSocUpperLimit(config.getSocUpperLimit());
+            detail.setSocLowerLimit(config.getSocLowerLimit());
+            detail.setOfflineTimeoutSec(config.getOfflineTimeoutSec());
+            detail.setAlarmRuleJson(config.getAlarmRuleJson());
+        }
+        return detail;
+    }
+
+    private void ensureCustomerExists(Long customerId) {
+        VppCustomer customer = customerMapper.selectById(customerId);
+        if (customer == null || VppAuditHelper.isDeleted(customer.getDeleteFlag())) {
+            throw new BusinessException("客户不存在");
+        }
+    }
+
+    private void validateSiteRequest(SiteRequest request, Long excludeId) {
+        if (request == null || !StringUtils.hasText(request.getSiteCode())) {
+            throw new BusinessException("站点编号不能为空");
+        }
+        if (!StringUtils.hasText(request.getSiteName())) {
+            throw new BusinessException("站点名称不能为空");
+        }
+        if (request.getCustomerId() == null) {
+            throw new BusinessException("所属客户不能为空");
+        }
+        LambdaQueryWrapper<VppSite> wrapper = new LambdaQueryWrapper<VppSite>()
+                .eq(VppSite::getSiteCode, request.getSiteCode())
+                .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+        if (excludeId != null) {
+            wrapper.ne(VppSite::getId, excludeId);
+        }
+        if (siteMapper.selectCount(wrapper) > 0) {
+            throw new BusinessException("站点编号已存在");
+        }
+    }
+
+    private void applySiteRequest(VppSite site, SiteRequest request) {
+        site.setSiteCode(request.getSiteCode());
+        site.setSiteName(request.getSiteName());
+        site.setCustomerId(request.getCustomerId());
+        site.setProvince(request.getProvince());
+        site.setCity(request.getCity());
+        site.setDistrict(request.getDistrict());
+        site.setAddress(request.getAddress());
+        site.setLongitude(request.getLongitude());
+        site.setLatitude(request.getLatitude());
+        site.setOwnerName(request.getOwnerName());
+        site.setContactName(request.getContactName());
+        site.setContactPhone(request.getContactPhone());
+        site.setResponsePriority(request.getResponsePriority());
+        site.setUnResourceId(request.getUnResourceId());
+        site.setRemark(request.getRemark());
+    }
+
+    private void applySiteConfig(VppSiteConfig config, SiteConfigRequest request) {
+        config.setCollectIntervalSec(request.getCollectIntervalSec());
+        config.setPowerUpperLimit(request.getPowerUpperLimit());
+        config.setPowerLowerLimit(request.getPowerLowerLimit());
+        config.setSocUpperLimit(request.getSocUpperLimit());
+        config.setSocLowerLimit(request.getSocLowerLimit());
+        config.setOfflineTimeoutSec(request.getOfflineTimeoutSec());
+        config.setAlarmRuleJson(request.getAlarmRuleJson());
+    }
+
+    private CommonPage<VppSite> toCommonPage(Page<VppSite> page) {
+        return new CommonPage<>(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize());
+    }
+}

+ 14 - 3
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppUnDrSyncServiceImpl.java

@@ -5,9 +5,11 @@ 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.domain.VppSite;
 import com.usky.vpp.mapper.VppCustomerMapper;
 import com.usky.vpp.mapper.VppDrParticipationMapper;
 import com.usky.vpp.mapper.VppResourcePointMapper;
+import com.usky.vpp.mapper.VppSiteMapper;
 import com.usky.vpp.service.VppUnDrSyncService;
 import com.usky.vpp.util.VppAuditHelper;
 import com.usky.vpp.util.VppUnEventParser;
@@ -29,6 +31,8 @@ public class VppUnDrSyncServiceImpl implements VppUnDrSyncService {
     @Autowired
     private VppCustomerMapper customerMapper;
     @Autowired
+    private VppSiteMapper siteMapper;
+    @Autowired
     private VppResourcePointMapper resourcePointMapper;
     @Autowired
     private VppDrParticipationMapper participationMapper;
@@ -71,10 +75,17 @@ public class VppUnDrSyncServiceImpl implements VppUnDrSyncService {
             return;
         }
 
-        VppResourcePoint resourcePoint = resourcePointMapper.selectOne(new LambdaQueryWrapper<VppResourcePoint>()
-                .eq(VppResourcePoint::getCustomerId, customer.getId())
-                .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+        VppSite site = siteMapper.selectOne(new LambdaQueryWrapper<VppSite>()
+                .eq(VppSite::getCustomerId, customer.getId())
+                .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                 .last("LIMIT 1"));
+        VppResourcePoint resourcePoint = null;
+        if (site != null) {
+            resourcePoint = resourcePointMapper.selectOne(new LambdaQueryWrapper<VppResourcePoint>()
+                    .eq(VppResourcePoint::getSiteId, site.getId())
+                    .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                    .last("LIMIT 1"));
+        }
 
         Long resourceId = resourcePoint != null ? resourcePoint.getId() : null;
         LambdaQueryWrapper<VppDrParticipation> wrapper = new LambdaQueryWrapper<VppDrParticipation>()

+ 24 - 18
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppUnReportServiceImpl.java

@@ -1,12 +1,14 @@
 package com.usky.vpp.service.impl;
 
-import com.usky.vpp.util.VppAuditHelper;
 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.domain.VppSite;
 import com.usky.vpp.mapper.VppResourcePointMapper;
 import com.usky.vpp.service.VppUnReportService;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppSiteResourceHelper;
 import com.usky.vpp.util.VppUnMessageBuilder;
 import com.usky.vpp.util.VppUnPayloadHelper;
 import org.slf4j.Logger;
@@ -29,6 +31,8 @@ public class VppUnReportServiceImpl implements VppUnReportService {
     private VppUnClient unClient;
     @Autowired
     private VppResourcePointMapper resourcePointMapper;
+    @Autowired
+    private VppSiteResourceHelper siteResourceHelper;
 
     @Override
     public void handleCreateReportRequest(Map<String, Object> createReportRequest) {
@@ -61,39 +65,41 @@ public class VppUnReportServiceImpl implements VppUnReportService {
     }
 
     private void submitRegisterReport() {
-        List<VppResourcePoint> resources = resourcePointMapper.selectList(
-                new LambdaQueryWrapper<VppResourcePoint>()
-                        .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
-                        .orderByAsc(VppResourcePoint::getId));
-        Map<String, Object> request = VppUnMessageBuilder.buildRegisterReportRequest(resources, properties);
+        List<VppResourcePoint> resources = listActiveResources(false);
+        Map<Long, VppSite> siteMap = siteResourceHelper.loadSiteMap(resources);
+        Map<String, Object> request = VppUnMessageBuilder.buildRegisterReportRequest(resources, siteMap, properties);
         unClient.post("RegisterReportRequest", request, true);
         log.info("已提交 RegisterReportRequest");
     }
 
     private void submitIntervalDataReport(Map<String, Object> createReportRequest) {
-        List<VppResourcePoint> resources = resourcePointMapper.selectList(
-                new LambdaQueryWrapper<VppResourcePoint>()
-                        .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
-                        .eq(VppResourcePoint::getRunStatus, 1)
-                        .orderByAsc(VppResourcePoint::getId));
+        List<VppResourcePoint> resources = listActiveResources(true);
+        Map<Long, VppSite> siteMap = siteResourceHelper.loadSiteMap(resources);
         Map<String, Object> request = VppUnMessageBuilder.buildIntervalDataReportRequest(
-                createReportRequest, resources, properties);
+                createReportRequest, resources, siteMap, properties);
         unClient.post("IntervalDataReportRequest", request, true);
         log.info("已提交 IntervalDataReportRequest");
     }
 
     private void submitMomentDataReport(Map<String, Object> createReportRequest) {
-        List<VppResourcePoint> resources = resourcePointMapper.selectList(
-                new LambdaQueryWrapper<VppResourcePoint>()
-                        .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
-                        .eq(VppResourcePoint::getRunStatus, 1)
-                        .orderByAsc(VppResourcePoint::getId));
+        List<VppResourcePoint> resources = listActiveResources(true);
+        Map<Long, VppSite> siteMap = siteResourceHelper.loadSiteMap(resources);
         Map<String, Object> request = VppUnMessageBuilder.buildMomentDataReportRequest(
-                createReportRequest, resources, properties);
+                createReportRequest, resources, siteMap, properties);
         unClient.post("MomentDataReportRequest", request, true);
         log.info("已提交 MomentDataReportRequest");
     }
 
+    private List<VppResourcePoint> listActiveResources(boolean controllableOnly) {
+        LambdaQueryWrapper<VppResourcePoint> wrapper = new LambdaQueryWrapper<VppResourcePoint>()
+                .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .orderByAsc(VppResourcePoint::getId);
+        if (controllableOnly) {
+            wrapper.eq(VppResourcePoint::getIsControl, 1);
+        }
+        return resourcePointMapper.selectList(wrapper);
+    }
+
     @SuppressWarnings("unchecked")
     private String resolveReportRequestId(Map<String, Object> request) {
         String id = VppUnPayloadHelper.getString(request, "reportRequestID", "reportRequestId");

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

@@ -0,0 +1,19 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+/**
+ * 告警级别统计(模拟数据,可跳转告警列表)
+ */
+@Data
+public class AlarmLevelStatVO {
+
+    /** 告警级别:1紧急 2重要 3一般 */
+    private Integer alarmLevel;
+    /** 级别名称 */
+    private String alarmLevelLabel;
+    /** 数量 */
+    private Long count;
+    /** 跳转告警列表路径 */
+    private String alarmListPath;
+}

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

@@ -0,0 +1,25 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 设备创建/更新请求
+ */
+@Data
+public class DeviceRequest {
+
+    private String deviceCode;
+    private String deviceUuid;
+    private String deviceName;
+    private String deviceType;
+    private String manufacturer;
+    private String model;
+    private BigDecimal ratedPowerKw;
+    private Integer commStatus;
+    private Integer runStatus;
+    private String firmwareVersion;
+    private String gatewayId;
+    private String remark;
+}

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

@@ -0,0 +1,19 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 今日电能概况(模拟数据)
+ */
+@Data
+public class EnergyTodayVO {
+
+    /** 总发电量 kWh */
+    private BigDecimal totalGenerationKwh;
+    /** 总用电量 kWh */
+    private BigDecimal totalConsumptionKwh;
+    /** 绿电消纳比例 % */
+    private BigDecimal greenEnergyRatioPercent;
+}

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

@@ -0,0 +1,29 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 资源看板详情(异常资源点击查看)
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ResourceBoardDetailVO extends ResourceBoardItemVO {
+
+    private Long deviceId;
+    private String deviceName;
+    private String deviceCode;
+    /** 异常原因说明 */
+    private String abnormalReason;
+    /** 是否可控 1是 0否 */
+    private Integer isControl;
+    private Integer isSupportPeak;
+    private Integer isSupportFm;
+    private Integer responseMin;
+    private String siteAddress;
+    private String contactName;
+    private String contactPhone;
+    private LocalDateTime dataTime;
+}

+ 39 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResourceBoardItemVO.java

@@ -0,0 +1,39 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 资源看板列表项(含模拟实时运行数据)
+ */
+@Data
+public class ResourceBoardItemVO {
+
+    private Long resourceId;
+    private String resourceCode;
+    private String resourceName;
+    private String resourceType;
+    /** 资源类型中文名 */
+    private String resourceTypeLabel;
+    private Long siteId;
+    private String siteName;
+    private String province;
+    private String city;
+    private String district;
+    /** 运行状态:正常/异常/离线/运行/停机 */
+    private String runStatusLabel;
+    /** 是否异常(前端红色高亮) */
+    private Boolean abnormal;
+    /** 实时功率 kW */
+    private BigDecimal realtimePowerKw;
+    /** 电压 V */
+    private BigDecimal voltageV;
+    /** 电流 A */
+    private BigDecimal currentA;
+    /** 储能 SOC %,非储能为空 */
+    private BigDecimal socPercent;
+    /** 充电桩充电功率 kW,非充电桩为空 */
+    private BigDecimal evcsChargePowerKw;
+    private BigDecimal capacityKw;
+}

+ 32 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/ResourcePointListVO.java

@@ -0,0 +1,32 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 资源点列表展示(含站点、设备名称)
+ */
+@Data
+public class ResourcePointListVO {
+
+    private Long id;
+    private String resourceCode;
+    private String resourceName;
+    private Long siteId;
+    private String siteName;
+    private Long deviceId;
+    private String deviceName;
+    private String resourceType;
+    private String resourceTypeLabel;
+    private BigDecimal capacityKw;
+    private Integer isControl;
+    private Integer isSupportPeak;
+    private Integer isSupportFm;
+    private Integer responseMin;
+    private BigDecimal maxUpKw;
+    private BigDecimal minDownKw;
+    private String remark;
+    private LocalDateTime createTime;
+}

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

@@ -0,0 +1,25 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 资源点创建/更新请求
+ */
+@Data
+public class ResourcePointRequest {
+
+    private String resourceCode;
+    private String resourceName;
+    private Long deviceId;
+    private String resourceType;
+    private BigDecimal capacityKw;
+    private Integer isControl;
+    private Integer isSupportPeak;
+    private Integer isSupportFm;
+    private Integer responseMin;
+    private BigDecimal maxUpKw;
+    private BigDecimal minDownKw;
+    private String remark;
+}

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

@@ -0,0 +1,22 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 按资源类型分类统计
+ */
+@Data
+public class ResourceTypeStatVO {
+
+    private String resourceType;
+    /** 资源类型中文名 */
+    private String resourceTypeLabel;
+    /** 资源总数 */
+    private Long totalCount;
+    /** 总装机容量 kW */
+    private BigDecimal totalCapacityKw;
+    /** 总可调容量 kW(is_control=1 的装机容量之和) */
+    private BigDecimal totalAdjustableCapacityKw;
+}

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

@@ -0,0 +1,31 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 站点参数配置详情(含响应优先级)
+ */
+@Data
+public class SiteConfigDetailVO {
+
+    private Long siteId;
+    private String siteName;
+    /** 采集频率秒 60/300/900 */
+    private Integer collectIntervalSec;
+    /** 功率上限告警 kW */
+    private BigDecimal powerUpperLimit;
+    /** 功率下限告警 kW */
+    private BigDecimal powerLowerLimit;
+    /** SOC 上限 % */
+    private BigDecimal socUpperLimit;
+    /** SOC 下限 % */
+    private BigDecimal socLowerLimit;
+    /** 离线超时秒 */
+    private Integer offlineTimeoutSec;
+    /** 响应优先级 1-10 */
+    private Integer responsePriority;
+    /** 扩展告警规则 JSON */
+    private String alarmRuleJson;
+}

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

@@ -0,0 +1,22 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 站点运行参数请求
+ */
+@Data
+public class SiteConfigRequest {
+
+    private Integer collectIntervalSec;
+    private BigDecimal powerUpperLimit;
+    private BigDecimal powerLowerLimit;
+    private BigDecimal socUpperLimit;
+    private BigDecimal socLowerLimit;
+    private Integer offlineTimeoutSec;
+    private String alarmRuleJson;
+    /** 响应优先级 1-10(写入 vpp_site) */
+    private Integer responsePriority;
+}

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

@@ -0,0 +1,34 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 站点列表展示
+ */
+@Data
+public class SiteListVO {
+
+    private Long id;
+    private String siteCode;
+    private String siteName;
+    private Long customerId;
+    private String province;
+    private String city;
+    private String district;
+    private String address;
+    private BigDecimal longitude;
+    private BigDecimal latitude;
+    private String ownerName;
+    private String contactName;
+    private String contactPhone;
+    private Integer responsePriority;
+    /** 站点下资源类型列表(去重) */
+    private List<String> resourceTypes;
+    /** 站点下资源点数量 */
+    private Long resourceCount;
+    private LocalDateTime createTime;
+}

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

@@ -0,0 +1,28 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 站点创建/更新请求
+ */
+@Data
+public class SiteRequest {
+
+    private String siteCode;
+    private String siteName;
+    private Long customerId;
+    private String province;
+    private String city;
+    private String district;
+    private String address;
+    private BigDecimal longitude;
+    private BigDecimal latitude;
+    private String ownerName;
+    private String contactName;
+    private String contactPhone;
+    private Integer responsePriority;
+    private String unResourceId;
+    private String remark;
+}

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

@@ -48,6 +48,18 @@ public final class VppAuditHelper {
         fillUpdate(entity);
     }
 
+    public static boolean isDeletedAt(LocalDateTime deletedAt) {
+        return deletedAt != null;
+    }
+
+    public static void fillSoftDeleteAt(Object entity) {
+        fillSoftDelete(entity);
+    }
+
+    public static boolean isActive(Integer deleteFlag, LocalDateTime deletedAt) {
+        return !isDeleted(deleteFlag) && !isDeletedAt(deletedAt);
+    }
+
     public static boolean isDeleted(Integer deleteFlag) {
         return deleteFlag != null && deleteFlag == DELETED;
     }

+ 127 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppResourceMockHelper.java

@@ -0,0 +1,127 @@
+package com.usky.vpp.util;
+
+import com.usky.vpp.domain.VppDevice;
+import com.usky.vpp.domain.VppResourcePoint;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * 资源看板/概览模拟数据生成(联调阶段使用,后续可替换为 TSDB/IoT 实时数据)
+ */
+public final class VppResourceMockHelper {
+
+    private VppResourceMockHelper() {
+    }
+
+    public static BigDecimal mockRealtimePowerKw(VppResourcePoint resource, VppDevice device) {
+        BigDecimal base = resource.getCapacityKw() != null ? resource.getCapacityKw() : BigDecimal.valueOf(100);
+        double factor = 0.3 + pseudoRandom(resource.getId(), 0) * 0.6;
+        if (device != null && device.getCommStatus() != null && device.getCommStatus() == 0) {
+            return BigDecimal.ZERO;
+        }
+        return base.multiply(BigDecimal.valueOf(factor)).setScale(2, RoundingMode.HALF_UP);
+    }
+
+    public static BigDecimal mockVoltageV(VppResourcePoint resource) {
+        return BigDecimal.valueOf(380 + pseudoRandom(resource.getId(), 1) * 20).setScale(1, RoundingMode.HALF_UP);
+    }
+
+    public static BigDecimal mockCurrentA(VppResourcePoint resource, BigDecimal powerKw, BigDecimal voltageV) {
+        if (powerKw == null || voltageV == null || voltageV.compareTo(BigDecimal.ZERO) == 0) {
+            return BigDecimal.ZERO;
+        }
+        double current = powerKw.doubleValue() * 1000 / (voltageV.doubleValue() * 1.732);
+        return BigDecimal.valueOf(current).setScale(2, RoundingMode.HALF_UP);
+    }
+
+    public static BigDecimal mockSocPercent(VppResourcePoint resource) {
+        if (!"ESS".equalsIgnoreCase(resource.getResourceType())) {
+            return null;
+        }
+        return BigDecimal.valueOf(20 + pseudoRandom(resource.getId(), 2) * 70).setScale(1, RoundingMode.HALF_UP);
+    }
+
+    public static BigDecimal mockEvcsChargePowerKw(VppResourcePoint resource) {
+        if (!"EVCS".equalsIgnoreCase(resource.getResourceType())) {
+            return null;
+        }
+        BigDecimal base = resource.getCapacityKw() != null ? resource.getCapacityKw() : BigDecimal.valueOf(60);
+        return base.multiply(BigDecimal.valueOf(0.2 + pseudoRandom(resource.getId(), 3) * 0.7))
+                .setScale(2, RoundingMode.HALF_UP);
+    }
+
+    public static boolean mockAbnormal(VppResourcePoint resource, VppDevice device) {
+        if (device != null) {
+            if (device.getCommStatus() != null && device.getCommStatus() == 0) {
+                return true;
+            }
+            if (device.getRunStatus() != null && device.getRunStatus() == 2) {
+                return true;
+            }
+        }
+        return resource.getId() != null && resource.getId() % 11 == 0;
+    }
+
+    public static String mockAbnormalReason(VppResourcePoint resource, VppDevice device) {
+        if (device != null && device.getCommStatus() != null && device.getCommStatus() == 0) {
+            return "设备通信离线";
+        }
+        if (device != null && device.getRunStatus() != null && device.getRunStatus() == 2) {
+            return "设备运行故障";
+        }
+        if ("ESS".equalsIgnoreCase(resource.getResourceType())) {
+            return "SOC 低于告警阈值";
+        }
+        return "实时功率超出告警阈值";
+    }
+
+    public static String resourceTypeLabel(String resourceType) {
+        if (resourceType == null) {
+            return "未知";
+        }
+        switch (resourceType.toUpperCase()) {
+            case "PV":
+                return "光伏";
+            case "ESS":
+                return "储能";
+            case "EVCS":
+                return "充电桩";
+            case "IND_LOAD":
+                return "工业负荷";
+            case "COM_BLDG":
+                return "商业楼宇";
+            default:
+                return resourceType;
+        }
+    }
+
+    public static String runStatusLabel(VppDevice device, boolean abnormal) {
+        if (abnormal) {
+            return "异常";
+        }
+        if (device == null) {
+            return "未知";
+        }
+        if (device.getCommStatus() != null && device.getCommStatus() == 0) {
+            return "离线";
+        }
+        if (device.getRunStatus() != null && device.getRunStatus() == 1) {
+            return "运行";
+        }
+        if (device.getRunStatus() != null && device.getRunStatus() == 0) {
+            return "停机";
+        }
+        return "正常";
+    }
+
+    private static double pseudoRandom(Long seed, int salt) {
+        if (seed == null) {
+            return ThreadLocalRandom.current().nextDouble();
+        }
+        long mixed = seed * 31L + salt + LocalDateTime.now().getMinute();
+        return (mixed % 1000) / 1000.0;
+    }
+}

+ 48 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppSiteResourceHelper.java

@@ -0,0 +1,48 @@
+package com.usky.vpp.util;
+
+import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.domain.VppSite;
+import com.usky.vpp.mapper.VppSiteMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * 站点与资源点关联查询辅助
+ */
+@Component
+public class VppSiteResourceHelper {
+
+    @Autowired
+    private VppSiteMapper siteMapper;
+
+    public Map<Long, VppSite> loadSiteMap(List<VppResourcePoint> resources) {
+        if (resources == null || resources.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        List<Long> siteIds = resources.stream()
+                .map(VppResourcePoint::getSiteId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+        if (siteIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        return siteMapper.selectBatchIds(siteIds).stream()
+                .filter(site -> !VppAuditHelper.isDeleted(site.getDeleteFlag()))
+                .collect(Collectors.toMap(VppSite::getId, site -> site, (a, b) -> a));
+    }
+
+    public Long resolveCustomerId(VppResourcePoint resource, Map<Long, VppSite> siteMap) {
+        if (resource == null || resource.getSiteId() == null || siteMap == null) {
+            return null;
+        }
+        VppSite site = siteMap.get(resource.getSiteId());
+        return site != null ? site.getCustomerId() : null;
+    }
+}

+ 24 - 12
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppUnMessageBuilder.java

@@ -2,6 +2,7 @@ package com.usky.vpp.util;
 
 import com.usky.vpp.config.VppUnProperties;
 import com.usky.vpp.domain.VppResourcePoint;
+import com.usky.vpp.domain.VppSite;
 import org.springframework.util.StringUtils;
 
 import java.math.BigDecimal;
@@ -93,6 +94,7 @@ public final class VppUnMessageBuilder {
     }
 
     public static Map<String, Object> buildRegisterReportRequest(List<VppResourcePoint> resources,
+                                                                 Map<Long, VppSite> siteMap,
                                                                  VppUnProperties properties) {
         Map<String, Object> req = buildBaseRequest("RegisterReportRequest", properties);
         req.put("reportRequestID", "MetaDataReport");
@@ -100,7 +102,8 @@ public final class VppUnMessageBuilder {
         int rid = 0;
         if (resources != null) {
             for (VppResourcePoint resource : resources) {
-                reports.add(buildMetadataReportItem(resource, rid++, properties));
+                VppSite site = siteMap != null ? siteMap.get(resource.getSiteId()) : null;
+                reports.add(buildMetadataReportItem(resource, site, rid++, properties));
             }
         }
         if (reports.isEmpty()) {
@@ -112,21 +115,23 @@ public final class VppUnMessageBuilder {
 
     public static Map<String, Object> buildIntervalDataReportRequest(Map<String, Object> createReportRequest,
                                                                      List<VppResourcePoint> resources,
+                                                                     Map<Long, VppSite> siteMap,
                                                                      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));
+        req.put("pointData", buildPointData(resources, siteMap, properties));
         return req;
     }
 
     public static Map<String, Object> buildMomentDataReportRequest(Map<String, Object> createReportRequest,
                                                                    List<VppResourcePoint> resources,
+                                                                   Map<Long, VppSite> siteMap,
                                                                    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));
+        req.put("pointData", buildPointData(resources, siteMap, properties));
         return req;
     }
 
@@ -148,7 +153,7 @@ public final class VppUnMessageBuilder {
         return report;
     }
 
-    private static Map<String, Object> buildMetadataReportItem(VppResourcePoint resource, int rid,
+    private static Map<String, Object> buildMetadataReportItem(VppResourcePoint resource, VppSite site, int rid,
                                                                VppUnProperties properties) {
         Map<String, Object> report = new LinkedHashMap<>();
         report.put("createdDateTime", CREATED_DATE_TIME.format(LocalDateTime.now()));
@@ -160,9 +165,7 @@ public final class VppUnMessageBuilder {
         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());
+        String resourceId = resolveUnResourceId(resource, site, properties);
         dataSource.put("resourceID", java.util.Collections.singletonList(resourceId));
         description.put("reportDataSource", dataSource);
         description.put("readingType", "Summed");
@@ -171,6 +174,7 @@ public final class VppUnMessageBuilder {
     }
 
     private static List<Map<String, Object>> buildPointData(List<VppResourcePoint> resources,
+                                                            Map<Long, VppSite> siteMap,
                                                             VppUnProperties properties) {
         List<Map<String, Object>> pointData = new ArrayList<>();
         if (resources == null || resources.isEmpty()) {
@@ -184,11 +188,9 @@ public final class VppUnMessageBuilder {
         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;
+            VppSite site = siteMap != null ? siteMap.get(resource.getSiteId()) : null;
+            point.put("resourceID", resolveUnResourceId(resource, site, properties));
+            BigDecimal value = resource.getMaxUpKw() != null ? resource.getMaxUpKw() : BigDecimal.ZERO;
             point.put("value", value.stripTrailingZeros().toPlainString());
             point.put("timestamp", timestamp);
             pointData.add(point);
@@ -196,6 +198,16 @@ public final class VppUnMessageBuilder {
         return pointData;
     }
 
+    private static String resolveUnResourceId(VppResourcePoint resource, VppSite site, VppUnProperties properties) {
+        if (site != null && StringUtils.hasText(site.getUnResourceId())) {
+            return site.getUnResourceId();
+        }
+        if (StringUtils.hasText(resource.getResourceCode())) {
+            return resource.getResourceCode();
+        }
+        return properties.getDnId();
+    }
+
     @SuppressWarnings("unchecked")
     private static String resolveReportRequestIdFromPoll(Map<String, Object> createReportRequest, String fallback) {
         if (createReportRequest == null) {