From 63ed2641eee15fc79cbd687f5b97d00bd6a90367 Mon Sep 17 00:00:00 2001 From: dashixiong Date: Thu, 12 Feb 2026 21:05:11 +0800 Subject: [PATCH 01/10] =?UTF-8?q?=E4=B8=B4=E6=97=B6=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ems/EmsPointMatchController.java | 71 +++ .../ems/EmsSiteConfigController.java | 40 ++ .../ems/EmsSiteMonitorController.java | 31 ++ .../controller/ems/MqttMessageController.java | 139 +++--- ems-admin/src/main/resources/application.yml | 14 + ems-admin/src/main/resources/logback.xml | 4 +- .../common/constant/RedisKeyConstants.java | 6 + .../manager/MqttLifecycleManager.java | 103 +++- .../generator/controller/GenController.java | 45 +- .../xzzn/generator/mapper/GenTableMapper.java | 7 - .../service/GenTableServiceImpl.java | 14 +- .../generator/service/IGenTableService.java | 8 - .../mapper/generator/GenTableMapper.xml | 6 +- ems-system/pom.xml | 8 +- .../com/xzzn/ems/domain/EmsSiteSetting.java | 14 + .../xzzn/ems/domain/vo/PointNameRequest.java | 9 + .../ems/mapper/EmsPointEnumMatchMapper.java | 8 + .../xzzn/ems/mapper/EmsPointMatchMapper.java | 10 + .../ems/service/IEmsDeviceSettingService.java | 15 + .../ems/service/IEmsPointMatchService.java | 49 ++ .../com/xzzn/ems/service/IEmsSiteService.java | 4 + .../ems/service/IGeneralQueryService.java | 2 +- .../impl/DeviceDataProcessServiceImpl.java | 260 +++++++++- .../impl/EmsDeviceSettingServiceImpl.java | 457 ++++++++++++++++++ .../impl/EmsPointMatchServiceImpl.java | 118 +++++ .../ems/service/impl/EmsSiteServiceImpl.java | 64 ++- .../service/impl/GeneralQueryServiceImpl.java | 169 +++++-- .../mapper/ems/EmsPointEnumMatchMapper.xml | 45 +- .../mapper/ems/EmsPointMatchMapper.xml | 72 ++- 29 files changed, 1559 insertions(+), 233 deletions(-) diff --git a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointMatchController.java b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointMatchController.java index 8441650..fd8e697 100644 --- a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointMatchController.java +++ b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointMatchController.java @@ -7,8 +7,12 @@ import javax.validation.Valid; import org.apache.commons.collections4.CollectionUtils; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.xzzn.common.annotation.Log; @@ -19,6 +23,7 @@ import com.xzzn.ems.domain.EmsPointMatch; import com.xzzn.ems.domain.vo.DevicePointMatchExportVo; import com.xzzn.ems.domain.vo.DevicePointMatchVo; import com.xzzn.ems.domain.vo.ImportPointDataRequest; +import com.xzzn.ems.domain.vo.ImportPointTemplateRequest; import com.xzzn.ems.service.IEmsPointMatchService; import com.xzzn.common.utils.poi.ExcelUtil; import org.springframework.web.multipart.MultipartFile; @@ -49,6 +54,58 @@ public class EmsPointMatchController extends BaseController util.exportExcel(response, list, "点位匹配数据"); } + /** + * 查询点位配置列表 + */ + @GetMapping("/list") + public com.xzzn.common.core.page.TableDataInfo list(EmsPointMatch emsPointMatch) + { + startPage(); + List list = emsPointMatchService.selectPointMatchConfigList(emsPointMatch); + return getDataTable(list); + } + + /** + * 查询点位配置详情 + */ + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) + { + return success(emsPointMatchService.selectPointMatchById(id)); + } + + /** + * 新增点位配置 + */ + @Log(title = "点位配置", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody EmsPointMatch emsPointMatch) + { + emsPointMatch.setCreateBy(getUsername()); + return toAjax(emsPointMatchService.insertPointMatch(emsPointMatch)); + } + + /** + * 修改点位配置 + */ + @Log(title = "点位配置", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody EmsPointMatch emsPointMatch) + { + emsPointMatch.setUpdateBy(getUsername()); + return toAjax(emsPointMatchService.updatePointMatch(emsPointMatch)); + } + + /** + * 删除点位配置 + */ + @Log(title = "点位配置", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) + { + return toAjax(emsPointMatchService.deletePointMatchByIds(ids)); + } + /** * 上传点位清单 * @param file @@ -85,4 +142,18 @@ public class EmsPointMatchController extends BaseController } } + /** + * 根据站点导入模板点位配置 + * @param request 请求参数 + * @return 导入结果 + */ + @PreAuthorize("@ss.hasPermi('system:user:import')") + @Log(title = "点位配置", businessType = BusinessType.IMPORT) + @PostMapping("/importTemplateBySite") + public AjaxResult importTemplateBySite(@Valid @RequestBody ImportPointTemplateRequest request) + { + String message = emsPointMatchService.importTemplateBySite(request, getUsername()); + return success(message); + } + } diff --git a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteConfigController.java b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteConfigController.java index aec2ad4..bc24160 100644 --- a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteConfigController.java +++ b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteConfigController.java @@ -13,6 +13,7 @@ import com.xzzn.ems.domain.vo.DeviceUpdateRequest; import com.xzzn.ems.domain.vo.DevicesSettingVo; import com.xzzn.ems.domain.vo.PointDataRequest; import com.xzzn.ems.domain.vo.PointQueryResponse; +import com.xzzn.ems.domain.vo.SiteMonitorProjectPointMappingSaveRequest; import com.xzzn.ems.domain.vo.SiteDeviceListVo; import com.xzzn.ems.service.IEmsDeviceSettingService; import com.xzzn.ems.service.IEmsSiteService; @@ -59,6 +60,26 @@ public class EmsSiteConfigController extends BaseController{ return getDataTable(list); } + /** + * 新增站点 + */ + @PostMapping("/addSite") + public AjaxResult addSite(@RequestBody EmsSiteSetting emsSiteSetting) + { + emsSiteSetting.setCreateBy(getUsername()); + return toAjax(iEmsSiteService.addSite(emsSiteSetting)); + } + + /** + * 编辑站点 + */ + @PostMapping("/updateSite") + public AjaxResult updateSite(@RequestBody EmsSiteSetting emsSiteSetting) + { + emsSiteSetting.setUpdateBy(getUsername()); + return toAjax(iEmsSiteService.updateSite(emsSiteSetting)); + } + /** * 获取设备列表-分页 */ @@ -192,6 +213,25 @@ public class EmsSiteConfigController extends BaseController{ return success(iEmsDeviceSettingService.getDeviceListBySiteAndCategory(siteId, deviceCategory)); } + /** + * 获取单站监控项目点位映射 + */ + @GetMapping("/getSingleMonitorProjectPointMapping") + public AjaxResult getSingleMonitorProjectPointMapping(@RequestParam String siteId) + { + return success(iEmsDeviceSettingService.getSiteMonitorProjectPointMapping(siteId)); + } + + /** + * 保存单站监控项目点位映射 + */ + @PostMapping("/saveSingleMonitorProjectPointMapping") + public AjaxResult saveSingleMonitorProjectPointMapping(@RequestBody SiteMonitorProjectPointMappingSaveRequest request) + { + int rows = iEmsDeviceSettingService.saveSiteMonitorProjectPointMapping(request, getUsername()); + return AjaxResult.success(rows); + } + /** * PCS设备开关机 */ diff --git a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java index 3e959f5..8796797 100644 --- a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java +++ b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java @@ -9,6 +9,8 @@ import com.xzzn.ems.domain.vo.BatteryDataStatsListVo; import com.xzzn.ems.domain.vo.DateSearchRequest; import com.xzzn.ems.domain.vo.RunningGraphRequest; import com.xzzn.ems.domain.vo.SiteBatteryDataList; +import com.xzzn.ems.domain.vo.SiteMonitorDataSaveRequest; +import com.xzzn.ems.service.IEmsDeviceSettingService; import com.xzzn.ems.service.IEmsSiteService; import com.xzzn.ems.service.IEmsStatsReportService; import com.xzzn.ems.service.ISingleSiteService; @@ -33,6 +35,8 @@ public class EmsSiteMonitorController extends BaseController{ private IEmsSiteService iEmsSiteService; @Autowired private IEmsStatsReportService iemsStatsReportService; + @Autowired + private IEmsDeviceSettingService iEmsDeviceSettingService; /** * 获取单站首页数据 @@ -228,4 +232,31 @@ public class EmsSiteMonitorController extends BaseController{ return error("缺少必传项"); } } + + /** + * 单站监控项目点位配置查询 + */ + @GetMapping("/getProjectPointMapping") + public AjaxResult getProjectPointMapping(@RequestParam String siteId) + { + return success(iEmsDeviceSettingService.getSiteMonitorProjectPointMapping(siteId)); + } + + /** + * 单站监控项目展示数据查询(配置字段 + 字段值) + */ + @GetMapping("/getProjectDisplayData") + public AjaxResult getProjectDisplayData(@RequestParam String siteId) + { + return success(iEmsDeviceSettingService.getSiteMonitorProjectDisplay(siteId)); + } + + /** + * 单站监控项目展示数据写入 + */ + @PostMapping("/saveProjectDisplayData") + public AjaxResult saveProjectDisplayData(@RequestBody SiteMonitorDataSaveRequest request) + { + return AjaxResult.success(iEmsDeviceSettingService.saveSiteMonitorProjectData(request, getUsername())); + } } diff --git a/ems-admin/src/main/java/com/xzzn/web/controller/ems/MqttMessageController.java b/ems-admin/src/main/java/com/xzzn/web/controller/ems/MqttMessageController.java index ff74aa9..47ed489 100644 --- a/ems-admin/src/main/java/com/xzzn/web/controller/ems/MqttMessageController.java +++ b/ems-admin/src/main/java/com/xzzn/web/controller/ems/MqttMessageController.java @@ -8,7 +8,6 @@ import com.xzzn.ems.domain.EmsMqttTopicConfig; import com.xzzn.ems.mapper.EmsMqttTopicConfigMapper; import com.xzzn.ems.service.IDDSDataProcessService; import com.xzzn.ems.service.IDeviceDataProcessService; -import com.xzzn.ems.service.IEmsMqttMessageService; import com.xzzn.ems.service.IEmsStrategyService; import com.xzzn.ems.service.IFXXAlarmDataProcessService; import com.xzzn.ems.service.IFXXDataProcessService; @@ -27,6 +26,7 @@ import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -38,9 +38,6 @@ public class MqttMessageController implements MqttPublisher, MqttSubscriber { private final MqttLifecycleManager mqttLifecycleManager; - @Autowired - private IEmsMqttMessageService emsMqttMessageService; - @Autowired private IFXXDataProcessService fXXDataProcessService; @@ -61,6 +58,8 @@ public class MqttMessageController implements MqttPublisher, MqttSubscriber { @Autowired private RedisCache redisCache; + @Autowired + private ThreadPoolTaskExecutor threadPoolTaskExecutor; @Autowired public MqttMessageController(MqttLifecycleManager mqttLifecycleManager) { @@ -136,51 +135,32 @@ public class MqttMessageController implements MqttPublisher, MqttSubscriber { private void handleSystemStatus(String topic, MqttMessage message) { String payload = new String(message.getPayload()); System.out.println("[SYSTEM] Status update: " + payload); - - try { - emsMqttMessageService.insertMqttOriginalMessage(topic,payload); - - } catch (Exception e) { - log.error("Failed to process system status message: " + e.getMessage(), e); - } } // 处理设备数据 private void handleDeviceData(String topic, MqttMessage message) { String payload = new String(message.getPayload()); - log.error("[DEVICE] data: " + payload); - try { - // 业务处理逻辑 -// if (topic.startsWith("021_DDS")) { -// dDSDataProcessService.handleDdsData(payload); -// } else if (topic.startsWith("021_FXX")) { -// fXXDataProcessService.handleFxData(payload); -// } - deviceDataProcessService.handleDeviceData(payload, getSiteIdByTopic(topic)); - - emsMqttMessageService.insertMqttOriginalMessage(topic, payload); - } catch (Exception e) { - log.error("Failed to process device data message: " + e.getMessage(), e); - } + threadPoolTaskExecutor.execute(() -> { + log.debug("[DEVICE] data: {}", payload); + try { + deviceDataProcessService.handleDeviceData(payload, getSiteIdByTopic(topic)); + } catch (Exception e) { + log.error("Failed to process device data message: {}", e.getMessage(), e); + } + }); } // 处理告警数据 private void handleAlarmData(String topic, MqttMessage message) { String payload = new String(message.getPayload()); - System.out.println("[DEVICE] data: " + payload); - try { - // 业务处理逻辑 -// if (topic.startsWith("021_FXX")) { -// fXXAlarmDataProcessService.handleFxAlarmData(payload); -// } - deviceDataProcessService.handleAlarmData(payload, getSiteIdByTopic(topic)); - - emsMqttMessageService.insertMqttOriginalMessage(topic, payload); - } catch (Exception e) { - e.printStackTrace(); - log.error("Failed to process device alarm data message: " + e.getMessage(), e); - } + threadPoolTaskExecutor.execute(() -> { + try { + deviceDataProcessService.handleAlarmData(payload, getSiteIdByTopic(topic)); + } catch (Exception e) { + log.error("Failed to process device alarm data message: {}", e.getMessage(), e); + } + }); } @@ -191,78 +171,67 @@ public class MqttMessageController implements MqttPublisher, MqttSubscriber { siteId = topicConfig.getSiteId(); redisCache.setCacheObject(RedisKeyConstants.SITE_ID + topic, siteId); } - log.info("当前处理数据站点:" + siteId + ",topic: " + topic); return siteId; } // 处理运行策略数据:云端-本地 private void handleStrategyData(String topic, MqttMessage message) { String payload = new String(message.getPayload()); - System.out.println("[处理运行策略数据] data: " + payload); - try { - // 业务处理逻辑 - iMqttSyncLogService.handleMqttStrategyData(payload); - - emsMqttMessageService.insertMqttOriginalMessage(topic,payload); - } catch (Exception e) { - log.error("Failed to process strategy data message: " + e.getMessage(), e); - } + threadPoolTaskExecutor.execute(() -> { + try { + iMqttSyncLogService.handleMqttStrategyData(payload); + } catch (Exception e) { + log.error("Failed to process strategy data message: {}", e.getMessage(), e); + } + }); } // 处理设备保护告警策略数据:云端-本地 private void handleFaultProtPlanData(String topic, MqttMessage message) { String payload = new String(message.getPayload()); - System.out.println("[处理设备保护告警策略数据] data: " + payload); - try { - // 业务处理逻辑 - iMqttSyncLogService.handleMqttPlanData(payload); - - emsMqttMessageService.insertMqttOriginalMessage(topic,payload); - } catch (Exception e) { - log.error("Failed to process fault plan data message: " + e.getMessage(), e); - } + threadPoolTaskExecutor.execute(() -> { + try { + iMqttSyncLogService.handleMqttPlanData(payload); + } catch (Exception e) { + log.error("Failed to process fault plan data message: {}", e.getMessage(), e); + } + }); } // 处理保护策略告警信息:本地-云端 private void handleFaultAlarmData(String topic, MqttMessage message) { String payload = new String(message.getPayload()); - System.out.println("[处理本地保护策略告警信息到云端] data: " + payload); - try { - // 业务处理逻辑 - iMqttSyncLogService.handleFaultAlarmData(payload); - - emsMqttMessageService.insertMqttOriginalMessage(topic,payload); - } catch (Exception e) { - log.error("Failed to process fault plan alarm data message: " + e.getMessage(), e); - } + threadPoolTaskExecutor.execute(() -> { + try { + iMqttSyncLogService.handleFaultAlarmData(payload); + } catch (Exception e) { + log.error("Failed to process fault plan alarm data message: {}", e.getMessage(), e); + } + }); } // 处理保护策略下发日志:本地-云端 private void handleFaultPlanIssueData(String topic, MqttMessage message) { String payload = new String(message.getPayload()); - System.out.println("[处理本地保护策略下发日志到云端] data: " + payload); - try { - // 业务处理逻辑 - iMqttSyncLogService.handleFaultPlanIssueData(payload); - - emsMqttMessageService.insertMqttOriginalMessage(topic,payload); - } catch (Exception e) { - log.error("Failed to process fault plan issue log message: " + e.getMessage(), e); - } + threadPoolTaskExecutor.execute(() -> { + try { + iMqttSyncLogService.handleFaultPlanIssueData(payload); + } catch (Exception e) { + log.error("Failed to process fault plan issue log message: {}", e.getMessage(), e); + } + }); } // 处理设备状态变更日志:本地-云端 private void handleDeviceChangeLogData(String topic, MqttMessage message) { String payload = new String(message.getPayload()); - System.out.println("[处理本地的保护策略告警信息到云端] data: " + payload); - try { - // 业务处理逻辑 - iMqttSyncLogService.handleDeviceChangeLogData(payload); - - emsMqttMessageService.insertMqttOriginalMessage(topic,payload); - } catch (Exception e) { - log.error("Failed to process device change log message: " + e.getMessage(), e); - } + threadPoolTaskExecutor.execute(() -> { + try { + iMqttSyncLogService.handleDeviceChangeLogData(payload); + } catch (Exception e) { + log.error("Failed to process device change log message: {}", e.getMessage(), e); + } + }); } @Override @@ -292,4 +261,4 @@ public class MqttMessageController implements MqttPublisher, MqttSubscriber { } } -} \ No newline at end of file +} diff --git a/ems-admin/src/main/resources/application.yml b/ems-admin/src/main/resources/application.yml index cf6c040..46df20a 100644 --- a/ems-admin/src/main/resources/application.yml +++ b/ems-admin/src/main/resources/application.yml @@ -199,6 +199,20 @@ mqtt: topic: siteId: +influxdb: + enabled: true + url: http://122.51.194.184:8086/ + api-token: F2XcmBzZsWcz90ikU2_t7UXY2fzWuf2ruVp1BkusNkIS_gwrQZuiaIjl33XQMQajm7vSI6TQSRnpPSx5CXThlA== + write-method: POST + read-method: GET + write-path: /api/v2/write + query-path: /query + org: ems + bucket: point_data + database: ems_point_data + retention-policy: autogen + measurement: mqtt_point_data + modbus: pool: max-total: 20 diff --git a/ems-admin/src/main/resources/logback.xml b/ems-admin/src/main/resources/logback.xml index 8f26067..96d503c 100644 --- a/ems-admin/src/main/resources/logback.xml +++ b/ems-admin/src/main/resources/logback.xml @@ -1,7 +1,7 @@ - + @@ -90,4 +90,4 @@ - \ No newline at end of file + diff --git a/ems-common/src/main/java/com/xzzn/common/constant/RedisKeyConstants.java b/ems-common/src/main/java/com/xzzn/common/constant/RedisKeyConstants.java index 82c0a75..c627bb4 100644 --- a/ems-common/src/main/java/com/xzzn/common/constant/RedisKeyConstants.java +++ b/ems-common/src/main/java/com/xzzn/common/constant/RedisKeyConstants.java @@ -122,4 +122,10 @@ public class RedisKeyConstants /** 每个设备最新数据-设置失效时间-判断是否正常同步数据 */ public static final String SYNC_DATA_ALARM = "SYNC_DATA_ALARM_"; + + /** 点位配置缓存(按站点+设备) */ + public static final String POINT_CONFIG_DEVICE = "POINT_CONFIG_DEVICE_"; + + /** 单站监控最新数据(按站点+模块) */ + public static final String SITE_MONITOR_LATEST = "SITE_MONITOR_LATEST_"; } diff --git a/ems-framework/src/main/java/com/xzzn/framework/manager/MqttLifecycleManager.java b/ems-framework/src/main/java/com/xzzn/framework/manager/MqttLifecycleManager.java index b4c05ce..bdc0b45 100644 --- a/ems-framework/src/main/java/com/xzzn/framework/manager/MqttLifecycleManager.java +++ b/ems-framework/src/main/java/com/xzzn/framework/manager/MqttLifecycleManager.java @@ -3,21 +3,37 @@ package com.xzzn.framework.manager; import com.xzzn.ems.service.IEmsAlarmRecordsService; import org.eclipse.paho.client.mqttv3.*; import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.context.SmartLifecycle; import org.springframework.stereotype.Component; -import java.util.HashSet; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; @Component -public class MqttLifecycleManager implements ApplicationRunner, SmartLifecycle, MqttCallback { +public class MqttLifecycleManager implements ApplicationRunner, SmartLifecycle, MqttCallbackExtended { + + private static final Logger log = LoggerFactory.getLogger(MqttLifecycleManager.class); + private static final long RECONNECT_DELAY_SECONDS = 5; private final MqttConnectOptions connectOptions; private final IEmsAlarmRecordsService iEmsAlarmRecordsService; + private final ScheduledExecutorService reconnectExecutor = Executors.newSingleThreadScheduledExecutor(r -> { + Thread thread = new Thread(r); + thread.setName("mqtt-reconnect"); + thread.setDaemon(true); + return thread; + }); + private final AtomicBoolean reconnectScheduled = new AtomicBoolean(false); + private volatile ScheduledFuture reconnectFuture; private MqttClient mqttClient; private volatile boolean running = false; @@ -41,7 +57,9 @@ public class MqttLifecycleManager implements ApplicationRunner, SmartLifecycle, if (running) return; try { - String clientId = connectOptions.getUserName() + "-" + System.currentTimeMillis(); + String prefix = connectOptions.getUserName() == null || connectOptions.getUserName().isEmpty() + ? "mqtt-client" : connectOptions.getUserName(); + String clientId = prefix + "-" + System.currentTimeMillis(); mqttClient = new MqttClient( connectOptions.getServerURIs()[0], clientId, @@ -51,27 +69,28 @@ public class MqttLifecycleManager implements ApplicationRunner, SmartLifecycle, mqttClient.setCallback(this); mqttClient.connect(connectOptions); - // 重连后自动重新订阅 resubscribeAll(); - running = true; - System.out.println("MQTT client connected to: " + connectOptions.getServerURIs()[0]); + log.info("MQTT client connected to: {}", connectOptions.getServerURIs()[0]); } catch (MqttException e) { - System.err.println("MQTT connection failed: " + e.getMessage()); - // 添加重试逻辑 + running = false; + log.error("MQTT connection failed: {}", e.getMessage(), e); + scheduleReconnect(); } } @Override public void stop() { + cancelReconnectTask(); if (mqttClient != null && mqttClient.isConnected()) { try { mqttClient.disconnect(); mqttClient.close(); } catch (MqttException e) { - System.err.println("Error disconnecting MQTT client: " + e.getMessage()); + log.warn("Error disconnecting MQTT client: {}", e.getMessage(), e); } } + reconnectExecutor.shutdownNow(); running = false; } @@ -83,9 +102,17 @@ public class MqttLifecycleManager implements ApplicationRunner, SmartLifecycle, // MQTT 回调方法 @Override public void connectionLost(Throwable cause) { - System.err.println("MQTT connection lost: " + cause.getMessage()); + log.warn("MQTT connection lost: {}", cause == null ? "unknown" : cause.getMessage(), cause); running = false; - // 自动重连由 MqttConnectOptions 处理 + scheduleReconnect(); + } + + @Override + public void connectComplete(boolean reconnect, String serverURI) { + running = true; + cancelReconnectTask(); + log.info("MQTT connection complete, reconnect: {}, serverURI: {}", reconnect, serverURI); + resubscribeAll(); } @Override @@ -106,14 +133,16 @@ public class MqttLifecycleManager implements ApplicationRunner, SmartLifecycle, try { if (mqttClient != null && mqttClient.isConnected()) { mqttClient.subscribe(topic, qos); + log.info("MQTT subscribe success, topic: {}, qos: {}", topic, qos); + } else { + log.warn("MQTT subscribe deferred, client not connected, topic: {}", topic); } subscriptions.put(topic, new SubscriptionInfo(listener, qos)); } catch (MqttException e) { - System.err.println("Subscribe failed: " + e.getMessage()); - // 订阅失败-增加告警 + log.error("Subscribe failed, topic: {}, err: {}", topic, e.getMessage(), e); iEmsAlarmRecordsService.addSubFailedAlarmRecord(topic); + scheduleReconnect(); } - // 订阅成功了-校验是否存在未处理或者处理中的订阅失败信息 iEmsAlarmRecordsService.checkFailedRecord(topic); } @@ -135,12 +164,52 @@ public class MqttLifecycleManager implements ApplicationRunner, SmartLifecycle, subscriptions.forEach((topic, info) -> { try { mqttClient.subscribe(topic, info.getQos()); + log.info("MQTT resubscribe success, topic: {}, qos: {}", topic, info.getQos()); } catch (MqttException e) { - System.err.println("Resubscribe failed for topic: " + topic); + log.error("Resubscribe failed for topic: {}, err: {}", topic, e.getMessage(), e); } }); } + private void scheduleReconnect() { + if (mqttClient == null || reconnectExecutor.isShutdown()) { + return; + } + if (!reconnectScheduled.compareAndSet(false, true)) { + return; + } + + reconnectFuture = reconnectExecutor.scheduleWithFixedDelay(() -> { + if (mqttClient == null) { + cancelReconnectTask(); + return; + } + if (mqttClient.isConnected()) { + cancelReconnectTask(); + return; + } + try { + log.info("MQTT reconnecting..."); + mqttClient.connect(connectOptions); + running = true; + cancelReconnectTask(); + resubscribeAll(); + log.info("MQTT reconnect success."); + } catch (MqttException e) { + running = false; + log.warn("MQTT reconnect failed: {}", e.getMessage()); + } + }, RECONNECT_DELAY_SECONDS, RECONNECT_DELAY_SECONDS, TimeUnit.SECONDS); + } + + private void cancelReconnectTask() { + reconnectScheduled.set(false); + ScheduledFuture future = reconnectFuture; + if (future != null && !future.isCancelled()) { + future.cancel(false); + } + } + // 订阅信息内部类 private static class SubscriptionInfo { private final IMqttMessageListener listener; @@ -159,4 +228,4 @@ public class MqttLifecycleManager implements ApplicationRunner, SmartLifecycle, return qos; } } -} \ No newline at end of file +} diff --git a/ems-generator/src/main/java/com/xzzn/generator/controller/GenController.java b/ems-generator/src/main/java/com/xzzn/generator/controller/GenController.java index a3c8e2d..248b0b6 100644 --- a/ems-generator/src/main/java/com/xzzn/generator/controller/GenController.java +++ b/ems-generator/src/main/java/com/xzzn/generator/controller/GenController.java @@ -1,7 +1,6 @@ package com.xzzn.generator.controller; import java.io.IOException; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -18,10 +17,6 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import com.alibaba.druid.DbType; -import com.alibaba.druid.sql.SQLUtils; -import com.alibaba.druid.sql.ast.SQLStatement; -import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlCreateTableStatement; import com.xzzn.common.annotation.Log; import com.xzzn.common.core.controller.BaseController; import com.xzzn.common.core.domain.AjaxResult; @@ -29,7 +24,6 @@ import com.xzzn.common.core.page.TableDataInfo; import com.xzzn.common.core.text.Convert; import com.xzzn.common.enums.BusinessType; import com.xzzn.common.utils.SecurityUtils; -import com.xzzn.common.utils.sql.SqlUtil; import com.xzzn.generator.config.GenConfig; import com.xzzn.generator.domain.GenTable; import com.xzzn.generator.domain.GenTableColumn; @@ -121,43 +115,6 @@ public class GenController extends BaseController return success(); } - /** - * 创建表结构(保存) - */ - @PreAuthorize("@ss.hasRole('admin')") - @Log(title = "创建表", businessType = BusinessType.OTHER) - @PostMapping("/createTable") - public AjaxResult createTableSave(String sql) - { - try - { - SqlUtil.filterKeyword(sql); - List sqlStatements = SQLUtils.parseStatements(sql, DbType.mysql); - List tableNames = new ArrayList<>(); - for (SQLStatement sqlStatement : sqlStatements) - { - if (sqlStatement instanceof MySqlCreateTableStatement) - { - MySqlCreateTableStatement createTableStatement = (MySqlCreateTableStatement) sqlStatement; - if (genTableService.createTable(createTableStatement.toString())) - { - String tableName = createTableStatement.getTableName().replaceAll("`", ""); - tableNames.add(tableName); - } - } - } - List tableList = genTableService.selectDbTableListByNames(tableNames.toArray(new String[tableNames.size()])); - String operName = SecurityUtils.getUsername(); - genTableService.importGenTable(tableList, operName); - return AjaxResult.success(); - } - catch (Exception e) - { - logger.error(e.getMessage(), e); - return AjaxResult.error("创建表结构异常"); - } - } - /** * 修改保存代码生成业务 */ @@ -260,4 +217,4 @@ public class GenController extends BaseController response.setContentType("application/octet-stream; charset=UTF-8"); IOUtils.write(data, response.getOutputStream()); } -} \ No newline at end of file +} diff --git a/ems-generator/src/main/java/com/xzzn/generator/mapper/GenTableMapper.java b/ems-generator/src/main/java/com/xzzn/generator/mapper/GenTableMapper.java index 0bcd959..c10cd22 100644 --- a/ems-generator/src/main/java/com/xzzn/generator/mapper/GenTableMapper.java +++ b/ems-generator/src/main/java/com/xzzn/generator/mapper/GenTableMapper.java @@ -81,11 +81,4 @@ public interface GenTableMapper */ public int deleteGenTableByIds(Long[] ids); - /** - * 创建表 - * - * @param sql 表结构 - * @return 结果 - */ - public int createTable(String sql); } diff --git a/ems-generator/src/main/java/com/xzzn/generator/service/GenTableServiceImpl.java b/ems-generator/src/main/java/com/xzzn/generator/service/GenTableServiceImpl.java index 7b0ff5a..0e51245 100644 --- a/ems-generator/src/main/java/com/xzzn/generator/service/GenTableServiceImpl.java +++ b/ems-generator/src/main/java/com/xzzn/generator/service/GenTableServiceImpl.java @@ -149,18 +149,6 @@ public class GenTableServiceImpl implements IGenTableService genTableColumnMapper.deleteGenTableColumnByIds(tableIds); } - /** - * 创建表 - * - * @param sql 创建表语句 - * @return 结果 - */ - @Override - public boolean createTable(String sql) - { - return genTableMapper.createTable(sql) == 0; - } - /** * 导入表结构 * @@ -528,4 +516,4 @@ public class GenTableServiceImpl implements IGenTableService } return genPath + File.separator + VelocityUtils.getFileName(template, table); } -} \ No newline at end of file +} diff --git a/ems-generator/src/main/java/com/xzzn/generator/service/IGenTableService.java b/ems-generator/src/main/java/com/xzzn/generator/service/IGenTableService.java index f64f132..6b78595 100644 --- a/ems-generator/src/main/java/com/xzzn/generator/service/IGenTableService.java +++ b/ems-generator/src/main/java/com/xzzn/generator/service/IGenTableService.java @@ -66,14 +66,6 @@ public interface IGenTableService */ public void deleteGenTableByIds(Long[] tableIds); - /** - * 创建表 - * - * @param sql 创建表语句 - * @return 结果 - */ - public boolean createTable(String sql); - /** * 导入表结构 * diff --git a/ems-generator/src/main/resources/mapper/generator/GenTableMapper.xml b/ems-generator/src/main/resources/mapper/generator/GenTableMapper.xml index 2322914..3bf7137 100644 --- a/ems-generator/src/main/resources/mapper/generator/GenTableMapper.xml +++ b/ems-generator/src/main/resources/mapper/generator/GenTableMapper.xml @@ -171,10 +171,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" ) - - ${sql} - - update gen_table @@ -207,4 +203,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - \ No newline at end of file + diff --git a/ems-system/pom.xml b/ems-system/pom.xml index 7f2b1e1..305a0f8 100644 --- a/ems-system/pom.xml +++ b/ems-system/pom.xml @@ -27,6 +27,12 @@ jaxb-runtime + + org.influxdb + influxdb-java + 2.24 + + - \ No newline at end of file + diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteSetting.java b/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteSetting.java index 9ef28ee..e67d0d2 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteSetting.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteSetting.java @@ -54,6 +54,9 @@ public class EmsSiteSetting extends BaseEntity @Excel(name = "站点id") private String siteId; + /** 授权状态:true-已授权,false-未授权 */ + private Boolean authorized; + public void setId(Long id) { this.id = id; @@ -144,6 +147,16 @@ public class EmsSiteSetting extends BaseEntity return siteId; } + public void setAuthorized(Boolean authorized) + { + this.authorized = authorized; + } + + public Boolean getAuthorized() + { + return authorized; + } + @Override public String toString() { return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) @@ -161,6 +174,7 @@ public class EmsSiteSetting extends BaseEntity .append("createTime", getCreateTime()) .append("updateTime", getUpdateTime()) .append("siteId", getSiteId()) + .append("authorized", getAuthorized()) .toString(); } } diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointNameRequest.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointNameRequest.java index 8552c37..9098c7a 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointNameRequest.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointNameRequest.java @@ -14,6 +14,7 @@ public class PointNameRequest { private String deviceCategory; private String pointName; + private List pointNames; /** 数据分组 1-分钟 2-小时 3-天 */ private int dataUnit; @@ -50,6 +51,14 @@ public class PointNameRequest { this.pointName = pointName; } + public List getPointNames() { + return pointNames; + } + + public void setPointNames(List pointNames) { + this.pointNames = pointNames; + } + public int getDataUnit() { return dataUnit; } diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointEnumMatchMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointEnumMatchMapper.java index f517c59..829b058 100644 --- a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointEnumMatchMapper.java +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointEnumMatchMapper.java @@ -67,4 +67,12 @@ public interface EmsPointEnumMatchMapper public List selectList(@Param("siteId") String siteId, @Param("deviceCategory") String deviceCategory, @Param("matchField") String matchField); + + int countBySiteId(@Param("siteId") String siteId); + + int deleteBySiteId(@Param("siteId") String siteId); + + int copyTemplateToSite(@Param("templateSiteId") String templateSiteId, + @Param("targetSiteId") String targetSiteId, + @Param("operName") String operName); } diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointMatchMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointMatchMapper.java index 0a9e0cf..622f584 100644 --- a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointMatchMapper.java +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointMatchMapper.java @@ -172,4 +172,14 @@ public interface EmsPointMatchMapper int getDevicePointAlarmNum(@Param("siteId") String siteId, @Param("deviceId") String deviceId, @Param("deviceCategory") String deviceCategory); List selectDeviceStatusPoint(@Param("request") DeviceUpdateRequest request); + + int countBySiteId(@Param("siteId") String siteId); + + int deleteBySiteId(@Param("siteId") String siteId); + + int copyTemplateToSite(@Param("templateSiteId") String templateSiteId, + @Param("targetSiteId") String targetSiteId, + @Param("operName") String operName); + + List selectBySiteId(@Param("siteId") String siteId); } diff --git a/ems-system/src/main/java/com/xzzn/ems/service/IEmsDeviceSettingService.java b/ems-system/src/main/java/com/xzzn/ems/service/IEmsDeviceSettingService.java index 554bdee..c6369bc 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/IEmsDeviceSettingService.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/IEmsDeviceSettingService.java @@ -6,7 +6,12 @@ import com.xzzn.ems.domain.vo.DeviceUpdateRequest; import com.xzzn.ems.domain.vo.DevicesSettingVo; import com.xzzn.ems.domain.vo.PointDataRequest; import com.xzzn.ems.domain.vo.PointQueryResponse; +import com.xzzn.ems.domain.vo.SiteMonitorDataSaveRequest; +import com.xzzn.ems.domain.vo.SiteMonitorProjectDisplayVo; +import com.xzzn.ems.domain.vo.SiteMonitorProjectPointMappingSaveRequest; +import com.xzzn.ems.domain.vo.SiteMonitorProjectPointMappingVo; +import java.util.Date; import java.util.List; import java.util.Map; @@ -36,4 +41,14 @@ public interface IEmsDeviceSettingService public List> getDeviceListBySiteAndCategory(String siteId, String deviceCategory); public boolean updateDeviceStatus(DeviceUpdateRequest request); + + public List getSiteMonitorProjectPointMapping(String siteId); + + public int saveSiteMonitorProjectPointMapping(SiteMonitorProjectPointMappingSaveRequest request, String operName); + + public List getSiteMonitorProjectDisplay(String siteId); + + public int saveSiteMonitorProjectData(SiteMonitorDataSaveRequest request, String operName); + + public int syncSiteMonitorDataByMqtt(String siteId, String deviceId, String jsonData, Date valueTime); } diff --git a/ems-system/src/main/java/com/xzzn/ems/service/IEmsPointMatchService.java b/ems-system/src/main/java/com/xzzn/ems/service/IEmsPointMatchService.java index 121ec50..394ceb5 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/IEmsPointMatchService.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/IEmsPointMatchService.java @@ -6,6 +6,7 @@ import com.xzzn.ems.domain.EmsPointMatch; import com.xzzn.ems.domain.vo.DevicePointMatchExportVo; import com.xzzn.ems.domain.vo.DevicePointMatchVo; import com.xzzn.ems.domain.vo.ImportPointDataRequest; +import com.xzzn.ems.domain.vo.ImportPointTemplateRequest; /** * 点位匹配Service接口 @@ -40,4 +41,52 @@ public interface IEmsPointMatchService * @throws Exception */ public List importDataByDevice(ImportPointDataRequest request, String operName); + + /** + * 按站点导入模板点位数据 + * @param request 请求参数 + * @param operName 操作人 + * @return 导入结果信息 + */ + public String importTemplateBySite(ImportPointTemplateRequest request, String operName); + + /** + * 查询点位配置列表 + * + * @param emsPointMatch 点位配置 + * @return 点位配置列表 + */ + public List selectPointMatchConfigList(EmsPointMatch emsPointMatch); + + /** + * 查询点位配置详情 + * + * @param id 主键ID + * @return 点位配置 + */ + public EmsPointMatch selectPointMatchById(Long id); + + /** + * 新增点位配置 + * + * @param emsPointMatch 点位配置 + * @return 结果 + */ + public int insertPointMatch(EmsPointMatch emsPointMatch); + + /** + * 修改点位配置 + * + * @param emsPointMatch 点位配置 + * @return 结果 + */ + public int updatePointMatch(EmsPointMatch emsPointMatch); + + /** + * 删除点位配置 + * + * @param ids 主键集合 + * @return 结果 + */ + public int deletePointMatchByIds(Long[] ids); } diff --git a/ems-system/src/main/java/com/xzzn/ems/service/IEmsSiteService.java b/ems-system/src/main/java/com/xzzn/ems/service/IEmsSiteService.java index 298266f..df32981 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/IEmsSiteService.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/IEmsSiteService.java @@ -32,4 +32,8 @@ public interface IEmsSiteService public List> getAllPcsInfo(String siteId); public List> getParentCategoryDeviceId(String siteId, String deviceCategory); + + int addSite(EmsSiteSetting emsSiteSetting); + + int updateSite(EmsSiteSetting emsSiteSetting); } diff --git a/ems-system/src/main/java/com/xzzn/ems/service/IGeneralQueryService.java b/ems-system/src/main/java/com/xzzn/ems/service/IGeneralQueryService.java index 325993f..c245f3d 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/IGeneralQueryService.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/IGeneralQueryService.java @@ -13,7 +13,7 @@ public interface IGeneralQueryService { // 模糊查询获取点位名称List - public List getPointNameList(PointNameRequest request); + public List getPointNameList(PointNameRequest request); // 根据条件获取点位数据变化 public List getPointValueList(PointNameRequest request); diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java index 6663e82..0f26d46 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java @@ -48,12 +48,14 @@ import com.xzzn.ems.mapper.EmsEmsDataMapper; import com.xzzn.ems.mapper.EmsPcsAlarmDataMapper; import com.xzzn.ems.mapper.EmsPcsBranchDataMapper; import com.xzzn.ems.mapper.EmsPcsDataMapper; +import com.xzzn.ems.mapper.EmsPointConfigMapper; import com.xzzn.ems.mapper.EmsStackAlarmDataMapper; import com.xzzn.ems.mapper.EmsXfDataMapper; import com.xzzn.ems.service.IDeviceDataProcessService; import com.xzzn.ems.service.IEmsAlarmRecordsService; import com.xzzn.ems.service.IEmsDeviceSettingService; import com.xzzn.ems.service.IEmsEnergyPriceConfigService; +import com.xzzn.ems.service.InfluxPointDataWriter; import com.xzzn.ems.utils.AbstractBatteryDataProcessor; import com.xzzn.ems.utils.DevicePointMatchDataProcessor; @@ -68,15 +70,25 @@ import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.Collections; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + import org.apache.commons.collections4.CollectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -91,6 +103,13 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i private static final Pattern PATTERN = Pattern.compile("(BMSD\\d{2})(ZT|SOC|SOH|DL|DY|BDSC)"); // 匹配DTDC+数字格式的正则(提取序号) private static final Pattern DTDC_PATTERN = Pattern.compile("DTDC(\\d+)([A-Za-z]*)"); + private static final Pattern DB_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$"); + private static final int POINT_QUEUE_CAPACITY = 100000; + private static final int POINT_FLUSH_BATCH_SIZE = 2000; + private static final int POINT_FLUSH_MAX_DRAIN_PER_RUN = 20000; + private static final int POINT_ENQUEUE_RETRY_TIMES = 3; + private static final long POINT_ENQUEUE_RETRY_WAIT_MS = 10; + private static final long POINT_FLUSH_INTERVAL_MS = 100; @Autowired private EmsBatteryClusterMapper emsBatteryClusterMapper; @@ -101,6 +120,8 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i @Autowired private EmsPcsDataMapper emsPcsDataMapper; @Autowired + private EmsPointConfigMapper emsPointConfigMapper; + @Autowired private EmsPcsBranchDataMapper emsPcsBranchDataMapper; @Autowired private EmsAmmeterDataMapper emsAmmeterDataMapper; @@ -145,17 +166,41 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i @Autowired private RedisCache redisCache; + @Autowired + private InfluxPointDataWriter influxPointDataWriter; + private final BlockingQueue pointDataQueue = new LinkedBlockingQueue<>(POINT_QUEUE_CAPACITY); + private final ScheduledExecutorService pointDataWriter = Executors.newSingleThreadScheduledExecutor(r -> { + Thread thread = new Thread(r); + thread.setName("point-data-writer"); + thread.setDaemon(true); + return thread; + }); + private final AtomicBoolean pointDataFlushing = new AtomicBoolean(false); // 构造方法(调用父类构造) public DeviceDataProcessServiceImpl(ObjectMapper objectMapper) { super(objectMapper); } + @PostConstruct + public void initPointDataBatchWriter() { + pointDataWriter.scheduleWithFixedDelay(this::flushPointDataSafely, + POINT_FLUSH_INTERVAL_MS, POINT_FLUSH_INTERVAL_MS, TimeUnit.MILLISECONDS); + } + + @PreDestroy + public void destroyPointDataBatchWriter() { + flushPointDataSafely(); + pointDataWriter.shutdown(); + } + @Override public void handleDeviceData(String message, String siteId) { JSONArray arraylist = parseJsonData(message); if (arraylist == null) return; - // 过滤掉空数据(空数据不处理,直接返回 + long startMs = System.currentTimeMillis(); + log.info("开始处理设备数据, siteId: {}, messageSize: {}", siteId, arraylist.size()); + Set deviceIds = new LinkedHashSet<>(); for (int i = 0; i < arraylist.size(); i++) { JSONObject obj = JSONObject.parseObject(arraylist.get(i).toString()); @@ -165,21 +210,210 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i Long timestamp = obj.getLong("timestamp"); Date dataUpdateTime = DateUtils.convertUpdateTime(timestamp); - log.info("deviceId:" + deviceId); - boolean isEmpty = checkJsonDataEmpty(jsonData); - if (isEmpty) { - // 添加设备告警 - iEmsAlarmRecordsService.addEmptyDataAlarmRecord(siteId, deviceId); - return; + if (StringUtils.isNotBlank(deviceId)) { + deviceIds.add(deviceId); + } + if (checkJsonDataEmpty(jsonData)) { + continue; } - // 存放mqtt原始每个设备最晚一次数据,便于后面点位获取数据 - redisCache.setCacheObject(RedisKeyConstants.ORIGINAL_MQTT_DATA + siteId + "_" + deviceId, obj); - // 存放每次同步数据,失效时间(同同步时间)-用于判断是否正常同步数据 - redisCache.setCacheObject(RedisKeyConstants.SYNC_DATA + siteId + "_" + deviceId, obj, 1, TimeUnit.MINUTES); + try { + // 保留每个设备最新一条原始报文,供“点位最新值”从 Redis 读取 + redisCache.setCacheObject(RedisKeyConstants.ORIGINAL_MQTT_DATA + siteId + "_" + deviceId, obj); + redisCache.setCacheObject(RedisKeyConstants.SYNC_DATA + siteId + "_" + deviceId, obj, 1, TimeUnit.MINUTES); - // 处理设备数据 - processingDeviceData(siteId, deviceId, jsonData, dataUpdateTime); + processPointConfigData(siteId, deviceId, jsonData, dataUpdateTime); + iEmsDeviceSettingService.syncSiteMonitorDataByMqtt(siteId, deviceId, jsonData, dataUpdateTime); + } catch (Exception e) { + log.warn("点位映射数据处理失败,siteId: {}, deviceId: {}, err: {}", + siteId, deviceId, e.getMessage(), e); + } + } + log.info("结束处理设备数据, siteId: {}, deviceCount: {}, deviceIds: {}, costMs: {}", + siteId, deviceIds.size(), String.join(",", deviceIds), System.currentTimeMillis() - startMs); + } + + private void processPointConfigData(String siteId, String deviceId, String jsonData, Date dataUpdateTime) { + if (StringUtils.isAnyBlank(siteId, deviceId, jsonData)) { + return; + } + if (!DB_NAME_PATTERN.matcher(siteId).matches()) { + log.warn("站点ID不合法,跳过点位映射落库,siteId: {}", siteId); + return; + } + + Map dataMap = JSON.parseObject(jsonData, new TypeReference>() { + }); + if (org.apache.commons.collections4.MapUtils.isEmpty(dataMap)) { + return; + } + + List pointConfigs = getPointConfigsWithCache(siteId, deviceId); + if (CollectionUtils.isEmpty(pointConfigs)) { + return; + } + + Map configByDataKey = pointConfigs.stream() + .filter(cfg -> StringUtils.isNotBlank(cfg.getDataKey())) + .collect(Collectors.toMap( + cfg -> cfg.getDataKey().toUpperCase(), + cfg -> cfg, + (oldValue, newValue) -> oldValue + )); + + for (Map.Entry entry : dataMap.entrySet()) { + String dataKey = entry.getKey(); + if (StringUtils.isBlank(dataKey)) { + continue; + } + EmsPointConfig pointConfig = configByDataKey.get(dataKey.toUpperCase()); + if (pointConfig == null) { + continue; + } + + BigDecimal pointValue = StringUtils.getBigDecimal(entry.getValue()); + if (pointValue == null) { + continue; + } + + // 支持按配置进行二次转换:f(x)=A*x^2 + K*x + B + pointValue = convertPointValue(pointValue, pointConfig); + + enqueuePointData(siteId, deviceId, pointConfig.getDataKey(), pointValue, dataUpdateTime); + } + } + + private void enqueuePointData(String siteId, String deviceId, String pointKey, BigDecimal pointValue, Date dataTime) { + if (StringUtils.isAnyBlank(siteId, deviceId, pointKey) || pointValue == null) { + return; + } + PointDataRecord record = new PointDataRecord(siteId, deviceId, pointKey, pointValue, dataTime); + if (pointDataQueue.offer(record)) { + if (pointDataQueue.remainingCapacity() <= POINT_FLUSH_BATCH_SIZE) { + flushPointDataSafely(); + } + return; + } + + // 队列压力较高时,优先触发冲刷并短暂重试,避免 MQTT 处理线程直接阻塞在数据库写入上。 + for (int i = 0; i < POINT_ENQUEUE_RETRY_TIMES; i++) { + flushPointDataSafely(); + try { + if (pointDataQueue.offer(record, POINT_ENQUEUE_RETRY_WAIT_MS, TimeUnit.MILLISECONDS)) { + return; + } + } catch (InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + break; + } + } + + log.warn("点位写入队列持续拥塞,降级同步写入 InfluxDB,siteId: {}, queueSize: {}", + siteId, pointDataQueue.size()); + writePointDataToInflux(Collections.singletonList(record)); + } + + private BigDecimal convertPointValue(BigDecimal sourceValue, EmsPointConfig pointConfig) { + if (sourceValue == null || pointConfig == null) { + return sourceValue; + } + BigDecimal a = pointConfig.getDataA() == null ? BigDecimal.ZERO : pointConfig.getDataA(); + BigDecimal k = pointConfig.getDataK() == null ? BigDecimal.ONE : pointConfig.getDataK(); + BigDecimal b = pointConfig.getDataB() == null ? BigDecimal.ZERO : pointConfig.getDataB(); + return a.multiply(sourceValue).multiply(sourceValue) + .add(k.multiply(sourceValue)) + .add(b); + } + + private void flushPointDataSafely() { + if (!pointDataFlushing.compareAndSet(false, true)) { + return; + } + try { + int drainedCount = 0; + while (drainedCount < POINT_FLUSH_MAX_DRAIN_PER_RUN) { + List batch = new ArrayList<>(POINT_FLUSH_BATCH_SIZE); + pointDataQueue.drainTo(batch, POINT_FLUSH_BATCH_SIZE); + if (batch.isEmpty()) { + break; + } + drainedCount += batch.size(); + writePointDataToInflux(batch); + } + + if (pointDataQueue.remainingCapacity() <= POINT_FLUSH_BATCH_SIZE) { + log.warn("点位写入队列积压较高,queueSize: {}", pointDataQueue.size()); + } + } catch (Exception e) { + log.error("批量写入点位数据失败: {}", e.getMessage(), e); + } finally { + pointDataFlushing.set(false); + } + } + + private void writePointDataToInflux(List records) { + if (CollectionUtils.isEmpty(records)) { + return; + } + List payloads = records.stream() + .map(item -> new InfluxPointDataWriter.PointWritePayload( + item.getSiteId(), + item.getDeviceId(), + item.getPointKey(), + item.getPointValue(), + item.getDataTime())) + .collect(Collectors.toList()); + influxPointDataWriter.writeBatch(payloads); + } + + private List getPointConfigsWithCache(String siteId, String deviceId) { + String cacheKey = RedisKeyConstants.POINT_CONFIG_DEVICE + siteId + "_" + deviceId; + List cached = redisCache.getCacheObject(cacheKey); + if (cached != null) { + return cached; + } + EmsPointConfig query = new EmsPointConfig(); + query.setSiteId(siteId); + query.setDeviceId(deviceId); + List latest = emsPointConfigMapper.selectEmsPointConfigList(query); + List cacheValue = latest == null ? new ArrayList<>() : latest; + redisCache.setCacheObject(cacheKey, cacheValue); + return cacheValue; + } + + private static class PointDataRecord { + private final String siteId; + private final String deviceId; + private final String pointKey; + private final BigDecimal pointValue; + private final Date dataTime; + + private PointDataRecord(String siteId, String deviceId, String pointKey, BigDecimal pointValue, Date dataTime) { + this.siteId = siteId; + this.deviceId = deviceId; + this.pointKey = pointKey; + this.pointValue = pointValue; + this.dataTime = dataTime; + } + + private String getSiteId() { + return siteId; + } + + private String getDeviceId() { + return deviceId; + } + + private String getPointKey() { + return pointKey; + } + + private BigDecimal getPointValue() { + return pointValue; + } + + private Date getDataTime() { + return dataTime; } } diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java index b9f1ab0..f950484 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java @@ -17,15 +17,26 @@ import com.xzzn.common.exception.ServiceException; import com.xzzn.common.utils.DateUtils; import com.xzzn.common.utils.StringUtils; import com.xzzn.ems.domain.EmsDevicesSetting; +import com.xzzn.ems.domain.EmsPointMatch; import com.xzzn.ems.domain.EmsPcsSetting; +import com.xzzn.ems.domain.EmsSiteMonitorItem; +import com.xzzn.ems.domain.EmsSiteMonitorPointMatch; import com.xzzn.ems.domain.vo.DeviceUpdateRequest; import com.xzzn.ems.domain.vo.DevicesSettingVo; import com.xzzn.ems.domain.vo.PointDataRequest; import com.xzzn.ems.domain.vo.PointQueryResponse; +import com.xzzn.ems.domain.vo.SiteMonitorDataSaveItemVo; +import com.xzzn.ems.domain.vo.SiteMonitorDataSaveRequest; +import com.xzzn.ems.domain.vo.SiteMonitorProjectDisplayVo; +import com.xzzn.ems.domain.vo.SiteMonitorProjectPointMappingSaveRequest; +import com.xzzn.ems.domain.vo.SiteMonitorProjectPointMappingVo; import com.xzzn.ems.mapper.EmsBatteryDataMinutesMapper; import com.xzzn.ems.mapper.EmsDevicesSettingMapper; import com.xzzn.ems.mapper.EmsPcsSettingMapper; import com.xzzn.ems.mapper.EmsPointMatchMapper; +import com.xzzn.ems.mapper.EmsSiteMonitorDataMapper; +import com.xzzn.ems.mapper.EmsSiteMonitorItemMapper; +import com.xzzn.ems.mapper.EmsSiteMonitorPointMatchMapper; import com.xzzn.ems.service.IEmsDeviceSettingService; import java.math.BigDecimal; @@ -37,6 +48,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -56,6 +68,12 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService private static final Logger log = LoggerFactory.getLogger(EmsDeviceSettingServiceImpl.class); private static final String DDS_SITE_ID = "021_DDS_01"; + private static final String MODULE_HOME = "HOME"; + private static final String MODULE_SBJK = "SBJK"; + private static final String MODULE_TJBB = "TJBB"; + private static final String HISTORY_TABLE_HOME = "ems_site_monitor_data_home_his"; + private static final String HISTORY_TABLE_SBJK = "ems_site_monitor_data_sbjk_his"; + private static final String HISTORY_TABLE_TJBB = "ems_site_monitor_data_tjbb_his"; @Autowired private EmsDevicesSettingMapper emsDevicesMapper; @Autowired @@ -70,6 +88,12 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService private EmsBatteryClusterServiceImpl emsBatteryClusterServiceImpl; @Autowired private ModbusProcessor modbusProcessor; + @Autowired + private EmsSiteMonitorItemMapper emsSiteMonitorItemMapper; + @Autowired + private EmsSiteMonitorPointMatchMapper emsSiteMonitorPointMatchMapper; + @Autowired + private EmsSiteMonitorDataMapper emsSiteMonitorDataMapper; /** * 获取设备详细信息 @@ -376,6 +400,439 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService return emsDevicesMapper.getAllDeviceCategoryBySiteId(siteId); } + @Override + public List getSiteMonitorProjectPointMapping(String siteId) { + List result = new ArrayList<>(); + if (StringUtils.isBlank(siteId)) { + return result; + } + List itemList = emsSiteMonitorItemMapper.selectEnabledList(); + if (itemList == null || itemList.isEmpty()) { + return result; + } + List mappingList = emsSiteMonitorPointMatchMapper.selectBySiteId(siteId); + Map pointMap = mappingList.stream() + .filter(item -> StringUtils.isNotBlank(item.getFieldCode())) + .collect(Collectors.toMap(EmsSiteMonitorPointMatch::getFieldCode, EmsSiteMonitorPointMatch::getDataPoint, (a, b) -> b)); + + itemList.forEach(item -> { + SiteMonitorProjectPointMappingVo vo = new SiteMonitorProjectPointMappingVo(); + vo.setModuleCode(item.getModuleCode()); + vo.setModuleName(item.getModuleName()); + vo.setMenuCode(item.getMenuCode()); + vo.setMenuName(item.getMenuName()); + vo.setSectionName(item.getSectionName()); + vo.setFieldCode(item.getFieldCode()); + vo.setFieldName(item.getFieldName()); + vo.setDataPoint(pointMap.getOrDefault(item.getFieldCode(), "")); + result.add(vo); + }); + return result; + } + + @Override + public int saveSiteMonitorProjectPointMapping(SiteMonitorProjectPointMappingSaveRequest request, String operName) { + if (request == null || StringUtils.isBlank(request.getSiteId())) { + throw new ServiceException("站点ID不能为空"); + } + String siteId = request.getSiteId(); + List itemList = emsSiteMonitorItemMapper.selectEnabledList(); + if (itemList == null || itemList.isEmpty()) { + throw new ServiceException("单站监控配置项为空,请先初始化 ems_site_monitor_item"); + } + Set fieldCodeSet = itemList.stream().map(EmsSiteMonitorItem::getFieldCode).collect(Collectors.toSet()); + emsSiteMonitorPointMatchMapper.deleteBySiteId(siteId); + + if (request.getMappings() == null || request.getMappings().isEmpty()) { + return 0; + } + List saveList = new ArrayList<>(); + for (SiteMonitorProjectPointMappingVo mapping : request.getMappings()) { + if (mapping == null || StringUtils.isBlank(mapping.getFieldCode()) || StringUtils.isBlank(mapping.getDataPoint())) { + continue; + } + String fieldCode = mapping.getFieldCode().trim(); + if (!fieldCodeSet.contains(fieldCode)) { + continue; + } + EmsSiteMonitorPointMatch pointMatch = new EmsSiteMonitorPointMatch(); + pointMatch.setSiteId(siteId); + pointMatch.setFieldCode(fieldCode); + pointMatch.setDataPoint(mapping.getDataPoint().trim()); + pointMatch.setCreateBy(operName); + pointMatch.setUpdateBy(operName); + saveList.add(pointMatch); + } + if (saveList.isEmpty()) { + return 0; + } + return emsSiteMonitorPointMatchMapper.insertBatch(saveList); + } + + @Override + public List getSiteMonitorProjectDisplay(String siteId) { + List mappingList = getSiteMonitorProjectPointMapping(siteId); + if (mappingList.isEmpty()) { + return new ArrayList<>(); + } + Map homeLatestMap = safeRedisMap(redisCache.getCacheMap(buildSiteMonitorLatestRedisKey(siteId, MODULE_HOME))); + Map sbjkLatestMap = safeRedisMap(redisCache.getCacheMap(buildSiteMonitorLatestRedisKey(siteId, MODULE_SBJK))); + Map tjbbLatestMap = safeRedisMap(redisCache.getCacheMap(buildSiteMonitorLatestRedisKey(siteId, MODULE_TJBB))); + + List result = new ArrayList<>(); + for (SiteMonitorProjectPointMappingVo mapping : mappingList) { + SiteMonitorProjectDisplayVo vo = new SiteMonitorProjectDisplayVo(); + BeanUtils.copyProperties(mapping, vo); + Object cacheObj = null; + if (MODULE_HOME.equals(mapping.getModuleCode())) { + cacheObj = homeLatestMap.get(mapping.getFieldCode()); + } else if (MODULE_SBJK.equals(mapping.getModuleCode())) { + cacheObj = sbjkLatestMap.get(mapping.getFieldCode()); + } else if (MODULE_TJBB.equals(mapping.getModuleCode())) { + cacheObj = tjbbLatestMap.get(mapping.getFieldCode()); + } + if (cacheObj != null) { + JSONObject snapshot = parseFieldSnapshot(cacheObj); + vo.setFieldValue(snapshot.getString("fieldValue")); + vo.setValueTime(parseValueTime(snapshot.get("valueTime"))); + } + result.add(vo); + } + return result; + } + + @Override + public int saveSiteMonitorProjectData(SiteMonitorDataSaveRequest request, String operName) { + if (request == null || StringUtils.isBlank(request.getSiteId())) { + throw new ServiceException("站点ID不能为空"); + } + if (request.getItems() == null || request.getItems().isEmpty()) { + return 0; + } + List itemList = emsSiteMonitorItemMapper.selectEnabledList(); + if (itemList == null || itemList.isEmpty()) { + throw new ServiceException("单站监控配置项为空,请先初始化 ems_site_monitor_item"); + } + Set fieldCodeSet = itemList.stream().map(EmsSiteMonitorItem::getFieldCode).collect(Collectors.toSet()); + + Map itemMap = itemList.stream() + .collect(Collectors.toMap(EmsSiteMonitorItem::getFieldCode, item -> item, (a, b) -> a)); + + Date now = DateUtils.getNowDate(); + Map homeLatestUpdates = new HashMap<>(); + Map sbjkLatestUpdates = new HashMap<>(); + Map tjbbLatestUpdates = new HashMap<>(); + Map homeHistoryByMinute = new HashMap<>(); + Map sbjkHistoryByMinute = new HashMap<>(); + Map tjbbHistoryByMinute = new HashMap<>(); + for (SiteMonitorDataSaveItemVo item : request.getItems()) { + if (item == null || StringUtils.isBlank(item.getFieldCode())) { + continue; + } + String fieldCode = item.getFieldCode().trim(); + if (!fieldCodeSet.contains(fieldCode)) { + continue; + } + EmsSiteMonitorItem itemDef = itemMap.get(fieldCode); + if (itemDef == null || StringUtils.isBlank(itemDef.getModuleCode())) { + continue; + } + Date valueTime = item.getValueTime() == null ? now : item.getValueTime(); + JSONObject snapshot = buildFieldSnapshot(item.getFieldValue(), valueTime); + Date statisMinute = truncateToMinute(valueTime); + if (MODULE_HOME.equals(itemDef.getModuleCode())) { + homeLatestUpdates.put(fieldCode, snapshot); + mergeHistorySnapshot(homeHistoryByMinute, statisMinute, fieldCode, snapshot); + } else if (MODULE_SBJK.equals(itemDef.getModuleCode())) { + sbjkLatestUpdates.put(fieldCode, snapshot); + mergeHistorySnapshot(sbjkHistoryByMinute, statisMinute, fieldCode, snapshot); + } else if (MODULE_TJBB.equals(itemDef.getModuleCode())) { + tjbbLatestUpdates.put(fieldCode, snapshot); + mergeHistorySnapshot(tjbbHistoryByMinute, statisMinute, fieldCode, snapshot); + } + } + if (homeLatestUpdates.isEmpty() && sbjkLatestUpdates.isEmpty() && tjbbLatestUpdates.isEmpty()) { + return 0; + } + int rows = 0; + if (!homeLatestUpdates.isEmpty()) { + upsertLatestToRedis(request.getSiteId(), MODULE_HOME, homeLatestUpdates); + rows += upsertHistoryByMinute(HISTORY_TABLE_HOME, request.getSiteId(), homeHistoryByMinute, operName); + } + if (!sbjkLatestUpdates.isEmpty()) { + upsertLatestToRedis(request.getSiteId(), MODULE_SBJK, sbjkLatestUpdates); + rows += upsertHistoryByMinute(HISTORY_TABLE_SBJK, request.getSiteId(), sbjkHistoryByMinute, operName); + } + if (!tjbbLatestUpdates.isEmpty()) { + upsertLatestToRedis(request.getSiteId(), MODULE_TJBB, tjbbLatestUpdates); + rows += upsertHistoryByMinute(HISTORY_TABLE_TJBB, request.getSiteId(), tjbbHistoryByMinute, operName); + } + return rows; + } + + @Override + public int syncSiteMonitorDataByMqtt(String siteId, String deviceId, String jsonData, Date valueTime) { + if (StringUtils.isAnyBlank(siteId, jsonData)) { + return 0; + } + Map dataMap = JSON.parseObject(jsonData, new TypeReference>() {}); + if (dataMap == null || dataMap.isEmpty()) { + return 0; + } + Map upperDataMap = new HashMap<>(); + dataMap.forEach((k, v) -> { + if (StringUtils.isNotBlank(k)) { + upperDataMap.put(k.toUpperCase(), v); + } + }); + + List mappingList = emsSiteMonitorPointMatchMapper.selectBySiteId(siteId); + if (mappingList == null || mappingList.isEmpty()) { + return 0; + } + List itemList = emsSiteMonitorItemMapper.selectEnabledList(); + if (itemList == null || itemList.isEmpty()) { + return 0; + } + Map itemMap = itemList.stream() + .filter(item -> StringUtils.isNotBlank(item.getFieldCode())) + .collect(Collectors.toMap(EmsSiteMonitorItem::getFieldCode, item -> item, (a, b) -> a)); + + Date actualValueTime = valueTime == null ? DateUtils.getNowDate() : valueTime; + Date statisMinute = truncateToMinute(actualValueTime); + Map homeLatestUpdates = new HashMap<>(); + Map sbjkLatestUpdates = new HashMap<>(); + Map tjbbLatestUpdates = new HashMap<>(); + Map homeHistoryByMinute = new HashMap<>(); + Map sbjkHistoryByMinute = new HashMap<>(); + Map tjbbHistoryByMinute = new HashMap<>(); + + for (EmsSiteMonitorPointMatch mapping : mappingList) { + if (mapping == null || StringUtils.isAnyBlank(mapping.getFieldCode(), mapping.getDataPoint())) { + continue; + } + EmsSiteMonitorItem itemDef = itemMap.get(mapping.getFieldCode()); + if (itemDef == null || StringUtils.isBlank(itemDef.getModuleCode())) { + continue; + } + + String point = mapping.getDataPoint().trim(); + Object value = upperDataMap.get(point.toUpperCase()); + if (value == null && StringUtils.isNotBlank(deviceId)) { + value = upperDataMap.get((deviceId + point).toUpperCase()); + } + if (value == null) { + continue; + } + + JSONObject snapshot = buildFieldSnapshot(String.valueOf(value), actualValueTime); + + if (MODULE_HOME.equals(itemDef.getModuleCode())) { + homeLatestUpdates.put(mapping.getFieldCode(), snapshot); + mergeHistorySnapshot(homeHistoryByMinute, statisMinute, mapping.getFieldCode(), snapshot); + } else if (MODULE_SBJK.equals(itemDef.getModuleCode())) { + sbjkLatestUpdates.put(mapping.getFieldCode(), snapshot); + mergeHistorySnapshot(sbjkHistoryByMinute, statisMinute, mapping.getFieldCode(), snapshot); + } else if (MODULE_TJBB.equals(itemDef.getModuleCode())) { + tjbbLatestUpdates.put(mapping.getFieldCode(), snapshot); + mergeHistorySnapshot(tjbbHistoryByMinute, statisMinute, mapping.getFieldCode(), snapshot); + } + } + + int rows = 0; + if (!homeLatestUpdates.isEmpty()) { + upsertLatestToRedis(siteId, MODULE_HOME, homeLatestUpdates); + rows += upsertHistoryByMinute(HISTORY_TABLE_HOME, siteId, homeHistoryByMinute, "mqtt"); + } + if (!sbjkLatestUpdates.isEmpty()) { + upsertLatestToRedis(siteId, MODULE_SBJK, sbjkLatestUpdates); + rows += upsertHistoryByMinute(HISTORY_TABLE_SBJK, siteId, sbjkHistoryByMinute, "mqtt"); + } + if (!tjbbLatestUpdates.isEmpty()) { + upsertLatestToRedis(siteId, MODULE_TJBB, tjbbLatestUpdates); + rows += upsertHistoryByMinute(HISTORY_TABLE_TJBB, siteId, tjbbHistoryByMinute, "mqtt"); + } + return rows; + } + + private int upsertHistoryByMinute(String tableName, String siteId, Map minuteSnapshotMap, String operName) { + int rows = 0; + for (Map.Entry entry : minuteSnapshotMap.entrySet()) { + Date statisMinute = entry.getKey(); + JSONObject merged = mergeHistoryRecord(tableName, siteId, statisMinute, entry.getValue()); + rows += emsSiteMonitorDataMapper.upsertHistoryJsonByMinute( + tableName, + siteId, + statisMinute, + merged.toJSONString(), + operName + ); + Map hotColumns = extractHotColumns(merged); + rows += emsSiteMonitorDataMapper.updateHistoryHotColumns( + tableName, + siteId, + statisMinute, + hotColumns.get("hotSoc"), + hotColumns.get("hotTotalActivePower"), + hotColumns.get("hotTotalReactivePower"), + hotColumns.get("hotDayChargedCap"), + hotColumns.get("hotDayDisChargedCap"), + operName + ); + } + return rows; + } + + private JSONObject mergeHistoryRecord(String tableName, String siteId, Date statisMinute, JSONObject updates) { + String existingJson = emsSiteMonitorDataMapper.selectHistoryJsonByMinute(tableName, siteId, statisMinute); + JSONObject merged = StringUtils.isBlank(existingJson) ? new JSONObject() : JSON.parseObject(existingJson); + if (merged == null) { + merged = new JSONObject(); + } + if (updates != null) { + merged.putAll(updates); + } + return merged; + } + + private void upsertLatestToRedis(String siteId, String moduleCode, Map latestUpdates) { + if (latestUpdates == null || latestUpdates.isEmpty()) { + return; + } + String redisKey = buildSiteMonitorLatestRedisKey(siteId, moduleCode); + Map redisMap = new HashMap<>(); + latestUpdates.forEach((fieldCode, snapshot) -> redisMap.put(fieldCode, snapshot.toJSONString())); + redisCache.setAllCacheMapValue(redisKey, redisMap); + redisCache.expire(redisKey, 7, TimeUnit.DAYS); + } + + private void mergeHistorySnapshot(Map historyByMinute, Date statisMinute, String fieldCode, JSONObject snapshot) { + JSONObject minuteObj = historyByMinute.computeIfAbsent(statisMinute, k -> new JSONObject()); + minuteObj.put(fieldCode, snapshot); + } + + private Map safeRedisMap(Map source) { + return source == null ? new HashMap<>() : source; + } + + private Map extractHotColumns(JSONObject merged) { + Map result = new HashMap<>(); + result.put("hotSoc", extractFieldValue(merged, + new String[]{"SBJK_SSYX__soc", "TJBB_DCDQX__soc_stat", "HOME__avgSoc"}, + new String[]{"__soc", "__soc_stat", "__avgSoc"})); + result.put("hotTotalActivePower", extractFieldValue(merged, + new String[]{"SBJK_SSYX__totalActivePower"}, + new String[]{"__totalActivePower", "__activePower", "__activePower_stat"})); + result.put("hotTotalReactivePower", extractFieldValue(merged, + new String[]{"SBJK_SSYX__totalReactivePower"}, + new String[]{"__totalReactivePower", "__reactivePower", "__reactivePower_stat"})); + result.put("hotDayChargedCap", extractFieldValue(merged, + new String[]{"HOME__dayChargedCap", "SBJK_SSYX__dayChargedCap_rt", "TJBB_GLTJ__chargedCap_stat"}, + new String[]{"__dayChargedCap", "__dayChargedCap_rt", "__chargedCap_stat"})); + result.put("hotDayDisChargedCap", extractFieldValue(merged, + new String[]{"HOME__dayDisChargedCap", "SBJK_SSYX__dayDisChargedCap_rt", "TJBB_GLTJ__disChargedCap_stat"}, + new String[]{"__dayDisChargedCap", "__dayDisChargedCap_rt", "__disChargedCap_stat"})); + return result; + } + + private String extractFieldValue(JSONObject merged, String[] exactKeys, String[] suffixKeys) { + if (merged == null || merged.isEmpty()) { + return null; + } + if (exactKeys != null) { + for (String key : exactKeys) { + String value = getSnapshotFieldValue(merged.get(key)); + if (StringUtils.isNotBlank(value)) { + return value; + } + } + } + if (suffixKeys != null) { + for (Map.Entry entry : merged.entrySet()) { + String fieldCode = entry.getKey(); + if (StringUtils.isBlank(fieldCode)) { + continue; + } + for (String suffix : suffixKeys) { + if (StringUtils.isNotBlank(suffix) && fieldCode.endsWith(suffix)) { + String value = getSnapshotFieldValue(entry.getValue()); + if (StringUtils.isNotBlank(value)) { + return value; + } + } + } + } + } + return null; + } + + private String getSnapshotFieldValue(Object snapshotObj) { + if (snapshotObj == null) { + return null; + } + if (snapshotObj instanceof JSONObject) { + return ((JSONObject) snapshotObj).getString("fieldValue"); + } + if (snapshotObj instanceof Map) { + Object val = ((Map) snapshotObj).get("fieldValue"); + return val == null ? null : String.valueOf(val); + } + return String.valueOf(snapshotObj); + } + + private JSONObject buildFieldSnapshot(String fieldValue, Date valueTime) { + JSONObject snapshot = new JSONObject(); + snapshot.put("fieldValue", fieldValue); + snapshot.put("valueTime", valueTime == null ? null : valueTime.getTime()); + return snapshot; + } + + private JSONObject parseFieldSnapshot(Object cacheObj) { + if (cacheObj == null) { + return new JSONObject(); + } + if (cacheObj instanceof JSONObject) { + return (JSONObject) cacheObj; + } + if (cacheObj instanceof Map) { + return new JSONObject((Map) cacheObj); + } + try { + JSONObject snapshot = JSON.parseObject(String.valueOf(cacheObj)); + return snapshot == null ? new JSONObject() : snapshot; + } catch (Exception e) { + return new JSONObject(); + } + } + + private Date parseValueTime(Object valueTimeObj) { + if (valueTimeObj == null) { + return null; + } + if (valueTimeObj instanceof Date) { + return (Date) valueTimeObj; + } + if (valueTimeObj instanceof Number) { + return new Date(((Number) valueTimeObj).longValue()); + } + try { + return DateUtils.parseDate(valueTimeObj.toString()); + } catch (Exception e) { + return null; + } + } + + private Date truncateToMinute(Date date) { + Date source = date == null ? DateUtils.getNowDate() : date; + long millis = source.getTime(); + long minuteMillis = 60_000L; + return new Date((millis / minuteMillis) * minuteMillis); + } + + private String buildSiteMonitorLatestRedisKey(String siteId, String moduleCode) { + return RedisKeyConstants.SITE_MONITOR_LATEST + siteId + "_" + moduleCode; + } + // 辅助方法:根据值查找对应的对象(用于比较器中获取完整对象) private PointQueryResponse findByValue(List list, Object value) { return list.stream() diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointMatchServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointMatchServiceImpl.java index 22231a3..6e712a3 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointMatchServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointMatchServiceImpl.java @@ -12,6 +12,7 @@ import com.xzzn.ems.domain.EmsPointMatch; import com.xzzn.ems.domain.vo.DevicePointMatchExportVo; import com.xzzn.ems.domain.vo.DevicePointMatchVo; import com.xzzn.ems.domain.vo.ImportPointDataRequest; +import com.xzzn.ems.domain.vo.ImportPointTemplateRequest; import com.xzzn.ems.enums.DeviceMatchTable; import com.xzzn.ems.mapper.EmsPointEnumMatchMapper; import com.xzzn.ems.mapper.EmsPointMatchMapper; @@ -20,8 +21,10 @@ import com.xzzn.ems.utils.DevicePointMatchDataProcessor; import java.io.IOException; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import javax.validation.Validator; @@ -213,6 +216,99 @@ public class EmsPointMatchServiceImpl implements IEmsPointMatchService { return errorList; } + /** + * 按站点导入模板点位 + * + * @param request + * @param operName + * @return + */ + @Override + @Transactional(rollbackFor = Exception.class) + public String importTemplateBySite(ImportPointTemplateRequest request, String operName) { + String targetSiteId = request.getSiteId(); + if (SITE_ID.equals(targetSiteId)) { + throw new ServiceException("模板站点不支持作为导入目标站点"); + } + + int templatePointCount = emsPointMatchMapper.countBySiteId(SITE_ID); + if (templatePointCount <= 0) { + throw new ServiceException("模板点位数据不存在,无法导入"); + } + + boolean overwrite = Boolean.TRUE.equals(request.getOverwrite()); + int targetPointCount = emsPointMatchMapper.countBySiteId(targetSiteId); + if (targetPointCount > 0 && !overwrite) { + throw new ServiceException("目标站点已存在点位数据,请勾选“覆盖已存在数据”后重试"); + } + + if (targetPointCount > 0) { + emsPointMatchMapper.deleteBySiteId(targetSiteId); + } + + int targetEnumCount = emsPointEnumMatchMapper.countBySiteId(targetSiteId); + if (targetEnumCount > 0) { + emsPointEnumMatchMapper.deleteBySiteId(targetSiteId); + } + + int importPointCount = emsPointMatchMapper.copyTemplateToSite(SITE_ID, targetSiteId, operName); + int importEnumCount = 0; + if (emsPointEnumMatchMapper.countBySiteId(SITE_ID) > 0) { + importEnumCount = emsPointEnumMatchMapper.copyTemplateToSite(SITE_ID, targetSiteId, operName); + } + + syncSiteToRedis(targetSiteId); + return String.format("导入成功:站点 %s,点位 %d 条,枚举 %d 条", targetSiteId, importPointCount, importEnumCount); + } + + @Override + public List selectPointMatchConfigList(EmsPointMatch emsPointMatch) { + return emsPointMatchMapper.selectEmsPointMatchList(emsPointMatch); + } + + @Override + public EmsPointMatch selectPointMatchById(Long id) { + return emsPointMatchMapper.selectEmsPointMatchById(id); + } + + @Override + public int insertPointMatch(EmsPointMatch emsPointMatch) { + int rows = emsPointMatchMapper.insertEmsPointMatch(emsPointMatch); + if (rows > 0 && StringUtils.isNotBlank(emsPointMatch.getSiteId()) && StringUtils.isNotBlank(emsPointMatch.getDeviceCategory())) { + syncToRedis(emsPointMatch.getSiteId(), emsPointMatch.getDeviceId(), emsPointMatch.getDeviceCategory()); + } + return rows; + } + + @Override + public int updatePointMatch(EmsPointMatch emsPointMatch) { + int rows = emsPointMatchMapper.updateEmsPointMatch(emsPointMatch); + if (rows > 0 && StringUtils.isNotBlank(emsPointMatch.getSiteId()) && StringUtils.isNotBlank(emsPointMatch.getDeviceCategory())) { + syncToRedis(emsPointMatch.getSiteId(), emsPointMatch.getDeviceId(), emsPointMatch.getDeviceCategory()); + } + return rows; + } + + @Override + public int deletePointMatchByIds(Long[] ids) { + List deletedList = new ArrayList<>(); + for (Long id : ids) { + EmsPointMatch pointMatch = emsPointMatchMapper.selectEmsPointMatchById(id); + if (pointMatch != null) { + deletedList.add(pointMatch); + } + } + int rows = emsPointMatchMapper.deleteEmsPointMatchByIds(ids); + if (rows > 0) { + deletedList.forEach(pointMatch -> { + if (StringUtils.isNotBlank(pointMatch.getSiteId()) && StringUtils.isNotBlank(pointMatch.getDeviceCategory())) { + syncToRedis(pointMatch.getSiteId(), pointMatch.getDeviceId(), pointMatch.getDeviceCategory()); + } + }); + } + return rows; + } + private boolean validDevicePointMatch(DevicePointMatchVo pointMatch, List errorList) { StringBuilder errorMsg = new StringBuilder(); if (StringUtils.isBlank(pointMatch.getMatchField())) { @@ -238,6 +334,9 @@ public class EmsPointMatchServiceImpl implements IEmsPointMatchService { List pointMatchData = emsPointMatchMapper.getDevicePointMatchList(siteId, deviceId, deviceCategory); // log.info("同步点位匹配数据到Redis key:{} data:{}", pointMatchKey, pointMatchData); if (CollectionUtils.isEmpty(pointMatchData)) { + if (redisCache.hasKey(pointMatchKey)) { + redisCache.deleteObject(pointMatchKey); + } return; } if (redisCache.hasKey(pointMatchKey)) { @@ -247,6 +346,10 @@ public class EmsPointMatchServiceImpl implements IEmsPointMatchService { log.info("点位匹配数据同步完成 data:{}", JSON.toJSONString(redisCache.getCacheList(pointMatchKey))); // 点位枚举匹配数据同步到Redis + syncPointEnumToRedis(siteId, deviceCategory); + } + + private void syncPointEnumToRedis(String siteId, String deviceCategory) { String pointEnumMatchKey = DevicePointMatchDataProcessor.getPointEnumMacthCacheKey(siteId, deviceCategory); List pointEnumMatchList = emsPointEnumMatchMapper.selectList(siteId, deviceCategory, null); if (!CollectionUtils.isEmpty(pointEnumMatchList)) { @@ -256,7 +359,22 @@ public class EmsPointMatchServiceImpl implements IEmsPointMatchService { redisCache.setCacheList(pointEnumMatchKey, pointEnumMatchList); log.info("点位枚举匹配数据同步完成 data:{}", JSON.toJSONString(redisCache.getCacheList(pointEnumMatchKey))); } + } + private void syncSiteToRedis(String siteId) { + List sitePointList = emsPointMatchMapper.selectBySiteId(siteId); + if (CollectionUtils.isEmpty(sitePointList)) { + return; + } + Set categorySet = new HashSet<>(); + for (EmsPointMatch pointMatch : sitePointList) { + categorySet.add(pointMatch.getDeviceCategory()); + if (StringUtils.isBlank(pointMatch.getDeviceId())) { + continue; + } + syncToRedis(siteId, pointMatch.getDeviceId(), pointMatch.getDeviceCategory()); + } + categorySet.forEach(category -> syncPointEnumToRedis(siteId, category)); } private void savePointMatchEnum(String matchFieldEnum, String dataEnum, EmsPointMatch savePoint) { diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsSiteServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsSiteServiceImpl.java index 1abb914..e391e8c 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsSiteServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsSiteServiceImpl.java @@ -2,7 +2,10 @@ package com.xzzn.ems.service.impl; import com.xzzn.common.constant.RedisKeyConstants; import com.xzzn.common.core.redis.RedisCache; +import com.xzzn.common.exception.ServiceException; import com.xzzn.common.enums.DeviceCategory; +import com.xzzn.common.utils.DateUtils; +import com.xzzn.common.utils.StringUtils; import com.xzzn.ems.domain.EmsBatteryData; import com.xzzn.ems.domain.EmsSiteSetting; import com.xzzn.ems.domain.vo.SiteDeviceListVo; @@ -12,9 +15,11 @@ import com.xzzn.ems.mapper.EmsSiteSettingMapper; import com.xzzn.ems.service.IEmsSiteService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; /** * 站点信息 服务层实现 @@ -23,6 +28,7 @@ import java.util.Map; @Service public class EmsSiteServiceImpl implements IEmsSiteService { + private static final Pattern SITE_ID_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$"); @Autowired private EmsSiteSettingMapper emsSiteMapper; @@ -70,7 +76,14 @@ public class EmsSiteServiceImpl implements IEmsSiteService */ @Override public List getAllSiteInfoList(String siteName, String startTime, String endTime) { - return emsSiteMapper.getSiteInfoList(siteName,startTime,endTime); + List list = emsSiteMapper.getSiteInfoList(siteName,startTime,endTime); + if (list == null || list.isEmpty()) { + return list; + } + for (EmsSiteSetting site : list) { + site.setAuthorized(Boolean.TRUE); + } + return list; } /** @@ -145,4 +158,53 @@ public class EmsSiteServiceImpl implements IEmsSiteService List> deviceIdList = emsDevicesMapper.getDeviceInfosBySiteIdAndCategory(siteId, parentCategory); return deviceIdList; } + + @Override + @Transactional(rollbackFor = Exception.class) + public int addSite(EmsSiteSetting emsSiteSetting) { + validateSiteSetting(emsSiteSetting, true); + String siteId = emsSiteSetting.getSiteId(); + EmsSiteSetting existing = emsSiteMapper.selectEmsSiteSettingBySiteId(siteId); + if (existing != null) { + throw new ServiceException("站点ID已存在,请更换后重试"); + } + emsSiteSetting.setCreateTime(DateUtils.getNowDate()); + return emsSiteMapper.insertEmsSiteSetting(emsSiteSetting); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int updateSite(EmsSiteSetting emsSiteSetting) { + if (emsSiteSetting == null || emsSiteSetting.getId() == null) { + throw new ServiceException("站点主键不能为空"); + } + validateSiteSetting(emsSiteSetting, false); + EmsSiteSetting dbData = emsSiteMapper.selectEmsSiteSettingById(emsSiteSetting.getId()); + if (dbData == null) { + throw new ServiceException("站点不存在"); + } + if (StringUtils.isNotEmpty(emsSiteSetting.getSiteId()) + && !StringUtils.equals(emsSiteSetting.getSiteId(), dbData.getSiteId())) { + throw new ServiceException("不支持修改站点ID"); + } + emsSiteSetting.setSiteId(dbData.getSiteId()); + emsSiteSetting.setUpdateTime(DateUtils.getNowDate()); + return emsSiteMapper.updateEmsSiteSetting(emsSiteSetting); + } + + private void validateSiteSetting(EmsSiteSetting emsSiteSetting, boolean requireSiteId) { + if (emsSiteSetting == null) { + throw new ServiceException("参数不能为空"); + } + if (StringUtils.isBlank(emsSiteSetting.getSiteName())) { + throw new ServiceException("站点名称不能为空"); + } + if (requireSiteId && StringUtils.isBlank(emsSiteSetting.getSiteId())) { + throw new ServiceException("站点ID不能为空"); + } + if (StringUtils.isNotBlank(emsSiteSetting.getSiteId()) && !SITE_ID_PATTERN.matcher(emsSiteSetting.getSiteId()).matches()) { + throw new ServiceException("站点ID仅支持字母、数字、下划线"); + } + } + } diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/GeneralQueryServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/GeneralQueryServiceImpl.java index 70b2170..8e634fd 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/GeneralQueryServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/GeneralQueryServiceImpl.java @@ -2,12 +2,15 @@ package com.xzzn.ems.service.impl; import com.xzzn.common.enums.DeviceCategory; import com.xzzn.common.utils.DateUtils; +import com.xzzn.ems.domain.EmsPointConfig; import com.xzzn.ems.domain.EmsPointMatch; import com.xzzn.ems.domain.vo.*; import com.xzzn.ems.mapper.EmsBatteryDataMonthMapper; +import com.xzzn.ems.mapper.EmsPointConfigMapper; import com.xzzn.ems.mapper.EmsDevicesSettingMapper; import com.xzzn.ems.mapper.EmsPointMatchMapper; import com.xzzn.ems.service.IGeneralQueryService; +import com.xzzn.ems.service.InfluxPointDataWriter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -20,8 +23,6 @@ import java.time.temporal.TemporalAdjusters; import java.util.*; import java.util.stream.Collectors; -import javax.lang.model.util.ElementScanner6; - /** * 综合查询 服务层实现 * @@ -34,23 +35,29 @@ public class GeneralQueryServiceImpl implements IGeneralQueryService @Autowired private EmsPointMatchMapper emsPointMatchMapper; @Autowired + private EmsPointConfigMapper emsPointConfigMapper; + @Autowired private EmsDevicesSettingMapper emsDevicesSettingMapper; @Autowired private EmsBatteryDataMonthMapper emsBatteryDataMonthMapper; + @Autowired + private InfluxPointDataWriter influxPointDataWriter; @Override - public List getPointNameList(PointNameRequest request) { + public List getPointNameList(PointNameRequest request) { List siteIds = request.getSiteIds(); if (siteIds == null || siteIds.isEmpty()) { return Collections.emptyList(); } String deviceCategory = request.getDeviceCategory(); - if (deviceCategory == null) { + String deviceId = request.getDeviceId(); + if ((deviceCategory == null || "".equals(deviceCategory.trim())) + && (deviceId == null || "".equals(deviceId.trim()))) { return Collections.emptyList(); } - return emsPointMatchMapper.getPointNameList(siteIds,deviceCategory,request.getPointName()); + return emsPointConfigMapper.getPointNameList(siteIds, deviceCategory, deviceId, request.getPointName()); } @Override @@ -81,48 +88,142 @@ public class GeneralQueryServiceImpl implements IGeneralQueryService @Override public List getPointValueList(PointNameRequest request){ - List result = new ArrayList<>(); - List querySiteIds = new ArrayList<>(); - List siteIds = request.getSiteIds(); - String deviceCategory = request.getDeviceCategory(); - // 根据入参获取点位对应的表和字段 - List matchInfo = emsPointMatchMapper.getMatchInfo(siteIds,deviceCategory,request.getPointName()); - if (matchInfo == null || matchInfo.size() == 0) { - return result; - } else { - for (EmsPointMatch emsPointMatch : matchInfo) { - querySiteIds.add(emsPointMatch.getSiteId()); - } + if (siteIds == null || siteIds.isEmpty()) { + return Collections.emptyList(); } - // 单体电池特殊校验 - Map> siteDeviceMap = request.getSiteDeviceMap(); - if (DeviceCategory.BATTERY.getCode().equals(deviceCategory) && (siteDeviceMap == null || siteDeviceMap.size() == 0)) { - return result; + String deviceCategory = request.getDeviceCategory(); + String requestDeviceId = request.getDeviceId(); + if ((deviceCategory == null || "".equals(deviceCategory.trim())) + && (requestDeviceId == null || "".equals(requestDeviceId.trim())) + ) { + return Collections.emptyList(); + } + + List pointNames = resolvePointNames(request); + if (pointNames.isEmpty()) { + return Collections.emptyList(); } // 处理时间范围,如果未传根据数据单位设默认值 dealDataTime(request); + Date startDate = DateUtils.dateTime(DateUtils.YYYY_MM_DD_HH_MM_SS, request.getStartDate()); + Date endDate = DateUtils.dateTime(DateUtils.YYYY_MM_DD_HH_MM_SS, request.getEndDate()); + if (request.getDataUnit() == 3) { + endDate = DateUtils.adjustToEndOfDay(request.getEndDate()); + } - try { - // 不同的site_id根据设备类型和字段,默认取第一个匹配到的表和表字段只会有一个, - String tableName = matchInfo.get(0).getMatchTable(); - String tableField = matchInfo.get(0).getMatchField(); - Long dataType = matchInfo.get(0).getDataType(); - if (DeviceCategory.BATTERY.getCode().equals(deviceCategory)) { - // 单体电池数据特殊处理 - result = generalQueryBatteryData(querySiteIds,tableName,tableField,request,deviceCategory,dataType); - } else { - // 其他设备数据 - result = generalQueryCommonData(querySiteIds,tableName,tableField,request,deviceCategory,dataType); + List selectedDeviceIds = resolveSelectedDeviceIds(request); + List pointConfigs = emsPointConfigMapper.getConfigListForGeneralQuery( + siteIds, deviceCategory, pointNames, selectedDeviceIds + ); + if (pointConfigs == null || pointConfigs.isEmpty()) { + return Collections.emptyList(); + } + + List dataVoList = new ArrayList<>(); + for (EmsPointConfig pointConfig : pointConfigs) { + dataVoList.addAll(queryPointCurve(pointConfig, request.getDataUnit(), startDate, endDate)); + } + + if (dataVoList.isEmpty()) { + return Collections.emptyList(); + } + + if (request.getDataUnit() == 1) { + try { + dataVoList = dealWithMinutesData(siteIds, dataVoList, deviceCategory, request.getStartDate(), request.getEndDate()); + } catch (ParseException e) { + throw new RuntimeException(e); } - } catch (ParseException e) { - throw new RuntimeException(e); + } + return convertCommonToResultList(dataVoList, 1L); + } + + private List resolvePointNames(PointNameRequest request) { + List names = new ArrayList<>(); + if (request.getPointNames() != null && !request.getPointNames().isEmpty()) { + names.addAll(request.getPointNames()); + } else if (request.getPointName() != null && !"".equals(request.getPointName().trim())) { + names.addAll(Arrays.stream(request.getPointName().split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList())); + } + return names.stream().distinct().collect(Collectors.toList()); + } + + private List resolveSelectedDeviceIds(PointNameRequest request) { + List selected = new ArrayList<>(); + if (request.getDeviceId() != null && !"".equals(request.getDeviceId().trim())) { + selected.add(request.getDeviceId().trim()); + } + Map> siteDeviceMap = request.getSiteDeviceMap(); + if (siteDeviceMap != null && !siteDeviceMap.isEmpty()) { + for (List devices : siteDeviceMap.values()) { + if (devices == null) { + continue; + } + for (String deviceId : devices) { + if (deviceId != null && !"".equals(deviceId.trim())) { + selected.add(deviceId.trim()); + } + } + } + } + return selected.stream().distinct().collect(Collectors.toList()); + } + + private List queryPointCurve(EmsPointConfig config, int dataUnit, Date startDate, Date endDate) { + if (config == null || config.getSiteId() == null || config.getDeviceId() == null || config.getDataKey() == null) { + return Collections.emptyList(); + } + List values = influxPointDataWriter.queryCurveData( + config.getSiteId(), config.getDeviceId(), config.getDataKey(), startDate, endDate + ); + if (values == null || values.isEmpty()) { + return Collections.emptyList(); + } + + // 每个时间桶取该桶最后一条值 + Map latestByBucket = new LinkedHashMap<>(); + for (InfluxPointDataWriter.PointValue value : values) { + latestByBucket.put(formatByDataUnit(value.getDataTime(), dataUnit), value.getPointValue()); + } + + List result = new ArrayList<>(); + String displayDeviceId = buildDisplayDeviceId(config); + for (Map.Entry entry : latestByBucket.entrySet()) { + GeneralQueryDataVo vo = new GeneralQueryDataVo(); + vo.setSiteId(config.getSiteId()); + vo.setDeviceId(displayDeviceId); + vo.setValueDate(entry.getKey()); + vo.setPointValue(entry.getValue()); + result.add(vo); } return result; } + private String buildDisplayDeviceId(EmsPointConfig config) { + String pointName = config.getPointName() == null || "".equals(config.getPointName().trim()) + ? config.getDataKey() : config.getPointName().trim(); + return config.getDeviceId() + "-" + pointName; + } + + private String formatByDataUnit(Date dataTime, int dataUnit) { + if (dataTime == null) { + return ""; + } + if (dataUnit == 3) { + return new SimpleDateFormat("yyyy-MM-dd").format(dataTime); + } + if (dataUnit == 2) { + return new SimpleDateFormat("yyyy-MM-dd HH:00").format(dataTime); + } + return new SimpleDateFormat("yyyy-MM-dd HH:mm:00").format(dataTime); + } + private List generalQueryCommonData(List querySiteIds, String tableName, String tableField, PointNameRequest request, String deviceCategory, Long dataType) throws ParseException { diff --git a/ems-system/src/main/resources/mapper/ems/EmsPointEnumMatchMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsPointEnumMatchMapper.xml index 7ba0254..2f12e8d 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsPointEnumMatchMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsPointEnumMatchMapper.xml @@ -119,4 +119,47 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - \ No newline at end of file + + + + delete from ems_point_enum_match + where site_id = #{siteId} + + + + insert into ems_point_enum_match ( + match_field, + site_id, + device_category, + enum_code, + enum_name, + enum_desc, + data_enum_code, + create_by, + create_time, + update_by, + update_time, + remark + ) + select + match_field, + #{targetSiteId}, + device_category, + enum_code, + enum_name, + enum_desc, + data_enum_code, + #{operName}, + now(), + #{operName}, + now(), + remark + from ems_point_enum_match + where site_id = #{templateSiteId} + + + diff --git a/ems-system/src/main/resources/mapper/ems/EmsPointMatchMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsPointMatchMapper.xml index 6abab7a..1a964e4 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsPointMatchMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsPointMatchMapper.xml @@ -570,4 +570,74 @@ - \ No newline at end of file + + + + delete from ems_point_match + where site_id = #{siteId} + + + + insert into ems_point_match ( + point_name, + match_table, + match_field, + site_id, + device_category, + data_point, + data_point_name, + data_device, + data_unit, + ip_address, + ip_port, + data_type, + need_diff_device_id, + create_by, + create_time, + update_by, + update_time, + remark, + is_alarm, + device_id, + a, + k, + b + ) + select + point_name, + match_table, + match_field, + #{targetSiteId}, + device_category, + data_point, + data_point_name, + data_device, + data_unit, + ip_address, + ip_port, + data_type, + need_diff_device_id, + #{operName}, + now(), + #{operName}, + now(), + remark, + is_alarm, + device_id, + a, + k, + b + from ems_point_match + where site_id = #{templateSiteId} + + + + + -- 2.49.0 From 21673ecd1e4db0ab0a5f947423f439e38877574d Mon Sep 17 00:00:00 2001 From: dashixiong Date: Thu, 12 Feb 2026 21:07:41 +0800 Subject: [PATCH 02/10] =?UTF-8?q?=E4=B8=B4=E6=97=B6=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ems/EmsPointConfigController.java | 96 +++ .../com/xzzn/ems/domain/EmsPointConfig.java | 217 ++++++ .../xzzn/ems/domain/EmsSiteMonitorData.java | 58 ++ .../xzzn/ems/domain/EmsSiteMonitorItem.java | 101 +++ .../ems/domain/EmsSiteMonitorPointMatch.java | 47 ++ .../domain/vo/GeneralQueryPointOptionVo.java | 31 + .../domain/vo/ImportPointTemplateRequest.java | 32 + .../domain/vo/PointConfigCurveRequest.java | 58 ++ .../domain/vo/PointConfigCurveValueVo.java | 24 + .../vo/PointConfigLatestValueItemVo.java | 31 + .../vo/PointConfigLatestValueRequest.java | 15 + .../domain/vo/PointConfigLatestValueVo.java | 51 ++ .../domain/vo/SiteMonitorDataSaveItemVo.java | 33 + .../domain/vo/SiteMonitorDataSaveRequest.java | 24 + .../vo/SiteMonitorProjectDisplayVo.java | 27 + ...MonitorProjectPointMappingSaveRequest.java | 26 + .../vo/SiteMonitorProjectPointMappingVo.java | 84 +++ .../xzzn/ems/mapper/EmsPointConfigMapper.java | 44 ++ .../ems/mapper/EmsSiteMonitorDataMapper.java | 28 + .../ems/mapper/EmsSiteMonitorItemMapper.java | 12 + .../EmsSiteMonitorPointMatchMapper.java | 17 + .../ems/service/IEmsPointConfigService.java | 33 + .../ems/service/InfluxPointDataWriter.java | 616 ++++++++++++++++ .../impl/EmsPointConfigServiceImpl.java | 668 ++++++++++++++++++ .../mapper/ems/EmsPointConfigMapper.xml | 231 ++++++ .../mapper/ems/EmsSiteMonitorDataMapper.xml | 40 ++ .../mapper/ems/EmsSiteMonitorItemMapper.xml | 33 + .../ems/EmsSiteMonitorPointMatchMapper.xml | 40 ++ 28 files changed, 2717 insertions(+) create mode 100644 ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointConfigController.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/EmsPointConfig.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorData.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorItem.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorPointMatch.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/vo/GeneralQueryPointOptionVo.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/vo/ImportPointTemplateRequest.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigCurveRequest.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigCurveValueVo.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueItemVo.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueRequest.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueVo.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorDataSaveItemVo.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorDataSaveRequest.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectDisplayVo.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingSaveRequest.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingVo.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorDataMapper.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorItemMapper.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorPointMatchMapper.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/service/IEmsPointConfigService.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/service/InfluxPointDataWriter.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java create mode 100644 ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml create mode 100644 ems-system/src/main/resources/mapper/ems/EmsSiteMonitorDataMapper.xml create mode 100644 ems-system/src/main/resources/mapper/ems/EmsSiteMonitorItemMapper.xml create mode 100644 ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml diff --git a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointConfigController.java b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointConfigController.java new file mode 100644 index 0000000..f9d530e --- /dev/null +++ b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointConfigController.java @@ -0,0 +1,96 @@ +package com.xzzn.web.controller.ems; + +import com.xzzn.common.annotation.Log; +import com.xzzn.common.core.controller.BaseController; +import com.xzzn.common.core.domain.AjaxResult; +import com.xzzn.common.core.page.TableDataInfo; +import com.xzzn.common.enums.BusinessType; +import com.xzzn.ems.domain.EmsPointConfig; +import com.xzzn.ems.domain.vo.ImportPointTemplateRequest; +import com.xzzn.ems.domain.vo.PointConfigCurveRequest; +import com.xzzn.ems.domain.vo.PointConfigLatestValueRequest; +import com.xzzn.ems.service.IEmsPointConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.Valid; +import java.util.List; + +@RestController +@RequestMapping("/ems/pointConfig") +public class EmsPointConfigController extends BaseController { + + @Autowired + private IEmsPointConfigService pointConfigService; + + @GetMapping("/list") + public TableDataInfo list(EmsPointConfig pointConfig) { + startPage(); + List list = pointConfigService.selectPointConfigList(pointConfig); + return getDataTable(list); + } + + @GetMapping("/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) { + return success(pointConfigService.selectPointConfigById(id)); + } + + @Log(title = "点位配置", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody EmsPointConfig pointConfig) { + return toAjax(pointConfigService.insertPointConfig(pointConfig, getUsername())); + } + + @Log(title = "点位配置", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody EmsPointConfig pointConfig) { + return toAjax(pointConfigService.updatePointConfig(pointConfig, getUsername())); + } + + @Log(title = "点位配置", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) { + return toAjax(pointConfigService.deletePointConfigByIds(ids)); + } + + @Log(title = "点位配置", businessType = BusinessType.IMPORT) + @PostMapping("/importTemplateBySite") + public AjaxResult importTemplateBySite(@Valid @RequestBody ImportPointTemplateRequest request) { + return success(pointConfigService.importTemplateBySite(request, getUsername())); + } + + @Log(title = "点位配置", businessType = BusinessType.IMPORT) + @PostMapping("/importCsv") + public AjaxResult importCsv(@RequestParam String siteId, + @RequestParam(required = false) Boolean overwrite, + @RequestParam("file") MultipartFile file) { + return success(pointConfigService.importCsvBySite(siteId, overwrite, file, getUsername())); + } + + @GetMapping("/registerAddress") + public AjaxResult getRegisterAddress(@RequestParam String siteId, + @RequestParam String deviceCategory, + @RequestParam String deviceId, + @RequestParam String dataKey) { + return success(pointConfigService.getRegisterAddress(siteId, deviceCategory, deviceId, dataKey)); + } + + @PostMapping("/latestValues") + public AjaxResult latestValues(@RequestBody PointConfigLatestValueRequest request) { + return success(pointConfigService.getLatestValues(request)); + } + + @PostMapping("/curve") + public AjaxResult curve(@RequestBody PointConfigCurveRequest request) { + return success(pointConfigService.getCurveData(request)); + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/EmsPointConfig.java b/ems-system/src/main/java/com/xzzn/ems/domain/EmsPointConfig.java new file mode 100644 index 0000000..309c0ea --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/EmsPointConfig.java @@ -0,0 +1,217 @@ +package com.xzzn.ems.domain; + +import com.xzzn.common.annotation.Excel; +import com.xzzn.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.math.BigDecimal; + +/** + * 点位配置对象 ems_point_config + */ +public class EmsPointConfig extends BaseEntity { + private static final long serialVersionUID = 1L; + + private Long id; + + @Excel(name = "站点ID") + private String siteId; + + @Excel(name = "设备类型") + private String deviceCategory; + + @Excel(name = "设备ID") + private String deviceId; + + @Excel(name = "点位名称") + private String pointName; + + @Excel(name = "数据键") + private String dataKey; + + @Excel(name = "点位描述") + private String pointDesc; + + @Excel(name = "寄存器地址") + private String registerAddress; + + @Excel(name = "单位") + private String dataUnit; + + @Excel(name = "A系数") + private BigDecimal dataA; + + @Excel(name = "K系数") + private BigDecimal dataK; + + @Excel(name = "B系数") + private BigDecimal dataB; + + @Excel(name = "位偏移") + private Integer dataBit; + + @Excel(name = "是否报警点位", readConverterExp = "0=否,1=是") + private Integer isAlarm; + + @Excel(name = "点位类型") + private String pointType; + + @Excel(name = "计算表达式") + private String calcExpression; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getSiteId() { + return siteId; + } + + public void setSiteId(String siteId) { + this.siteId = siteId; + } + + public String getDeviceCategory() { + return deviceCategory; + } + + public void setDeviceCategory(String deviceCategory) { + this.deviceCategory = deviceCategory; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getPointName() { + return pointName; + } + + public void setPointName(String pointName) { + this.pointName = pointName; + } + + public String getDataKey() { + return dataKey; + } + + public void setDataKey(String dataKey) { + this.dataKey = dataKey; + } + + public String getPointDesc() { + return pointDesc; + } + + public void setPointDesc(String pointDesc) { + this.pointDesc = pointDesc; + } + + public String getRegisterAddress() { + return registerAddress; + } + + public void setRegisterAddress(String registerAddress) { + this.registerAddress = registerAddress; + } + + public String getDataUnit() { + return dataUnit; + } + + public void setDataUnit(String dataUnit) { + this.dataUnit = dataUnit; + } + + public BigDecimal getDataA() { + return dataA; + } + + public void setDataA(BigDecimal dataA) { + this.dataA = dataA; + } + + public BigDecimal getDataK() { + return dataK; + } + + public void setDataK(BigDecimal dataK) { + this.dataK = dataK; + } + + public BigDecimal getDataB() { + return dataB; + } + + public void setDataB(BigDecimal dataB) { + this.dataB = dataB; + } + + public Integer getDataBit() { + return dataBit; + } + + public void setDataBit(Integer dataBit) { + this.dataBit = dataBit; + } + + public Integer getIsAlarm() { + return isAlarm; + } + + public void setIsAlarm(Integer isAlarm) { + this.isAlarm = isAlarm; + } + + public String getPointType() { + return pointType; + } + + public void setPointType(String pointType) { + this.pointType = pointType; + } + + public String getCalcExpression() { + return calcExpression; + } + + public void setCalcExpression(String calcExpression) { + this.calcExpression = calcExpression; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) + .append("id", getId()) + .append("siteId", getSiteId()) + .append("deviceCategory", getDeviceCategory()) + .append("deviceId", getDeviceId()) + .append("pointName", getPointName()) + .append("dataKey", getDataKey()) + .append("pointDesc", getPointDesc()) + .append("registerAddress", getRegisterAddress()) + .append("dataUnit", getDataUnit()) + .append("dataA", getDataA()) + .append("dataK", getDataK()) + .append("dataB", getDataB()) + .append("dataBit", getDataBit()) + .append("isAlarm", getIsAlarm()) + .append("pointType", getPointType()) + .append("calcExpression", getCalcExpression()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorData.java b/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorData.java new file mode 100644 index 0000000..684b117 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorData.java @@ -0,0 +1,58 @@ +package com.xzzn.ems.domain; + +import com.xzzn.common.core.domain.BaseEntity; + +import java.util.Date; + +/** + * 单站监控展示数据对象(按分组落到 ems_site_monitor_data_home/sbjk/tjbb) + */ +public class EmsSiteMonitorData extends BaseEntity { + private static final long serialVersionUID = 1L; + + private Long id; + private String siteId; + private String fieldCode; + private String fieldValue; + private Date valueTime; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getSiteId() { + return siteId; + } + + public void setSiteId(String siteId) { + this.siteId = siteId; + } + + public String getFieldCode() { + return fieldCode; + } + + public void setFieldCode(String fieldCode) { + this.fieldCode = fieldCode; + } + + public String getFieldValue() { + return fieldValue; + } + + public void setFieldValue(String fieldValue) { + this.fieldValue = fieldValue; + } + + public Date getValueTime() { + return valueTime; + } + + public void setValueTime(Date valueTime) { + this.valueTime = valueTime; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorItem.java b/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorItem.java new file mode 100644 index 0000000..84c8857 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorItem.java @@ -0,0 +1,101 @@ +package com.xzzn.ems.domain; + +import com.xzzn.common.core.domain.BaseEntity; + +/** + * 单站监控配置项定义对象 ems_site_monitor_item + */ +public class EmsSiteMonitorItem extends BaseEntity { + private static final long serialVersionUID = 1L; + + private Long id; + private String moduleCode; + private String moduleName; + private String menuCode; + private String menuName; + private String sectionName; + private String fieldCode; + private String fieldName; + private Integer sortNo; + private Integer status; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getModuleCode() { + return moduleCode; + } + + public void setModuleCode(String moduleCode) { + this.moduleCode = moduleCode; + } + + public String getModuleName() { + return moduleName; + } + + public void setModuleName(String moduleName) { + this.moduleName = moduleName; + } + + public String getMenuCode() { + return menuCode; + } + + public void setMenuCode(String menuCode) { + this.menuCode = menuCode; + } + + public String getMenuName() { + return menuName; + } + + public void setMenuName(String menuName) { + this.menuName = menuName; + } + + public String getSectionName() { + return sectionName; + } + + public void setSectionName(String sectionName) { + this.sectionName = sectionName; + } + + public String getFieldCode() { + return fieldCode; + } + + public void setFieldCode(String fieldCode) { + this.fieldCode = fieldCode; + } + + public String getFieldName() { + return fieldName; + } + + public void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + public Integer getSortNo() { + return sortNo; + } + + public void setSortNo(Integer sortNo) { + this.sortNo = sortNo; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorPointMatch.java b/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorPointMatch.java new file mode 100644 index 0000000..e6db1d3 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorPointMatch.java @@ -0,0 +1,47 @@ +package com.xzzn.ems.domain; + +import com.xzzn.common.core.domain.BaseEntity; + +/** + * 单站监控字段点位映射对象 ems_site_monitor_point_match + */ +public class EmsSiteMonitorPointMatch extends BaseEntity { + private static final long serialVersionUID = 1L; + + private Long id; + private String siteId; + private String fieldCode; + private String dataPoint; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getSiteId() { + return siteId; + } + + public void setSiteId(String siteId) { + this.siteId = siteId; + } + + public String getFieldCode() { + return fieldCode; + } + + public void setFieldCode(String fieldCode) { + this.fieldCode = fieldCode; + } + + public String getDataPoint() { + return dataPoint; + } + + public void setDataPoint(String dataPoint) { + this.dataPoint = dataPoint; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/GeneralQueryPointOptionVo.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/GeneralQueryPointOptionVo.java new file mode 100644 index 0000000..d48520f --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/GeneralQueryPointOptionVo.java @@ -0,0 +1,31 @@ +package com.xzzn.ems.domain.vo; + +public class GeneralQueryPointOptionVo { + private String pointName; + private String dataKey; + private String pointDesc; + + public String getPointName() { + return pointName; + } + + public void setPointName(String pointName) { + this.pointName = pointName; + } + + public String getDataKey() { + return dataKey; + } + + public void setDataKey(String dataKey) { + this.dataKey = dataKey; + } + + public String getPointDesc() { + return pointDesc; + } + + public void setPointDesc(String pointDesc) { + this.pointDesc = pointDesc; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/ImportPointTemplateRequest.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/ImportPointTemplateRequest.java new file mode 100644 index 0000000..1ca6a31 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/ImportPointTemplateRequest.java @@ -0,0 +1,32 @@ +package com.xzzn.ems.domain.vo; + +import javax.validation.constraints.NotBlank; + +/** + * 按站点导入点位模板请求参数 + */ +public class ImportPointTemplateRequest { + + /** 目标站点ID */ + @NotBlank(message = "站点ID不能为空") + private String siteId; + + /** 是否覆盖目标站点已有点位配置 */ + private Boolean overwrite; + + public String getSiteId() { + return siteId; + } + + public void setSiteId(String siteId) { + this.siteId = siteId; + } + + public Boolean getOverwrite() { + return overwrite; + } + + public void setOverwrite(Boolean overwrite) { + this.overwrite = overwrite; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigCurveRequest.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigCurveRequest.java new file mode 100644 index 0000000..7777305 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigCurveRequest.java @@ -0,0 +1,58 @@ +package com.xzzn.ems.domain.vo; + +public class PointConfigCurveRequest { + private String siteId; + private String deviceId; + private String dataKey; + private String rangeType; + private String startTime; + private String endTime; + + public String getSiteId() { + return siteId; + } + + public void setSiteId(String siteId) { + this.siteId = siteId; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getDataKey() { + return dataKey; + } + + public void setDataKey(String dataKey) { + this.dataKey = dataKey; + } + + public String getRangeType() { + return rangeType; + } + + public void setRangeType(String rangeType) { + this.rangeType = rangeType; + } + + public String getStartTime() { + return startTime; + } + + public void setStartTime(String startTime) { + this.startTime = startTime; + } + + public String getEndTime() { + return endTime; + } + + public void setEndTime(String endTime) { + this.endTime = endTime; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigCurveValueVo.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigCurveValueVo.java new file mode 100644 index 0000000..0ba51db --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigCurveValueVo.java @@ -0,0 +1,24 @@ +package com.xzzn.ems.domain.vo; + +import java.util.Date; + +public class PointConfigCurveValueVo { + private Date dataTime; + private Object pointValue; + + public Date getDataTime() { + return dataTime; + } + + public void setDataTime(Date dataTime) { + this.dataTime = dataTime; + } + + public Object getPointValue() { + return pointValue; + } + + public void setPointValue(Object pointValue) { + this.pointValue = pointValue; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueItemVo.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueItemVo.java new file mode 100644 index 0000000..4f8016d --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueItemVo.java @@ -0,0 +1,31 @@ +package com.xzzn.ems.domain.vo; + +public class PointConfigLatestValueItemVo { + private String siteId; + private String deviceId; + private String dataKey; + + public String getSiteId() { + return siteId; + } + + public void setSiteId(String siteId) { + this.siteId = siteId; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getDataKey() { + return dataKey; + } + + public void setDataKey(String dataKey) { + this.dataKey = dataKey; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueRequest.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueRequest.java new file mode 100644 index 0000000..37eb506 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueRequest.java @@ -0,0 +1,15 @@ +package com.xzzn.ems.domain.vo; + +import java.util.List; + +public class PointConfigLatestValueRequest { + private List points; + + public List getPoints() { + return points; + } + + public void setPoints(List points) { + this.points = points; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueVo.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueVo.java new file mode 100644 index 0000000..76831c2 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueVo.java @@ -0,0 +1,51 @@ +package com.xzzn.ems.domain.vo; + +import java.util.Date; + +public class PointConfigLatestValueVo { + private String siteId; + private String deviceId; + private String dataKey; + private Object pointValue; + private Date dataTime; + + public String getSiteId() { + return siteId; + } + + public void setSiteId(String siteId) { + this.siteId = siteId; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getDataKey() { + return dataKey; + } + + public void setDataKey(String dataKey) { + this.dataKey = dataKey; + } + + public Object getPointValue() { + return pointValue; + } + + public void setPointValue(Object pointValue) { + this.pointValue = pointValue; + } + + public Date getDataTime() { + return dataTime; + } + + public void setDataTime(Date dataTime) { + this.dataTime = dataTime; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorDataSaveItemVo.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorDataSaveItemVo.java new file mode 100644 index 0000000..b71d607 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorDataSaveItemVo.java @@ -0,0 +1,33 @@ +package com.xzzn.ems.domain.vo; + +import java.util.Date; + +public class SiteMonitorDataSaveItemVo { + private String fieldCode; + private String fieldValue; + private Date valueTime; + + public String getFieldCode() { + return fieldCode; + } + + public void setFieldCode(String fieldCode) { + this.fieldCode = fieldCode; + } + + public String getFieldValue() { + return fieldValue; + } + + public void setFieldValue(String fieldValue) { + this.fieldValue = fieldValue; + } + + public Date getValueTime() { + return valueTime; + } + + public void setValueTime(Date valueTime) { + this.valueTime = valueTime; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorDataSaveRequest.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorDataSaveRequest.java new file mode 100644 index 0000000..252b4ed --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorDataSaveRequest.java @@ -0,0 +1,24 @@ +package com.xzzn.ems.domain.vo; + +import java.util.List; + +public class SiteMonitorDataSaveRequest { + private String siteId; + private List items; + + public String getSiteId() { + return siteId; + } + + public void setSiteId(String siteId) { + this.siteId = siteId; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectDisplayVo.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectDisplayVo.java new file mode 100644 index 0000000..d1dd6a1 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectDisplayVo.java @@ -0,0 +1,27 @@ +package com.xzzn.ems.domain.vo; + +import java.util.Date; + +/** + * 单站监控展示数据(配置项 + 当前值) + */ +public class SiteMonitorProjectDisplayVo extends SiteMonitorProjectPointMappingVo { + private String fieldValue; + private Date valueTime; + + public String getFieldValue() { + return fieldValue; + } + + public void setFieldValue(String fieldValue) { + this.fieldValue = fieldValue; + } + + public Date getValueTime() { + return valueTime; + } + + public void setValueTime(Date valueTime) { + this.valueTime = valueTime; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingSaveRequest.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingSaveRequest.java new file mode 100644 index 0000000..c1df3ab --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingSaveRequest.java @@ -0,0 +1,26 @@ +package com.xzzn.ems.domain.vo; + +import java.util.List; + +public class SiteMonitorProjectPointMappingSaveRequest { + + private String siteId; + + private List mappings; + + public String getSiteId() { + return siteId; + } + + public void setSiteId(String siteId) { + this.siteId = siteId; + } + + public List getMappings() { + return mappings; + } + + public void setMappings(List mappings) { + this.mappings = mappings; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingVo.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingVo.java new file mode 100644 index 0000000..3d518ed --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingVo.java @@ -0,0 +1,84 @@ +package com.xzzn.ems.domain.vo; + +public class SiteMonitorProjectPointMappingVo { + + private String moduleCode; + + private String moduleName; + + private String menuCode; + + private String menuName; + + private String sectionName; + + private String fieldCode; + + private String fieldName; + + private String dataPoint; + + public String getModuleCode() { + return moduleCode; + } + + public void setModuleCode(String moduleCode) { + this.moduleCode = moduleCode; + } + + public String getModuleName() { + return moduleName; + } + + public void setModuleName(String moduleName) { + this.moduleName = moduleName; + } + + public String getFieldCode() { + return fieldCode; + } + + public String getMenuCode() { + return menuCode; + } + + public void setMenuCode(String menuCode) { + this.menuCode = menuCode; + } + + public String getMenuName() { + return menuName; + } + + public void setMenuName(String menuName) { + this.menuName = menuName; + } + + public String getSectionName() { + return sectionName; + } + + public void setSectionName(String sectionName) { + this.sectionName = sectionName; + } + + public void setFieldCode(String fieldCode) { + this.fieldCode = fieldCode; + } + + public String getFieldName() { + return fieldName; + } + + public void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + public String getDataPoint() { + return dataPoint; + } + + public void setDataPoint(String dataPoint) { + this.dataPoint = dataPoint; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java new file mode 100644 index 0000000..76deb82 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java @@ -0,0 +1,44 @@ +package com.xzzn.ems.mapper; + +import com.xzzn.ems.domain.EmsPointConfig; +import com.xzzn.ems.domain.vo.GeneralQueryPointOptionVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface EmsPointConfigMapper { + EmsPointConfig selectEmsPointConfigById(Long id); + + List selectEmsPointConfigList(EmsPointConfig emsPointConfig); + + int insertEmsPointConfig(EmsPointConfig emsPointConfig); + + int updateEmsPointConfig(EmsPointConfig emsPointConfig); + + int deleteEmsPointConfigById(Long id); + + int deleteEmsPointConfigByIds(Long[] ids); + + int countBySiteId(@Param("siteId") String siteId); + + int deleteBySiteId(@Param("siteId") String siteId); + + int copyTemplateToSite(@Param("templateSiteId") String templateSiteId, + @Param("targetSiteId") String targetSiteId, + @Param("operName") String operName); + + String getRegisterAddress(@Param("siteId") String siteId, + @Param("deviceCategory") String deviceCategory, + @Param("deviceId") String deviceId, + @Param("dataKey") String dataKey); + + List getPointNameList(@Param("siteIds") List siteIds, + @Param("deviceCategory") String deviceCategory, + @Param("deviceId") String deviceId, + @Param("pointName") String pointName); + + List getConfigListForGeneralQuery(@Param("siteIds") List siteIds, + @Param("deviceCategory") String deviceCategory, + @Param("pointNames") List pointNames, + @Param("deviceIds") List deviceIds); +} diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorDataMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorDataMapper.java new file mode 100644 index 0000000..7f55eff --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorDataMapper.java @@ -0,0 +1,28 @@ +package com.xzzn.ems.mapper; + +import org.apache.ibatis.annotations.Param; + +/** + * 单站监控展示数据 Mapper + */ +public interface EmsSiteMonitorDataMapper { + String selectHistoryJsonByMinute(@Param("tableName") String tableName, + @Param("siteId") String siteId, + @Param("statisMinute") java.util.Date statisMinute); + + int upsertHistoryJsonByMinute(@Param("tableName") String tableName, + @Param("siteId") String siteId, + @Param("statisMinute") java.util.Date statisMinute, + @Param("dataJson") String dataJson, + @Param("operName") String operName); + + int updateHistoryHotColumns(@Param("tableName") String tableName, + @Param("siteId") String siteId, + @Param("statisMinute") java.util.Date statisMinute, + @Param("hotSoc") String hotSoc, + @Param("hotTotalActivePower") String hotTotalActivePower, + @Param("hotTotalReactivePower") String hotTotalReactivePower, + @Param("hotDayChargedCap") String hotDayChargedCap, + @Param("hotDayDisChargedCap") String hotDayDisChargedCap, + @Param("operName") String operName); +} diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorItemMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorItemMapper.java new file mode 100644 index 0000000..16b18c3 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorItemMapper.java @@ -0,0 +1,12 @@ +package com.xzzn.ems.mapper; + +import com.xzzn.ems.domain.EmsSiteMonitorItem; + +import java.util.List; + +/** + * 单站监控配置项定义 Mapper + */ +public interface EmsSiteMonitorItemMapper { + List selectEnabledList(); +} diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorPointMatchMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorPointMatchMapper.java new file mode 100644 index 0000000..0f001d6 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorPointMatchMapper.java @@ -0,0 +1,17 @@ +package com.xzzn.ems.mapper; + +import com.xzzn.ems.domain.EmsSiteMonitorPointMatch; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 单站监控字段点位映射 Mapper + */ +public interface EmsSiteMonitorPointMatchMapper { + List selectBySiteId(@Param("siteId") String siteId); + + int deleteBySiteId(@Param("siteId") String siteId); + + int insertBatch(@Param("list") List list); +} diff --git a/ems-system/src/main/java/com/xzzn/ems/service/IEmsPointConfigService.java b/ems-system/src/main/java/com/xzzn/ems/service/IEmsPointConfigService.java new file mode 100644 index 0000000..04abb2a --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/service/IEmsPointConfigService.java @@ -0,0 +1,33 @@ +package com.xzzn.ems.service; + +import com.xzzn.ems.domain.EmsPointConfig; +import com.xzzn.ems.domain.vo.ImportPointTemplateRequest; +import com.xzzn.ems.domain.vo.PointConfigCurveRequest; +import com.xzzn.ems.domain.vo.PointConfigCurveValueVo; +import com.xzzn.ems.domain.vo.PointConfigLatestValueRequest; +import com.xzzn.ems.domain.vo.PointConfigLatestValueVo; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +public interface IEmsPointConfigService { + List selectPointConfigList(EmsPointConfig pointConfig); + + EmsPointConfig selectPointConfigById(Long id); + + int insertPointConfig(EmsPointConfig pointConfig, String operName); + + int updatePointConfig(EmsPointConfig pointConfig, String operName); + + int deletePointConfigByIds(Long[] ids); + + String importTemplateBySite(ImportPointTemplateRequest request, String operName); + + String importCsvBySite(String siteId, Boolean overwrite, MultipartFile file, String operName); + + String getRegisterAddress(String siteId, String deviceCategory, String deviceId, String dataKey); + + List getLatestValues(PointConfigLatestValueRequest request); + + List getCurveData(PointConfigCurveRequest request); +} diff --git a/ems-system/src/main/java/com/xzzn/ems/service/InfluxPointDataWriter.java b/ems-system/src/main/java/com/xzzn/ems/service/InfluxPointDataWriter.java new file mode 100644 index 0000000..0799678 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/service/InfluxPointDataWriter.java @@ -0,0 +1,616 @@ +package com.xzzn.ems.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.math.BigDecimal; +import java.net.HttpURLConnection; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.HashMap; +import java.util.Map; + +@Component +public class InfluxPointDataWriter { + + private static final Logger log = LoggerFactory.getLogger(InfluxPointDataWriter.class); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Value("${influxdb.enabled:true}") + private boolean enabled; + + @Value("${influxdb.url:}") + private String url; + + @Value("${influxdb.username:}") + private String username; + + @Value("${influxdb.password:}") + private String password; + + @Value("${influxdb.api-token:}") + private String apiToken; + + @Value("${influxdb.database:ems_point_data}") + private String database; + + @Value("${influxdb.retention-policy:autogen}") + private String retentionPolicy; + + @Value("${influxdb.measurement:mqtt_point_data}") + private String measurement; + + @Value("${influxdb.write-method:POST}") + private String writeMethod; + + @Value("${influxdb.read-method:GET}") + private String readMethod; + + @Value("${influxdb.write-path:/write}") + private String writePath; + + @Value("${influxdb.query-path:/query}") + private String queryPath; + + @Value("${influxdb.org:}") + private String org; + + @Value("${influxdb.bucket:}") + private String bucket; + + @PostConstruct + public void init() { + if (!enabled) { + log.info("InfluxDB 写入已禁用"); + return; + } + if (url == null || url.trim().isEmpty()) { + log.warn("InfluxDB URL 未配置,跳过初始化"); + return; + } + log.info("InfluxDB 已启用 HTTP 接入, url: {}, database: {}", url, database); + } + + public void writeBatch(List payloads) { + if (!enabled || payloads == null || payloads.isEmpty()) { + return; + } + try { + StringBuilder body = new StringBuilder(); + for (PointWritePayload payload : payloads) { + if (payload == null || payload.getPointValue() == null) { + continue; + } + long time = payload.getDataTime() == null ? System.currentTimeMillis() : payload.getDataTime().getTime(); + body.append(measurement) + .append(",site_id=").append(escapeLineTag(payload.getSiteId())) + .append(",device_id=").append(escapeLineTag(payload.getDeviceId())) + .append(",point_key=").append(escapeLineTag(payload.getPointKey())) + .append(" value=").append(payload.getPointValue().toPlainString()) + .append(" ").append(time) + .append("\n"); + } + if (body.length() == 0) { + return; + } + String writeUrl = buildWriteUrl(); + if (isBlank(writeUrl)) { + log.warn("写入 InfluxDB 失败:v2 写入地址未构建成功,请检查 influxdb.org / influxdb.bucket 配置"); + return; + } + HttpResult result = executeRequest(methodOrDefault(writeMethod, "POST"), writeUrl, body.toString()); + if (result.code < 200 || result.code >= 300) { + if (result.code == 404 && isV2WritePath() && isOrgOrBucketMissing(result.body)) { + if (ensureV2OrgAndBucket()) { + HttpResult retryResult = executeRequest(methodOrDefault(writeMethod, "POST"), writeUrl, body.toString()); + if (retryResult.code >= 200 && retryResult.code < 300) { + log.info("InfluxDB org/bucket 自动创建成功,写入已恢复"); + return; + } + log.warn("InfluxDB 重试写入失败,HTTP状态码: {}, url: {}, body: {}", retryResult.code, writeUrl, safeLog(retryResult.body)); + return; + } + } + log.warn("写入 InfluxDB 失败,HTTP状态码: {}, url: {}, body: {}", result.code, writeUrl, safeLog(result.body)); + } + } catch (Exception e) { + log.warn("写入 InfluxDB 失败: {}", e.getMessage()); + } + } + + public List queryCurveData(String siteId, String deviceId, String pointKey, Date startTime, Date endTime) { + if (!enabled) { + return Collections.emptyList(); + } + if (isBlank(siteId) || isBlank(deviceId) || isBlank(pointKey) || startTime == null || endTime == null) { + return Collections.emptyList(); + } + + String normalizedSiteId = siteId.trim(); + String normalizedDeviceId = deviceId.trim(); + String normalizedPointKey = pointKey.trim(); + + String influxQl = String.format( + "SELECT \"value\" FROM \"%s\" WHERE \"site_id\" = '%s' AND \"device_id\" = '%s' AND \"point_key\" = '%s' " + + "AND time >= %dms AND time <= %dms ORDER BY time ASC", + measurement, + escapeTagValue(normalizedSiteId), + escapeTagValue(normalizedDeviceId), + escapeTagValue(normalizedPointKey), + startTime.getTime(), + endTime.getTime() + ); + + try { + String queryUrl = buildQueryUrl(influxQl); + List values = parseInfluxQlResponse(executeRequestWithResponse(methodOrDefault(readMethod, "GET"), queryUrl)); + if (!values.isEmpty()) { + return values; + } + + // 兼容 dataKey 大小写差异,避免点位 key 字母大小写不一致导致查不到曲线 + String regexQuery = String.format( + "SELECT \"value\" FROM \"%s\" WHERE \"site_id\" = '%s' AND \"device_id\" = '%s' AND \"point_key\" =~ /(?i)^%s$/ " + + "AND time >= %dms AND time <= %dms ORDER BY time ASC", + measurement, + escapeTagValue(normalizedSiteId), + escapeTagValue(normalizedDeviceId), + escapeRegex(normalizedPointKey), + startTime.getTime(), + endTime.getTime() + ); + values = parseInfluxQlResponse(executeRequestWithResponse(methodOrDefault(readMethod, "GET"), buildQueryUrl(regexQuery))); + return values; + } catch (Exception e) { + log.warn("查询 InfluxDB 曲线失败: {}", e.getMessage()); + return Collections.emptyList(); + } + } + + private String buildWriteUrl() { + if (isV2WritePath()) { + return buildV2WriteUrl(); + } + StringBuilder sb = new StringBuilder(trimTrailingSlash(url)); + sb.append(normalizePath(writePath)).append("?db=").append(urlEncode(database)); + if (!isBlank(retentionPolicy)) { + sb.append("&rp=").append(urlEncode(retentionPolicy)); + } + sb.append("&precision=ms"); + return sb.toString(); + } + + private String buildQueryUrl(String influxQl) { + String queryDb = database; + if (isV2WritePath() && !isBlank(bucket)) { + queryDb = bucket; + } + StringBuilder queryUrl = new StringBuilder(trimTrailingSlash(url)) + .append(normalizePath(queryPath)) + .append("?db=").append(urlEncode(queryDb)) + .append("&epoch=ms&q=").append(urlEncode(influxQl)); + if (!isBlank(retentionPolicy)) { + queryUrl.append("&rp=").append(urlEncode(retentionPolicy)); + } + return queryUrl.toString(); + } + + private String buildV2WriteUrl() { + String currentBucket = isBlank(bucket) ? database : bucket; + if (isBlank(org) || isBlank(currentBucket)) { + return null; + } + return trimTrailingSlash(url) + + "/api/v2/write?org=" + urlEncode(org) + + "&bucket=" + urlEncode(currentBucket) + + "&precision=ms"; + } + + private HttpResult executeRequest(String method, String requestUrl, String body) throws Exception { + HttpURLConnection connection = openConnection(method, requestUrl, "text/plain; charset=UTF-8"); + try { + if (body != null) { + connection.setDoOutput(true); + try (OutputStream os = connection.getOutputStream()) { + os.write(body.getBytes(StandardCharsets.UTF_8)); + } + } + int code = connection.getResponseCode(); + InputStream is = (code >= 200 && code < 300) ? connection.getInputStream() : connection.getErrorStream(); + return new HttpResult(code, readStream(is)); + } finally { + connection.disconnect(); + } + } + + private String executeRequestWithResponse(String method, String requestUrl) throws Exception { + HttpURLConnection connection = openConnection(method, requestUrl, "text/plain; charset=UTF-8"); + try { + int code = connection.getResponseCode(); + InputStream is = (code >= 200 && code < 300) ? connection.getInputStream() : connection.getErrorStream(); + return readStream(is); + } finally { + connection.disconnect(); + } + } + + private String readStream(InputStream is) throws Exception { + if (is == null) { + return ""; + } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + return sb.toString(); + } + } + + private HttpResult executeJsonRequest(String method, String requestUrl, String jsonBody) throws Exception { + HttpURLConnection connection = openConnection(method, requestUrl, "application/json; charset=UTF-8"); + try { + if (jsonBody != null) { + connection.setDoOutput(true); + try (OutputStream os = connection.getOutputStream()) { + os.write(jsonBody.getBytes(StandardCharsets.UTF_8)); + } + } + int code = connection.getResponseCode(); + InputStream is = (code >= 200 && code < 300) ? connection.getInputStream() : connection.getErrorStream(); + return new HttpResult(code, readStream(is)); + } finally { + connection.disconnect(); + } + } + + private HttpURLConnection openConnection(String method, String requestUrl, String contentType) throws Exception { + HttpURLConnection connection = (HttpURLConnection) new java.net.URL(requestUrl).openConnection(); + connection.setRequestMethod(method); + connection.setConnectTimeout(5000); + connection.setReadTimeout(8000); + connection.setRequestProperty("Accept", "application/json"); + connection.setRequestProperty("Content-Type", contentType); + if (!isBlank(apiToken)) { + connection.setRequestProperty("Authorization", "Token " + apiToken.trim()); + } else if (!isBlank(username) || !isBlank(password)) { + String basic = java.util.Base64.getEncoder() + .encodeToString((safe(username) + ":" + safe(password)).getBytes(StandardCharsets.UTF_8)); + connection.setRequestProperty("Authorization", "Basic " + basic); + } + return connection; + } + + private String escapeLineTag(String value) { + if (value == null) { + return ""; + } + return value.replace("\\", "\\\\") + .replace(",", "\\,") + .replace(" ", "\\ ") + .replace("=", "\\="); + } + + private String trimTrailingSlash(String v) { + if (v == null) { + return ""; + } + String result = v.trim(); + while (result.endsWith("/")) { + result = result.substring(0, result.length() - 1); + } + return result; + } + + private String urlEncode(String value) { + try { + return URLEncoder.encode(safe(value), StandardCharsets.UTF_8.name()); + } catch (Exception e) { + return safe(value); + } + } + + private String safe(String value) { + return value == null ? "" : value; + } + + private boolean isV2WritePath() { + return "/api/v2/write".equals(normalizePath(writePath)); + } + + private boolean isOrgOrBucketMissing(String responseBody) { + if (isBlank(responseBody)) { + return false; + } + String lower = responseBody.toLowerCase(); + return (lower.contains("organization") && lower.contains("not found")) + || (lower.contains("bucket") && lower.contains("not found")); + } + + private boolean ensureV2OrgAndBucket() { + try { + if (isBlank(org)) { + log.warn("InfluxDB 自动创建 organization 失败:org 配置为空"); + return false; + } + String currentBucket = isBlank(bucket) ? database : bucket; + String orgId = queryOrgId(org); + if (isBlank(orgId)) { + orgId = createOrg(org); + } + if (isBlank(orgId)) { + log.warn("InfluxDB 自动创建 organization 失败,org={}", org); + return false; + } + + if (!bucketExists(org, currentBucket)) { + if (!createBucket(orgId, currentBucket)) { + log.warn("InfluxDB 自动创建 bucket 失败,org={}, bucket={}", org, currentBucket); + return false; + } + } + return true; + } catch (Exception ex) { + log.warn("InfluxDB 自动创建 org/bucket 异常: {}", ex.getMessage()); + return false; + } + } + + private String queryOrgId(String orgName) throws Exception { + String requestUrl = trimTrailingSlash(url) + "/api/v2/orgs?org=" + urlEncode(orgName); + HttpResult result = executeJsonRequest("GET", requestUrl, null); + if (result.code < 200 || result.code >= 300 || isBlank(result.body)) { + log.warn("查询 organization 失败,status={}, org={}, body={}", result.code, orgName, safeLog(result.body)); + return null; + } + JsonNode root = OBJECT_MAPPER.readTree(result.body); + JsonNode orgs = root.path("orgs"); + if (orgs.isArray() && orgs.size() > 0) { + JsonNode first = orgs.get(0); + if (first != null) { + String id = first.path("id").asText(null); + if (!isBlank(id)) { + return id; + } + } + } + return null; + } + + private String createOrg(String orgName) throws Exception { + String requestUrl = trimTrailingSlash(url) + "/api/v2/orgs"; + Map payload = new HashMap<>(); + payload.put("name", orgName); + String body = OBJECT_MAPPER.writeValueAsString(payload); + HttpResult result = executeJsonRequest("POST", requestUrl, body); + if ((result.code < 200 || result.code >= 300) || isBlank(result.body)) { + log.warn("创建 organization 失败,status={}, org={}, body={}", result.code, orgName, safeLog(result.body)); + return null; + } + JsonNode root = OBJECT_MAPPER.readTree(result.body); + String id = root.path("id").asText(null); + return isBlank(id) ? null : id; + } + + private boolean bucketExists(String orgName, String bucketName) throws Exception { + String requestUrl = trimTrailingSlash(url) + + "/api/v2/buckets?name=" + urlEncode(bucketName) + + "&org=" + urlEncode(orgName); + HttpResult result = executeJsonRequest("GET", requestUrl, null); + if (result.code < 200 || result.code >= 300 || isBlank(result.body)) { + log.warn("查询 bucket 失败,status={}, org={}, bucket={}, body={}", result.code, orgName, bucketName, safeLog(result.body)); + return false; + } + JsonNode root = OBJECT_MAPPER.readTree(result.body); + JsonNode buckets = root.path("buckets"); + return buckets.isArray() && buckets.size() > 0; + } + + private boolean createBucket(String orgId, String bucketName) throws Exception { + String requestUrl = trimTrailingSlash(url) + "/api/v2/buckets"; + Map payload = new HashMap<>(); + payload.put("orgID", orgId); + payload.put("name", bucketName); + String body = OBJECT_MAPPER.writeValueAsString(payload); + HttpResult result = executeJsonRequest("POST", requestUrl, body); + if (result.code < 200 || result.code >= 300) { + log.warn("创建 bucket 失败,status={}, orgId={}, bucket={}, body={}", result.code, orgId, bucketName, safeLog(result.body)); + } + return result.code >= 200 && result.code < 300; + } + + private String methodOrDefault(String method, String defaultMethod) { + return isBlank(method) ? defaultMethod : method.trim().toUpperCase(); + } + + private String normalizePath(String path) { + if (isBlank(path)) { + return "/"; + } + String p = path.trim(); + return p.startsWith("/") ? p : "/" + p; + } + + private String safeLog(String body) { + if (body == null) { + return ""; + } + return body.length() > 200 ? body.substring(0, 200) : body; + } + + private Date parseInfluxTime(Object timeObject) { + if (timeObject == null) { + return null; + } + if (timeObject instanceof Number) { + return new Date(((Number) timeObject).longValue()); + } + try { + return Date.from(Instant.parse(String.valueOf(timeObject))); + } catch (Exception e) { + return null; + } + } + + private BigDecimal toBigDecimal(Object valueObject) { + if (valueObject == null) { + return null; + } + if (valueObject instanceof BigDecimal) { + return (BigDecimal) valueObject; + } + if (valueObject instanceof Number) { + return new BigDecimal(valueObject.toString()); + } + try { + return new BigDecimal(String.valueOf(valueObject)); + } catch (Exception e) { + return null; + } + } + + private String escapeTagValue(String value) { + return value == null ? "" : value.replace("\\", "\\\\").replace("'", "\\'"); + } + + private String escapeRegex(String value) { + if (value == null) { + return ""; + } + return value.replace("\\", "\\\\") + .replace("/", "\\/") + .replace(".", "\\.") + .replace("(", "\\(") + .replace(")", "\\)") + .replace("[", "\\[") + .replace("]", "\\]") + .replace("{", "\\{") + .replace("}", "\\}") + .replace("^", "\\^") + .replace("$", "\\$") + .replace("*", "\\*") + .replace("+", "\\+") + .replace("?", "\\?") + .replace("|", "\\|"); + } + + private List parseInfluxQlResponse(String response) throws Exception { + if (isBlank(response)) { + return Collections.emptyList(); + } + List values = new ArrayList<>(); + JsonNode root = OBJECT_MAPPER.readTree(response); + JsonNode resultsNode = root.path("results"); + if (!resultsNode.isArray()) { + return values; + } + + for (JsonNode result : resultsNode) { + JsonNode seriesArray = result.path("series"); + if (!seriesArray.isArray()) { + continue; + } + for (JsonNode series : seriesArray) { + JsonNode rows = series.path("values"); + if (!rows.isArray()) { + continue; + } + for (JsonNode row : rows) { + if (!row.isArray() || row.size() < 2) { + continue; + } + Date dataTime = parseInfluxTime(row.get(0).isNumber() ? row.get(0).asLong() : row.get(0).asText()); + BigDecimal pointValue = toBigDecimal(row.get(1).isNumber() ? row.get(1).asText() : row.get(1).asText(null)); + if (dataTime == null || pointValue == null) { + continue; + } + values.add(new PointValue(dataTime, pointValue)); + } + } + } + return values; + } + + private boolean isBlank(String value) { + return value == null || value.trim().isEmpty(); + } + + public static class PointWritePayload { + private final String siteId; + private final String deviceId; + private final String pointKey; + private final BigDecimal pointValue; + private final Date dataTime; + + public PointWritePayload(String siteId, String deviceId, String pointKey, BigDecimal pointValue, Date dataTime) { + this.siteId = siteId; + this.deviceId = deviceId; + this.pointKey = pointKey; + this.pointValue = pointValue; + this.dataTime = dataTime; + } + + public String getSiteId() { + return siteId; + } + + public String getDeviceId() { + return deviceId; + } + + public String getPointKey() { + return pointKey; + } + + public BigDecimal getPointValue() { + return pointValue; + } + + public Date getDataTime() { + return dataTime; + } + } + + public static class PointValue { + private final Date dataTime; + private final BigDecimal pointValue; + + public PointValue(Date dataTime, BigDecimal pointValue) { + this.dataTime = dataTime; + this.pointValue = pointValue; + } + + public Date getDataTime() { + return dataTime; + } + + public BigDecimal getPointValue() { + return pointValue; + } + } + + private static class HttpResult { + private final int code; + private final String body; + + private HttpResult(int code, String body) { + this.code = code; + this.body = body; + } + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java new file mode 100644 index 0000000..d39dd20 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java @@ -0,0 +1,668 @@ +package com.xzzn.ems.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.xzzn.common.exception.ServiceException; +import com.xzzn.common.constant.RedisKeyConstants; +import com.xzzn.common.core.redis.RedisCache; +import com.xzzn.common.utils.DateUtils; +import com.xzzn.common.utils.StringUtils; +import com.xzzn.ems.domain.EmsPointConfig; +import com.xzzn.ems.domain.vo.ImportPointTemplateRequest; +import com.xzzn.ems.domain.vo.PointConfigCurveRequest; +import com.xzzn.ems.domain.vo.PointConfigCurveValueVo; +import com.xzzn.ems.domain.vo.PointConfigLatestValueItemVo; +import com.xzzn.ems.domain.vo.PointConfigLatestValueRequest; +import com.xzzn.ems.domain.vo.PointConfigLatestValueVo; +import com.xzzn.ems.mapper.EmsPointConfigMapper; +import com.xzzn.ems.service.IEmsPointConfigService; +import com.xzzn.ems.service.InfluxPointDataWriter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Locale; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Service +public class EmsPointConfigServiceImpl implements IEmsPointConfigService { + private static final String TEMPLATE_SITE_ID = "DEFAULT"; + private static final Pattern DB_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$"); + private static final Pattern INTEGER_PATTERN = Pattern.compile("^-?\\d+$"); + private static final Pattern DECIMAL_PATTERN = Pattern.compile("^-?\\d+(\\.\\d+)?$"); + private static final Pattern CALC_EXPRESSION_PATTERN = Pattern.compile("^[0-9A-Za-z_+\\-*/().\\s]+$"); + private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + @Autowired + private EmsPointConfigMapper emsPointConfigMapper; + @Autowired + private RedisCache redisCache; + @Autowired + private InfluxPointDataWriter influxPointDataWriter; + + @Override + public List selectPointConfigList(EmsPointConfig pointConfig) { + return emsPointConfigMapper.selectEmsPointConfigList(pointConfig); + } + + @Override + public EmsPointConfig selectPointConfigById(Long id) { + return emsPointConfigMapper.selectEmsPointConfigById(id); + } + + @Override + public int insertPointConfig(EmsPointConfig pointConfig, String operName) { + if (pointConfig == null || StringUtils.isBlank(pointConfig.getSiteId())) { + throw new ServiceException("站点ID不能为空"); + } + normalizeAndValidatePointConfig(pointConfig); + pointConfig.setCreateBy(operName); + pointConfig.setUpdateBy(operName); + int rows = emsPointConfigMapper.insertEmsPointConfig(pointConfig); + if (rows > 0) { + invalidatePointConfigCache(pointConfig.getSiteId(), pointConfig.getDeviceId()); + } + return rows; + } + + @Override + public int updatePointConfig(EmsPointConfig pointConfig, String operName) { + EmsPointConfig oldConfig = pointConfig.getId() == null ? null : emsPointConfigMapper.selectEmsPointConfigById(pointConfig.getId()); + normalizeAndValidatePointConfig(pointConfig); + pointConfig.setUpdateBy(operName); + int rows = emsPointConfigMapper.updateEmsPointConfig(pointConfig); + if (rows > 0) { + if (oldConfig != null) { + invalidatePointConfigCache(oldConfig.getSiteId(), oldConfig.getDeviceId()); + } + invalidatePointConfigCache(pointConfig.getSiteId(), pointConfig.getDeviceId()); + } + return rows; + } + + @Override + public int deletePointConfigByIds(Long[] ids) { + if (ids != null) { + for (Long id : ids) { + EmsPointConfig oldConfig = emsPointConfigMapper.selectEmsPointConfigById(id); + if (oldConfig != null) { + invalidatePointConfigCache(oldConfig.getSiteId(), oldConfig.getDeviceId()); + } + } + } + return emsPointConfigMapper.deleteEmsPointConfigByIds(ids); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String importTemplateBySite(ImportPointTemplateRequest request, String operName) { + String targetSiteId = request.getSiteId(); + if (StringUtils.isBlank(targetSiteId)) { + throw new ServiceException("站点ID不能为空"); + } + if (TEMPLATE_SITE_ID.equals(targetSiteId)) { + throw new ServiceException("模板站点不支持作为导入目标站点"); + } + + int templateCount = emsPointConfigMapper.countBySiteId(TEMPLATE_SITE_ID); + if (templateCount <= 0) { + throw new ServiceException("模板点位配置不存在,无法导入"); + } + + boolean overwrite = Boolean.TRUE.equals(request.getOverwrite()); + int targetCount = emsPointConfigMapper.countBySiteId(targetSiteId); + if (targetCount > 0 && !overwrite) { + throw new ServiceException("目标站点已存在点位配置,请勾选“覆盖已存在数据”后重试"); + } + + if (targetCount > 0) { + emsPointConfigMapper.deleteBySiteId(targetSiteId); + } + + int importCount = emsPointConfigMapper.copyTemplateToSite(TEMPLATE_SITE_ID, targetSiteId, operName); + invalidatePointConfigCacheBySite(targetSiteId); + return String.format("导入成功:站点 %s,点位 %d 条", targetSiteId, importCount); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String importCsvBySite(String siteId, Boolean overwrite, MultipartFile file, String operName) { + if (StringUtils.isBlank(siteId)) { + throw new ServiceException("站点ID不能为空"); + } + if (!DB_NAME_PATTERN.matcher(siteId).matches()) { + throw new ServiceException(String.format("站点ID不合法(siteId=%s)", siteId)); + } + if (TEMPLATE_SITE_ID.equals(siteId)) { + throw new ServiceException("模板站点不支持作为导入目标站点"); + } + if (file == null || file.isEmpty()) { + throw new ServiceException("请上传CSV文件"); + } + if (!StringUtils.endsWithIgnoreCase(file.getOriginalFilename(), ".csv")) { + throw new ServiceException("仅支持上传CSV文件"); + } + + boolean overwriteFlag = Boolean.TRUE.equals(overwrite); + int targetCount = emsPointConfigMapper.countBySiteId(siteId); + if (targetCount > 0 && !overwriteFlag) { + throw new ServiceException("目标站点已存在点位配置,请勾选“覆盖已存在点位数据”后重试"); + } + + List pointConfigList = parseCsv(file, siteId); + if (pointConfigList.isEmpty()) { + throw new ServiceException("CSV没有可导入的数据"); + } + + if (targetCount > 0) { + emsPointConfigMapper.deleteBySiteId(siteId); + } + + int importCount = 0; + for (EmsPointConfig pointConfig : pointConfigList) { + pointConfig.setCreateBy(operName); + pointConfig.setUpdateBy(operName); + importCount += emsPointConfigMapper.insertEmsPointConfig(pointConfig); + } + + invalidatePointConfigCacheBySite(siteId); + return String.format("导入成功:站点 %s,点位 %d 条", siteId, importCount); + } + + @Override + public String getRegisterAddress(String siteId, String deviceCategory, String deviceId, String dataKey) { + if (StringUtils.isAnyBlank(siteId, deviceCategory, deviceId, dataKey)) { + return null; + } + return emsPointConfigMapper.getRegisterAddress(siteId, deviceCategory, deviceId, dataKey); + } + + @Override + public List getLatestValues(PointConfigLatestValueRequest request) { + List result = new ArrayList<>(); + if (request == null || request.getPoints() == null || request.getPoints().isEmpty()) { + return result; + } + + Map> configMapCache = new HashMap<>(); + for (PointConfigLatestValueItemVo item : request.getPoints()) { + if (item == null || StringUtils.isAnyBlank(item.getSiteId(), item.getDeviceId(), item.getDataKey())) { + continue; + } + PointConfigLatestValueVo latestValue = queryLatestValueFromRedis(item, configMapCache); + result.add(latestValue); + } + return result; + } + + @Override + public List getCurveData(PointConfigCurveRequest request) { + if (request == null || StringUtils.isAnyBlank(request.getSiteId(), request.getDeviceId(), request.getDataKey())) { + return new ArrayList<>(); + } + String siteId = StringUtils.trim(request.getSiteId()); + String deviceId = StringUtils.trim(request.getDeviceId()); + String dataKey = StringUtils.trim(request.getDataKey()); + if (StringUtils.isAnyBlank(siteId, deviceId, dataKey)) { + return new ArrayList<>(); + } + Date[] range = resolveTimeRange(request); + return queryCurveDataFromInflux(siteId, deviceId, dataKey, range[0], range[1]); + } + + private PointConfigLatestValueVo queryLatestValueFromRedis(PointConfigLatestValueItemVo item, + Map> configMapCache) { + PointConfigLatestValueVo vo = new PointConfigLatestValueVo(); + vo.setSiteId(item.getSiteId()); + vo.setDeviceId(item.getDeviceId()); + vo.setDataKey(item.getDataKey()); + + String redisKey = RedisKeyConstants.ORIGINAL_MQTT_DATA + item.getSiteId() + "_" + item.getDeviceId(); + Object raw = redisCache.getCacheObject(redisKey); + if (raw == null) { + return vo; + } + + JSONObject root = toJsonObject(raw); + if (root == null) { + return vo; + } + JSONObject dataObject = extractDataObject(root); + if (dataObject == null) { + return vo; + } + + Object rawValue = getValueIgnoreCase(dataObject, item.getDataKey()); + BigDecimal pointValue = StringUtils.getBigDecimal(rawValue); + if (pointValue != null) { + EmsPointConfig pointConfig = getPointConfig(item.getSiteId(), item.getDeviceId(), item.getDataKey(), configMapCache); + vo.setPointValue(convertPointValue(pointValue, pointConfig)); + } + vo.setDataTime(extractDataTime(root)); + return vo; + } + + private List queryCurveDataFromInflux(String siteId, String deviceId, String dataKey, Date startTime, Date endTime) { + List values = influxPointDataWriter.queryCurveData(siteId, deviceId, dataKey, startTime, endTime); + if (values == null || values.isEmpty()) { + return new ArrayList<>(); + } + return values.stream().map(value -> { + PointConfigCurveValueVo vo = new PointConfigCurveValueVo(); + vo.setDataTime(value.getDataTime()); + vo.setPointValue(value.getPointValue()); + return vo; + }).collect(Collectors.toList()); + } + + private Date[] resolveTimeRange(PointConfigCurveRequest request) { + LocalDateTime end = LocalDateTime.now(); + String rangeType = StringUtils.defaultIfBlank(request.getRangeType(), "day").toLowerCase(Locale.ROOT); + LocalDateTime start; + if ("custom".equals(rangeType)) { + if (StringUtils.isAnyBlank(request.getStartTime(), request.getEndTime())) { + throw new ServiceException("自定义时间范围必须传入开始时间和结束时间"); + } + start = parseDateTime(request.getStartTime()); + end = parseDateTime(request.getEndTime()); + } else if ("week".equals(rangeType)) { + start = end.minusDays(7); + } else if ("month".equals(rangeType)) { + start = end.minusDays(30); + } else { + start = end.minusDays(1); + } + if (start.isAfter(end)) { + throw new ServiceException("开始时间不能晚于结束时间"); + } + return new Date[]{ + Timestamp.valueOf(start), + Timestamp.valueOf(end) + }; + } + + private LocalDateTime parseDateTime(String value) { + try { + return LocalDateTime.parse(value, DATETIME_FORMATTER); + } catch (DateTimeParseException ex) { + throw new ServiceException("时间格式错误,请使用 yyyy-MM-dd HH:mm:ss"); + } + } + + private void invalidatePointConfigCache(String siteId, String deviceId) { + if (StringUtils.isAnyBlank(siteId, deviceId)) { + return; + } + redisCache.deleteObject(RedisKeyConstants.POINT_CONFIG_DEVICE + siteId + "_" + deviceId); + } + + private void invalidatePointConfigCacheBySite(String siteId) { + if (StringUtils.isBlank(siteId)) { + return; + } + Collection keys = redisCache.keys(RedisKeyConstants.POINT_CONFIG_DEVICE + siteId + "_*"); + if (keys != null && !keys.isEmpty()) { + redisCache.deleteObject(keys); + } + } + + private JSONObject toJsonObject(Object raw) { + if (raw == null) { + return null; + } + if (raw instanceof JSONObject) { + return (JSONObject) raw; + } + try { + return JSON.parseObject(JSON.toJSONString(raw)); + } catch (Exception ex) { + return null; + } + } + + private JSONObject extractDataObject(JSONObject root) { + if (root == null) { + return null; + } + JSONObject dataObject = root.getJSONObject("Data"); + if (dataObject != null) { + return dataObject; + } + String dataJson = root.getString("Data"); + if (StringUtils.isBlank(dataJson)) { + return null; + } + try { + return JSON.parseObject(dataJson); + } catch (Exception ex) { + return null; + } + } + + private Date extractDataTime(JSONObject root) { + if (root == null) { + return null; + } + Long timestamp = root.getLong("timestamp"); + if (timestamp != null) { + return DateUtils.convertUpdateTime(timestamp); + } + return null; + } + + private Object getValueIgnoreCase(JSONObject dataObject, String dataKey) { + if (dataObject == null || StringUtils.isBlank(dataKey)) { + return null; + } + Object directValue = dataObject.get(dataKey); + if (directValue != null) { + return directValue; + } + for (String key : dataObject.keySet()) { + if (key != null && key.equalsIgnoreCase(dataKey)) { + return dataObject.get(key); + } + } + return null; + } + + private EmsPointConfig getPointConfig(String siteId, + String deviceId, + String dataKey, + Map> configMapCache) { + if (StringUtils.isAnyBlank(siteId, deviceId, dataKey)) { + return null; + } + String cacheKey = siteId + "|" + deviceId; + Map configByKey = configMapCache.get(cacheKey); + if (configByKey == null) { + EmsPointConfig query = new EmsPointConfig(); + query.setSiteId(siteId); + query.setDeviceId(deviceId); + List configList = emsPointConfigMapper.selectEmsPointConfigList(query); + configByKey = new HashMap<>(); + if (configList != null) { + for (EmsPointConfig config : configList) { + if (config != null && StringUtils.isNotBlank(config.getDataKey())) { + configByKey.put(config.getDataKey().toUpperCase(Locale.ROOT), config); + } + } + } + configMapCache.put(cacheKey, configByKey); + } + return configByKey.get(dataKey.toUpperCase(Locale.ROOT)); + } + + private BigDecimal convertPointValue(BigDecimal sourceValue, EmsPointConfig pointConfig) { + if (sourceValue == null || pointConfig == null) { + return sourceValue; + } + BigDecimal a = pointConfig.getDataA() == null ? BigDecimal.ZERO : pointConfig.getDataA(); + BigDecimal k = pointConfig.getDataK() == null ? BigDecimal.ONE : pointConfig.getDataK(); + BigDecimal b = pointConfig.getDataB() == null ? BigDecimal.ZERO : pointConfig.getDataB(); + return a.multiply(sourceValue).multiply(sourceValue) + .add(k.multiply(sourceValue)) + .add(b); + } + + private List parseCsv(MultipartFile file, String siteId) { + List result = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) { + String headerLine = reader.readLine(); + if (StringUtils.isBlank(headerLine)) { + throw new ServiceException("CSV表头不能为空"); + } + List headerList = parseCsvLine(removeUtf8Bom(headerLine)); + Map headerIndex = buildHeaderIndex(headerList); + assertRequiredHeader(headerIndex, "device_category", "device_category"); + assertRequiredHeader(headerIndex, "device_id", "device_id"); + assertRequiredHeader(headerIndex, "data_key", "data_key"); + assertRequiredHeader(headerIndex, "point_desc", "point_desc"); + + String line; + int lineNo = 1; + while ((line = reader.readLine()) != null) { + lineNo++; + if (StringUtils.isBlank(line)) { + continue; + } + List valueList = parseCsvLine(line); + EmsPointConfig pointConfig = new EmsPointConfig(); + pointConfig.setSiteId(siteId); + pointConfig.setDeviceCategory(getRequiredString(valueList, headerIndex, "device_category", lineNo)); + pointConfig.setDeviceId(getRequiredString(valueList, headerIndex, "device_id", lineNo)); + pointConfig.setDataKey(getRequiredString(valueList, headerIndex, "data_key", lineNo)); + pointConfig.setPointDesc(getRequiredString(valueList, headerIndex, "point_desc", lineNo)); + String registerAddress = getString(valueList, headerIndex, "register_address"); + pointConfig.setRegisterAddress(StringUtils.isBlank(registerAddress) ? "" : registerAddress); + pointConfig.setPointName(getString(valueList, headerIndex, "point_name")); + pointConfig.setDataUnit(getString(valueList, headerIndex, "data_unit")); + pointConfig.setDataA(getDecimal(valueList, headerIndex, "data_a", lineNo)); + pointConfig.setDataK(getDecimal(valueList, headerIndex, "data_k", lineNo)); + pointConfig.setDataB(getDecimal(valueList, headerIndex, "data_b", lineNo)); + pointConfig.setDataBit(getInteger(valueList, headerIndex, "data_bit", lineNo)); + pointConfig.setIsAlarm(getInteger(valueList, headerIndex, "is_alarm", lineNo)); + pointConfig.setPointType(getString(valueList, headerIndex, "point_type")); + pointConfig.setCalcExpression(getString(valueList, headerIndex, "calc_expression")); + pointConfig.setRemark(getString(valueList, headerIndex, "remark")); + normalizeAndValidatePointConfig(pointConfig); + result.add(pointConfig); + } + } catch (IOException e) { + throw new ServiceException("CSV读取失败: " + e.getMessage()); + } + return result; + } + + private void assertRequiredHeader(Map headerIndex, String headerKey, String headerName) { + if (!headerIndex.containsKey(headerKey)) { + throw new ServiceException("CSV缺少必填列: " + headerName); + } + } + + private Map buildHeaderIndex(List headerList) { + Map headerIndex = new HashMap<>(); + for (int i = 0; i < headerList.size(); i++) { + String normalizedHeader = toCanonicalHeaderKey(headerList.get(i)); + if (StringUtils.isBlank(normalizedHeader)) { + continue; + } + headerIndex.putIfAbsent(normalizedHeader, i); + } + return headerIndex; + } + + private String getRequiredString(List valueList, Map headerIndex, String key, int lineNo) { + String value = getString(valueList, headerIndex, key); + if (StringUtils.isBlank(value)) { + throw new ServiceException(String.format("CSV第%d行字段[%s]不能为空", lineNo, key)); + } + return value; + } + + private String getString(List valueList, Map headerIndex, String key) { + Integer index = headerIndex.get(key); + if (index == null || index < 0 || index >= valueList.size()) { + return null; + } + String value = valueList.get(index); + return StringUtils.isBlank(value) ? null : value.trim(); + } + + private Integer getInteger(List valueList, Map headerIndex, String key, int lineNo) { + String value = getString(valueList, headerIndex, key); + if (StringUtils.isBlank(value)) { + return null; + } + if (!INTEGER_PATTERN.matcher(value).matches()) { + throw new ServiceException(String.format("CSV第%d行字段[%s]必须是整数", lineNo, key)); + } + return Integer.parseInt(value); + } + + private BigDecimal getDecimal(List valueList, Map headerIndex, String key, int lineNo) { + String value = getString(valueList, headerIndex, key); + if (StringUtils.isBlank(value)) { + return null; + } + if (!DECIMAL_PATTERN.matcher(value).matches()) { + throw new ServiceException(String.format("CSV第%d行字段[%s]必须是数字", lineNo, key)); + } + return new BigDecimal(value); + } + + private String removeUtf8Bom(String content) { + if (StringUtils.isBlank(content)) { + return content; + } + if (content.charAt(0) == '\ufeff') { + return content.substring(1); + } + return content; + } + + private String normalizeHeader(String header) { + if (header == null) { + return ""; + } + return header.trim().toLowerCase().replace("-", "_").replace(" ", ""); + } + + private String toCanonicalHeaderKey(String header) { + String normalized = normalizeHeader(header); + if (StringUtils.isBlank(normalized)) { + return ""; + } + String compact = normalized.replace("_", ""); + switch (compact) { + case "siteid": + case "站点id": + return "site_id"; + case "devicecategory": + case "设备类型": + return "device_category"; + case "deviceid": + case "设备id": + return "device_id"; + case "pointname": + case "点位名称": + return "point_name"; + case "datakey": + case "数据键": + return "data_key"; + case "pointdesc": + case "点位描述": + return "point_desc"; + case "registeraddress": + case "寄存器地址": + return "register_address"; + case "dataunit": + case "datounit": + case "单位": + return "data_unit"; + case "dataa": + case "datoa": + case "a系数": + return "data_a"; + case "datak": + case "datok": + case "k系数": + return "data_k"; + case "datab": + case "datob": + case "b系数": + return "data_b"; + case "databit": + case "datobit": + case "位偏移": + return "data_bit"; + case "isalarm": + case "报警点位": + return "is_alarm"; + case "pointtype": + case "点位类型": + return "point_type"; + case "calcexpression": + case "计算表达式": + return "calc_expression"; + case "remark": + case "备注": + return "remark"; + default: + return normalized; + } + } + + private List parseCsvLine(String line) { + List result = new ArrayList<>(); + if (line == null) { + return result; + } + StringBuilder current = new StringBuilder(); + boolean inQuotes = false; + for (int i = 0; i < line.length(); i++) { + char c = line.charAt(i); + if (c == '"') { + if (inQuotes && i + 1 < line.length() && line.charAt(i + 1) == '"') { + current.append('"'); + i++; + } else { + inQuotes = !inQuotes; + } + continue; + } + if (c == ',' && !inQuotes) { + result.add(current.toString()); + current.setLength(0); + continue; + } + current.append(c); + } + result.add(current.toString()); + return result; + } + + private void normalizeAndValidatePointConfig(EmsPointConfig pointConfig) { + if (pointConfig == null) { + return; + } + pointConfig.setPointType(normalizePointType(pointConfig.getPointType())); + pointConfig.setCalcExpression(StringUtils.trimToNull(pointConfig.getCalcExpression())); + if ("calc".equals(pointConfig.getPointType())) { + if (StringUtils.isBlank(pointConfig.getCalcExpression())) { + throw new ServiceException("计算点必须填写计算表达式"); + } + if (!CALC_EXPRESSION_PATTERN.matcher(pointConfig.getCalcExpression()).matches()) { + throw new ServiceException("计算表达式仅支持数字、字母、下划线、空格和四则运算符"); + } + } else { + pointConfig.setCalcExpression(null); + } + } + + private String normalizePointType(String pointType) { + String normalized = StringUtils.trimToEmpty(pointType).toLowerCase(Locale.ROOT); + if ("calc".equals(normalized) || "calculate".equals(normalized) || "计算点".equals(normalized)) { + return "calc"; + } + return "data"; + } + +} diff --git a/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml new file mode 100644 index 0000000..6a0add3 --- /dev/null +++ b/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml @@ -0,0 +1,231 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select id, site_id, device_category, device_id, point_name, data_key, point_desc, register_address, + data_unit, data_a, data_k, data_b, data_bit, is_alarm, point_type, calc_expression, + create_by, create_time, update_by, update_time, remark + from ems_point_config + + + + + + + + insert into ems_point_config + + site_id, + device_category, + device_id, + point_name, + data_key, + point_desc, + register_address, + data_unit, + data_a, + data_k, + data_b, + data_bit, + is_alarm, + point_type, + calc_expression, + create_by, + create_time, + update_by, + update_time, + remark, + + + #{siteId}, + #{deviceCategory}, + #{deviceId}, + #{pointName}, + #{dataKey}, + #{pointDesc}, + #{registerAddress}, + #{dataUnit}, + #{dataA}, + #{dataK}, + #{dataB}, + #{dataBit}, + #{isAlarm}, + #{pointType}, + #{calcExpression}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{remark}, + + + + + update ems_point_config + + site_id = #{siteId}, + device_category = #{deviceCategory}, + device_id = #{deviceId}, + point_name = #{pointName}, + data_key = #{dataKey}, + point_desc = #{pointDesc}, + register_address = #{registerAddress}, + data_unit = #{dataUnit}, + data_a = #{dataA}, + data_k = #{dataK}, + data_b = #{dataB}, + data_bit = #{dataBit}, + is_alarm = #{isAlarm}, + point_type = #{pointType}, + calc_expression = #{calcExpression}, + update_by = #{updateBy}, + update_time = #{updateTime}, + remark = #{remark}, + + where id = #{id} + + + + delete from ems_point_config where id = #{id} + + + + delete from ems_point_config where id in + + #{id} + + + + + + + delete from ems_point_config + where site_id = #{siteId} + + + + insert into ems_point_config ( + site_id, device_category, device_id, point_name, data_key, point_desc, register_address, + data_unit, data_a, data_k, data_b, data_bit, is_alarm, point_type, calc_expression, + create_by, create_time, update_by, update_time, remark + ) + select + #{targetSiteId}, device_category, device_id, point_name, data_key, point_desc, register_address, + data_unit, data_a, data_k, data_b, data_bit, is_alarm, point_type, calc_expression, + #{operName}, now(), #{operName}, now(), remark + from ems_point_config + where site_id = #{templateSiteId} + + + + + + + + + diff --git a/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorDataMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorDataMapper.xml new file mode 100644 index 0000000..ce0109c --- /dev/null +++ b/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorDataMapper.xml @@ -0,0 +1,40 @@ + + + + + + + + insert into ${tableName} ( + site_id, statis_minute, data_json, create_by, create_time, update_by, update_time + ) values ( + #{siteId}, #{statisMinute}, #{dataJson}, #{operName}, now(), #{operName}, now() + ) + on duplicate key update + data_json = values(data_json), + update_by = values(update_by), + update_time = now() + + + + update ${tableName} + set hot_soc = #{hotSoc}, + hot_total_active_power = #{hotTotalActivePower}, + hot_total_reactive_power = #{hotTotalReactivePower}, + hot_day_charged_cap = #{hotDayChargedCap}, + hot_day_dis_charged_cap = #{hotDayDisChargedCap}, + update_by = #{operName}, + update_time = now() + where site_id = #{siteId} + and statis_minute = #{statisMinute} + + + diff --git a/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorItemMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorItemMapper.xml new file mode 100644 index 0000000..14635c6 --- /dev/null +++ b/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorItemMapper.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml new file mode 100644 index 0000000..6627ba2 --- /dev/null +++ b/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + delete from ems_site_monitor_point_match + where site_id = #{siteId} + + + + insert into ems_site_monitor_point_match + (site_id, field_code, data_point, create_by, create_time, update_by, update_time) + values + + (#{item.siteId}, #{item.fieldCode}, #{item.dataPoint}, #{item.createBy}, now(), #{item.updateBy}, now()) + + + + -- 2.49.0 From 6253fb6b2d58ba12c016f666f50b8e8dcffda2ae Mon Sep 17 00:00:00 2001 From: dashixiong Date: Fri, 13 Feb 2026 21:41:23 +0800 Subject: [PATCH 03/10] =?UTF-8?q?=E4=B8=B4=E6=97=B6=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ems/EmsPointCalcConfigController.java | 51 ++ .../EmsStrategyRuntimeConfigController.java | 48 ++ .../com/xzzn/quartz/task/StrategyPoller.java | 145 +++- .../com/xzzn/ems/domain/EmsPcsSetting.java | 15 + .../xzzn/ems/domain/EmsPointCalcConfig.java | 128 ++++ .../com/xzzn/ems/domain/EmsPointConfig.java | 12 + .../ems/domain/EmsSiteMonitorPointMatch.java | 18 + .../ems/domain/EmsStrategyRuntimeConfig.java | 144 ++++ .../ems/domain/EmsStrategyTempTimeConfig.java | 30 + .../domain/vo/PointConfigCurveRequest.java | 18 + ...MonitorProjectPointMappingSaveRequest.java | 10 + .../vo/SiteMonitorProjectPointMappingVo.java | 20 + .../ems/mapper/EmsPointCalcConfigMapper.java | 24 + .../xzzn/ems/mapper/EmsPointConfigMapper.java | 5 + .../EmsStrategyRuntimeConfigMapper.java | 36 + .../service/IEmsPointCalcConfigService.java | 19 + .../IEmsStrategyRuntimeConfigService.java | 28 + .../impl/DeviceDataProcessServiceImpl.java | 624 +++++++++++++++++- .../impl/EmsDeviceSettingServiceImpl.java | 303 ++++++++- .../impl/EmsPointCalcConfigServiceImpl.java | 144 ++++ .../impl/EmsPointConfigServiceImpl.java | 172 ++++- .../EmsStrategyRuntimeConfigServiceImpl.java | 87 +++ .../impl/EmsStrategyTempServiceImpl.java | 3 + .../service/impl/GeneralQueryServiceImpl.java | 18 +- .../mapper/ems/EmsPcsSettingMapper.xml | 10 +- .../mapper/ems/EmsPointCalcConfigMapper.xml | 116 ++++ .../mapper/ems/EmsPointConfigMapper.xml | 54 +- .../ems/EmsSiteMonitorPointMatchMapper.xml | 8 +- .../ems/EmsStrategyRuntimeConfigMapper.xml | 98 +++ 29 files changed, 2277 insertions(+), 111 deletions(-) create mode 100644 ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointCalcConfigController.java create mode 100644 ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsStrategyRuntimeConfigController.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/EmsPointCalcConfig.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/EmsStrategyRuntimeConfig.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointCalcConfigMapper.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/mapper/EmsStrategyRuntimeConfigMapper.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/service/IEmsPointCalcConfigService.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/service/IEmsStrategyRuntimeConfigService.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointCalcConfigServiceImpl.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStrategyRuntimeConfigServiceImpl.java create mode 100644 ems-system/src/main/resources/mapper/ems/EmsPointCalcConfigMapper.xml create mode 100644 ems-system/src/main/resources/mapper/ems/EmsStrategyRuntimeConfigMapper.xml diff --git a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointCalcConfigController.java b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointCalcConfigController.java new file mode 100644 index 0000000..465054c --- /dev/null +++ b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointCalcConfigController.java @@ -0,0 +1,51 @@ +package com.xzzn.web.controller.ems; + +import com.xzzn.common.annotation.Log; +import com.xzzn.common.core.controller.BaseController; +import com.xzzn.common.core.domain.AjaxResult; +import com.xzzn.common.core.page.TableDataInfo; +import com.xzzn.common.enums.BusinessType; +import com.xzzn.ems.domain.EmsPointCalcConfig; +import com.xzzn.ems.service.IEmsPointCalcConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/ems/pointCalcConfig") +public class EmsPointCalcConfigController extends BaseController { + + @Autowired + private IEmsPointCalcConfigService pointCalcConfigService; + + @GetMapping("/list") + public TableDataInfo list(EmsPointCalcConfig pointCalcConfig) { + startPage(); + List list = pointCalcConfigService.selectPointCalcConfigList(pointCalcConfig); + return getDataTable(list); + } + + @GetMapping("/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) { + return success(pointCalcConfigService.selectPointCalcConfigById(id)); + } + + @Log(title = "计算点配置", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody EmsPointCalcConfig pointCalcConfig) { + return toAjax(pointCalcConfigService.insertPointCalcConfig(pointCalcConfig, getUsername())); + } + + @Log(title = "计算点配置", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody EmsPointCalcConfig pointCalcConfig) { + return toAjax(pointCalcConfigService.updatePointCalcConfig(pointCalcConfig, getUsername())); + } + + @Log(title = "计算点配置", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) { + return toAjax(pointCalcConfigService.deletePointCalcConfigByIds(ids)); + } +} diff --git a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsStrategyRuntimeConfigController.java b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsStrategyRuntimeConfigController.java new file mode 100644 index 0000000..0067ecc --- /dev/null +++ b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsStrategyRuntimeConfigController.java @@ -0,0 +1,48 @@ +package com.xzzn.web.controller.ems; + +import com.xzzn.common.core.controller.BaseController; +import com.xzzn.common.core.domain.AjaxResult; +import com.xzzn.common.utils.StringUtils; +import com.xzzn.ems.domain.EmsStrategyRuntimeConfig; +import com.xzzn.ems.service.IEmsStrategyRuntimeConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 策略运行参数配置Controller + */ +@RestController +@RequestMapping("/system/strategyRuntimeConfig") +public class EmsStrategyRuntimeConfigController extends BaseController { + + @Autowired + private IEmsStrategyRuntimeConfigService runtimeConfigService; + + /** + * 按站点ID获取策略运行参数 + */ + @GetMapping("/getBySiteId") + public AjaxResult getBySiteId(String siteId) { + if (StringUtils.isEmpty(siteId)) { + return error("缺少必填字段siteId"); + } + return success(runtimeConfigService.getBySiteId(siteId)); + } + + /** + * 保存策略运行参数(按siteId新增/更新) + */ + @PostMapping("/save") + public AjaxResult save(@RequestBody EmsStrategyRuntimeConfig config) { + if (config == null || StringUtils.isEmpty(config.getSiteId())) { + return error("缺少必填字段siteId"); + } + config.setCreateBy(getUsername()); + config.setUpdateBy(getUsername()); + return toAjax(runtimeConfigService.saveBySiteId(config)); + } +} diff --git a/ems-quartz/src/main/java/com/xzzn/quartz/task/StrategyPoller.java b/ems-quartz/src/main/java/com/xzzn/quartz/task/StrategyPoller.java index be613db..7ec40b3 100644 --- a/ems-quartz/src/main/java/com/xzzn/quartz/task/StrategyPoller.java +++ b/ems-quartz/src/main/java/com/xzzn/quartz/task/StrategyPoller.java @@ -16,6 +16,7 @@ import com.xzzn.ems.domain.EmsAmmeterData; import com.xzzn.ems.domain.EmsBatteryStack; import com.xzzn.ems.domain.EmsDevicesSetting; import com.xzzn.ems.domain.EmsPcsSetting; +import com.xzzn.ems.domain.EmsStrategyRuntimeConfig; import com.xzzn.ems.domain.EmsStrategyLog; import com.xzzn.ems.domain.EmsStrategyTemp; import com.xzzn.ems.domain.EmsStrategyTimeConfig; @@ -25,17 +26,20 @@ import com.xzzn.ems.mapper.EmsBatteryStackMapper; import com.xzzn.ems.mapper.EmsDevicesSettingMapper; import com.xzzn.ems.mapper.EmsPcsSettingMapper; import com.xzzn.ems.mapper.EmsStrategyLogMapper; +import com.xzzn.ems.mapper.EmsStrategyRuntimeConfigMapper; import com.xzzn.ems.mapper.EmsStrategyRunningMapper; import com.xzzn.ems.mapper.EmsStrategyTempMapper; import com.xzzn.ems.mapper.EmsStrategyTimeConfigMapper; import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -57,16 +61,20 @@ public class StrategyPoller { private static final ConcurrentHashMap strategyLocks = new ConcurrentHashMap<>(); // SOC 上下限值,默认为0%-100% - private static final BigDecimal SOC_DOWN = new BigDecimal(0); - private static final BigDecimal SOC_UP = new BigDecimal(100); + private static final BigDecimal DEFAULT_SOC_DOWN = BigDecimal.ZERO; + private static final BigDecimal DEFAULT_SOC_UP = new BigDecimal(100); // 逆变器功率下限值,默认为30kW - private static final BigDecimal ANTI_REVERSE_THRESHOLD = new BigDecimal(30); + private static final BigDecimal DEFAULT_ANTI_REVERSE_THRESHOLD = new BigDecimal(30); // 逆变器下限值范围,默认为20% - private static final BigDecimal ANTI_REVERSE_RANGE_PERCENT = new BigDecimal(20); + private static final BigDecimal DEFAULT_ANTI_REVERSE_RANGE_PERCENT = new BigDecimal(20); // 逆变器功率上限值,默认为100kW - private static final BigDecimal ANTI_REVERSE_UP = new BigDecimal(100); + private static final BigDecimal DEFAULT_ANTI_REVERSE_UP = new BigDecimal(100); // PCS功率降幅,默认为10% - private static final BigDecimal ANTI_REVERSE_POWER_DOWN_PERCENT = new BigDecimal(10); + private static final BigDecimal DEFAULT_ANTI_REVERSE_POWER_DOWN_PERCENT = new BigDecimal(10); + // 电网有功功率低于20kW时,强制待机 + private static final BigDecimal DEFAULT_ANTI_REVERSE_HARD_STOP_THRESHOLD = new BigDecimal(20); + // 除法精度,避免BigDecimal除不尽异常 + private static final int POWER_SCALE = 4; @Autowired private EmsStrategyRunningMapper emsStrategyRunningMapper; @@ -85,6 +93,8 @@ public class StrategyPoller { @Autowired private EmsStrategyLogMapper emsStrategyLogMapper; @Autowired + private EmsStrategyRuntimeConfigMapper runtimeConfigMapper; + @Autowired private ModbusProcessor modbusProcessor; @Resource(name = "modbusExecutor") @@ -132,6 +142,7 @@ public class StrategyPoller { } private void dealStrategyCurveData(Long strategyId, String siteId) { + EmsStrategyRuntimeConfig runtimeConfig = getRuntimeConfig(siteId); // 1.获取当前策略的所有模板 List> temps = emsStrategyTempMapper.getTempNameList(strategyId, siteId); if (CollectionUtils.isEmpty(temps)) { @@ -174,24 +185,38 @@ public class StrategyPoller { continue; } // 判断SOC上下限 - if (isSocInRange(emsStrategyTemp)) { - BigDecimal avgChargeDischargePower = emsStrategyTemp.getChargeDischargePower().divide(new BigDecimal(pcsDeviceList.size())); + if (isSocInRange(emsStrategyTemp, runtimeConfig)) { + Map pcsSettingCache = new HashMap<>(); + BigDecimal avgChargeDischargePower = emsStrategyTemp.getChargeDischargePower() + .divide(new BigDecimal(pcsDeviceList.size()), POWER_SCALE, RoundingMode.HALF_UP); + BigDecimal totalActivePower = null; + if (ChargeStatus.DISCHARGING.getCode().equals(emsStrategyTemp.getChargeStatus())) { + // 同一站点同一轮执行只读取一次电网电表,降低重复查库和数据抖动 + EmsAmmeterData emsAmmeterData = emsAmmeterDataMapper.getLastData(emsStrategyTemp.getSiteId(), SiteDevice.LOAD.name()); + if (emsAmmeterData != null) { + totalActivePower = emsAmmeterData.getTotalActivePower(); + } + } for (EmsDevicesSetting pcsDevice : pcsDeviceList) { - EmsPcsSetting pcsSetting = emsPcsSettingMapper.selectEmsPcsSettingByDeviceId(pcsDevice.getId()); + EmsPcsSetting pcsSetting = pcsSettingCache.computeIfAbsent( + pcsDevice.getId(), + id -> emsPcsSettingMapper.selectEmsPcsSettingByDeviceId(id) + ); if (pcsSetting == null || pcsSetting.getClusterNum() < 1) { logger.info("当前站点: {}, PCS设备: {} 未获取电池簇数量", siteId, pcsDevice.getDeviceId()); continue; } // 功率默认放大10倍,平均功率值,根据电池簇数量进行平均分配 - avgChargeDischargePower = avgChargeDischargePower.multiply(new BigDecimal(10)).divide(new BigDecimal(pcsSetting.getClusterNum())); + BigDecimal strategyPower = avgChargeDischargePower.multiply(new BigDecimal(10)) + .divide(new BigDecimal(pcsSetting.getClusterNum()), POWER_SCALE, RoundingMode.HALF_UP); // 根据充电状态,处理数据 if (ChargeStatus.CHARGING.getCode().equals(emsStrategyTemp.getChargeStatus())) { // 发送Modbus命令控制设备-充电 - sendModbusCommand(Collections.singletonList(pcsDevice), pcsSetting, ChargeStatus.CHARGING, avgChargeDischargePower, emsStrategyTemp, false, null); + sendModbusCommand(Collections.singletonList(pcsDevice), pcsSetting, ChargeStatus.CHARGING, strategyPower, emsStrategyTemp, false, null); } else if (ChargeStatus.DISCHARGING.getCode().equals(emsStrategyTemp.getChargeStatus())) { boolean needAntiReverseFlow = false; Integer powerDownType = null; - BigDecimal chargeDischargePower = avgChargeDischargePower; + BigDecimal chargeDischargePower = strategyPower; // 查询策略运行日志 EmsStrategyLog lastStrategyLog = getLastStrategyLog(pcsDevice.getDeviceId(), emsStrategyTemp); if (lastStrategyLog != null) { @@ -204,32 +229,54 @@ public class StrategyPoller { } // 查询电网电表的正向有功功率,36kW-50kW范围内,稳定运行,低于36kW,降功率,高于50kW,增加功率 - EmsAmmeterData emsAmmeterData = emsAmmeterDataMapper.getLastData(emsStrategyTemp.getSiteId(), SiteDevice.LOAD.name()); - if (emsAmmeterData == null || emsAmmeterData.getTotalActivePower() == null) { - logger.info("当前站点: {}, 未获取到最新电表数据", emsStrategyTemp.getSiteId()); + if (totalActivePower == null) { + logger.warn("当前站点: {}, 未获取到最新电表数据,执行保守策略并切换待机", emsStrategyTemp.getSiteId()); + sendModbusCommand(Collections.singletonList(pcsDevice), pcsSetting, ChargeStatus.STANDBY, BigDecimal.ZERO, emsStrategyTemp, true, 0); + continue; } else { + // 电网功率过低,直接待机,不再放电 + if (totalActivePower.compareTo(runtimeConfig.getAntiReverseHardStopThreshold()) < 0) { + sendModbusCommand(Collections.singletonList(pcsDevice), pcsSetting, ChargeStatus.STANDBY, BigDecimal.ZERO, emsStrategyTemp, true, 0); + continue; + } + // 放电开始先按差值限幅:差值=电网功率-防逆流阈值 + BigDecimal diffPower = totalActivePower.subtract(runtimeConfig.getAntiReverseThreshold()); + BigDecimal targetPower = diffPower.compareTo(BigDecimal.ZERO) > 0 ? diffPower : BigDecimal.ZERO; + if (targetPower.compareTo(strategyPower) > 0) { + targetPower = strategyPower; + } + if (chargeDischargePower.compareTo(targetPower) > 0) { + chargeDischargePower = targetPower; + } // 判断是否需要防逆流 - needAntiReverseFlow = isNeedAntiReverseFlow(emsAmmeterData.getTotalActivePower()); - BigDecimal power = avgChargeDischargePower.multiply(ANTI_REVERSE_POWER_DOWN_PERCENT).divide(new BigDecimal(100)); + needAntiReverseFlow = isNeedAntiReverseFlow(totalActivePower, runtimeConfig); + BigDecimal power = strategyPower.multiply(runtimeConfig.getAntiReversePowerDownPercent()) + .divide(new BigDecimal(100), POWER_SCALE, RoundingMode.HALF_UP); if (needAntiReverseFlow) { // 降功率 chargeDischargePower = chargeDischargePower.subtract(power); powerDownType = 0; } else { // 判断是否需要增加功率, - if (powerDownType != null && emsAmmeterData.getTotalActivePower().compareTo(ANTI_REVERSE_UP) > 0) { - if (chargeDischargePower.compareTo(avgChargeDischargePower) == 0) { - // 功率增加到平均值则停止 + if (powerDownType != null && totalActivePower.compareTo(runtimeConfig.getAntiReverseUp()) > 0) { + if (chargeDischargePower.compareTo(targetPower) >= 0) { + // 功率增加到限幅值则停止 continue; } // 增加功率 chargeDischargePower = chargeDischargePower.add(power); + if (chargeDischargePower.compareTo(targetPower) > 0) { + chargeDischargePower = targetPower; + } powerDownType = 1; needAntiReverseFlow = true; } } } + if (chargeDischargePower.compareTo(BigDecimal.ZERO) < 0) { + chargeDischargePower = BigDecimal.ZERO; + } if (BigDecimal.ZERO.compareTo(chargeDischargePower) == 0) { // 如果已经降功率到0,则设备直接待机 // 发送Modbus命令控制设备-待机 @@ -297,11 +344,11 @@ public class StrategyPoller { return emsStrategyLogMapper.getLastStrategyLog(query); } - private boolean isNeedAntiReverseFlow(BigDecimal totalActivePower) { + private boolean isNeedAntiReverseFlow(BigDecimal totalActivePower, EmsStrategyRuntimeConfig runtimeConfig) { // 获取当前设定的防逆流阈值(30kW) - BigDecimal threshold = ANTI_REVERSE_THRESHOLD; + BigDecimal threshold = runtimeConfig.getAntiReverseThreshold(); // 计算20%范围的上限(36kW) - BigDecimal upperLimit = threshold.multiply(ANTI_REVERSE_RANGE_PERCENT).divide(new BigDecimal(100)).add(threshold); + BigDecimal upperLimit = threshold.multiply(runtimeConfig.getAntiReverseRangePercent()).divide(new BigDecimal(100)).add(threshold); // 判断电网电表正向有功功率是否小于36kW(接近30kW的20%范围) return totalActivePower.compareTo(upperLimit) < 0; @@ -409,8 +456,9 @@ public class StrategyPoller { continue; } else { // 充、放电,则先开机设备 - switchDevice(pcsDevice, pcsSetting, WorkStatus.NORMAL); - continue; + if (!switchDevice(pcsDevice, pcsSetting, WorkStatus.NORMAL)) { + continue; + } } } @@ -440,14 +488,17 @@ public class StrategyPoller { private boolean switchDevice(EmsDevicesSetting pcsDevice, EmsPcsSetting pcsSetting, WorkStatus workStatus) { String siteId = pcsDevice.getSiteId(); String deviceId = pcsDevice.getDeviceId(); + String originalWorkStatus = pcsDevice.getWorkStatus(); pcsDevice.setWorkStatus(workStatus.getCode()); DeviceConfig deviceConfig = getDeviceConfig(siteId, deviceId, pcsDevice, pcsSetting , null, 1); if (deviceConfig == null) { + pcsDevice.setWorkStatus(originalWorkStatus); return false; } boolean result = modbusProcessor.writeDataToDeviceWithRetry(deviceConfig); if (!result) { - logger.info("当前站点: {}, PCS设备: {} modbus控制设备{}指令发送失败", siteId, deviceConfig, workStatus.getInfo()); + pcsDevice.setWorkStatus(originalWorkStatus); + logger.info("当前站点: {}, PCS设备: {} modbus控制设备{}指令发送失败", siteId, deviceId, workStatus.getInfo()); } return result; } @@ -461,13 +512,17 @@ public class StrategyPoller { LocalTime endLocalTime = endTime.toInstant() .atZone(zoneId) .toLocalTime(); - return now.equals(startLocalTime) || (now.isAfter(startLocalTime) && now.isBefore(endLocalTime)); + // 支持跨天时段,如23:00-01:00;边界采用闭区间 + if (!startLocalTime.isAfter(endLocalTime)) { + return !now.isBefore(startLocalTime) && !now.isAfter(endLocalTime); + } + return !now.isBefore(startLocalTime) || !now.isAfter(endLocalTime); } // 判断SOC上限和下限 - private boolean isSocInRange(EmsStrategyTemp emsStrategyTemp) { - BigDecimal socDown = SOC_DOWN; - BigDecimal socUp = SOC_UP; + private boolean isSocInRange(EmsStrategyTemp emsStrategyTemp, EmsStrategyRuntimeConfig runtimeConfig) { + BigDecimal socDown = runtimeConfig.getSocDown(); + BigDecimal socUp = runtimeConfig.getSocUp(); if (SocLimit.ON.getCode().equals(emsStrategyTemp.getSdcLimit())) { socDown = emsStrategyTemp.getSdcDown(); socUp = emsStrategyTemp.getSdcUp(); @@ -491,4 +546,34 @@ public class StrategyPoller { return true; } + private EmsStrategyRuntimeConfig getRuntimeConfig(String siteId) { + EmsStrategyRuntimeConfig config = runtimeConfigMapper.selectBySiteId(siteId); + if (config == null) { + config = new EmsStrategyRuntimeConfig(); + config.setSiteId(siteId); + } + if (config.getSocDown() == null) { + config.setSocDown(DEFAULT_SOC_DOWN); + } + if (config.getSocUp() == null) { + config.setSocUp(DEFAULT_SOC_UP); + } + if (config.getAntiReverseThreshold() == null) { + config.setAntiReverseThreshold(DEFAULT_ANTI_REVERSE_THRESHOLD); + } + if (config.getAntiReverseRangePercent() == null) { + config.setAntiReverseRangePercent(DEFAULT_ANTI_REVERSE_RANGE_PERCENT); + } + if (config.getAntiReverseUp() == null) { + config.setAntiReverseUp(DEFAULT_ANTI_REVERSE_UP); + } + if (config.getAntiReversePowerDownPercent() == null) { + config.setAntiReversePowerDownPercent(DEFAULT_ANTI_REVERSE_POWER_DOWN_PERCENT); + } + if (config.getAntiReverseHardStopThreshold() == null) { + config.setAntiReverseHardStopThreshold(DEFAULT_ANTI_REVERSE_HARD_STOP_THRESHOLD); + } + return config; + } + } diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/EmsPcsSetting.java b/ems-system/src/main/java/com/xzzn/ems/domain/EmsPcsSetting.java index 2327f61..7b71d6a 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/EmsPcsSetting.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/EmsPcsSetting.java @@ -49,6 +49,10 @@ public class EmsPcsSetting extends BaseEntity @Excel(name = "关机目标功率") private BigDecimal stopPower; + /** 目标功率倍率 */ + @Excel(name = "目标功率倍率") + private BigDecimal powerMultiplier; + /** 电池簇数 */ @Excel(name = "电池簇数") private Integer clusterNum; @@ -135,6 +139,16 @@ public class EmsPcsSetting extends BaseEntity return stopPower; } + public BigDecimal getPowerMultiplier() + { + return powerMultiplier; + } + + public void setPowerMultiplier(BigDecimal powerMultiplier) + { + this.powerMultiplier = powerMultiplier; + } + public void setClusterNum(Integer clusterNum) { this.clusterNum = clusterNum; @@ -165,6 +179,7 @@ public class EmsPcsSetting extends BaseEntity .append("stopCommand", getStopCommand()) .append("startPower", getStartPower()) .append("stopPower", getStopPower()) + .append("powerMultiplier", getPowerMultiplier()) .append("clusterNum", getClusterNum()) .append("clusterPointAddress", getClusterPointAddress()) .append("createBy", getCreateBy()) diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/EmsPointCalcConfig.java b/ems-system/src/main/java/com/xzzn/ems/domain/EmsPointCalcConfig.java new file mode 100644 index 0000000..7c628bd --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/EmsPointCalcConfig.java @@ -0,0 +1,128 @@ +package com.xzzn.ems.domain; + +import com.xzzn.common.annotation.Excel; +import com.xzzn.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +public class EmsPointCalcConfig extends BaseEntity { + private static final long serialVersionUID = 1L; + + private Long id; + + @Excel(name = "点位ID") + private String pointId; + + @Excel(name = "站点ID") + private String siteId; + + @Excel(name = "设备类型") + private String deviceCategory; + + @Excel(name = "点位名称") + private String pointName; + + @Excel(name = "数据键") + private String dataKey; + + @Excel(name = "点位描述") + private String pointDesc; + + @Excel(name = "单位") + private String dataUnit; + + @Excel(name = "计算表达式") + private String calcExpression; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getPointId() { + return pointId; + } + + public void setPointId(String pointId) { + this.pointId = pointId; + } + + public String getSiteId() { + return siteId; + } + + public void setSiteId(String siteId) { + this.siteId = siteId; + } + + public String getPointName() { + return pointName; + } + + public void setPointName(String pointName) { + this.pointName = pointName; + } + + public String getDeviceCategory() { + return deviceCategory; + } + + public void setDeviceCategory(String deviceCategory) { + this.deviceCategory = deviceCategory; + } + + public String getDataKey() { + return dataKey; + } + + public void setDataKey(String dataKey) { + this.dataKey = dataKey; + } + + public String getPointDesc() { + return pointDesc; + } + + public void setPointDesc(String pointDesc) { + this.pointDesc = pointDesc; + } + + public String getDataUnit() { + return dataUnit; + } + + public void setDataUnit(String dataUnit) { + this.dataUnit = dataUnit; + } + + public String getCalcExpression() { + return calcExpression; + } + + public void setCalcExpression(String calcExpression) { + this.calcExpression = calcExpression; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) + .append("id", getId()) + .append("pointId", getPointId()) + .append("siteId", getSiteId()) + .append("deviceCategory", getDeviceCategory()) + .append("pointName", getPointName()) + .append("dataKey", getDataKey()) + .append("pointDesc", getPointDesc()) + .append("dataUnit", getDataUnit()) + .append("calcExpression", getCalcExpression()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/EmsPointConfig.java b/ems-system/src/main/java/com/xzzn/ems/domain/EmsPointConfig.java index 309c0ea..c8a144b 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/EmsPointConfig.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/EmsPointConfig.java @@ -15,6 +15,9 @@ public class EmsPointConfig extends BaseEntity { private Long id; + @Excel(name = "点位ID") + private String pointId; + @Excel(name = "站点ID") private String siteId; @@ -68,6 +71,14 @@ public class EmsPointConfig extends BaseEntity { this.id = id; } + public String getPointId() { + return pointId; + } + + public void setPointId(String pointId) { + this.pointId = pointId; + } + public String getSiteId() { return siteId; } @@ -192,6 +203,7 @@ public class EmsPointConfig extends BaseEntity { public String toString() { return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) .append("id", getId()) + .append("pointId", getPointId()) .append("siteId", getSiteId()) .append("deviceCategory", getDeviceCategory()) .append("deviceId", getDeviceId()) diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorPointMatch.java b/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorPointMatch.java index e6db1d3..206ebf4 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorPointMatch.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorPointMatch.java @@ -12,6 +12,8 @@ public class EmsSiteMonitorPointMatch extends BaseEntity { private String siteId; private String fieldCode; private String dataPoint; + private String fixedDataPoint; + private Integer useFixedDisplay; public Long getId() { return id; @@ -44,4 +46,20 @@ public class EmsSiteMonitorPointMatch extends BaseEntity { public void setDataPoint(String dataPoint) { this.dataPoint = dataPoint; } + + public String getFixedDataPoint() { + return fixedDataPoint; + } + + public void setFixedDataPoint(String fixedDataPoint) { + this.fixedDataPoint = fixedDataPoint; + } + + public Integer getUseFixedDisplay() { + return useFixedDisplay; + } + + public void setUseFixedDisplay(Integer useFixedDisplay) { + this.useFixedDisplay = useFixedDisplay; + } } diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/EmsStrategyRuntimeConfig.java b/ems-system/src/main/java/com/xzzn/ems/domain/EmsStrategyRuntimeConfig.java new file mode 100644 index 0000000..a2058a1 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/EmsStrategyRuntimeConfig.java @@ -0,0 +1,144 @@ +package com.xzzn.ems.domain; + +import com.xzzn.common.annotation.Excel; +import com.xzzn.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.math.BigDecimal; + +/** + * 策略运行参数配置对象 ems_strategy_runtime_config + * + * @author xzzn + * @date 2026-02-13 + */ +public class EmsStrategyRuntimeConfig extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** 主键 */ + private Long id; + + /** 站点ID */ + @Excel(name = "站点ID") + private String siteId; + + /** SOC下限(%) */ + @Excel(name = "SOC下限(%)") + private BigDecimal socDown; + + /** SOC上限(%) */ + @Excel(name = "SOC上限(%)") + private BigDecimal socUp; + + /** 防逆流阈值(kW) */ + @Excel(name = "防逆流阈值(kW)") + private BigDecimal antiReverseThreshold; + + /** 防逆流阈值上浮比例(%) */ + @Excel(name = "防逆流阈值上浮比例(%)") + private BigDecimal antiReverseRangePercent; + + /** 防逆流恢复上限(kW) */ + @Excel(name = "防逆流恢复上限(kW)") + private BigDecimal antiReverseUp; + + /** 防逆流降功率比例(%) */ + @Excel(name = "防逆流降功率比例(%)") + private BigDecimal antiReversePowerDownPercent; + /** 防逆流硬停阈值(kW) */ + @Excel(name = "防逆流硬停阈值(kW)") + private BigDecimal antiReverseHardStopThreshold; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getSiteId() { + return siteId; + } + + public void setSiteId(String siteId) { + this.siteId = siteId; + } + + public BigDecimal getSocDown() { + return socDown; + } + + public void setSocDown(BigDecimal socDown) { + this.socDown = socDown; + } + + public BigDecimal getSocUp() { + return socUp; + } + + public void setSocUp(BigDecimal socUp) { + this.socUp = socUp; + } + + public BigDecimal getAntiReverseThreshold() { + return antiReverseThreshold; + } + + public void setAntiReverseThreshold(BigDecimal antiReverseThreshold) { + this.antiReverseThreshold = antiReverseThreshold; + } + + public BigDecimal getAntiReverseRangePercent() { + return antiReverseRangePercent; + } + + public void setAntiReverseRangePercent(BigDecimal antiReverseRangePercent) { + this.antiReverseRangePercent = antiReverseRangePercent; + } + + public BigDecimal getAntiReverseUp() { + return antiReverseUp; + } + + public void setAntiReverseUp(BigDecimal antiReverseUp) { + this.antiReverseUp = antiReverseUp; + } + + public BigDecimal getAntiReversePowerDownPercent() { + return antiReversePowerDownPercent; + } + + public void setAntiReversePowerDownPercent(BigDecimal antiReversePowerDownPercent) { + this.antiReversePowerDownPercent = antiReversePowerDownPercent; + } + + public BigDecimal getAntiReverseHardStopThreshold() { + return antiReverseHardStopThreshold; + } + + public void setAntiReverseHardStopThreshold(BigDecimal antiReverseHardStopThreshold) { + this.antiReverseHardStopThreshold = antiReverseHardStopThreshold; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) + .append("id", getId()) + .append("siteId", getSiteId()) + .append("socDown", getSocDown()) + .append("socUp", getSocUp()) + .append("antiReverseThreshold", getAntiReverseThreshold()) + .append("antiReverseRangePercent", getAntiReverseRangePercent()) + .append("antiReverseUp", getAntiReverseUp()) + .append("antiReversePowerDownPercent", getAntiReversePowerDownPercent()) + .append("antiReverseHardStopThreshold", getAntiReverseHardStopThreshold()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/EmsStrategyTempTimeConfig.java b/ems-system/src/main/java/com/xzzn/ems/domain/EmsStrategyTempTimeConfig.java index 1cf9f9f..746d511 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/EmsStrategyTempTimeConfig.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/EmsStrategyTempTimeConfig.java @@ -35,6 +35,14 @@ public class EmsStrategyTempTimeConfig extends BaseEntity @Excel(name = "充放功率 (kW)") private BigDecimal chargeDischargePower; + /** SDC下限 (%) */ + @Excel(name = "SDC下限 (%)") + private BigDecimal sdcDown; + + /** SDC上限 (%) */ + @Excel(name = "SDC上限 (%)") + private BigDecimal sdcUp; + /** 充电状态,如“1-充电”、“2-待机” */ @Excel(name = "充电状态,如“1-充电”、“2-待机”") private String chargeStatus; @@ -83,6 +91,26 @@ public class EmsStrategyTempTimeConfig extends BaseEntity return chargeDischargePower; } + public void setSdcDown(BigDecimal sdcDown) + { + this.sdcDown = sdcDown; + } + + public BigDecimal getSdcDown() + { + return sdcDown; + } + + public void setSdcUp(BigDecimal sdcUp) + { + this.sdcUp = sdcUp; + } + + public BigDecimal getSdcUp() + { + return sdcUp; + } + public void setChargeStatus(String chargeStatus) { this.chargeStatus = chargeStatus; @@ -110,6 +138,8 @@ public class EmsStrategyTempTimeConfig extends BaseEntity .append("startTime", getStartTime()) .append("endTime", getEndTime()) .append("chargeDischargePower", getChargeDischargePower()) + .append("sdcDown", getSdcDown()) + .append("sdcUp", getSdcUp()) .append("chargeStatus", getChargeStatus()) .append("createBy", getCreateBy()) .append("createTime", getCreateTime()) diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigCurveRequest.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigCurveRequest.java index 7777305..fc15700 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigCurveRequest.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigCurveRequest.java @@ -3,7 +3,9 @@ package com.xzzn.ems.domain.vo; public class PointConfigCurveRequest { private String siteId; private String deviceId; + private String pointId; private String dataKey; + private String pointType; private String rangeType; private String startTime; private String endTime; @@ -24,6 +26,14 @@ public class PointConfigCurveRequest { this.deviceId = deviceId; } + public String getPointId() { + return pointId; + } + + public void setPointId(String pointId) { + this.pointId = pointId; + } + public String getDataKey() { return dataKey; } @@ -32,6 +42,14 @@ public class PointConfigCurveRequest { this.dataKey = dataKey; } + public String getPointType() { + return pointType; + } + + public void setPointType(String pointType) { + this.pointType = pointType; + } + public String getRangeType() { return rangeType; } diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingSaveRequest.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingSaveRequest.java index c1df3ab..4dc6369 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingSaveRequest.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingSaveRequest.java @@ -8,6 +8,8 @@ public class SiteMonitorProjectPointMappingSaveRequest { private List mappings; + private List deletedFieldCodes; + public String getSiteId() { return siteId; } @@ -23,4 +25,12 @@ public class SiteMonitorProjectPointMappingSaveRequest { public void setMappings(List mappings) { this.mappings = mappings; } + + public List getDeletedFieldCodes() { + return deletedFieldCodes; + } + + public void setDeletedFieldCodes(List deletedFieldCodes) { + this.deletedFieldCodes = deletedFieldCodes; + } } diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingVo.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingVo.java index 3d518ed..b9ce240 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingVo.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingVo.java @@ -18,6 +18,10 @@ public class SiteMonitorProjectPointMappingVo { private String dataPoint; + private String fixedDataPoint; + + private Integer useFixedDisplay; + public String getModuleCode() { return moduleCode; } @@ -81,4 +85,20 @@ public class SiteMonitorProjectPointMappingVo { public void setDataPoint(String dataPoint) { this.dataPoint = dataPoint; } + + public String getFixedDataPoint() { + return fixedDataPoint; + } + + public void setFixedDataPoint(String fixedDataPoint) { + this.fixedDataPoint = fixedDataPoint; + } + + public Integer getUseFixedDisplay() { + return useFixedDisplay; + } + + public void setUseFixedDisplay(Integer useFixedDisplay) { + this.useFixedDisplay = useFixedDisplay; + } } diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointCalcConfigMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointCalcConfigMapper.java new file mode 100644 index 0000000..edaef0a --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointCalcConfigMapper.java @@ -0,0 +1,24 @@ +package com.xzzn.ems.mapper; + +import com.xzzn.ems.domain.EmsPointCalcConfig; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface EmsPointCalcConfigMapper { + EmsPointCalcConfig selectEmsPointCalcConfigById(Long id); + + List selectEmsPointCalcConfigList(EmsPointCalcConfig emsPointCalcConfig); + + int insertEmsPointCalcConfig(EmsPointCalcConfig emsPointCalcConfig); + + int updateEmsPointCalcConfig(EmsPointCalcConfig emsPointCalcConfig); + + int deleteEmsPointCalcConfigById(Long id); + + int deleteEmsPointCalcConfigByIds(Long[] ids); + + int countBySiteId(@Param("siteId") String siteId); + + int deleteBySiteId(@Param("siteId") String siteId); +} diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java index 76deb82..0e800ab 100644 --- a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java @@ -13,6 +13,8 @@ public interface EmsPointConfigMapper { int insertEmsPointConfig(EmsPointConfig emsPointConfig); + int insertBatchEmsPointConfig(@Param("list") List list); + int updateEmsPointConfig(EmsPointConfig emsPointConfig); int deleteEmsPointConfigById(Long id); @@ -41,4 +43,7 @@ public interface EmsPointConfigMapper { @Param("deviceCategory") String deviceCategory, @Param("pointNames") List pointNames, @Param("deviceIds") List deviceIds); + + List selectBySiteIdAndPointIds(@Param("siteId") String siteId, + @Param("pointIds") List pointIds); } diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsStrategyRuntimeConfigMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsStrategyRuntimeConfigMapper.java new file mode 100644 index 0000000..41fc0f6 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsStrategyRuntimeConfigMapper.java @@ -0,0 +1,36 @@ +package com.xzzn.ems.mapper; + +import com.xzzn.ems.domain.EmsStrategyRuntimeConfig; + +/** + * 策略运行参数配置Mapper接口 + * + * @author xzzn + * @date 2026-02-13 + */ +public interface EmsStrategyRuntimeConfigMapper { + + /** + * 根据站点ID查询参数配置 + * + * @param siteId 站点ID + * @return 参数配置 + */ + EmsStrategyRuntimeConfig selectBySiteId(String siteId); + + /** + * 新增参数配置 + * + * @param config 参数配置 + * @return 结果 + */ + int insert(EmsStrategyRuntimeConfig config); + + /** + * 按站点ID更新参数配置 + * + * @param config 参数配置 + * @return 结果 + */ + int updateBySiteId(EmsStrategyRuntimeConfig config); +} diff --git a/ems-system/src/main/java/com/xzzn/ems/service/IEmsPointCalcConfigService.java b/ems-system/src/main/java/com/xzzn/ems/service/IEmsPointCalcConfigService.java new file mode 100644 index 0000000..0637ff6 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/service/IEmsPointCalcConfigService.java @@ -0,0 +1,19 @@ +package com.xzzn.ems.service; + +import com.xzzn.ems.domain.EmsPointCalcConfig; + +import java.util.List; + +public interface IEmsPointCalcConfigService { + List selectPointCalcConfigList(EmsPointCalcConfig pointCalcConfig); + + EmsPointCalcConfig selectPointCalcConfigById(Long id); + + int insertPointCalcConfig(EmsPointCalcConfig pointCalcConfig, String operName); + + int updatePointCalcConfig(EmsPointCalcConfig pointCalcConfig, String operName); + + int deletePointCalcConfigByIds(Long[] ids); + + int deleteBySiteId(String siteId); +} diff --git a/ems-system/src/main/java/com/xzzn/ems/service/IEmsStrategyRuntimeConfigService.java b/ems-system/src/main/java/com/xzzn/ems/service/IEmsStrategyRuntimeConfigService.java new file mode 100644 index 0000000..5d31fae --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/service/IEmsStrategyRuntimeConfigService.java @@ -0,0 +1,28 @@ +package com.xzzn.ems.service; + +import com.xzzn.ems.domain.EmsStrategyRuntimeConfig; + +/** + * 策略运行参数配置Service接口 + * + * @author xzzn + * @date 2026-02-13 + */ +public interface IEmsStrategyRuntimeConfigService { + + /** + * 按站点ID获取参数配置(不存在时返回默认值) + * + * @param siteId 站点ID + * @return 参数配置 + */ + EmsStrategyRuntimeConfig getBySiteId(String siteId); + + /** + * 保存参数配置(按siteId新增或更新) + * + * @param config 参数配置 + * @return 结果 + */ + int saveBySiteId(EmsStrategyRuntimeConfig config); +} diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java index 0f26d46..296cf6e 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java @@ -61,15 +61,19 @@ import com.xzzn.ems.utils.DevicePointMatchDataProcessor; import java.lang.reflect.Field; import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -77,6 +81,7 @@ import java.util.Objects; import java.util.Set; import java.util.Collections; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledExecutorService; @@ -104,12 +109,14 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i // 匹配DTDC+数字格式的正则(提取序号) private static final Pattern DTDC_PATTERN = Pattern.compile("DTDC(\\d+)([A-Za-z]*)"); private static final Pattern DB_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$"); + private static final Pattern VARIABLE_SAFE_PATTERN = Pattern.compile("[^A-Za-z0-9_]"); private static final int POINT_QUEUE_CAPACITY = 100000; private static final int POINT_FLUSH_BATCH_SIZE = 2000; private static final int POINT_FLUSH_MAX_DRAIN_PER_RUN = 20000; private static final int POINT_ENQUEUE_RETRY_TIMES = 3; private static final long POINT_ENQUEUE_RETRY_WAIT_MS = 10; private static final long POINT_FLUSH_INTERVAL_MS = 100; + private static final String SITE_LEVEL_CALC_DEVICE_ID = "SITE_CALC"; @Autowired private EmsBatteryClusterMapper emsBatteryClusterMapper; @@ -169,6 +176,7 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i @Autowired private InfluxPointDataWriter influxPointDataWriter; private final BlockingQueue pointDataQueue = new LinkedBlockingQueue<>(POINT_QUEUE_CAPACITY); + private final Map> calcExpressionCache = new ConcurrentHashMap<>(); private final ScheduledExecutorService pointDataWriter = Executors.newSingleThreadScheduledExecutor(r -> { Thread thread = new Thread(r); thread.setName("point-data-writer"); @@ -201,6 +209,9 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i long startMs = System.currentTimeMillis(); log.info("开始处理设备数据, siteId: {}, messageSize: {}", siteId, arraylist.size()); Set deviceIds = new LinkedHashSet<>(); + Map deviceCategoryMap = buildDeviceCategoryMap(siteId, arraylist); + Map batchContextBySite = buildBatchContextBySite(siteId, arraylist, deviceCategoryMap); + Date latestDataUpdateTime = null; for (int i = 0; i < arraylist.size(); i++) { JSONObject obj = JSONObject.parseObject(arraylist.get(i).toString()); @@ -209,6 +220,9 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i String jsonData = obj.getString("Data"); Long timestamp = obj.getLong("timestamp"); Date dataUpdateTime = DateUtils.convertUpdateTime(timestamp); + if (dataUpdateTime != null && (latestDataUpdateTime == null || dataUpdateTime.after(latestDataUpdateTime))) { + latestDataUpdateTime = dataUpdateTime; + } if (StringUtils.isNotBlank(deviceId)) { deviceIds.add(deviceId); @@ -222,65 +236,210 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i redisCache.setCacheObject(RedisKeyConstants.ORIGINAL_MQTT_DATA + siteId + "_" + deviceId, obj); redisCache.setCacheObject(RedisKeyConstants.SYNC_DATA + siteId + "_" + deviceId, obj, 1, TimeUnit.MINUTES); - processPointConfigData(siteId, deviceId, jsonData, dataUpdateTime); - iEmsDeviceSettingService.syncSiteMonitorDataByMqtt(siteId, deviceId, jsonData, dataUpdateTime); + String deviceCategory = resolveDeviceCategory(siteId, deviceId, deviceCategoryMap); + Map pointIdValueMap = processPointConfigData(siteId, deviceId, deviceCategory, jsonData, dataUpdateTime); + iEmsDeviceSettingService.syncSiteMonitorDataByMqtt(siteId, deviceId, JSON.toJSONString(pointIdValueMap), dataUpdateTime); } catch (Exception e) { log.warn("点位映射数据处理失败,siteId: {}, deviceId: {}, err: {}", siteId, deviceId, e.getMessage(), e); } } + Map calcPointIdValueMap = executeCalcPointConfigs( + siteId, SITE_LEVEL_CALC_DEVICE_ID, getSiteCalcPointConfigs(siteId), batchContextBySite, latestDataUpdateTime + ); + if (!calcPointIdValueMap.isEmpty()) { + iEmsDeviceSettingService.syncSiteMonitorDataByMqtt( + siteId, SITE_LEVEL_CALC_DEVICE_ID, JSON.toJSONString(calcPointIdValueMap), latestDataUpdateTime + ); + } log.info("结束处理设备数据, siteId: {}, deviceCount: {}, deviceIds: {}, costMs: {}", siteId, deviceIds.size(), String.join(",", deviceIds), System.currentTimeMillis() - startMs); } - private void processPointConfigData(String siteId, String deviceId, String jsonData, Date dataUpdateTime) { + private Map processPointConfigData(String siteId, String deviceId, String deviceCategory, String jsonData, Date dataUpdateTime) { + Map pointIdValueMap = new HashMap<>(); if (StringUtils.isAnyBlank(siteId, deviceId, jsonData)) { - return; + return pointIdValueMap; } if (!DB_NAME_PATTERN.matcher(siteId).matches()) { log.warn("站点ID不合法,跳过点位映射落库,siteId: {}", siteId); - return; + return pointIdValueMap; } Map dataMap = JSON.parseObject(jsonData, new TypeReference>() { }); if (org.apache.commons.collections4.MapUtils.isEmpty(dataMap)) { - return; + return pointIdValueMap; } - List pointConfigs = getPointConfigsWithCache(siteId, deviceId); + List pointConfigs = getPointConfigsWithCache(siteId, deviceId, deviceCategory); if (CollectionUtils.isEmpty(pointConfigs)) { - return; + return pointIdValueMap; } - Map configByDataKey = pointConfigs.stream() - .filter(cfg -> StringUtils.isNotBlank(cfg.getDataKey())) - .collect(Collectors.toMap( - cfg -> cfg.getDataKey().toUpperCase(), - cfg -> cfg, - (oldValue, newValue) -> oldValue - )); - + List dataPointConfigs = new ArrayList<>(); + Set uniquePointKeys = new HashSet<>(); + for (EmsPointConfig pointConfig : pointConfigs) { + String pointContextKey = resolvePointContextKey(pointConfig); + if (pointConfig == null || StringUtils.isBlank(pointContextKey)) { + continue; + } + if (!uniquePointKeys.add(pointContextKey)) { + continue; + } + if (isCalcPoint(pointConfig)) { + continue; + } + dataPointConfigs.add(pointConfig); + } + Map rawDataValues = new HashMap<>(); for (Map.Entry entry : dataMap.entrySet()) { - String dataKey = entry.getKey(); - if (StringUtils.isBlank(dataKey)) { + if (StringUtils.isBlank(entry.getKey())) { continue; } - EmsPointConfig pointConfig = configByDataKey.get(dataKey.toUpperCase()); - if (pointConfig == null) { - continue; - } - BigDecimal pointValue = StringUtils.getBigDecimal(entry.getValue()); if (pointValue == null) { continue; } - - // 支持按配置进行二次转换:f(x)=A*x^2 + K*x + B - pointValue = convertPointValue(pointValue, pointConfig); - - enqueuePointData(siteId, deviceId, pointConfig.getDataKey(), pointValue, dataUpdateTime); + rawDataValues.put(entry.getKey().trim().toUpperCase(), pointValue); } + + for (EmsPointConfig dataPointConfig : dataPointConfigs) { + BigDecimal sourceValue = getSourceValueFromRawData(dataPointConfig, rawDataValues); + if (sourceValue == null) { + continue; + } + BigDecimal convertedValue = convertPointValue(sourceValue, dataPointConfig); + String pointId = resolveInfluxPointKey(dataPointConfig); + if (StringUtils.isBlank(pointId)) { + continue; + } + enqueuePointData(siteId, deviceId, pointId, convertedValue, dataUpdateTime); + pointIdValueMap.put(pointId, convertedValue); + } + return pointIdValueMap; + } + + private Map buildDeviceCategoryMap(String siteId, JSONArray arraylist) { + Map deviceCategoryMap = new HashMap<>(); + if (arraylist == null || arraylist.isEmpty()) { + return deviceCategoryMap; + } + for (int i = 0; i < arraylist.size(); i++) { + JSONObject item = JSONObject.parseObject(arraylist.get(i).toString()); + if (item == null) { + continue; + } + String deviceId = item.getString("Device"); + if (StringUtils.isBlank(deviceId) || deviceCategoryMap.containsKey(deviceId)) { + continue; + } + String category = getDeviceCategory(siteId, deviceId); + if (StringUtils.isNotBlank(category)) { + deviceCategoryMap.put(deviceId, category); + } + } + return deviceCategoryMap; + } + + private String resolveDeviceCategory(String siteId, String deviceId, Map deviceCategoryMap) { + if (StringUtils.isBlank(deviceId)) { + return ""; + } + String mappedCategory = deviceCategoryMap == null ? null : deviceCategoryMap.get(deviceId); + if (StringUtils.isNotBlank(mappedCategory)) { + return mappedCategory; + } + String runtimeCategory = getDeviceCategory(siteId, deviceId); + if (StringUtils.isNotBlank(runtimeCategory) && deviceCategoryMap != null) { + deviceCategoryMap.put(deviceId, runtimeCategory); + } + return StringUtils.defaultString(runtimeCategory); + } + + private Map buildBatchContextBySite(String siteId, JSONArray arraylist, Map deviceCategoryMap) { + Map contextBySite = new HashMap<>(); + if (arraylist == null || arraylist.isEmpty()) { + return contextBySite; + } + for (int i = 0; i < arraylist.size(); i++) { + JSONObject item = JSONObject.parseObject(arraylist.get(i).toString()); + if (item == null) { + continue; + } + String deviceId = item.getString("Device"); + String devicePrefix = normalizeVariablePart(deviceId); + String deviceCategory = resolveDeviceCategory(siteId, deviceId, deviceCategoryMap); + String jsonData = item.getString("Data"); + if (checkJsonDataEmpty(jsonData)) { + continue; + } + Map dataMap = JSON.parseObject(jsonData, new TypeReference>() { + }); + if (org.apache.commons.collections4.MapUtils.isEmpty(dataMap)) { + continue; + } + Map pointConfigByDataKey = new HashMap<>(); + List pointConfigs = getPointConfigsWithCache(siteId, deviceId, deviceCategory); + if (!CollectionUtils.isEmpty(pointConfigs)) { + for (EmsPointConfig pointConfig : pointConfigs) { + if (isCalcPoint(pointConfig)) { + continue; + } + String dataKey = StringUtils.defaultString(pointConfig.getDataKey()).trim().toUpperCase(); + if (StringUtils.isBlank(dataKey) || pointConfigByDataKey.containsKey(dataKey)) { + continue; + } + pointConfigByDataKey.put(dataKey, pointConfig); + } + } + for (Map.Entry entry : dataMap.entrySet()) { + if (StringUtils.isBlank(entry.getKey())) { + continue; + } + BigDecimal pointValue = StringUtils.getBigDecimal(entry.getValue()); + if (pointValue == null) { + continue; + } + String normalizedKey = normalizeVariablePart(entry.getKey()); + if (StringUtils.isBlank(normalizedKey)) { + continue; + } + // 兼容旧表达式:继续支持直接使用变量名(如 WD4) + contextBySite.put(normalizedKey, pointValue); + // 新规则:支持设备前缀变量(如 DONGHUAN_WD4) + if (StringUtils.isNotBlank(devicePrefix)) { + contextBySite.put(devicePrefix + "_" + normalizedKey, pointValue); + } + // 同时支持按点位ID引用(如 POINT4092) + EmsPointConfig pointConfig = pointConfigByDataKey.get(entry.getKey().trim().toUpperCase()); + if (pointConfig != null) { + String pointIdKey = resolvePointContextKey(pointConfig); + if (StringUtils.isNotBlank(pointIdKey)) { + BigDecimal convertedValue = convertPointValue(pointValue, pointConfig); + contextBySite.put(pointIdKey, convertedValue); + } + } + } + } + return contextBySite; + } + + private String normalizeVariablePart(String source) { + if (StringUtils.isBlank(source)) { + return null; + } + String normalized = VARIABLE_SAFE_PATTERN.matcher(source.trim().toUpperCase()).replaceAll("_"); + if (StringUtils.isBlank(normalized)) { + return null; + } + normalized = normalized.replaceAll("_+", "_"); + normalized = StringUtils.strip(normalized, "_"); + return StringUtils.isBlank(normalized) ? null : normalized; + } + + private boolean isCalcPoint(EmsPointConfig pointConfig) { + return pointConfig != null && "calc".equalsIgnoreCase(pointConfig.getPointType()); } private void enqueuePointData(String siteId, String deviceId, String pointKey, BigDecimal pointValue, Date dataTime) { @@ -325,6 +484,315 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i .add(b); } + private String resolveInfluxPointKey(EmsPointConfig pointConfig) { + if (pointConfig == null) { + return null; + } + if (StringUtils.isNotBlank(pointConfig.getPointId())) { + return pointConfig.getPointId().trim(); + } + return null; + } + + private Map executeCalcPointConfigs(String siteId, String deviceId, List calcPointConfigs, + Map contextValues, Date dataUpdateTime) { + Map calcPointIdValueMap = new HashMap<>(); + if (CollectionUtils.isEmpty(calcPointConfigs)) { + return calcPointIdValueMap; + } + Map remaining = new HashMap<>(); + for (EmsPointConfig calcPointConfig : calcPointConfigs) { + String calcContextKey = resolveCalcContextKey(calcPointConfig); + if (calcPointConfig == null || StringUtils.isBlank(calcContextKey)) { + continue; + } + remaining.putIfAbsent(calcContextKey, calcPointConfig); + } + if (remaining.isEmpty()) { + return calcPointIdValueMap; + } + + while (!remaining.isEmpty()) { + boolean progressed = false; + List finishedKeys = new ArrayList<>(); + List unresolvedKeys = new ArrayList<>(); + + for (Map.Entry entry : remaining.entrySet()) { + String calcDataKey = entry.getKey(); + EmsPointConfig calcPointConfig = entry.getValue(); + try { + BigDecimal calcValue = evaluateCalcExpression(calcPointConfig.getCalcExpression(), contextValues); + contextValues.put(calcDataKey, calcValue); + putPointValueToContext(calcPointConfig, calcValue, contextValues); + // 计算点按站点维度统一落库,不再按配置中的 device_id 分流 + String pointId = resolveInfluxPointKey(calcPointConfig); + if (StringUtils.isNotBlank(pointId)) { + enqueuePointData(siteId, deviceId, pointId, calcValue, dataUpdateTime); + calcPointIdValueMap.put(pointId, calcValue); + } + finishedKeys.add(calcDataKey); + progressed = true; + } catch (MissingVariableException missingVariableException) { + unresolvedKeys.add(calcDataKey + " <- " + missingVariableException.getVariable()); + } catch (IllegalArgumentException expressionException) { + log.warn("计算点表达式执行失败,siteId: {}, deviceId: {}, dataKey: {}, expression: {}, err: {}", + siteId, deviceId, calcPointConfig.getDataKey(), calcPointConfig.getCalcExpression(), + expressionException.getMessage()); + finishedKeys.add(calcDataKey); + } + } + finishedKeys.forEach(remaining::remove); + + if (!progressed) { + if (!unresolvedKeys.isEmpty()) { + log.warn("计算点依赖未就绪,跳过本轮计算,siteId: {}, deviceId: {}, unresolved: {}", + siteId, deviceId, String.join(", ", unresolvedKeys)); + } + break; + } + } + return calcPointIdValueMap; + } + + private BigDecimal getSourceValueFromRawData(EmsPointConfig pointConfig, Map rawDataValues) { + if (pointConfig == null || rawDataValues == null || rawDataValues.isEmpty()) { + return null; + } + String dataKey = StringUtils.defaultString(pointConfig.getDataKey()).trim().toUpperCase(); + return StringUtils.isBlank(dataKey) ? null : rawDataValues.get(dataKey); + } + + private void putPointValueToContext(EmsPointConfig pointConfig, BigDecimal pointValue, Map contextValues) { + if (pointConfig == null || pointValue == null || contextValues == null) { + return; + } + String pointKey = resolvePointContextKey(pointConfig); + if (StringUtils.isNotBlank(pointKey)) { + contextValues.put(pointKey, pointValue); + } + } + + private String resolveCalcContextKey(EmsPointConfig pointConfig) { + return resolvePointContextKey(pointConfig); + } + + private String resolvePointContextKey(EmsPointConfig pointConfig) { + if (pointConfig == null) { + return null; + } + String pointId = StringUtils.defaultString(pointConfig.getPointId()).trim().toUpperCase(); + return StringUtils.isNotBlank(pointId) ? pointId : null; + } + + private BigDecimal evaluateCalcExpression(String expression, Map contextValues) { + if (StringUtils.isBlank(expression)) { + throw new IllegalArgumentException("计算表达式为空"); + } + List rpnTokens = calcExpressionCache.computeIfAbsent(expression, this::compileExpressionToRpn); + return evaluateRpnTokens(rpnTokens, contextValues == null ? Collections.emptyMap() : contextValues); + } + + private List compileExpressionToRpn(String expression) { + if (StringUtils.isBlank(expression)) { + throw new IllegalArgumentException("计算表达式为空"); + } + List output = new ArrayList<>(); + Deque operators = new ArrayDeque<>(); + int index = 0; + ExpressionToken previous = null; + + while (index < expression.length()) { + char ch = expression.charAt(index); + if (Character.isWhitespace(ch)) { + index++; + continue; + } + if (Character.isDigit(ch) || ch == '.') { + int start = index; + boolean hasDot = ch == '.'; + index++; + while (index < expression.length()) { + char next = expression.charAt(index); + if (Character.isDigit(next)) { + index++; + continue; + } + if (next == '.' && !hasDot) { + hasDot = true; + index++; + continue; + } + break; + } + String numberText = expression.substring(start, index); + try { + output.add(ExpressionToken.number(new BigDecimal(numberText))); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("数值格式错误: " + numberText); + } + previous = output.get(output.size() - 1); + continue; + } + if (Character.isLetter(ch) || ch == '_') { + int start = index; + index++; + while (index < expression.length()) { + char next = expression.charAt(index); + if (Character.isLetterOrDigit(next) || next == '_') { + index++; + continue; + } + break; + } + String variable = expression.substring(start, index).trim().toUpperCase(); + output.add(ExpressionToken.variable(variable)); + previous = output.get(output.size() - 1); + continue; + } + if (ch == '(') { + operators.push(ExpressionToken.leftParen()); + previous = ExpressionToken.leftParen(); + index++; + continue; + } + if (ch == ')') { + boolean matched = false; + while (!operators.isEmpty()) { + ExpressionToken token = operators.pop(); + if (token.getType() == ExpressionTokenType.LEFT_PAREN) { + matched = true; + break; + } + output.add(token); + } + if (!matched) { + throw new IllegalArgumentException("括号不匹配"); + } + previous = ExpressionToken.rightParen(); + index++; + continue; + } + if (isOperator(ch)) { + boolean unaryMinus = ch == '-' && (previous == null + || previous.getType() == ExpressionTokenType.OPERATOR + || previous.getType() == ExpressionTokenType.LEFT_PAREN); + String operatorText = unaryMinus ? "~" : String.valueOf(ch); + ExpressionToken currentOperator = ExpressionToken.operator(operatorText); + while (!operators.isEmpty() && operators.peek().getType() == ExpressionTokenType.OPERATOR) { + ExpressionToken top = operators.peek(); + if (shouldPopOperator(currentOperator, top)) { + output.add(operators.pop()); + } else { + break; + } + } + operators.push(currentOperator); + previous = currentOperator; + index++; + continue; + } + throw new IllegalArgumentException("表达式包含非法字符: " + ch); + } + + while (!operators.isEmpty()) { + ExpressionToken token = operators.pop(); + if (token.getType() == ExpressionTokenType.LEFT_PAREN) { + throw new IllegalArgumentException("括号不匹配"); + } + output.add(token); + } + return output; + } + + private BigDecimal evaluateRpnTokens(List rpnTokens, Map contextValues) { + Deque values = new ArrayDeque<>(); + for (ExpressionToken token : rpnTokens) { + if (token.getType() == ExpressionTokenType.NUMBER) { + values.push(token.getNumber()); + continue; + } + if (token.getType() == ExpressionTokenType.VARIABLE) { + BigDecimal variableValue = contextValues.get(token.getText()); + if (variableValue == null) { + throw new MissingVariableException(token.getText()); + } + values.push(variableValue); + continue; + } + if (token.getType() != ExpressionTokenType.OPERATOR) { + throw new IllegalArgumentException("表达式令牌类型不合法: " + token.getType()); + } + if ("~".equals(token.getText())) { + if (values.isEmpty()) { + throw new IllegalArgumentException("表达式不合法,缺少操作数"); + } + values.push(values.pop().negate()); + continue; + } + if (values.size() < 2) { + throw new IllegalArgumentException("表达式不合法,缺少操作数"); + } + BigDecimal right = values.pop(); + BigDecimal left = values.pop(); + values.push(applyOperator(left, right, token.getText())); + } + if (values.size() != 1) { + throw new IllegalArgumentException("表达式不合法,无法归约到单值"); + } + return values.pop(); + } + + private BigDecimal applyOperator(BigDecimal left, BigDecimal right, String operator) { + switch (operator) { + case "+": + return left.add(right); + case "-": + return left.subtract(right); + case "*": + return left.multiply(right); + case "/": + if (BigDecimal.ZERO.compareTo(right) == 0) { + throw new IllegalArgumentException("除数不能为0"); + } + return left.divide(right, 10, RoundingMode.HALF_UP); + default: + throw new IllegalArgumentException("不支持的操作符: " + operator); + } + } + + private boolean shouldPopOperator(ExpressionToken currentOperator, ExpressionToken stackOperator) { + if (currentOperator == null || stackOperator == null) { + return false; + } + int currentPriority = getOperatorPriority(currentOperator.getText()); + int stackPriority = getOperatorPriority(stackOperator.getText()); + if (isRightAssociative(currentOperator.getText())) { + return stackPriority > currentPriority; + } + return stackPriority >= currentPriority; + } + + private int getOperatorPriority(String operator) { + if ("~".equals(operator)) { + return 3; + } + if ("*".equals(operator) || "/".equals(operator)) { + return 2; + } + if ("+".equals(operator) || "-".equals(operator)) { + return 1; + } + return 0; + } + + private boolean isRightAssociative(String operator) { + return "~".equals(operator); + } + + private boolean isOperator(char ch) { + return ch == '+' || ch == '-' || ch == '*' || ch == '/'; + } + private void flushPointDataSafely() { if (!pointDataFlushing.compareAndSet(false, true)) { return; @@ -366,21 +834,48 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i influxPointDataWriter.writeBatch(payloads); } - private List getPointConfigsWithCache(String siteId, String deviceId) { - String cacheKey = RedisKeyConstants.POINT_CONFIG_DEVICE + siteId + "_" + deviceId; + private List getPointConfigsWithCache(String siteId, String deviceId, String deviceCategory) { + String normalizedCategory = StringUtils.defaultString(deviceCategory).trim().toUpperCase(); + String cacheKey = RedisKeyConstants.POINT_CONFIG_DEVICE + siteId + "_" + deviceId + "_" + normalizedCategory; List cached = redisCache.getCacheObject(cacheKey); if (cached != null) { return cached; } + + // 1) 设备级点位(原有逻辑) EmsPointConfig query = new EmsPointConfig(); query.setSiteId(siteId); query.setDeviceId(deviceId); - List latest = emsPointConfigMapper.selectEmsPointConfigList(query); - List cacheValue = latest == null ? new ArrayList<>() : latest; + List cacheValue = emsPointConfigMapper.selectEmsPointConfigList(query); redisCache.setCacheObject(cacheKey, cacheValue); return cacheValue; } + private List getSiteCalcPointConfigs(String siteId) { + if (StringUtils.isBlank(siteId)) { + return Collections.emptyList(); + } + EmsPointConfig query = new EmsPointConfig(); + query.setSiteId(siteId); + query.setPointType("calc"); + List calcPoints = emsPointConfigMapper.selectEmsPointConfigList(query); + if (CollectionUtils.isEmpty(calcPoints)) { + return Collections.emptyList(); + } + Map uniqueByPointId = new LinkedHashMap<>(); + for (EmsPointConfig calcPoint : calcPoints) { + if (!isCalcPoint(calcPoint)) { + continue; + } + String calcKey = resolvePointContextKey(calcPoint); + if (StringUtils.isBlank(calcKey)) { + continue; + } + uniqueByPointId.putIfAbsent(calcKey, calcPoint); + } + return new ArrayList<>(uniqueByPointId.values()); + } + private static class PointDataRecord { private final String siteId; private final String deviceId; @@ -417,6 +912,71 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i } } + private static class MissingVariableException extends IllegalArgumentException { + private final String variable; + + private MissingVariableException(String variable) { + super("缺少变量: " + variable); + this.variable = variable; + } + + private String getVariable() { + return variable; + } + } + + private enum ExpressionTokenType { + NUMBER, + VARIABLE, + OPERATOR, + LEFT_PAREN, + RIGHT_PAREN + } + + private static class ExpressionToken { + private final ExpressionTokenType type; + private final String text; + private final BigDecimal number; + + private ExpressionToken(ExpressionTokenType type, String text, BigDecimal number) { + this.type = type; + this.text = text; + this.number = number; + } + + private static ExpressionToken number(BigDecimal value) { + return new ExpressionToken(ExpressionTokenType.NUMBER, null, value); + } + + private static ExpressionToken variable(String variable) { + return new ExpressionToken(ExpressionTokenType.VARIABLE, variable, null); + } + + private static ExpressionToken operator(String operator) { + return new ExpressionToken(ExpressionTokenType.OPERATOR, operator, null); + } + + private static ExpressionToken leftParen() { + return new ExpressionToken(ExpressionTokenType.LEFT_PAREN, "(", null); + } + + private static ExpressionToken rightParen() { + return new ExpressionToken(ExpressionTokenType.RIGHT_PAREN, ")", null); + } + + private ExpressionTokenType getType() { + return type; + } + + private String getText() { + return text; + } + + private BigDecimal getNumber() { + return number; + } + } + private JSONArray parseJsonData(String message) { try { JSONObject object = JSONObject.parseObject(message); diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java index f950484..71b0323 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java @@ -17,6 +17,7 @@ import com.xzzn.common.exception.ServiceException; import com.xzzn.common.utils.DateUtils; import com.xzzn.common.utils.StringUtils; import com.xzzn.ems.domain.EmsDevicesSetting; +import com.xzzn.ems.domain.EmsPointConfig; import com.xzzn.ems.domain.EmsPointMatch; import com.xzzn.ems.domain.EmsPcsSetting; import com.xzzn.ems.domain.EmsSiteMonitorItem; @@ -33,6 +34,7 @@ import com.xzzn.ems.domain.vo.SiteMonitorProjectPointMappingVo; import com.xzzn.ems.mapper.EmsBatteryDataMinutesMapper; import com.xzzn.ems.mapper.EmsDevicesSettingMapper; import com.xzzn.ems.mapper.EmsPcsSettingMapper; +import com.xzzn.ems.mapper.EmsPointConfigMapper; import com.xzzn.ems.mapper.EmsPointMatchMapper; import com.xzzn.ems.mapper.EmsSiteMonitorDataMapper; import com.xzzn.ems.mapper.EmsSiteMonitorItemMapper; @@ -45,12 +47,14 @@ import java.util.Arrays; import java.util.Comparator; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import java.util.Collections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -71,9 +75,12 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService private static final String MODULE_HOME = "HOME"; private static final String MODULE_SBJK = "SBJK"; private static final String MODULE_TJBB = "TJBB"; + private static final Integer USE_FIXED_DISPLAY_YES = 1; private static final String HISTORY_TABLE_HOME = "ems_site_monitor_data_home_his"; private static final String HISTORY_TABLE_SBJK = "ems_site_monitor_data_sbjk_his"; private static final String HISTORY_TABLE_TJBB = "ems_site_monitor_data_tjbb_his"; + private static final String DELETED_FIELD_MARK = "__DELETED__"; + private static final long MONITOR_ITEM_CACHE_TTL_MS = 60_000L; @Autowired private EmsDevicesSettingMapper emsDevicesMapper; @Autowired @@ -81,6 +88,8 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService @Autowired private EmsPcsSettingMapper emsPcsSettingMapper; @Autowired + private EmsPointConfigMapper emsPointConfigMapper; + @Autowired private RedisCache redisCache; @Autowired private EmsBatteryDataMinutesMapper emsBatteryDataMinutesMapper; @@ -95,6 +104,9 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService @Autowired private EmsSiteMonitorDataMapper emsSiteMonitorDataMapper; + private volatile List monitorItemCache = Collections.emptyList(); + private volatile long monitorItemCacheExpireAt = 0L; + /** * 获取设备详细信息 * @param id @@ -406,16 +418,30 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService if (StringUtils.isBlank(siteId)) { return result; } - List itemList = emsSiteMonitorItemMapper.selectEnabledList(); + List itemList = getEnabledMonitorItems(); if (itemList == null || itemList.isEmpty()) { return result; } List mappingList = emsSiteMonitorPointMatchMapper.selectBySiteId(siteId); + Set deletedFieldCodeSet = mappingList.stream() + .filter(item -> StringUtils.isNotBlank(item.getFieldCode())) + .filter(item -> DELETED_FIELD_MARK.equals(item.getDataPoint())) + .map(EmsSiteMonitorPointMatch::getFieldCode) + .collect(Collectors.toSet()); + Map mappingByFieldCode = mappingList.stream() + .filter(item -> StringUtils.isNotBlank(item.getFieldCode())) + .filter(item -> !DELETED_FIELD_MARK.equals(item.getDataPoint())) + .collect(Collectors.toMap(EmsSiteMonitorPointMatch::getFieldCode, item -> item, (a, b) -> b)); Map pointMap = mappingList.stream() .filter(item -> StringUtils.isNotBlank(item.getFieldCode())) + .filter(item -> !DELETED_FIELD_MARK.equals(item.getDataPoint())) .collect(Collectors.toMap(EmsSiteMonitorPointMatch::getFieldCode, EmsSiteMonitorPointMatch::getDataPoint, (a, b) -> b)); itemList.forEach(item -> { + if (deletedFieldCodeSet.contains(item.getFieldCode())) { + return; + } + EmsSiteMonitorPointMatch pointMatch = mappingByFieldCode.get(item.getFieldCode()); SiteMonitorProjectPointMappingVo vo = new SiteMonitorProjectPointMappingVo(); vo.setModuleCode(item.getModuleCode()); vo.setModuleName(item.getModuleName()); @@ -425,6 +451,8 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService vo.setFieldCode(item.getFieldCode()); vo.setFieldName(item.getFieldName()); vo.setDataPoint(pointMap.getOrDefault(item.getFieldCode(), "")); + vo.setFixedDataPoint(pointMatch == null ? "" : pointMatch.getFixedDataPoint()); + vo.setUseFixedDisplay(pointMatch == null ? 0 : (pointMatch.getUseFixedDisplay() == null ? 0 : pointMatch.getUseFixedDisplay())); result.add(vo); }); return result; @@ -436,37 +464,70 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService throw new ServiceException("站点ID不能为空"); } String siteId = request.getSiteId(); - List itemList = emsSiteMonitorItemMapper.selectEnabledList(); + List itemList = getEnabledMonitorItems(); if (itemList == null || itemList.isEmpty()) { throw new ServiceException("单站监控配置项为空,请先初始化 ems_site_monitor_item"); } Set fieldCodeSet = itemList.stream().map(EmsSiteMonitorItem::getFieldCode).collect(Collectors.toSet()); - emsSiteMonitorPointMatchMapper.deleteBySiteId(siteId); - - if (request.getMappings() == null || request.getMappings().isEmpty()) { - return 0; + Set deletedFieldCodeSet = new HashSet<>(); + if (request.getDeletedFieldCodes() != null) { + request.getDeletedFieldCodes().stream() + .filter(StringUtils::isNotBlank) + .map(String::trim) + .filter(fieldCodeSet::contains) + .forEach(deletedFieldCodeSet::add); } + int deletedRows = emsSiteMonitorPointMatchMapper.deleteBySiteId(siteId); + List saveList = new ArrayList<>(); - for (SiteMonitorProjectPointMappingVo mapping : request.getMappings()) { - if (mapping == null || StringUtils.isBlank(mapping.getFieldCode()) || StringUtils.isBlank(mapping.getDataPoint())) { - continue; - } - String fieldCode = mapping.getFieldCode().trim(); - if (!fieldCodeSet.contains(fieldCode)) { - continue; + if (request.getMappings() != null) { + for (SiteMonitorProjectPointMappingVo mapping : request.getMappings()) { + if (mapping == null || StringUtils.isBlank(mapping.getFieldCode())) { + continue; + } + String fieldCode = mapping.getFieldCode().trim(); + if (!fieldCodeSet.contains(fieldCode) || deletedFieldCodeSet.contains(fieldCode)) { + continue; + } + String dataPoint = StringUtils.isBlank(mapping.getDataPoint()) ? "" : mapping.getDataPoint().trim(); + String fixedDataPoint = StringUtils.isBlank(mapping.getFixedDataPoint()) ? null : mapping.getFixedDataPoint().trim(); + Integer useFixedDisplay = mapping.getUseFixedDisplay() == null ? 0 : mapping.getUseFixedDisplay(); + boolean hasMapping = StringUtils.isNotBlank(dataPoint) || StringUtils.isNotBlank(fixedDataPoint) || USE_FIXED_DISPLAY_YES.equals(useFixedDisplay); + if (!hasMapping) { + continue; + } + EmsSiteMonitorPointMatch pointMatch = new EmsSiteMonitorPointMatch(); + pointMatch.setSiteId(siteId); + pointMatch.setFieldCode(fieldCode); + pointMatch.setDataPoint(dataPoint); + pointMatch.setFixedDataPoint(fixedDataPoint); + pointMatch.setUseFixedDisplay(useFixedDisplay); + pointMatch.setCreateBy(operName); + pointMatch.setUpdateBy(operName); + saveList.add(pointMatch); } + } + + for (String deletedFieldCode : deletedFieldCodeSet) { EmsSiteMonitorPointMatch pointMatch = new EmsSiteMonitorPointMatch(); pointMatch.setSiteId(siteId); - pointMatch.setFieldCode(fieldCode); - pointMatch.setDataPoint(mapping.getDataPoint().trim()); + pointMatch.setFieldCode(deletedFieldCode); + pointMatch.setDataPoint(DELETED_FIELD_MARK); + pointMatch.setFixedDataPoint(null); + pointMatch.setUseFixedDisplay(0); pointMatch.setCreateBy(operName); pointMatch.setUpdateBy(operName); saveList.add(pointMatch); } + + // 点位映射变更后清理“单站监控最新值”缓存,避免页面回退读取到旧快照 + clearSiteMonitorLatestCache(siteId); + if (saveList.isEmpty()) { - return 0; + return deletedRows; } - return emsSiteMonitorPointMatchMapper.insertBatch(saveList); + int insertedRows = emsSiteMonitorPointMatchMapper.insertBatch(saveList); + return deletedRows + insertedRows; } @Override @@ -475,6 +536,12 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService if (mappingList.isEmpty()) { return new ArrayList<>(); } + Set pointIds = mappingList.stream() + .map(SiteMonitorProjectPointMappingVo::getDataPoint) + .filter(StringUtils::isNotBlank) + .map(item -> item.trim().toUpperCase()) + .collect(Collectors.toSet()); + Map pointConfigByPointId = buildPointConfigByPointId(siteId, pointIds); Map homeLatestMap = safeRedisMap(redisCache.getCacheMap(buildSiteMonitorLatestRedisKey(siteId, MODULE_HOME))); Map sbjkLatestMap = safeRedisMap(redisCache.getCacheMap(buildSiteMonitorLatestRedisKey(siteId, MODULE_SBJK))); Map tjbbLatestMap = safeRedisMap(redisCache.getCacheMap(buildSiteMonitorLatestRedisKey(siteId, MODULE_TJBB))); @@ -483,6 +550,20 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService for (SiteMonitorProjectPointMappingVo mapping : mappingList) { SiteMonitorProjectDisplayVo vo = new SiteMonitorProjectDisplayVo(); BeanUtils.copyProperties(mapping, vo); + if (USE_FIXED_DISPLAY_YES.equals(mapping.getUseFixedDisplay()) && StringUtils.isNotBlank(mapping.getFixedDataPoint())) { + vo.setFieldValue(mapping.getFixedDataPoint()); + vo.setValueTime(null); + result.add(vo); + continue; + } + // 与“点位配置列表最新值”一致:按 pointId -> 点位配置(dataKey/deviceId) -> MQTT 最新报文读取 + PointLatestSnapshot latestSnapshot = getLatestSnapshotByPointId(siteId, mapping.getDataPoint(), pointConfigByPointId); + if (latestSnapshot != null && latestSnapshot.getFieldValue() != null) { + vo.setFieldValue(latestSnapshot.getFieldValue()); + vo.setValueTime(latestSnapshot.getValueTime()); + result.add(vo); + continue; + } Object cacheObj = null; if (MODULE_HOME.equals(mapping.getModuleCode())) { cacheObj = homeLatestMap.get(mapping.getFieldCode()); @@ -501,6 +582,139 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService return result; } + private Map buildPointConfigByPointId(String siteId, Set pointIds) { + Map result = new HashMap<>(); + if (StringUtils.isBlank(siteId) || pointIds == null || pointIds.isEmpty()) { + return result; + } + List configList = emsPointConfigMapper.selectBySiteIdAndPointIds( + siteId.trim(), + new ArrayList<>(pointIds) + ); + if (configList == null || configList.isEmpty()) { + return result; + } + for (EmsPointConfig config : configList) { + if (config == null || StringUtils.isBlank(config.getPointId())) { + continue; + } + String pointId = config.getPointId().trim().toUpperCase(); + result.putIfAbsent(pointId, config); + } + return result; + } + + private PointLatestSnapshot getLatestSnapshotByPointId(String siteId, String pointId, Map pointConfigByPointId) { + if (StringUtils.isAnyBlank(siteId, pointId) || pointConfigByPointId == null || pointConfigByPointId.isEmpty()) { + return null; + } + EmsPointConfig pointConfig = pointConfigByPointId.get(pointId.trim().toUpperCase()); + if (pointConfig == null || StringUtils.isAnyBlank(pointConfig.getDeviceId(), pointConfig.getDataKey())) { + return null; + } + String redisKey = RedisKeyConstants.ORIGINAL_MQTT_DATA + siteId + "_" + pointConfig.getDeviceId().trim(); + Object raw = redisCache.getCacheObject(redisKey); + if (raw == null) { + return null; + } + JSONObject root = toJsonObject(raw); + if (root == null) { + return null; + } + JSONObject dataObject = extractDataObject(root); + if (dataObject == null) { + return null; + } + Object rawValue = getValueIgnoreCase(dataObject, pointConfig.getDataKey()); + BigDecimal sourceValue = StringUtils.getBigDecimal(rawValue); + if (sourceValue == null) { + return null; + } + BigDecimal pointValue = convertPointValue(sourceValue, pointConfig); + Long timestamp = root.getLong("timestamp"); + Date valueTime = timestamp == null ? null : DateUtils.convertUpdateTime(timestamp); + return new PointLatestSnapshot(pointValue.stripTrailingZeros().toPlainString(), valueTime); + } + + private JSONObject toJsonObject(Object raw) { + if (raw == null) { + return null; + } + if (raw instanceof JSONObject) { + return (JSONObject) raw; + } + try { + return JSON.parseObject(JSON.toJSONString(raw)); + } catch (Exception ex) { + return null; + } + } + + private JSONObject extractDataObject(JSONObject root) { + if (root == null) { + return null; + } + JSONObject dataObject = root.getJSONObject("Data"); + if (dataObject != null) { + return dataObject; + } + String dataJson = root.getString("Data"); + if (StringUtils.isBlank(dataJson)) { + return null; + } + try { + return JSON.parseObject(dataJson); + } catch (Exception ex) { + return null; + } + } + + private Object getValueIgnoreCase(JSONObject dataObject, String dataKey) { + if (dataObject == null || StringUtils.isBlank(dataKey)) { + return null; + } + Object directValue = dataObject.get(dataKey); + if (directValue != null) { + return directValue; + } + for (String key : dataObject.keySet()) { + if (key != null && key.equalsIgnoreCase(dataKey)) { + return dataObject.get(key); + } + } + return null; + } + + private BigDecimal convertPointValue(BigDecimal sourceValue, EmsPointConfig pointConfig) { + if (sourceValue == null || pointConfig == null) { + return sourceValue; + } + BigDecimal a = pointConfig.getDataA() == null ? BigDecimal.ZERO : pointConfig.getDataA(); + BigDecimal k = pointConfig.getDataK() == null ? BigDecimal.ONE : pointConfig.getDataK(); + BigDecimal b = pointConfig.getDataB() == null ? BigDecimal.ZERO : pointConfig.getDataB(); + return a.multiply(sourceValue).multiply(sourceValue) + .add(k.multiply(sourceValue)) + .add(b); + } + + private static class PointLatestSnapshot { + private final String fieldValue; + private final Date valueTime; + + private PointLatestSnapshot(String fieldValue, Date valueTime) { + this.fieldValue = fieldValue; + this.valueTime = valueTime; + } + + public String getFieldValue() { + return fieldValue; + } + + public Date getValueTime() { + return valueTime; + } + } + @Override public int saveSiteMonitorProjectData(SiteMonitorDataSaveRequest request, String operName) { if (request == null || StringUtils.isBlank(request.getSiteId())) { @@ -509,7 +723,7 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService if (request.getItems() == null || request.getItems().isEmpty()) { return 0; } - List itemList = emsSiteMonitorItemMapper.selectEnabledList(); + List itemList = getEnabledMonitorItems(); if (itemList == null || itemList.isEmpty()) { throw new ServiceException("单站监控配置项为空,请先初始化 ems_site_monitor_item"); } @@ -590,7 +804,7 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService if (mappingList == null || mappingList.isEmpty()) { return 0; } - List itemList = emsSiteMonitorItemMapper.selectEnabledList(); + List itemList = getEnabledMonitorItems(); if (itemList == null || itemList.isEmpty()) { return 0; } @@ -608,15 +822,24 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService Map tjbbHistoryByMinute = new HashMap<>(); for (EmsSiteMonitorPointMatch mapping : mappingList) { - if (mapping == null || StringUtils.isAnyBlank(mapping.getFieldCode(), mapping.getDataPoint())) { + if (mapping == null || StringUtils.isBlank(mapping.getFieldCode())) { + continue; + } + if (DELETED_FIELD_MARK.equals(mapping.getDataPoint())) { continue; } EmsSiteMonitorItem itemDef = itemMap.get(mapping.getFieldCode()); if (itemDef == null || StringUtils.isBlank(itemDef.getModuleCode())) { continue; } - - String point = mapping.getDataPoint().trim(); + if (USE_FIXED_DISPLAY_YES.equals(mapping.getUseFixedDisplay())) { + continue; + } + String point = mapping.getDataPoint(); + if (StringUtils.isBlank(point)) { + continue; + } + point = point.trim(); Object value = upperDataMap.get(point.toUpperCase()); if (value == null && StringUtils.isNotBlank(deviceId)) { value = upperDataMap.get((deviceId + point).toUpperCase()); @@ -715,6 +938,27 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService return source == null ? new HashMap<>() : source; } + private List getEnabledMonitorItems() { + long now = System.currentTimeMillis(); + List cached = monitorItemCache; + if (now < monitorItemCacheExpireAt && cached != null) { + return cached; + } + synchronized (this) { + now = System.currentTimeMillis(); + if (now < monitorItemCacheExpireAt && monitorItemCache != null) { + return monitorItemCache; + } + List latest = emsSiteMonitorItemMapper.selectEnabledList(); + if (latest == null) { + latest = Collections.emptyList(); + } + monitorItemCache = latest; + monitorItemCacheExpireAt = now + MONITOR_ITEM_CACHE_TTL_MS; + return latest; + } + } + private Map extractHotColumns(JSONObject merged) { Map result = new HashMap<>(); result.put("hotSoc", extractFieldValue(merged, @@ -833,6 +1077,15 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService return RedisKeyConstants.SITE_MONITOR_LATEST + siteId + "_" + moduleCode; } + private void clearSiteMonitorLatestCache(String siteId) { + if (StringUtils.isBlank(siteId)) { + return; + } + redisCache.deleteObject(buildSiteMonitorLatestRedisKey(siteId, MODULE_HOME)); + redisCache.deleteObject(buildSiteMonitorLatestRedisKey(siteId, MODULE_SBJK)); + redisCache.deleteObject(buildSiteMonitorLatestRedisKey(siteId, MODULE_TJBB)); + } + // 辅助方法:根据值查找对应的对象(用于比较器中获取完整对象) private PointQueryResponse findByValue(List list, Object value) { return list.stream() @@ -1050,6 +1303,10 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService public List getWriteTags(String workStatus, EmsPcsSetting pcsSetting) { List writeTags = new ArrayList<>(); BigDecimal power; + BigDecimal powerMultiplier = pcsSetting.getPowerMultiplier(); + if (powerMultiplier == null || BigDecimal.ZERO.compareTo(powerMultiplier) == 0) { + powerMultiplier = BigDecimal.ONE; + } if (WorkStatus.NORMAL.getCode().equals(workStatus)) { // 开机先发送开机指令再发送有功功率给定值 @@ -1073,7 +1330,7 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService if (power == null) { power = BigDecimal.ZERO; } - clusterWriteTag.setValue(power); + clusterWriteTag.setValue(power.multiply(powerMultiplier)); writeTags.add(clusterWriteTag); } if (WorkStatus.STOP.getCode().equals(workStatus)) { diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointCalcConfigServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointCalcConfigServiceImpl.java new file mode 100644 index 0000000..8d1b753 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointCalcConfigServiceImpl.java @@ -0,0 +1,144 @@ +package com.xzzn.ems.service.impl; + +import com.xzzn.common.exception.ServiceException; +import com.xzzn.common.utils.StringUtils; +import com.xzzn.ems.domain.EmsPointConfig; +import com.xzzn.ems.domain.EmsPointCalcConfig; +import com.xzzn.ems.service.IEmsPointConfigService; +import com.xzzn.ems.service.IEmsPointCalcConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Service +public class EmsPointCalcConfigServiceImpl implements IEmsPointCalcConfigService { + private static final Pattern CALC_EXPRESSION_PATTERN = Pattern.compile("^[0-9A-Za-z_+\\-*/().\\s]+$"); + private static final String CALC_POINT_TYPE = "calc"; + + @Autowired + private IEmsPointConfigService pointConfigService; + + @Override + public List selectPointCalcConfigList(EmsPointCalcConfig pointCalcConfig) { + EmsPointConfig query = toPointConfigQuery(pointCalcConfig); + List configList = pointConfigService.selectPointConfigList(query); + if (configList == null || configList.isEmpty()) { + return new ArrayList<>(); + } + return configList.stream().map(this::toCalcConfig).collect(Collectors.toList()); + } + + @Override + public EmsPointCalcConfig selectPointCalcConfigById(Long id) { + EmsPointConfig config = pointConfigService.selectPointConfigById(id); + if (config == null || !CALC_POINT_TYPE.equalsIgnoreCase(config.getPointType())) { + return null; + } + return toCalcConfig(config); + } + + @Override + public int insertPointCalcConfig(EmsPointCalcConfig pointCalcConfig, String operName) { + if (pointCalcConfig == null || StringUtils.isBlank(pointCalcConfig.getSiteId())) { + throw new ServiceException("站点ID不能为空"); + } + if (StringUtils.isBlank(pointCalcConfig.getPointId())) { + throw new ServiceException("点位ID不能为空"); + } + validateCalcExpression(pointCalcConfig.getCalcExpression()); + return pointConfigService.insertPointConfig(toPointConfig(pointCalcConfig), operName); + } + + @Override + public int updatePointCalcConfig(EmsPointCalcConfig pointCalcConfig, String operName) { + if (pointCalcConfig == null || StringUtils.isBlank(pointCalcConfig.getSiteId())) { + throw new ServiceException("站点ID不能为空"); + } + if (StringUtils.isBlank(pointCalcConfig.getPointId())) { + throw new ServiceException("点位ID不能为空"); + } + validateCalcExpression(pointCalcConfig.getCalcExpression()); + return pointConfigService.updatePointConfig(toPointConfig(pointCalcConfig), operName); + } + + @Override + public int deletePointCalcConfigByIds(Long[] ids) { + return pointConfigService.deletePointConfigByIds(ids); + } + + @Override + public int deleteBySiteId(String siteId) { + if (StringUtils.isBlank(siteId)) { + return 0; + } + EmsPointConfig query = new EmsPointConfig(); + query.setSiteId(siteId); + query.setPointType(CALC_POINT_TYPE); + List configList = pointConfigService.selectPointConfigList(query); + if (configList == null || configList.isEmpty()) { + return 0; + } + Long[] ids = configList.stream().map(EmsPointConfig::getId).toArray(Long[]::new); + return pointConfigService.deletePointConfigByIds(ids); + } + + private void validateCalcExpression(String expression) { + if (StringUtils.isBlank(expression)) { + throw new ServiceException("计算表达式不能为空"); + } + if (!CALC_EXPRESSION_PATTERN.matcher(expression).matches()) { + throw new ServiceException("计算表达式仅支持数字、字母、下划线、空格和四则运算符"); + } + } + + private EmsPointConfig toPointConfig(EmsPointCalcConfig calcConfig) { + EmsPointConfig pointConfig = new EmsPointConfig(); + pointConfig.setId(calcConfig.getId()); + pointConfig.setPointId(calcConfig.getPointId()); + pointConfig.setSiteId(calcConfig.getSiteId()); + pointConfig.setPointType(CALC_POINT_TYPE); + pointConfig.setDeviceCategory(calcConfig.getDeviceCategory()); + pointConfig.setDeviceId(""); + pointConfig.setRegisterAddress(""); + pointConfig.setPointName(calcConfig.getPointName()); + pointConfig.setDataKey(calcConfig.getDataKey()); + pointConfig.setPointDesc(calcConfig.getPointDesc()); + pointConfig.setDataUnit(calcConfig.getDataUnit()); + pointConfig.setCalcExpression(calcConfig.getCalcExpression()); + pointConfig.setRemark(calcConfig.getRemark()); + return pointConfig; + } + + private EmsPointConfig toPointConfigQuery(EmsPointCalcConfig calcConfig) { + EmsPointConfig query = new EmsPointConfig(); + query.setSiteId(calcConfig == null ? null : calcConfig.getSiteId()); + query.setDeviceCategory(calcConfig == null ? null : calcConfig.getDeviceCategory()); + query.setDataKey(calcConfig == null ? null : calcConfig.getDataKey()); + query.setPointDesc(calcConfig == null ? null : calcConfig.getPointDesc()); + query.setPointType(CALC_POINT_TYPE); + return query; + } + + private EmsPointCalcConfig toCalcConfig(EmsPointConfig pointConfig) { + EmsPointCalcConfig calcConfig = new EmsPointCalcConfig(); + calcConfig.setId(pointConfig.getId()); + calcConfig.setPointId(pointConfig.getPointId()); + calcConfig.setSiteId(pointConfig.getSiteId()); + calcConfig.setDeviceCategory(pointConfig.getDeviceCategory()); + calcConfig.setPointName(pointConfig.getPointName()); + calcConfig.setDataKey(pointConfig.getDataKey()); + calcConfig.setPointDesc(pointConfig.getPointDesc()); + calcConfig.setDataUnit(pointConfig.getDataUnit()); + calcConfig.setCalcExpression(pointConfig.getCalcExpression()); + calcConfig.setRemark(pointConfig.getRemark()); + calcConfig.setCreateBy(pointConfig.getCreateBy()); + calcConfig.setCreateTime(pointConfig.getCreateTime()); + calcConfig.setUpdateBy(pointConfig.getUpdateBy()); + calcConfig.setUpdateTime(pointConfig.getUpdateTime()); + return calcConfig; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java index d39dd20..1b9b939 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java @@ -17,7 +17,12 @@ import com.xzzn.ems.domain.vo.PointConfigLatestValueVo; import com.xzzn.ems.mapper.EmsPointConfigMapper; import com.xzzn.ems.service.IEmsPointConfigService; import com.xzzn.ems.service.InfluxPointDataWriter; +import org.apache.commons.collections4.CollectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -43,7 +48,10 @@ import java.util.stream.Collectors; @Service public class EmsPointConfigServiceImpl implements IEmsPointConfigService { + private static final Logger log = LoggerFactory.getLogger(EmsPointConfigServiceImpl.class); private static final String TEMPLATE_SITE_ID = "DEFAULT"; + private static final String SITE_LEVEL_CALC_DEVICE_ID = "SITE_CALC"; + private static final int CSV_IMPORT_BATCH_SIZE = 200; private static final Pattern DB_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$"); private static final Pattern INTEGER_PATTERN = Pattern.compile("^-?\\d+$"); private static final Pattern DECIMAL_PATTERN = Pattern.compile("^-?\\d+(\\.\\d+)?$"); @@ -75,7 +83,12 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { normalizeAndValidatePointConfig(pointConfig); pointConfig.setCreateBy(operName); pointConfig.setUpdateBy(operName); - int rows = emsPointConfigMapper.insertEmsPointConfig(pointConfig); + int rows; + try { + rows = emsPointConfigMapper.insertEmsPointConfig(pointConfig); + } catch (Exception ex) { + throw translatePointConfigPersistenceException(ex, pointConfig == null ? null : pointConfig.getPointId()); + } if (rows > 0) { invalidatePointConfigCache(pointConfig.getSiteId(), pointConfig.getDeviceId()); } @@ -87,7 +100,12 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { EmsPointConfig oldConfig = pointConfig.getId() == null ? null : emsPointConfigMapper.selectEmsPointConfigById(pointConfig.getId()); normalizeAndValidatePointConfig(pointConfig); pointConfig.setUpdateBy(operName); - int rows = emsPointConfigMapper.updateEmsPointConfig(pointConfig); + int rows; + try { + rows = emsPointConfigMapper.updateEmsPointConfig(pointConfig); + } catch (Exception ex) { + throw translatePointConfigPersistenceException(ex, pointConfig == null ? null : pointConfig.getPointId()); + } if (rows > 0) { if (oldConfig != null) { invalidatePointConfigCache(oldConfig.getSiteId(), oldConfig.getDeviceId()); @@ -175,12 +193,25 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { emsPointConfigMapper.deleteBySiteId(siteId); } - int importCount = 0; for (EmsPointConfig pointConfig : pointConfigList) { pointConfig.setCreateBy(operName); pointConfig.setUpdateBy(operName); - importCount += emsPointConfigMapper.insertEmsPointConfig(pointConfig); } + int importCount = 0; + long importStart = System.currentTimeMillis(); + for (int fromIndex = 0; fromIndex < pointConfigList.size(); fromIndex += CSV_IMPORT_BATCH_SIZE) { + int toIndex = Math.min(fromIndex + CSV_IMPORT_BATCH_SIZE, pointConfigList.size()); + List batchList = pointConfigList.subList(fromIndex, toIndex); + long batchStart = System.currentTimeMillis(); + try { + importCount += emsPointConfigMapper.insertBatchEmsPointConfig(batchList); + } catch (Exception ex) { + throw translatePointConfigPersistenceException(ex, null); + } + log.info("点位CSV导入批量入库完成,siteId={}, batch={}~{}, batchSize={}, costMs={}", + siteId, fromIndex + 1, toIndex, batchList.size(), System.currentTimeMillis() - batchStart); + } + log.info("点位CSV导入完成,siteId={}, total={}, costMs={}", siteId, importCount, System.currentTimeMillis() - importStart); invalidatePointConfigCacheBySite(siteId); return String.format("导入成功:站点 %s,点位 %d 条", siteId, importCount); @@ -214,17 +245,23 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { @Override public List getCurveData(PointConfigCurveRequest request) { - if (request == null || StringUtils.isAnyBlank(request.getSiteId(), request.getDeviceId(), request.getDataKey())) { + if (request == null || StringUtils.isBlank(request.getSiteId())) { return new ArrayList<>(); } String siteId = StringUtils.trim(request.getSiteId()); String deviceId = StringUtils.trim(request.getDeviceId()); - String dataKey = StringUtils.trim(request.getDataKey()); - if (StringUtils.isAnyBlank(siteId, deviceId, dataKey)) { + String pointId = StringUtils.trim(request.getPointId()); + if (StringUtils.isAnyBlank(siteId, pointId)) { + return new ArrayList<>(); + } + EmsPointConfig pointConfig = resolvePointConfigForCurve(siteId, pointId); + String pointType = resolvePointTypeForCurve(request, pointConfig); + String queryDeviceId = resolveCurveDeviceId(pointType, deviceId, pointConfig); + if (StringUtils.isBlank(queryDeviceId)) { return new ArrayList<>(); } Date[] range = resolveTimeRange(request); - return queryCurveDataFromInflux(siteId, deviceId, dataKey, range[0], range[1]); + return queryCurveDataFromInflux(siteId, queryDeviceId, pointId, pointConfig, range[0], range[1]); } private PointConfigLatestValueVo queryLatestValueFromRedis(PointConfigLatestValueItemVo item, @@ -259,8 +296,13 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { return vo; } - private List queryCurveDataFromInflux(String siteId, String deviceId, String dataKey, Date startTime, Date endTime) { - List values = influxPointDataWriter.queryCurveData(siteId, deviceId, dataKey, startTime, endTime); + private List queryCurveDataFromInflux(String siteId, String deviceId, String pointId, + EmsPointConfig pointConfig, Date startTime, Date endTime) { + String influxPointKey = resolveInfluxPointKey(pointConfig, pointId); + if (StringUtils.isBlank(influxPointKey)) { + return new ArrayList<>(); + } + List values = influxPointDataWriter.queryCurveData(siteId, deviceId, influxPointKey, startTime, endTime); if (values == null || values.isEmpty()) { return new ArrayList<>(); } @@ -306,11 +348,53 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { } } + private RuntimeException translatePointConfigPersistenceException(Exception ex, String pointId) { + if (isPointIdDuplicateException(ex)) { + String normalizedPointId = StringUtils.trim(pointId); + if (StringUtils.isBlank(normalizedPointId)) { + return new ServiceException("点位ID已存在(point_id 全局唯一),请修改后重试"); + } + return new ServiceException("点位ID已存在:" + normalizedPointId + "(point_id 全局唯一),请修改后重试"); + } + if (ex instanceof RuntimeException) { + return (RuntimeException) ex; + } + return new ServiceException("点位配置保存失败,请稍后重试"); + } + + private boolean isPointIdDuplicateException(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + if (current instanceof DuplicateKeyException || current instanceof DataIntegrityViolationException) { + String message = current.getMessage(); + if (StringUtils.isNotBlank(message) && message.toLowerCase(Locale.ROOT).contains("uk_point_id")) { + return true; + } + } + String message = current.getMessage(); + if (StringUtils.isNotBlank(message)) { + String lowerMessage = message.toLowerCase(Locale.ROOT); + if (lowerMessage.contains("duplicate entry") && lowerMessage.contains("uk_point_id")) { + return true; + } + } + current = current.getCause(); + } + return false; + } + private void invalidatePointConfigCache(String siteId, String deviceId) { - if (StringUtils.isAnyBlank(siteId, deviceId)) { + if (StringUtils.isBlank(siteId)) { return; } - redisCache.deleteObject(RedisKeyConstants.POINT_CONFIG_DEVICE + siteId + "_" + deviceId); + if (StringUtils.isBlank(deviceId)) { + invalidatePointConfigCacheBySite(siteId); + return; + } + Collection keys = redisCache.keys(RedisKeyConstants.POINT_CONFIG_DEVICE + siteId + "_" + deviceId + "_*"); + if (keys != null && !keys.isEmpty()) { + redisCache.deleteObject(keys); + } } private void invalidatePointConfigCacheBySite(String siteId) { @@ -410,6 +494,53 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { return configByKey.get(dataKey.toUpperCase(Locale.ROOT)); } + private String resolveInfluxPointKey(EmsPointConfig pointConfig, String pointId) { + if (pointConfig != null && StringUtils.isNotBlank(pointConfig.getPointId())) { + return pointConfig.getPointId().trim(); + } + if (StringUtils.isNotBlank(pointId)) { + return pointId.trim(); + } + return null; + } + + private EmsPointConfig resolvePointConfigForCurve(String siteId, String pointId) { + if (StringUtils.isAnyBlank(siteId, pointId)) { + return null; + } + EmsPointConfig query = new EmsPointConfig(); + query.setSiteId(siteId); + query.setPointId(pointId); + List pointConfigs = emsPointConfigMapper.selectEmsPointConfigList(query); + if (CollectionUtils.isEmpty(pointConfigs)) { + return null; + } + return pointConfigs.get(0); + } + + private String resolvePointTypeForCurve(PointConfigCurveRequest request, EmsPointConfig pointConfig) { + if (pointConfig != null && StringUtils.isNotBlank(pointConfig.getPointType())) { + return StringUtils.trim(pointConfig.getPointType()).toLowerCase(Locale.ROOT); + } + if (request == null || StringUtils.isBlank(request.getPointType())) { + return "data"; + } + return StringUtils.trim(request.getPointType()).toLowerCase(Locale.ROOT); + } + + private String resolveCurveDeviceId(String pointType, String requestDeviceId, EmsPointConfig pointConfig) { + if ("calc".equalsIgnoreCase(StringUtils.defaultString(pointType))) { + return SITE_LEVEL_CALC_DEVICE_ID; + } + if (StringUtils.isNotBlank(requestDeviceId)) { + return StringUtils.trim(requestDeviceId); + } + if (pointConfig != null && StringUtils.isNotBlank(pointConfig.getDeviceId())) { + return StringUtils.trim(pointConfig.getDeviceId()); + } + return null; + } + private BigDecimal convertPointValue(BigDecimal sourceValue, EmsPointConfig pointConfig) { if (sourceValue == null || pointConfig == null) { return sourceValue; @@ -431,8 +562,7 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { } List headerList = parseCsvLine(removeUtf8Bom(headerLine)); Map headerIndex = buildHeaderIndex(headerList); - assertRequiredHeader(headerIndex, "device_category", "device_category"); - assertRequiredHeader(headerIndex, "device_id", "device_id"); + assertRequiredHeader(headerIndex, "point_id", "point_id"); assertRequiredHeader(headerIndex, "data_key", "data_key"); assertRequiredHeader(headerIndex, "point_desc", "point_desc"); @@ -446,8 +576,9 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { List valueList = parseCsvLine(line); EmsPointConfig pointConfig = new EmsPointConfig(); pointConfig.setSiteId(siteId); - pointConfig.setDeviceCategory(getRequiredString(valueList, headerIndex, "device_category", lineNo)); - pointConfig.setDeviceId(getRequiredString(valueList, headerIndex, "device_id", lineNo)); + pointConfig.setPointId(getRequiredString(valueList, headerIndex, "point_id", lineNo)); + pointConfig.setDeviceCategory(getString(valueList, headerIndex, "device_category")); + pointConfig.setDeviceId(getString(valueList, headerIndex, "device_id")); pointConfig.setDataKey(getRequiredString(valueList, headerIndex, "data_key", lineNo)); pointConfig.setPointDesc(getRequiredString(valueList, headerIndex, "point_desc", lineNo)); String registerAddress = getString(valueList, headerIndex, "register_address"); @@ -555,6 +686,9 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { case "siteid": case "站点id": return "site_id"; + case "pointid": + case "点位id": + return "point_id"; case "devicecategory": case "设备类型": return "device_category"; @@ -643,6 +777,12 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { if (pointConfig == null) { return; } + pointConfig.setPointId(StringUtils.trimToNull(pointConfig.getPointId())); + if (StringUtils.isBlank(pointConfig.getPointId())) { + throw new ServiceException("点位ID不能为空"); + } + pointConfig.setDeviceCategory(StringUtils.trimToNull(pointConfig.getDeviceCategory())); + pointConfig.setDeviceId(StringUtils.trimToNull(pointConfig.getDeviceId())); pointConfig.setPointType(normalizePointType(pointConfig.getPointType())); pointConfig.setCalcExpression(StringUtils.trimToNull(pointConfig.getCalcExpression())); if ("calc".equals(pointConfig.getPointType())) { diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStrategyRuntimeConfigServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStrategyRuntimeConfigServiceImpl.java new file mode 100644 index 0000000..2f19806 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStrategyRuntimeConfigServiceImpl.java @@ -0,0 +1,87 @@ +package com.xzzn.ems.service.impl; + +import com.xzzn.common.utils.DateUtils; +import com.xzzn.common.utils.StringUtils; +import com.xzzn.ems.domain.EmsStrategyRuntimeConfig; +import com.xzzn.ems.mapper.EmsStrategyRuntimeConfigMapper; +import com.xzzn.ems.service.IEmsStrategyRuntimeConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +/** + * 策略运行参数配置Service业务层处理 + * + * @author xzzn + * @date 2026-02-13 + */ +@Service +public class EmsStrategyRuntimeConfigServiceImpl implements IEmsStrategyRuntimeConfigService { + + private static final BigDecimal DEFAULT_SOC_DOWN = BigDecimal.ZERO; + private static final BigDecimal DEFAULT_SOC_UP = new BigDecimal("100"); + private static final BigDecimal DEFAULT_ANTI_REVERSE_THRESHOLD = new BigDecimal("30"); + private static final BigDecimal DEFAULT_ANTI_REVERSE_RANGE_PERCENT = new BigDecimal("20"); + private static final BigDecimal DEFAULT_ANTI_REVERSE_UP = new BigDecimal("100"); + private static final BigDecimal DEFAULT_ANTI_REVERSE_POWER_DOWN_PERCENT = new BigDecimal("10"); + private static final BigDecimal DEFAULT_ANTI_REVERSE_HARD_STOP_THRESHOLD = new BigDecimal("20"); + + @Autowired + private EmsStrategyRuntimeConfigMapper runtimeConfigMapper; + + @Override + public EmsStrategyRuntimeConfig getBySiteId(String siteId) { + EmsStrategyRuntimeConfig config = runtimeConfigMapper.selectBySiteId(siteId); + if (config == null) { + return buildDefaultConfig(siteId); + } + fillMissingWithDefault(config, siteId); + return config; + } + + @Override + public int saveBySiteId(EmsStrategyRuntimeConfig config) { + fillMissingWithDefault(config, config.getSiteId()); + EmsStrategyRuntimeConfig exist = runtimeConfigMapper.selectBySiteId(config.getSiteId()); + if (exist == null) { + config.setCreateTime(DateUtils.getNowDate()); + return runtimeConfigMapper.insert(config); + } + config.setUpdateTime(DateUtils.getNowDate()); + return runtimeConfigMapper.updateBySiteId(config); + } + + private EmsStrategyRuntimeConfig buildDefaultConfig(String siteId) { + EmsStrategyRuntimeConfig config = new EmsStrategyRuntimeConfig(); + fillMissingWithDefault(config, siteId); + return config; + } + + private void fillMissingWithDefault(EmsStrategyRuntimeConfig config, String siteId) { + if (StringUtils.isNotEmpty(siteId)) { + config.setSiteId(siteId); + } + if (config.getSocDown() == null) { + config.setSocDown(DEFAULT_SOC_DOWN); + } + if (config.getSocUp() == null) { + config.setSocUp(DEFAULT_SOC_UP); + } + if (config.getAntiReverseThreshold() == null) { + config.setAntiReverseThreshold(DEFAULT_ANTI_REVERSE_THRESHOLD); + } + if (config.getAntiReverseRangePercent() == null) { + config.setAntiReverseRangePercent(DEFAULT_ANTI_REVERSE_RANGE_PERCENT); + } + if (config.getAntiReverseUp() == null) { + config.setAntiReverseUp(DEFAULT_ANTI_REVERSE_UP); + } + if (config.getAntiReversePowerDownPercent() == null) { + config.setAntiReversePowerDownPercent(DEFAULT_ANTI_REVERSE_POWER_DOWN_PERCENT); + } + if (config.getAntiReverseHardStopThreshold() == null) { + config.setAntiReverseHardStopThreshold(DEFAULT_ANTI_REVERSE_HARD_STOP_THRESHOLD); + } + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStrategyTempServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStrategyTempServiceImpl.java index 640cc91..c9b5bac 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStrategyTempServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStrategyTempServiceImpl.java @@ -92,6 +92,9 @@ public class EmsStrategyTempServiceImpl implements IEmsStrategyTempService temp.setStartTime(timeConfig.getStartTime()); temp.setEndTime(timeConfig.getEndTime()); temp.setChargeDischargePower(timeConfig.getChargeDischargePower()); + // 每个时间段可独立配置SOC上下限;为空时兼容旧的模板级配置 + temp.setSdcDown(timeConfig.getSdcDown() != null ? timeConfig.getSdcDown() : publicTemp.getSdcDown()); + temp.setSdcUp(timeConfig.getSdcUp() != null ? timeConfig.getSdcUp() : publicTemp.getSdcUp()); temp.setChargeStatus(timeConfig.getChargeStatus()); emsStrategyTempMapper.insertEmsStrategyTemp(temp); } diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/GeneralQueryServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/GeneralQueryServiceImpl.java index 8e634fd..7f66cf2 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/GeneralQueryServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/GeneralQueryServiceImpl.java @@ -176,11 +176,15 @@ public class GeneralQueryServiceImpl implements IGeneralQueryService } private List queryPointCurve(EmsPointConfig config, int dataUnit, Date startDate, Date endDate) { - if (config == null || config.getSiteId() == null || config.getDeviceId() == null || config.getDataKey() == null) { + if (config == null || config.getSiteId() == null || config.getDeviceId() == null) { + return Collections.emptyList(); + } + String influxPointKey = resolveInfluxPointKey(config); + if (influxPointKey == null) { return Collections.emptyList(); } List values = influxPointDataWriter.queryCurveData( - config.getSiteId(), config.getDeviceId(), config.getDataKey(), startDate, endDate + config.getSiteId(), config.getDeviceId(), influxPointKey, startDate, endDate ); if (values == null || values.isEmpty()) { return Collections.emptyList(); @@ -205,6 +209,16 @@ public class GeneralQueryServiceImpl implements IGeneralQueryService return result; } + private String resolveInfluxPointKey(EmsPointConfig config) { + if (config == null) { + return null; + } + if (config.getPointId() != null && !"".equals(config.getPointId().trim())) { + return config.getPointId().trim(); + } + return null; + } + private String buildDisplayDeviceId(EmsPointConfig config) { String pointName = config.getPointName() == null || "".equals(config.getPointName().trim()) ? config.getDataKey() : config.getPointName().trim(); diff --git a/ems-system/src/main/resources/mapper/ems/EmsPcsSettingMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsPcsSettingMapper.xml index 287ab57..7a69e04 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsPcsSettingMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsPcsSettingMapper.xml @@ -13,6 +13,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -23,7 +24,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - select id, device_setting_id, point_address, power_address, start_command, stop_command, start_power, stop_power, cluster_num, cluster_point_address, create_by, create_time, update_by, update_time, remark from ems_pcs_setting + select id, device_setting_id, point_address, power_address, start_command, stop_command, start_power, stop_power, power_multiplier, cluster_num, cluster_point_address, create_by, create_time, update_by, update_time, remark from ems_pcs_setting + + where id = #{id} + + + + + + insert into ems_point_calc_config + + site_id, + device_category, + point_name, + data_key, + point_desc, + data_unit, + calc_expression, + create_by, + create_time, + update_by, + update_time, + remark, + + + #{siteId}, + #{deviceCategory}, + #{pointName}, + #{dataKey}, + #{pointDesc}, + #{dataUnit}, + #{calcExpression}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{remark}, + + + + + update ems_point_calc_config + + site_id = #{siteId}, + device_category = #{deviceCategory}, + point_name = #{pointName}, + data_key = #{dataKey}, + point_desc = #{pointDesc}, + data_unit = #{dataUnit}, + calc_expression = #{calcExpression}, + update_by = #{updateBy}, + update_time = #{updateTime}, + remark = #{remark}, + + where id = #{id} + + + + delete from ems_point_calc_config where id = #{id} + + + + delete from ems_point_calc_config where id in + + #{id} + + + + + + + delete from ems_point_calc_config + where site_id = #{siteId} + + + diff --git a/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml index 6a0add3..bd43748 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml @@ -6,6 +6,7 @@ + @@ -29,7 +30,7 @@ - select id, site_id, device_category, device_id, point_name, data_key, point_desc, register_address, + select id, point_id, site_id, device_category, device_id, point_name, data_key, point_desc, register_address, data_unit, data_a, data_k, data_b, data_bit, is_alarm, point_type, calc_expression, create_by, create_time, update_by, update_time, remark from ems_point_config @@ -44,6 +45,7 @@ and site_id = #{siteId} + and point_id = #{pointId} and device_category = #{deviceCategory} and device_id = #{deviceId} and data_key like concat('%', #{dataKey}, '%') @@ -57,6 +59,7 @@ insert into ems_point_config + point_id, site_id, device_category, device_id, @@ -79,6 +82,7 @@ remark, + #{pointId}, #{siteId}, #{deviceCategory}, #{deviceId}, @@ -102,9 +106,44 @@ + + insert into ems_point_config ( + point_id, site_id, device_category, device_id, point_name, data_key, point_desc, register_address, + data_unit, data_a, data_k, data_b, data_bit, is_alarm, point_type, calc_expression, + create_by, create_time, update_by, update_time, remark + ) + values + + ( + #{item.pointId}, + #{item.siteId}, + #{item.deviceCategory}, + #{item.deviceId}, + #{item.pointName}, + #{item.dataKey}, + #{item.pointDesc}, + #{item.registerAddress}, + #{item.dataUnit}, + #{item.dataA}, + #{item.dataK}, + #{item.dataB}, + #{item.dataBit}, + #{item.isAlarm}, + #{item.pointType}, + #{item.calcExpression}, + #{item.createBy}, + now(), + #{item.updateBy}, + now(), + #{item.remark} + ) + + + update ems_point_config + point_id = #{pointId}, site_id = #{siteId}, device_category = #{deviceCategory}, device_id = #{deviceId}, @@ -151,12 +190,12 @@ insert into ems_point_config ( - site_id, device_category, device_id, point_name, data_key, point_desc, register_address, + point_id, site_id, device_category, device_id, point_name, data_key, point_desc, register_address, data_unit, data_a, data_k, data_b, data_bit, is_alarm, point_type, calc_expression, create_by, create_time, update_by, update_time, remark ) select - #{targetSiteId}, device_category, device_id, point_name, data_key, point_desc, register_address, + concat('PT_', replace(uuid(), '-', '')), #{targetSiteId}, device_category, device_id, point_name, data_key, point_desc, register_address, data_unit, data_a, data_k, data_b, data_bit, is_alarm, point_type, calc_expression, #{operName}, now(), #{operName}, now(), remark from ems_point_config @@ -228,4 +267,13 @@ + + diff --git a/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml index 6627ba2..c3e96f3 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml @@ -9,6 +9,8 @@ + + @@ -17,7 +19,7 @@ + + where site_id = #{siteId} + limit 1 + + + + insert into ems_strategy_runtime_config + + site_id, + soc_down, + soc_up, + anti_reverse_threshold, + anti_reverse_range_percent, + anti_reverse_up, + anti_reverse_power_down_percent, + anti_reverse_hard_stop_threshold, + create_by, + create_time, + update_by, + update_time, + remark, + + + #{siteId}, + #{socDown}, + #{socUp}, + #{antiReverseThreshold}, + #{antiReverseRangePercent}, + #{antiReverseUp}, + #{antiReversePowerDownPercent}, + #{antiReverseHardStopThreshold}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{remark}, + + + + + update ems_strategy_runtime_config + + soc_down = #{socDown}, + soc_up = #{socUp}, + anti_reverse_threshold = #{antiReverseThreshold}, + anti_reverse_range_percent = #{antiReverseRangePercent}, + anti_reverse_up = #{antiReverseUp}, + anti_reverse_power_down_percent = #{antiReversePowerDownPercent}, + anti_reverse_hard_stop_threshold = #{antiReverseHardStopThreshold}, + update_by = #{updateBy}, + update_time = #{updateTime}, + remark = #{remark}, + + where site_id = #{siteId} + + -- 2.49.0 From 71d0b0f6091554412c4e2947234309a031ddaa44 Mon Sep 17 00:00:00 2001 From: dashixiong Date: Sun, 15 Feb 2026 16:02:06 +0800 Subject: [PATCH 04/10] =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ems/EmsSiteConfigController.java | 20 + .../ems/EmsSiteMonitorController.java | 69 +- .../common/constant/RedisKeyConstants.java | 6 + .../xzzn/quartz/task/ProtectionPlanTask.java | 544 ++++++----- .../com/xzzn/quartz/task/StrategyPoller.java | 169 +++- .../ems/domain/EmsSiteMonitorPointMatch.java | 9 + .../ems/domain/EmsStrategyRuntimeConfig.java | 77 ++ .../domain/vo/GeneralQueryPointOptionVo.java | 9 + .../xzzn/ems/domain/vo/PointNameRequest.java | 18 + .../ems/domain/vo/RunningGraphRequest.java | 3 + .../vo/SiteMonitorProjectPointMappingVo.java | 20 + .../vo/WorkStatusEnumMappingSaveRequest.java | 26 + .../domain/vo/WorkStatusEnumMappingVo.java | 74 ++ .../xzzn/ems/mapper/EmsPointConfigMapper.java | 1 + .../ems/mapper/EmsPointEnumMatchMapper.java | 4 + .../ems/mapper/EmsSiteMonitorDataMapper.java | 15 +- .../ems/service/IEmsDeviceSettingService.java | 5 + .../ems/service/InfluxPointDataWriter.java | 98 ++ .../impl/DeviceDataProcessServiceImpl.java | 850 +++++++++++++----- .../impl/EmsDeviceSettingServiceImpl.java | 811 +++++++++++++++-- .../impl/EmsPointConfigServiceImpl.java | 36 +- .../EmsStrategyRuntimeConfigServiceImpl.java | 24 + .../service/impl/GeneralQueryServiceImpl.java | 83 +- .../service/impl/SingleSiteServiceImpl.java | 540 ++++++++--- .../mapper/ems/EmsPointConfigMapper.xml | 47 +- .../mapper/ems/EmsPointEnumMatchMapper.xml | 7 + .../mapper/ems/EmsSiteMonitorDataMapper.xml | 26 +- .../ems/EmsSiteMonitorPointMatchMapper.xml | 7 +- .../mapper/ems/EmsSiteSettingMapper.xml | 15 +- .../ems/EmsStrategyRuntimeConfigMapper.xml | 30 + 30 files changed, 2844 insertions(+), 799 deletions(-) create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/vo/WorkStatusEnumMappingSaveRequest.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/vo/WorkStatusEnumMappingVo.java diff --git a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteConfigController.java b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteConfigController.java index bc24160..1d2bc7a 100644 --- a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteConfigController.java +++ b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteConfigController.java @@ -15,6 +15,7 @@ import com.xzzn.ems.domain.vo.PointDataRequest; import com.xzzn.ems.domain.vo.PointQueryResponse; import com.xzzn.ems.domain.vo.SiteMonitorProjectPointMappingSaveRequest; import com.xzzn.ems.domain.vo.SiteDeviceListVo; +import com.xzzn.ems.domain.vo.WorkStatusEnumMappingSaveRequest; import com.xzzn.ems.service.IEmsDeviceSettingService; import com.xzzn.ems.service.IEmsSiteService; @@ -232,6 +233,25 @@ public class EmsSiteConfigController extends BaseController{ return AjaxResult.success(rows); } + /** + * 获取单站监控工作状态枚举映射(PCS) + */ + @GetMapping("/getSingleMonitorWorkStatusEnumMappings") + public AjaxResult getSingleMonitorWorkStatusEnumMappings(@RequestParam String siteId) + { + return success(iEmsDeviceSettingService.getSiteWorkStatusEnumMappings(siteId)); + } + + /** + * 保存单站监控工作状态枚举映射(PCS) + */ + @PostMapping("/saveSingleMonitorWorkStatusEnumMappings") + public AjaxResult saveSingleMonitorWorkStatusEnumMappings(@RequestBody WorkStatusEnumMappingSaveRequest request) + { + int rows = iEmsDeviceSettingService.saveSiteWorkStatusEnumMappings(request.getSiteId(), request.getMappings(), getUsername()); + return AjaxResult.success(rows); + } + /** * PCS设备开关机 */ diff --git a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java index 8796797..b788985 100644 --- a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java +++ b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java @@ -10,10 +10,13 @@ import com.xzzn.ems.domain.vo.DateSearchRequest; import com.xzzn.ems.domain.vo.RunningGraphRequest; import com.xzzn.ems.domain.vo.SiteBatteryDataList; import com.xzzn.ems.domain.vo.SiteMonitorDataSaveRequest; +import com.xzzn.ems.domain.vo.SiteMonitorRuningInfoVo; import com.xzzn.ems.service.IEmsDeviceSettingService; import com.xzzn.ems.service.IEmsSiteService; import com.xzzn.ems.service.IEmsStatsReportService; import com.xzzn.ems.service.ISingleSiteService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @@ -28,6 +31,8 @@ import java.util.List; @RestController @RequestMapping("/ems/siteMonitor") public class EmsSiteMonitorController extends BaseController{ + private static final Logger log = LoggerFactory.getLogger(EmsSiteMonitorController.class); + private static final String RUNNING_GRAPH_CTRL_DEBUG = "RunningGraphCtrlDebug"; @Autowired private ISingleSiteService iSingleSiteService; @@ -60,27 +65,75 @@ public class EmsSiteMonitorController extends BaseController{ * 单站监控-设备监控-实时运行曲线图数据 */ @GetMapping("/runningGraph/storagePower") - public AjaxResult getRunningGraphStorage(RunningGraphRequest request) + public AjaxResult getRunningGraphStorage(RunningGraphRequest request, + @RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate) { - return success(iSingleSiteService.getRunningGraphStorage(request)); + SiteMonitorRuningInfoVo data = iSingleSiteService.getRunningGraphStorage(request); + int deviceCount = data == null || data.getPcsPowerList() == null ? 0 : data.getPcsPowerList().size(); + log.info("{} storage, siteId={}, rawStartDate={}, rawEndDate={}, bindStartDate={}, bindEndDate={}, deviceCount={}", + RUNNING_GRAPH_CTRL_DEBUG, + request == null ? null : request.getSiteId(), + startDate, + endDate, + request == null ? null : request.getStartDate(), + request == null ? null : request.getEndDate(), + deviceCount); + return success(data); } @GetMapping("/runningGraph/pcsMaxTemp") - public AjaxResult getRunningGraphPcsMaxTemp(RunningGraphRequest request) + public AjaxResult getRunningGraphPcsMaxTemp(RunningGraphRequest request, + @RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate) { - return success(iSingleSiteService.getRunningGraphPcsMaxTemp(request)); + SiteMonitorRuningInfoVo data = iSingleSiteService.getRunningGraphPcsMaxTemp(request); + int deviceCount = data == null || data.getPcsMaxTempList() == null ? 0 : data.getPcsMaxTempList().size(); + log.info("{} pcsMaxTemp, siteId={}, rawStartDate={}, rawEndDate={}, bindStartDate={}, bindEndDate={}, deviceCount={}", + RUNNING_GRAPH_CTRL_DEBUG, + request == null ? null : request.getSiteId(), + startDate, + endDate, + request == null ? null : request.getStartDate(), + request == null ? null : request.getEndDate(), + deviceCount); + return success(data); } @GetMapping("/runningGraph/batteryAveSoc") - public AjaxResult getRunningGraphBatterySoc(RunningGraphRequest request) + public AjaxResult getRunningGraphBatterySoc(RunningGraphRequest request, + @RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate) { - return success(iSingleSiteService.getRunningGraphBatterySoc(request)); + SiteMonitorRuningInfoVo data = iSingleSiteService.getRunningGraphBatterySoc(request); + int pointCount = data == null || data.getBatteryAveSOCList() == null ? 0 : data.getBatteryAveSOCList().size(); + log.info("{} batteryAveSoc, siteId={}, rawStartDate={}, rawEndDate={}, bindStartDate={}, bindEndDate={}, pointCount={}", + RUNNING_GRAPH_CTRL_DEBUG, + request == null ? null : request.getSiteId(), + startDate, + endDate, + request == null ? null : request.getStartDate(), + request == null ? null : request.getEndDate(), + pointCount); + return success(data); } @GetMapping("/runningGraph/batteryAveTemp") - public AjaxResult getRunningGraphBatteryTemp(RunningGraphRequest request) + public AjaxResult getRunningGraphBatteryTemp(RunningGraphRequest request, + @RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate) { - return success(iSingleSiteService.getRunningGraphBatteryTemp(request)); + SiteMonitorRuningInfoVo data = iSingleSiteService.getRunningGraphBatteryTemp(request); + int pointCount = data == null || data.getBatteryAveTempList() == null ? 0 : data.getBatteryAveTempList().size(); + log.info("{} batteryAveTemp, siteId={}, rawStartDate={}, rawEndDate={}, bindStartDate={}, bindEndDate={}, pointCount={}", + RUNNING_GRAPH_CTRL_DEBUG, + request == null ? null : request.getSiteId(), + startDate, + endDate, + request == null ? null : request.getStartDate(), + request == null ? null : request.getEndDate(), + pointCount); + return success(data); } /** diff --git a/ems-common/src/main/java/com/xzzn/common/constant/RedisKeyConstants.java b/ems-common/src/main/java/com/xzzn/common/constant/RedisKeyConstants.java index c627bb4..0936828 100644 --- a/ems-common/src/main/java/com/xzzn/common/constant/RedisKeyConstants.java +++ b/ems-common/src/main/java/com/xzzn/common/constant/RedisKeyConstants.java @@ -128,4 +128,10 @@ public class RedisKeyConstants /** 单站监控最新数据(按站点+模块) */ public static final String SITE_MONITOR_LATEST = "SITE_MONITOR_LATEST_"; + + /** 单站监控点位映射(按站点) */ + public static final String SITE_MONITOR_POINT_MATCH = "SITE_MONITOR_POINT_MATCH_"; + + /** 站点保护约束(按站点) */ + public static final String PROTECTION_CONSTRAINT = "PROTECTION_CONSTRAINT_"; } diff --git a/ems-quartz/src/main/java/com/xzzn/quartz/task/ProtectionPlanTask.java b/ems-quartz/src/main/java/com/xzzn/quartz/task/ProtectionPlanTask.java index 9842bf4..65f0ec2 100644 --- a/ems-quartz/src/main/java/com/xzzn/quartz/task/ProtectionPlanTask.java +++ b/ems-quartz/src/main/java/com/xzzn/quartz/task/ProtectionPlanTask.java @@ -10,40 +10,14 @@ import com.xzzn.common.core.redis.RedisCache; import com.xzzn.common.enums.AlarmLevelStatus; import com.xzzn.common.enums.AlarmStatus; import com.xzzn.common.enums.ProtPlanStatus; -import com.xzzn.common.enums.StrategyStatus; import com.xzzn.common.utils.StringUtils; import com.xzzn.ems.domain.EmsAlarmRecords; -import com.xzzn.ems.domain.EmsDevicesSetting; -import com.xzzn.ems.domain.EmsFaultIssueLog; import com.xzzn.ems.domain.EmsFaultProtectionPlan; -import com.xzzn.ems.domain.EmsStrategyRunning; -import com.xzzn.ems.domain.vo.ProtectionPlanVo; +import com.xzzn.ems.domain.vo.ProtectionConstraintVo; import com.xzzn.ems.domain.vo.ProtectionSettingVo; import com.xzzn.ems.mapper.EmsAlarmRecordsMapper; -import com.xzzn.ems.mapper.EmsDevicesSettingMapper; -import com.xzzn.ems.mapper.EmsFaultIssueLogMapper; import com.xzzn.ems.mapper.EmsFaultProtectionPlanMapper; -import com.xzzn.ems.mapper.EmsStrategyRunningMapper; import com.xzzn.ems.service.IEmsFaultProtectionPlanService; -import com.xzzn.common.core.modbus.ModbusProcessor; -import com.xzzn.common.core.modbus.domain.DeviceConfig; -import com.xzzn.common.core.modbus.domain.WriteTagConfig; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.UUID; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.annotation.Resource; - import org.apache.commons.collections4.CollectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,14 +25,29 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.ObjectUtils; +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * 告警保护方案轮询 - * - * @author xzzn */ @Component("protectionPlanTask") public class ProtectionPlanTask { private static final Logger logger = LoggerFactory.getLogger(ProtectionPlanTask.class); + private static final BigDecimal DEFAULT_L1_POWER_RATIO = new BigDecimal("0.5"); + private static final int CONSTRAINT_TTL_SECONDS = 120; + @Resource(name = "scheduledExecutorService") private ScheduledExecutorService scheduledExecutorService; @Autowired @@ -66,18 +55,11 @@ public class ProtectionPlanTask { @Autowired private EmsAlarmRecordsMapper emsAlarmRecordsMapper; @Autowired - private EmsStrategyRunningMapper emsStrategyRunningMapper; - @Autowired private EmsFaultProtectionPlanMapper emsFaultProtectionPlanMapper; @Autowired private RedisCache redisCache; + private static final ObjectMapper objectMapper = new ObjectMapper(); - @Autowired - private EmsDevicesSettingMapper emsDevicesSettingMapper; - @Autowired - private ModbusProcessor modbusProcessor; - @Autowired - private EmsFaultIssueLogMapper emsFaultIssueLogMapper; public ProtectionPlanTask(IEmsFaultProtectionPlanService iEmsFaultProtectionPlanService) { this.iEmsFaultProtectionPlanService = iEmsFaultProtectionPlanService; @@ -86,303 +68,320 @@ public class ProtectionPlanTask { public void pollPlanList() { Long planId = 0L; try { - // 获取所有方案,轮询 List planList = iEmsFaultProtectionPlanService.selectEmsFaultProtectionPlanList(null); - for (EmsFaultProtectionPlan plan : planList) { planId = plan.getId(); String siteId = plan.getSiteId(); if (StringUtils.isEmpty(siteId)) { - return; + continue; } - // 保护前提 - String protectionSettings = plan.getProtectionSettings(); - final List protSettings = objectMapper.readValue( - protectionSettings, - new TypeReference>() {} - ); - if (protSettings == null) { - return; - } - - // 处理告警保护方案 - boolean isHighLevel = dealWithProtectionPlan(plan, protSettings); - if (isHighLevel) { - // 触发最高故障等级-结束循环 - return; + List protSettings = parseProtectionSettings(plan.getProtectionSettings()); + if (CollectionUtils.isEmpty(protSettings)) { + continue; } + dealWithProtectionPlan(plan, protSettings); } + refreshProtectionConstraintCache(planList); } catch (Exception e) { logger.error("轮询失败,方案id为:{}", planId, e); } } - // 处理告警保护方案-返回触发下发方案时是否最高等级 - // 需要同步云端 @SyncAfterInsert - private boolean dealWithProtectionPlan(EmsFaultProtectionPlan plan, List protSettings) { + private void dealWithProtectionPlan(EmsFaultProtectionPlan plan, List protSettings) { logger.info("<轮询保护方案> 站点:{},方案ID:{}", plan.getSiteId(), plan.getId()); - boolean isHighLevel = false; - String siteId = plan.getSiteId(); - final Integer isAlertAlarm = plan.getIsAlert(); - final Long status = plan.getStatus(); - // 看方案是否启用,走不同判断 + Integer isAlertAlarm = plan.getIsAlert(); + Long status = plan.getStatus(); + if (status == null) { + status = ProtPlanStatus.STOP.getCode(); + } + if (Objects.equals(status, ProtPlanStatus.STOP.getCode())) { - logger.info("<方案未启用> 站点:{},方案ID:{}", siteId, plan.getId()); - // 未启用,获取方案的故障值与最新数据判断是否需要下发方案 - if(checkIsNeedIssuedPlan(protSettings, siteId)){ - if("3".equals(plan.getFaultLevel())){ - isHighLevel = true;//最高故障等级 - } - // 延时 - final int faultDelay = plan.getFaultDelaySeconds().intValue(); - ScheduledFuture delayTask = scheduledExecutorService.schedule(() -> { - // 延时后再次确认是否仍满足触发条件(防止期间状态变化) - if (checkIsNeedIssuedPlan(protSettings, siteId)) { - // 判断是否需要生成告警 - if (isAlertAlarm == 1) { - logger.info("<生成告警> 方案ID:{},站点:{}", plan.getId(), siteId); - EmsAlarmRecords alarmRecords = addAlarmRecord(siteId,plan.getFaultName(), - getAlarmLevel(plan.getFaultLevel())); - emsAlarmRecordsMapper.insertEmsAlarmRecords(alarmRecords); - } - - // 是否有保护方案,有则通过modbus连接设备下发方案 - String protPlanJson = plan.getProtectionPlan(); - if (protPlanJson != null && !protPlanJson.isEmpty() && !"[]".equals(protPlanJson)) { - logger.info("<下发保护方案> 方案内容:{}", protPlanJson); - executeProtectionActions(protPlanJson,siteId,plan.getId(),plan.getFaultLevel()); // 执行Modbus指令 - } - - // 更新方案状态为“已启用” - logger.info("<方案已启用> 方案ID:{}", plan.getId()); - plan.setStatus(ProtPlanStatus.RUNNING.getCode()); - emsFaultProtectionPlanMapper.updateEmsFaultProtectionPlan(plan); - - // 更新该站点策略为暂停状态 - updateStrategyRunningStatus(siteId, StrategyStatus.SUSPENDED.getCode()); + if (checkIsNeedIssuedPlan(protSettings, siteId)) { + int faultDelay = safeDelaySeconds(plan.getFaultDelaySeconds(), 0); + scheduledExecutorService.schedule(() -> { + if (!checkIsNeedIssuedPlan(protSettings, siteId)) { + return; } - }, faultDelay, TimeUnit.SECONDS); - } - } else { - logger.info("<方案已启用> 站点:{},方案ID:{}", siteId, plan.getId()); - // 已启用,则获取方案的释放值与最新数据判断是否需要取消方案 - if(checkIsNeedCancelPlan(protSettings, siteId)){ - // 延时, - int releaseDelay = plan.getReleaseDelaySeconds().intValue(); - ScheduledFuture delayTask = scheduledExecutorService.schedule(() -> { - // 判断是否已存在未处理告警,有着取消 - if(isAlertAlarm == 1){ - logger.info("<取消告警>"); - EmsAlarmRecords emsAlarmRecords = emsAlarmRecordsMapper.getFailedRecord(siteId, - plan.getFaultName(),getAlarmLevel(plan.getFaultLevel())); - if(emsAlarmRecords != null){ - emsAlarmRecords.setStatus(AlarmStatus.DONE.getCode()); - emsAlarmRecordsMapper.updateEmsAlarmRecords(emsAlarmRecords); - } + if (Integer.valueOf(1).equals(isAlertAlarm)) { + EmsAlarmRecords alarmRecords = addAlarmRecord(siteId, plan.getFaultName(), getAlarmLevel(plan.getFaultLevel())); + emsAlarmRecordsMapper.insertEmsAlarmRecords(alarmRecords); } - // 更新方案状态为“未启用” - logger.info("<方案变更为未启用> 方案ID:{}", plan.getId()); - plan.setStatus(ProtPlanStatus.STOP.getCode()); + plan.setStatus(ProtPlanStatus.RUNNING.getCode()); plan.setUpdateBy("system"); emsFaultProtectionPlanMapper.updateEmsFaultProtectionPlan(plan); - // 更新该站点策略为启用状态 - updateStrategyRunningStatus(siteId, StrategyStatus.RUNNING.getCode()); - }, releaseDelay, TimeUnit.SECONDS); + refreshSiteProtectionConstraint(siteId); + }, faultDelay, TimeUnit.SECONDS); } - } - - return isHighLevel; - } - - // 下发保护方案 - private void executeProtectionActions(String protPlanJson, String siteId, Long planId, Integer faultLevel){ - final List protPlanList; - try { - protPlanList = objectMapper.readValue( - protPlanJson, - new TypeReference>() {} - ); - if (protPlanList == null) { - return; - } - - // 遍历保护方案 - for (ProtectionPlanVo plan : protPlanList) { - if (StringUtils.isEmpty(plan.getDeviceId()) || StringUtils.isEmpty(plan.getPoint())) { - return; - } - // 给设备发送指令记录日志,并同步云端 - EmsFaultIssueLog faultIssueLog = createLogEntity(plan,siteId); - faultIssueLog.setLogLevel(faultLevel); - emsFaultIssueLogMapper.insertEmsFaultIssueLog(faultIssueLog); - - // 通过modbus连接设备,发送数据 - executeSinglePlan(plan,siteId); - } - - - } catch (Exception e) { - logger.error("下发保护方案失败,方案id为:", planId, e); - } - } - - private EmsFaultIssueLog createLogEntity(ProtectionPlanVo plan,String siteId) { - EmsFaultIssueLog faultIssueLog = new EmsFaultIssueLog(); - faultIssueLog.setLogId(UUID.randomUUID().toString()); - faultIssueLog.setLogTime(new Date()); - faultIssueLog.setSiteId(siteId); - faultIssueLog.setDeviceId(plan.getDeviceId()); - faultIssueLog.setPoint(plan.getPoint()); - faultIssueLog.setValue(plan.getValue()); - faultIssueLog.setCreateBy("sys"); - faultIssueLog.setCreateTime(new Date()); - return faultIssueLog; - } - - private void executeSinglePlan(ProtectionPlanVo plan, String siteId) throws Exception { - String deviceId = plan.getDeviceId(); - // 获取设备地址信息 - EmsDevicesSetting device = emsDevicesSettingMapper.getDeviceBySiteAndDeviceId(deviceId, siteId); - if (device == null || StringUtils.isEmpty(device.getIpAddress()) || device.getIpPort()==null) { - logger.warn("设备信息不完整,deviceId:{}", deviceId); return; } - // 构建设备配置 - DeviceConfig config = new DeviceConfig(); - config.setHost(device.getIpAddress()); - config.setPort(device.getIpPort().intValue()); - config.setSlaveId(device.getSlaveId().intValue()); - config.setDeviceName(device.getDeviceName()); - config.setDeviceNumber(device.getDeviceId()); - - // 构建写入标签配置 - WriteTagConfig writeTag = new WriteTagConfig(); - writeTag.setAddress(plan.getPoint()); - writeTag.setValue(plan.getValue()); - - List writeTags = new ArrayList<>(); - writeTags.add(writeTag); - config.setWriteTags(writeTags); - - // 写入数据到设备 - boolean success = modbusProcessor.writeDataToDeviceWithRetry(config); - - if (!success) { - logger.error("写入失败,设备地址:{}", device.getIpAddress()); + if (checkIsNeedCancelPlan(protSettings, siteId)) { + int releaseDelay = safeDelaySeconds(plan.getReleaseDelaySeconds(), 0); + scheduledExecutorService.schedule(() -> { + if (Integer.valueOf(1).equals(isAlertAlarm)) { + EmsAlarmRecords emsAlarmRecords = emsAlarmRecordsMapper.getFailedRecord( + siteId, + plan.getFaultName(), + getAlarmLevel(plan.getFaultLevel()) + ); + if (emsAlarmRecords != null) { + emsAlarmRecords.setStatus(AlarmStatus.DONE.getCode()); + emsAlarmRecordsMapper.updateEmsAlarmRecords(emsAlarmRecords); + } + } + plan.setStatus(ProtPlanStatus.STOP.getCode()); + plan.setUpdateBy("system"); + emsFaultProtectionPlanMapper.updateEmsFaultProtectionPlan(plan); + refreshSiteProtectionConstraint(siteId); + }, releaseDelay, TimeUnit.SECONDS); } } - // 校验释放值是否取消方案 - private boolean checkIsNeedCancelPlan(List protSettings, String siteId) { - BigDecimal releaseValue = BigDecimal.ZERO; + private int safeDelaySeconds(Long delay, int defaultSeconds) { + if (delay == null || delay < 0) { + return defaultSeconds; + } + return delay.intValue(); + } + private List parseProtectionSettings(String settingsJson) { + if (StringUtils.isEmpty(settingsJson)) { + return new ArrayList<>(); + } + try { + return objectMapper.readValue(settingsJson, new TypeReference>() {}); + } catch (Exception e) { + logger.error("解析保护前提失败,json:{}", settingsJson, e); + return new ArrayList<>(); + } + } + + private void refreshProtectionConstraintCache(List allPlans) { + Map> planBySite = new HashMap<>(); + for (EmsFaultProtectionPlan plan : allPlans) { + if (StringUtils.isEmpty(plan.getSiteId())) { + continue; + } + planBySite.computeIfAbsent(plan.getSiteId(), k -> new ArrayList<>()).add(plan); + } + for (Map.Entry> entry : planBySite.entrySet()) { + writeSiteProtectionConstraint(entry.getKey(), entry.getValue()); + } + } + + private void refreshSiteProtectionConstraint(String siteId) { + if (StringUtils.isEmpty(siteId)) { + return; + } + EmsFaultProtectionPlan query = new EmsFaultProtectionPlan(); + query.setSiteId(siteId); + List sitePlans = iEmsFaultProtectionPlanService.selectEmsFaultProtectionPlanList(query); + writeSiteProtectionConstraint(siteId, sitePlans); + } + + private void writeSiteProtectionConstraint(String siteId, List sitePlans) { + List runningPlans = new ArrayList<>(); + for (EmsFaultProtectionPlan plan : sitePlans) { + if (Objects.equals(plan.getStatus(), ProtPlanStatus.RUNNING.getCode())) { + runningPlans.add(plan); + } + } + + String key = RedisKeyConstants.PROTECTION_CONSTRAINT + siteId; + if (runningPlans.isEmpty()) { + redisCache.deleteObject(key); + return; + } + + ProtectionConstraintVo merged = ProtectionConstraintVo.empty(); + for (EmsFaultProtectionPlan runningPlan : runningPlans) { + ProtectionConstraintVo single = buildConstraintFromPlan(runningPlan); + mergeConstraint(merged, single); + } + merged.setUpdateAt(System.currentTimeMillis()); + redisCache.setCacheObject(key, merged, CONSTRAINT_TTL_SECONDS, TimeUnit.SECONDS); + } + + private ProtectionConstraintVo buildConstraintFromPlan(EmsFaultProtectionPlan plan) { + ProtectionConstraintVo vo = ProtectionConstraintVo.empty(); + int level = plan.getFaultLevel() == null ? 0 : plan.getFaultLevel(); + vo.setLevel(level); + vo.setSourcePlanIds(new ArrayList<>()); + vo.getSourcePlanIds().add(plan.getId()); + + if (level == 1) { + vo.setPowerLimitRatio(DEFAULT_L1_POWER_RATIO); + } else if (level >= 3) { + vo.setForceStop(true); + vo.setForceStandby(true); + vo.setAllowCharge(false); + vo.setAllowDischarge(false); + vo.setPowerLimitRatio(BigDecimal.ZERO); + } + + String description = StringUtils.isEmpty(plan.getDescription()) ? "" : plan.getDescription(); + BigDecimal ratioByDesc = parseDerateRatio(description); + if (ratioByDesc != null) { + vo.setPowerLimitRatio(minRatio(vo.getPowerLimitRatio(), ratioByDesc)); + } + + if (description.contains("禁止充放电")) { + vo.setAllowCharge(false); + vo.setAllowDischarge(false); + } else { + if (description.contains("禁止充电")) { + vo.setAllowCharge(false); + } + if (description.contains("禁止放电")) { + vo.setAllowDischarge(false); + } + if (description.contains("允许充电")) { + vo.setAllowCharge(true); + } + if (description.contains("允许放电")) { + vo.setAllowDischarge(true); + } + } + + if (description.contains("待机")) { + vo.setForceStandby(true); + } + if (description.contains("停机") || description.contains("切断") || level >= 3) { + vo.setForceStop(true); + vo.setForceStandby(true); + vo.setAllowCharge(false); + vo.setAllowDischarge(false); + vo.setPowerLimitRatio(BigDecimal.ZERO); + } + + return vo; + } + + private BigDecimal parseDerateRatio(String text) { + if (StringUtils.isEmpty(text)) { + return null; + } + Matcher m = Pattern.compile("降功率\\s*(\\d+(?:\\.\\d+)?)%") + .matcher(text); + if (!m.find()) { + return null; + } + BigDecimal percent = new BigDecimal(m.group(1)); + if (percent.compareTo(BigDecimal.ZERO) < 0) { + return null; + } + return percent.divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP); + } + + private void mergeConstraint(ProtectionConstraintVo merged, ProtectionConstraintVo incoming) { + if (incoming == null) { + return; + } + merged.setLevel(Math.max(nullSafeInt(merged.getLevel()), nullSafeInt(incoming.getLevel()))); + merged.setAllowCharge(boolAnd(merged.getAllowCharge(), incoming.getAllowCharge())); + merged.setAllowDischarge(boolAnd(merged.getAllowDischarge(), incoming.getAllowDischarge())); + merged.setForceStandby(boolOr(merged.getForceStandby(), incoming.getForceStandby())); + merged.setForceStop(boolOr(merged.getForceStop(), incoming.getForceStop())); + merged.setPowerLimitRatio(minRatio(merged.getPowerLimitRatio(), incoming.getPowerLimitRatio())); + + if (incoming.getSourcePlanIds() != null && !incoming.getSourcePlanIds().isEmpty()) { + if (merged.getSourcePlanIds() == null) { + merged.setSourcePlanIds(new ArrayList<>()); + } + merged.getSourcePlanIds().addAll(incoming.getSourcePlanIds()); + } + } + + private BigDecimal minRatio(BigDecimal a, BigDecimal b) { + BigDecimal left = a == null ? BigDecimal.ONE : a; + BigDecimal right = b == null ? BigDecimal.ONE : b; + return left.min(right); + } + + private int nullSafeInt(Integer value) { + return value == null ? 0 : value; + } + + private Boolean boolAnd(Boolean a, Boolean b) { + boolean left = a == null || a; + boolean right = b == null || b; + return left && right; + } + + private Boolean boolOr(Boolean a, Boolean b) { + boolean left = a != null && a; + boolean right = b != null && b; + return left || right; + } + + private boolean checkIsNeedCancelPlan(List protSettings, String siteId) { StringBuilder conditionSb = new StringBuilder(); for (int i = 0; i < protSettings.size(); i++) { ProtectionSettingVo vo = protSettings.get(i); String deviceId = vo.getDeviceId(); String point = vo.getPoint(); - releaseValue = vo.getFaultValue(); - if(StringUtils.isEmpty(deviceId) || StringUtils.isEmpty(point) || releaseValue == null - || StringUtils.isEmpty(vo.getReleaseOperator())){ + BigDecimal releaseValue = vo.getReleaseValue(); + if (StringUtils.isEmpty(deviceId) + || StringUtils.isEmpty(point) + || releaseValue == null + || StringUtils.isEmpty(vo.getReleaseOperator())) { return false; } - // 获取点位最新值 BigDecimal lastPointValue = getPointLastValue(deviceId, point, siteId); - logger.info("checkIsNeedCancelPlan 点位:{},最新值:{},比较方式:{},释放值:{}", point, lastPointValue, vo.getReleaseOperator(), releaseValue); - if(lastPointValue == null){ + if (lastPointValue == null) { return false; } - // 拼接校验语句-最新值+比较方式+故障值+与下一点位关系(最后一个条件后不加关系) conditionSb.append(lastPointValue).append(vo.getReleaseOperator()).append(releaseValue); if (i < protSettings.size() - 1) { - String relation = vo.getRelationNext(); - conditionSb.append(" ").append(relation).append(" "); + conditionSb.append(" ").append(vo.getRelationNext()).append(" "); } - } - // 执行比较语句 return executeWithParser(conditionSb.toString()); } - // 校验故障值是否需要下发方案 private boolean checkIsNeedIssuedPlan(List protSettings, String siteId) { - BigDecimal faultValue = BigDecimal.ZERO; - StringBuilder conditionSb = new StringBuilder(); for (int i = 0; i < protSettings.size(); i++) { ProtectionSettingVo vo = protSettings.get(i); String deviceId = vo.getDeviceId(); String point = vo.getPoint(); - faultValue = vo.getFaultValue(); - if(StringUtils.isEmpty(deviceId) || StringUtils.isEmpty(point) || faultValue == null - || StringUtils.isEmpty(vo.getFaultOperator())){ + BigDecimal faultValue = vo.getFaultValue(); + if (StringUtils.isEmpty(deviceId) + || StringUtils.isEmpty(point) + || faultValue == null + || StringUtils.isEmpty(vo.getFaultOperator())) { return false; } - // 获取点位最新值 BigDecimal lastPointValue = getPointLastValue(deviceId, point, siteId); - logger.info("checkIsNeedIssuedPlan 点位:{},最新值:{},比较方式:{},故障值:{}", point, lastPointValue, vo.getFaultOperator(), faultValue); - if(lastPointValue == null){ + if (lastPointValue == null) { return false; } - // 拼接校验语句-最新值+比较方式+故障值+与下一点位关系(最后一个条件后不加关系) conditionSb.append(lastPointValue).append(vo.getFaultOperator()).append(faultValue); if (i < protSettings.size() - 1) { - String relation = vo.getRelationNext(); - conditionSb.append(" ").append(relation).append(" "); + conditionSb.append(" ").append(vo.getRelationNext()).append(" "); } - } - // 执行比较语句 return executeWithParser(conditionSb.toString()); } private BigDecimal getPointLastValue(String deviceId, String point, String siteId) { JSONObject mqttJson = redisCache.getCacheObject(RedisKeyConstants.SYNC_DATA + siteId + "_" + deviceId); - if(mqttJson == null){ + if (mqttJson == null) { return null; } String jsonData = mqttJson.get("Data").toString(); - if(StringUtils.isEmpty(jsonData)){ + if (StringUtils.isEmpty(jsonData)) { return null; } Map obj = JSON.parseObject(jsonData, new com.alibaba.fastjson2.TypeReference>() {}); return StringUtils.getBigDecimal(obj.get(point)); } - // 更新站点策略为启用 - private void updateStrategyRunningStatus(String siteId, String status) { - // 获取是否有正在运行的策略,如果有则不更改 - EmsStrategyRunning query = new EmsStrategyRunning(); - query.setSiteId(siteId); - query.setStatus(StrategyStatus.RUNNING.getCode().equals(status) ? StrategyStatus.SUSPENDED.getCode() : StrategyStatus.RUNNING.getCode()); - List strategyRunningList = emsStrategyRunningMapper.selectEmsStrategyRunningList(query); - if (CollectionUtils.isNotEmpty(strategyRunningList)) { - // 获取已存在并且状态为:未启用和已暂停的最晚一条策略,更新为已启用 - strategyRunningList.forEach(emsStrategyRunning -> { - emsStrategyRunning.setStatus(status); - emsStrategyRunningMapper.updateEmsStrategyRunning(emsStrategyRunning); - }); - } - } - - // 更新站点策略为启用 - private void updateStrategyRunning(String siteId) { - // 获取是否有正在运行的策略,如果有则不更改 - EmsStrategyRunning emsStrategyRunning = emsStrategyRunningMapper.getRunningStrategy(siteId); - if (emsStrategyRunning == null) { - // 获取已存在并且状态为:未启用和已暂停的最晚一条策略,更新为已启用 - emsStrategyRunning = emsStrategyRunningMapper.getPendingStrategy(siteId); - emsStrategyRunning.setStatus(StrategyStatus.RUNNING.getCode()); - emsStrategyRunningMapper.updateEmsStrategyRunning(emsStrategyRunning); - } - } - - private EmsAlarmRecords addAlarmRecord(String siteId, String content,String level) { + private EmsAlarmRecords addAlarmRecord(String siteId, String content, String level) { EmsAlarmRecords emsAlarmRecords = new EmsAlarmRecords(); emsAlarmRecords.setSiteId(siteId); emsAlarmRecords.setAlarmContent(content); @@ -395,29 +394,31 @@ public class ProtectionPlanTask { return emsAlarmRecords; } - // 故障等级-告警等级匹配 private String getAlarmLevel(Integer faultLevel) { if (ObjectUtils.isEmpty(faultLevel) || faultLevel < 1 || faultLevel > 3) { - logger.warn("非法故障等级:{},默认返回普通告警", faultLevel); + logger.warn("非法故障等级:{},默认返回紧急告警", faultLevel); return AlarmLevelStatus.EMERGENCY.getCode(); } switch (faultLevel) { - case 1: return AlarmLevelStatus.GENERAL.getCode(); - case 2: return AlarmLevelStatus.SERIOUS.getCode(); - case 3: return AlarmLevelStatus.EMERGENCY.getCode(); + case 1: + return AlarmLevelStatus.GENERAL.getCode(); + case 2: + return AlarmLevelStatus.SERIOUS.getCode(); + case 3: + return AlarmLevelStatus.EMERGENCY.getCode(); default: - logger.error("未匹配的故障等级:{}", faultLevel); return AlarmLevelStatus.EMERGENCY.getCode(); } } - // 自定义表达式解析器(仅支持简单运算符和逻辑关系) + /** + * 自定义表达式解析器(仅支持简单运算符和逻辑关系) + */ public boolean executeWithParser(String conditionStr) { if (conditionStr == null || conditionStr.isEmpty()) { return false; } - // 1. 拆分逻辑关系(提取 && 或 ||) List logicRelations = new ArrayList<>(); Pattern logicPattern = Pattern.compile("(&&|\\|\\|)"); Matcher logicMatcher = logicPattern.matcher(conditionStr); @@ -425,10 +426,7 @@ public class ProtectionPlanTask { logicRelations.add(logicMatcher.group()); } - // 2. 拆分原子条件(如 "3.55>3.52") String[] atomicConditions = logicPattern.split(conditionStr); - - // 3. 解析每个原子条件并计算结果 List atomicResults = new ArrayList<>(); Pattern conditionPattern = Pattern.compile("(\\d+\\.?\\d*)\\s*([><]=?|==)\\s*(\\d+\\.?\\d*)"); for (String atomic : atomicConditions) { @@ -437,11 +435,10 @@ public class ProtectionPlanTask { logger.error("无效的原子条件:{}", atomic); return false; } - double left = Double.parseDouble(matcher.group(1)); // 左值(最新值) - String operator = matcher.group(2); // 运算符 - double right = Double.parseDouble(matcher.group(3)); // 右值(故障值) + double left = Double.parseDouble(matcher.group(1)); + String operator = matcher.group(2); + double right = Double.parseDouble(matcher.group(3)); - // 执行比较 boolean result; switch (operator) { case ">": @@ -466,11 +463,10 @@ public class ProtectionPlanTask { atomicResults.add(result); } - // 4. 组合原子结果(根据逻辑关系) boolean finalResult = atomicResults.get(0); for (int i = 0; i < logicRelations.size(); i++) { String relation = logicRelations.get(i); - boolean nextResult = atomicResults.get(i+1); + boolean nextResult = atomicResults.get(i + 1); if ("&&".equals(relation)) { finalResult = finalResult && nextResult; } else if ("||".equals(relation)) { diff --git a/ems-quartz/src/main/java/com/xzzn/quartz/task/StrategyPoller.java b/ems-quartz/src/main/java/com/xzzn/quartz/task/StrategyPoller.java index 7ec40b3..da7f50b 100644 --- a/ems-quartz/src/main/java/com/xzzn/quartz/task/StrategyPoller.java +++ b/ems-quartz/src/main/java/com/xzzn/quartz/task/StrategyPoller.java @@ -2,9 +2,11 @@ package com.xzzn.quartz.task; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; +import com.xzzn.common.constant.RedisKeyConstants; import com.xzzn.common.core.modbus.ModbusProcessor; import com.xzzn.common.core.modbus.domain.DeviceConfig; import com.xzzn.common.core.modbus.domain.WriteTagConfig; +import com.xzzn.common.core.redis.RedisCache; import com.xzzn.common.enums.ChargeStatus; import com.xzzn.common.enums.DeviceCategory; import com.xzzn.common.enums.SiteDevice; @@ -20,6 +22,7 @@ import com.xzzn.ems.domain.EmsStrategyRuntimeConfig; import com.xzzn.ems.domain.EmsStrategyLog; import com.xzzn.ems.domain.EmsStrategyTemp; import com.xzzn.ems.domain.EmsStrategyTimeConfig; +import com.xzzn.ems.domain.vo.ProtectionConstraintVo; import com.xzzn.ems.domain.vo.StrategyRunningVo; import com.xzzn.ems.mapper.EmsAmmeterDataMapper; import com.xzzn.ems.mapper.EmsBatteryStackMapper; @@ -73,6 +76,20 @@ public class StrategyPoller { private static final BigDecimal DEFAULT_ANTI_REVERSE_POWER_DOWN_PERCENT = new BigDecimal(10); // 电网有功功率低于20kW时,强制待机 private static final BigDecimal DEFAULT_ANTI_REVERSE_HARD_STOP_THRESHOLD = new BigDecimal(20); + // 设定功率倍率,默认10 + private static final BigDecimal DEFAULT_POWER_SET_MULTIPLIER = new BigDecimal(10); + // 保护介入默认开启 + private static final Integer DEFAULT_PROTECT_INTERVENE_ENABLE = 1; + // 一级保护默认降额50% + private static final BigDecimal DEFAULT_PROTECT_L1_DERATE_PERCENT = new BigDecimal("50"); + // 保护约束失效保护时长(秒) + private static final Integer DEFAULT_PROTECT_RECOVERY_STABLE_SECONDS = 5; + // 三级保护默认锁存开启 + private static final Integer DEFAULT_PROTECT_L3_LATCH_ENABLE = 1; + // 保护冲突策略默认值 + private static final String DEFAULT_PROTECT_CONFLICT_POLICY = "MAX_LEVEL_WIN"; + // 保护约束默认功率比例 + private static final BigDecimal DEFAULT_PROTECTION_RATIO = BigDecimal.ONE; // 除法精度,避免BigDecimal除不尽异常 private static final int POWER_SCALE = 4; @@ -95,6 +112,8 @@ public class StrategyPoller { @Autowired private EmsStrategyRuntimeConfigMapper runtimeConfigMapper; @Autowired + private RedisCache redisCache; + @Autowired private ModbusProcessor modbusProcessor; @Resource(name = "modbusExecutor") @@ -186,6 +205,7 @@ public class StrategyPoller { } // 判断SOC上下限 if (isSocInRange(emsStrategyTemp, runtimeConfig)) { + ProtectionConstraintVo protectionConstraint = getProtectionConstraint(siteId); Map pcsSettingCache = new HashMap<>(); BigDecimal avgChargeDischargePower = emsStrategyTemp.getChargeDischargePower() .divide(new BigDecimal(pcsDeviceList.size()), POWER_SCALE, RoundingMode.HALF_UP); @@ -206,13 +226,27 @@ public class StrategyPoller { logger.info("当前站点: {}, PCS设备: {} 未获取电池簇数量", siteId, pcsDevice.getDeviceId()); continue; } - // 功率默认放大10倍,平均功率值,根据电池簇数量进行平均分配 - BigDecimal strategyPower = avgChargeDischargePower.multiply(new BigDecimal(10)) + // 平均功率值根据倍率放大后,再按电池簇数量平均分配 + BigDecimal strategyPower = avgChargeDischargePower.multiply(runtimeConfig.getPowerSetMultiplier()) .divide(new BigDecimal(pcsSetting.getClusterNum()), POWER_SCALE, RoundingMode.HALF_UP); // 根据充电状态,处理数据 if (ChargeStatus.CHARGING.getCode().equals(emsStrategyTemp.getChargeStatus())) { + StrategyCommandDecision decision = applyProtectionConstraint( + strategyPower, + ChargeStatus.CHARGING, + runtimeConfig, + protectionConstraint + ); // 发送Modbus命令控制设备-充电 - sendModbusCommand(Collections.singletonList(pcsDevice), pcsSetting, ChargeStatus.CHARGING, strategyPower, emsStrategyTemp, false, null); + sendModbusCommand( + Collections.singletonList(pcsDevice), + pcsSetting, + decision.getChargeStatus(), + decision.getPower(), + emsStrategyTemp, + false, + null + ); } else if (ChargeStatus.DISCHARGING.getCode().equals(emsStrategyTemp.getChargeStatus())) { boolean needAntiReverseFlow = false; Integer powerDownType = null; @@ -277,13 +311,21 @@ public class StrategyPoller { if (chargeDischargePower.compareTo(BigDecimal.ZERO) < 0) { chargeDischargePower = BigDecimal.ZERO; } - if (BigDecimal.ZERO.compareTo(chargeDischargePower) == 0) { + StrategyCommandDecision decision = applyProtectionConstraint( + chargeDischargePower, + ChargeStatus.DISCHARGING, + runtimeConfig, + protectionConstraint + ); + ChargeStatus finalStatus = decision.getChargeStatus(); + BigDecimal finalPower = decision.getPower(); + if (ChargeStatus.STANDBY.equals(finalStatus) || BigDecimal.ZERO.compareTo(finalPower) == 0) { // 如果已经降功率到0,则设备直接待机 // 发送Modbus命令控制设备-待机 sendModbusCommand(Collections.singletonList(pcsDevice), pcsSetting, ChargeStatus.STANDBY, BigDecimal.ZERO, emsStrategyTemp, needAntiReverseFlow, powerDownType); } else { // 发送Modbus命令控制设备-放电 - sendModbusCommand(Collections.singletonList(pcsDevice), pcsSetting, ChargeStatus.DISCHARGING, chargeDischargePower, emsStrategyTemp, needAntiReverseFlow, powerDownType); + sendModbusCommand(Collections.singletonList(pcsDevice), pcsSetting, finalStatus, finalPower, emsStrategyTemp, needAntiReverseFlow, powerDownType); } } else { // 发送Modbus命令控制设备-待机 @@ -503,6 +545,103 @@ public class StrategyPoller { return result; } + private ProtectionConstraintVo getProtectionConstraint(String siteId) { + ProtectionConstraintVo constraint = redisCache.getCacheObject(RedisKeyConstants.PROTECTION_CONSTRAINT + siteId); + if (constraint == null) { + return ProtectionConstraintVo.empty(); + } + if (constraint.getPowerLimitRatio() == null) { + constraint.setPowerLimitRatio(DEFAULT_PROTECTION_RATIO); + } + if (constraint.getAllowCharge() == null) { + constraint.setAllowCharge(true); + } + if (constraint.getAllowDischarge() == null) { + constraint.setAllowDischarge(true); + } + if (constraint.getForceStandby() == null) { + constraint.setForceStandby(false); + } + if (constraint.getForceStop() == null) { + constraint.setForceStop(false); + } + if (constraint.getLevel() == null) { + constraint.setLevel(0); + } + return constraint; + } + + private StrategyCommandDecision applyProtectionConstraint(BigDecimal targetPower, + ChargeStatus targetStatus, + EmsStrategyRuntimeConfig runtimeConfig, + ProtectionConstraintVo constraint) { + if (!Integer.valueOf(1).equals(runtimeConfig.getProtectInterveneEnable())) { + return new StrategyCommandDecision(targetStatus, safePower(targetPower)); + } + if (constraint == null || nullSafeInt(constraint.getLevel()) <= 0) { + return new StrategyCommandDecision(targetStatus, safePower(targetPower)); + } + if (Boolean.TRUE.equals(constraint.getForceStop()) || Boolean.TRUE.equals(constraint.getForceStandby())) { + return new StrategyCommandDecision(ChargeStatus.STANDBY, BigDecimal.ZERO); + } + if (ChargeStatus.CHARGING.equals(targetStatus) && Boolean.FALSE.equals(constraint.getAllowCharge())) { + return new StrategyCommandDecision(ChargeStatus.STANDBY, BigDecimal.ZERO); + } + if (ChargeStatus.DISCHARGING.equals(targetStatus) && Boolean.FALSE.equals(constraint.getAllowDischarge())) { + return new StrategyCommandDecision(ChargeStatus.STANDBY, BigDecimal.ZERO); + } + BigDecimal ratio = getPowerLimitRatio(constraint, runtimeConfig); + BigDecimal finalPower = safePower(targetPower).multiply(ratio).setScale(POWER_SCALE, RoundingMode.HALF_UP); + if (finalPower.compareTo(BigDecimal.ZERO) <= 0) { + return new StrategyCommandDecision(ChargeStatus.STANDBY, BigDecimal.ZERO); + } + return new StrategyCommandDecision(targetStatus, finalPower); + } + + private BigDecimal getPowerLimitRatio(ProtectionConstraintVo constraint, EmsStrategyRuntimeConfig runtimeConfig) { + BigDecimal ratio = constraint.getPowerLimitRatio(); + if (ratio == null || ratio.compareTo(BigDecimal.ZERO) < 0 || ratio.compareTo(BigDecimal.ONE) > 0) { + ratio = DEFAULT_PROTECTION_RATIO; + } + if (nullSafeInt(constraint.getLevel()) == 1 && DEFAULT_PROTECTION_RATIO.compareTo(ratio) == 0) { + BigDecimal deratePercent = runtimeConfig.getProtectL1DeratePercent(); + if (deratePercent == null || deratePercent.compareTo(BigDecimal.ZERO) < 0 || deratePercent.compareTo(new BigDecimal("100")) > 0) { + deratePercent = DEFAULT_PROTECT_L1_DERATE_PERCENT; + } + ratio = deratePercent.divide(new BigDecimal("100"), POWER_SCALE, RoundingMode.HALF_UP); + } + return ratio; + } + + private BigDecimal safePower(BigDecimal power) { + if (power == null || power.compareTo(BigDecimal.ZERO) < 0) { + return BigDecimal.ZERO; + } + return power; + } + + private int nullSafeInt(Integer value) { + return value == null ? 0 : value; + } + + private static class StrategyCommandDecision { + private final ChargeStatus chargeStatus; + private final BigDecimal power; + + private StrategyCommandDecision(ChargeStatus chargeStatus, BigDecimal power) { + this.chargeStatus = chargeStatus; + this.power = power; + } + + public ChargeStatus getChargeStatus() { + return chargeStatus; + } + + public BigDecimal getPower() { + return power; + } + } + // 判断当前时间是否在时间范围内 private static boolean isTimeInRange(LocalTime now, Date startTime, Date endTime) { ZoneId zoneId = ZoneId.of("Asia/Shanghai"); @@ -573,6 +712,26 @@ public class StrategyPoller { if (config.getAntiReverseHardStopThreshold() == null) { config.setAntiReverseHardStopThreshold(DEFAULT_ANTI_REVERSE_HARD_STOP_THRESHOLD); } + if (config.getPowerSetMultiplier() == null || config.getPowerSetMultiplier().compareTo(BigDecimal.ZERO) <= 0) { + config.setPowerSetMultiplier(DEFAULT_POWER_SET_MULTIPLIER); + } + if (config.getProtectInterveneEnable() == null) { + config.setProtectInterveneEnable(DEFAULT_PROTECT_INTERVENE_ENABLE); + } + if (config.getProtectL1DeratePercent() == null + || config.getProtectL1DeratePercent().compareTo(BigDecimal.ZERO) < 0 + || config.getProtectL1DeratePercent().compareTo(new BigDecimal("100")) > 0) { + config.setProtectL1DeratePercent(DEFAULT_PROTECT_L1_DERATE_PERCENT); + } + if (config.getProtectRecoveryStableSeconds() == null || config.getProtectRecoveryStableSeconds() < 0) { + config.setProtectRecoveryStableSeconds(DEFAULT_PROTECT_RECOVERY_STABLE_SECONDS); + } + if (config.getProtectL3LatchEnable() == null) { + config.setProtectL3LatchEnable(DEFAULT_PROTECT_L3_LATCH_ENABLE); + } + if (StringUtils.isEmpty(config.getProtectConflictPolicy())) { + config.setProtectConflictPolicy(DEFAULT_PROTECT_CONFLICT_POLICY); + } return config; } diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorPointMatch.java b/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorPointMatch.java index 206ebf4..4b86b6b 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorPointMatch.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteMonitorPointMatch.java @@ -11,6 +11,7 @@ public class EmsSiteMonitorPointMatch extends BaseEntity { private Long id; private String siteId; private String fieldCode; + private String deviceId; private String dataPoint; private String fixedDataPoint; private Integer useFixedDisplay; @@ -47,6 +48,14 @@ public class EmsSiteMonitorPointMatch extends BaseEntity { this.dataPoint = dataPoint; } + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + public String getFixedDataPoint() { return fixedDataPoint; } diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/EmsStrategyRuntimeConfig.java b/ems-system/src/main/java/com/xzzn/ems/domain/EmsStrategyRuntimeConfig.java index a2058a1..1391e26 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/EmsStrategyRuntimeConfig.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/EmsStrategyRuntimeConfig.java @@ -49,6 +49,29 @@ public class EmsStrategyRuntimeConfig extends BaseEntity { /** 防逆流硬停阈值(kW) */ @Excel(name = "防逆流硬停阈值(kW)") private BigDecimal antiReverseHardStopThreshold; + /** 设定功率倍率 */ + @Excel(name = "设定功率倍率") + private BigDecimal powerSetMultiplier; + + /** 保护介入开关(1-启用,0-禁用) */ + @Excel(name = "保护介入开关") + private Integer protectInterveneEnable; + + /** 一级保护降额比例(%) */ + @Excel(name = "一级保护降额比例(%)") + private BigDecimal protectL1DeratePercent; + + /** 保护释放稳定时长(秒) */ + @Excel(name = "保护释放稳定时长(秒)") + private Integer protectRecoveryStableSeconds; + + /** 三级保护锁存开关(1-启用,0-禁用) */ + @Excel(name = "三级保护锁存开关") + private Integer protectL3LatchEnable; + + /** 保护冲突策略 */ + @Excel(name = "保护冲突策略") + private String protectConflictPolicy; public Long getId() { return id; @@ -122,6 +145,54 @@ public class EmsStrategyRuntimeConfig extends BaseEntity { this.antiReverseHardStopThreshold = antiReverseHardStopThreshold; } + public BigDecimal getPowerSetMultiplier() { + return powerSetMultiplier; + } + + public void setPowerSetMultiplier(BigDecimal powerSetMultiplier) { + this.powerSetMultiplier = powerSetMultiplier; + } + + public Integer getProtectInterveneEnable() { + return protectInterveneEnable; + } + + public void setProtectInterveneEnable(Integer protectInterveneEnable) { + this.protectInterveneEnable = protectInterveneEnable; + } + + public BigDecimal getProtectL1DeratePercent() { + return protectL1DeratePercent; + } + + public void setProtectL1DeratePercent(BigDecimal protectL1DeratePercent) { + this.protectL1DeratePercent = protectL1DeratePercent; + } + + public Integer getProtectRecoveryStableSeconds() { + return protectRecoveryStableSeconds; + } + + public void setProtectRecoveryStableSeconds(Integer protectRecoveryStableSeconds) { + this.protectRecoveryStableSeconds = protectRecoveryStableSeconds; + } + + public Integer getProtectL3LatchEnable() { + return protectL3LatchEnable; + } + + public void setProtectL3LatchEnable(Integer protectL3LatchEnable) { + this.protectL3LatchEnable = protectL3LatchEnable; + } + + public String getProtectConflictPolicy() { + return protectConflictPolicy; + } + + public void setProtectConflictPolicy(String protectConflictPolicy) { + this.protectConflictPolicy = protectConflictPolicy; + } + @Override public String toString() { return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) @@ -134,6 +205,12 @@ public class EmsStrategyRuntimeConfig extends BaseEntity { .append("antiReverseUp", getAntiReverseUp()) .append("antiReversePowerDownPercent", getAntiReversePowerDownPercent()) .append("antiReverseHardStopThreshold", getAntiReverseHardStopThreshold()) + .append("powerSetMultiplier", getPowerSetMultiplier()) + .append("protectInterveneEnable", getProtectInterveneEnable()) + .append("protectL1DeratePercent", getProtectL1DeratePercent()) + .append("protectRecoveryStableSeconds", getProtectRecoveryStableSeconds()) + .append("protectL3LatchEnable", getProtectL3LatchEnable()) + .append("protectConflictPolicy", getProtectConflictPolicy()) .append("createBy", getCreateBy()) .append("createTime", getCreateTime()) .append("updateBy", getUpdateBy()) diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/GeneralQueryPointOptionVo.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/GeneralQueryPointOptionVo.java index d48520f..87abe84 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/vo/GeneralQueryPointOptionVo.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/GeneralQueryPointOptionVo.java @@ -1,10 +1,19 @@ package com.xzzn.ems.domain.vo; public class GeneralQueryPointOptionVo { + private String pointId; private String pointName; private String dataKey; private String pointDesc; + public String getPointId() { + return pointId; + } + + public void setPointId(String pointId) { + this.pointId = pointId; + } + public String getPointName() { return pointName; } diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointNameRequest.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointNameRequest.java index 9098c7a..c1f13d5 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointNameRequest.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointNameRequest.java @@ -15,6 +15,8 @@ public class PointNameRequest { private String pointName; private List pointNames; + private String pointId; + private List pointIds; /** 数据分组 1-分钟 2-小时 3-天 */ private int dataUnit; @@ -59,6 +61,22 @@ public class PointNameRequest { this.pointNames = pointNames; } + public String getPointId() { + return pointId; + } + + public void setPointId(String pointId) { + this.pointId = pointId; + } + + public List getPointIds() { + return pointIds; + } + + public void setPointIds(List pointIds) { + this.pointIds = pointIds; + } + public int getDataUnit() { return dataUnit; } diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/RunningGraphRequest.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/RunningGraphRequest.java index a144147..e90f6b5 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/vo/RunningGraphRequest.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/RunningGraphRequest.java @@ -1,6 +1,7 @@ package com.xzzn.ems.domain.vo; import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; import java.util.Date; @@ -10,10 +11,12 @@ import java.util.Date; public class RunningGraphRequest { /** 开始时间 */ + @DateTimeFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd") private Date startDate; /** 结束时间 */ + @DateTimeFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd") private Date endDate; diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingVo.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingVo.java index b9ce240..8594ebc 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingVo.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/SiteMonitorProjectPointMappingVo.java @@ -16,6 +16,10 @@ public class SiteMonitorProjectPointMappingVo { private String fieldName; + private String deviceId; + + private String deviceName; + private String dataPoint; private String fixedDataPoint; @@ -86,6 +90,22 @@ public class SiteMonitorProjectPointMappingVo { this.dataPoint = dataPoint; } + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getDeviceName() { + return deviceName; + } + + public void setDeviceName(String deviceName) { + this.deviceName = deviceName; + } + public String getFixedDataPoint() { return fixedDataPoint; } diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/WorkStatusEnumMappingSaveRequest.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/WorkStatusEnumMappingSaveRequest.java new file mode 100644 index 0000000..da7513a --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/WorkStatusEnumMappingSaveRequest.java @@ -0,0 +1,26 @@ +package com.xzzn.ems.domain.vo; + +import java.util.List; + +public class WorkStatusEnumMappingSaveRequest { + + private String siteId; + + private List mappings; + + public String getSiteId() { + return siteId; + } + + public void setSiteId(String siteId) { + this.siteId = siteId; + } + + public List getMappings() { + return mappings; + } + + public void setMappings(List mappings) { + this.mappings = mappings; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/WorkStatusEnumMappingVo.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/WorkStatusEnumMappingVo.java new file mode 100644 index 0000000..4ebc081 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/WorkStatusEnumMappingVo.java @@ -0,0 +1,74 @@ +package com.xzzn.ems.domain.vo; + +public class WorkStatusEnumMappingVo { + + private String deviceCategory; + + private String matchField; + + private String matchFieldName; + + private String enumCode; + + private String enumName; + + private String enumDesc; + + private String dataEnumCode; + + public String getEnumCode() { + return enumCode; + } + + public void setEnumCode(String enumCode) { + this.enumCode = enumCode; + } + + public String getEnumName() { + return enumName; + } + + public void setEnumName(String enumName) { + this.enumName = enumName; + } + + public String getEnumDesc() { + return enumDesc; + } + + public void setEnumDesc(String enumDesc) { + this.enumDesc = enumDesc; + } + + public String getDataEnumCode() { + return dataEnumCode; + } + + public void setDataEnumCode(String dataEnumCode) { + this.dataEnumCode = dataEnumCode; + } + + public String getDeviceCategory() { + return deviceCategory; + } + + public void setDeviceCategory(String deviceCategory) { + this.deviceCategory = deviceCategory; + } + + public String getMatchField() { + return matchField; + } + + public void setMatchField(String matchField) { + this.matchField = matchField; + } + + public String getMatchFieldName() { + return matchFieldName; + } + + public void setMatchFieldName(String matchFieldName) { + this.matchFieldName = matchFieldName; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java index 0e800ab..4784df8 100644 --- a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java @@ -41,6 +41,7 @@ public interface EmsPointConfigMapper { List getConfigListForGeneralQuery(@Param("siteIds") List siteIds, @Param("deviceCategory") String deviceCategory, + @Param("pointIds") List pointIds, @Param("pointNames") List pointNames, @Param("deviceIds") List deviceIds); diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointEnumMatchMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointEnumMatchMapper.java index 829b058..6739130 100644 --- a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointEnumMatchMapper.java +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointEnumMatchMapper.java @@ -75,4 +75,8 @@ public interface EmsPointEnumMatchMapper int copyTemplateToSite(@Param("templateSiteId") String templateSiteId, @Param("targetSiteId") String targetSiteId, @Param("operName") String operName); + + int deleteByScope(@Param("siteId") String siteId, + @Param("deviceCategory") String deviceCategory, + @Param("matchField") String matchField); } diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorDataMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorDataMapper.java index 7f55eff..0759dfa 100644 --- a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorDataMapper.java +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorDataMapper.java @@ -14,15 +14,10 @@ public interface EmsSiteMonitorDataMapper { @Param("siteId") String siteId, @Param("statisMinute") java.util.Date statisMinute, @Param("dataJson") String dataJson, + @Param("hotSoc") String hotSoc, + @Param("hotTotalActivePower") String hotTotalActivePower, + @Param("hotTotalReactivePower") String hotTotalReactivePower, + @Param("hotDayChargedCap") String hotDayChargedCap, + @Param("hotDayDisChargedCap") String hotDayDisChargedCap, @Param("operName") String operName); - - int updateHistoryHotColumns(@Param("tableName") String tableName, - @Param("siteId") String siteId, - @Param("statisMinute") java.util.Date statisMinute, - @Param("hotSoc") String hotSoc, - @Param("hotTotalActivePower") String hotTotalActivePower, - @Param("hotTotalReactivePower") String hotTotalReactivePower, - @Param("hotDayChargedCap") String hotDayChargedCap, - @Param("hotDayDisChargedCap") String hotDayDisChargedCap, - @Param("operName") String operName); } diff --git a/ems-system/src/main/java/com/xzzn/ems/service/IEmsDeviceSettingService.java b/ems-system/src/main/java/com/xzzn/ems/service/IEmsDeviceSettingService.java index c6369bc..72d8f5d 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/IEmsDeviceSettingService.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/IEmsDeviceSettingService.java @@ -10,6 +10,7 @@ import com.xzzn.ems.domain.vo.SiteMonitorDataSaveRequest; import com.xzzn.ems.domain.vo.SiteMonitorProjectDisplayVo; import com.xzzn.ems.domain.vo.SiteMonitorProjectPointMappingSaveRequest; import com.xzzn.ems.domain.vo.SiteMonitorProjectPointMappingVo; +import com.xzzn.ems.domain.vo.WorkStatusEnumMappingVo; import java.util.Date; import java.util.List; @@ -46,6 +47,10 @@ public interface IEmsDeviceSettingService public int saveSiteMonitorProjectPointMapping(SiteMonitorProjectPointMappingSaveRequest request, String operName); + public List getSiteWorkStatusEnumMappings(String siteId); + + public int saveSiteWorkStatusEnumMappings(String siteId, List mappings, String operName); + public List getSiteMonitorProjectDisplay(String siteId); public int saveSiteMonitorProjectData(SiteMonitorDataSaveRequest request, String operName); diff --git a/ems-system/src/main/java/com/xzzn/ems/service/InfluxPointDataWriter.java b/ems-system/src/main/java/com/xzzn/ems/service/InfluxPointDataWriter.java index 0799678..747c37a 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/InfluxPointDataWriter.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/InfluxPointDataWriter.java @@ -181,6 +181,102 @@ public class InfluxPointDataWriter { } } + public List queryCurveDataByPointKey(String siteId, String pointKey, Date startTime, Date endTime) { + if (!enabled) { + return Collections.emptyList(); + } + if (isBlank(siteId) || isBlank(pointKey) || startTime == null || endTime == null) { + return Collections.emptyList(); + } + + String normalizedSiteId = siteId.trim(); + String normalizedPointKey = pointKey.trim(); + + String influxQl = String.format( + "SELECT \"value\" FROM \"%s\" WHERE \"site_id\" = '%s' AND \"point_key\" = '%s' " + + "AND time >= %dms AND time <= %dms ORDER BY time ASC", + measurement, + escapeTagValue(normalizedSiteId), + escapeTagValue(normalizedPointKey), + startTime.getTime(), + endTime.getTime() + ); + + try { + String queryUrl = buildQueryUrl(influxQl); + List values = parseInfluxQlResponse(executeRequestWithResponse(methodOrDefault(readMethod, "GET"), queryUrl)); + if (!values.isEmpty()) { + return values; + } + + // 兼容 pointId 大小写差异 + String regexQuery = String.format( + "SELECT \"value\" FROM \"%s\" WHERE \"site_id\" = '%s' AND \"point_key\" =~ /(?i)^%s$/ " + + "AND time >= %dms AND time <= %dms ORDER BY time ASC", + measurement, + escapeTagValue(normalizedSiteId), + escapeRegex(normalizedPointKey), + startTime.getTime(), + endTime.getTime() + ); + return parseInfluxQlResponse( + executeRequestWithResponse(methodOrDefault(readMethod, "GET"), buildQueryUrl(regexQuery)) + ); + } catch (Exception e) { + log.warn("按 pointKey 查询 InfluxDB 曲线失败: {}", e.getMessage()); + return Collections.emptyList(); + } + } + + + public PointValue queryLatestPointValueByPointKey(String siteId, String pointKey, Date startTime, Date endTime) { + if (!enabled) { + return null; + } + if (isBlank(siteId) || isBlank(pointKey) || startTime == null || endTime == null) { + return null; + } + + String normalizedSiteId = siteId.trim(); + String normalizedPointKey = pointKey.trim(); + + String influxQl = String.format( + "SELECT \"value\" FROM \"%s\" WHERE \"site_id\" = '%s' AND \"point_key\" = '%s' " + + "AND time >= %dms AND time <= %dms ORDER BY time DESC LIMIT 1", + measurement, + escapeTagValue(normalizedSiteId), + escapeTagValue(normalizedPointKey), + startTime.getTime(), + endTime.getTime() + ); + + try { + List values = parseInfluxQlResponse( + executeRequestWithResponse(methodOrDefault(readMethod, "GET"), buildQueryUrl(influxQl)) + ); + if (!values.isEmpty()) { + return values.get(0); + } + + String regexQuery = String.format( + "SELECT \"value\" FROM \"%s\" WHERE \"site_id\" = '%s' AND \"point_key\" =~ /(?i)^%s$/ " + + "AND time >= %dms AND time <= %dms ORDER BY time DESC LIMIT 1", + measurement, + escapeTagValue(normalizedSiteId), + escapeRegex(normalizedPointKey), + startTime.getTime(), + endTime.getTime() + ); + values = parseInfluxQlResponse( + executeRequestWithResponse(methodOrDefault(readMethod, "GET"), buildQueryUrl(regexQuery)) + ); + return values.isEmpty() ? null : values.get(0); + } catch (Exception e) { + log.warn("按 pointKey 查询 InfluxDB 最新值失败: {}", e.getMessage()); + return null; + } + } + private String buildWriteUrl() { if (isV2WritePath()) { return buildV2WriteUrl(); @@ -488,6 +584,7 @@ public class InfluxPointDataWriter { return value == null ? "" : value.replace("\\", "\\\\").replace("'", "\\'"); } + private String escapeRegex(String value) { if (value == null) { return ""; @@ -546,6 +643,7 @@ public class InfluxPointDataWriter { return values; } + private boolean isBlank(String value) { return value == null || value.trim().isEmpty(); } diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java index 296cf6e..06685e1 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java @@ -110,6 +110,7 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i private static final Pattern DTDC_PATTERN = Pattern.compile("DTDC(\\d+)([A-Za-z]*)"); private static final Pattern DB_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$"); private static final Pattern VARIABLE_SAFE_PATTERN = Pattern.compile("[^A-Za-z0-9_]"); + private static final Pattern SIMPLE_CALC_EXPRESSION_PATTERN = Pattern.compile("^[0-9A-Za-z_+\\-*/().\\s]+$"); private static final int POINT_QUEUE_CAPACITY = 100000; private static final int POINT_FLUSH_BATCH_SIZE = 2000; private static final int POINT_FLUSH_MAX_DRAIN_PER_RUN = 20000; @@ -176,7 +177,7 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i @Autowired private InfluxPointDataWriter influxPointDataWriter; private final BlockingQueue pointDataQueue = new LinkedBlockingQueue<>(POINT_QUEUE_CAPACITY); - private final Map> calcExpressionCache = new ConcurrentHashMap<>(); + private final Map calcExpressionCache = new ConcurrentHashMap<>(); private final ScheduledExecutorService pointDataWriter = Executors.newSingleThreadScheduledExecutor(r -> { Thread thread = new Thread(r); thread.setName("point-data-writer"); @@ -287,7 +288,7 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i if (!uniquePointKeys.add(pointContextKey)) { continue; } - if (isCalcPoint(pointConfig)) { + if (isComputedPoint(pointConfig)) { continue; } dataPointConfigs.add(pointConfig); @@ -383,7 +384,7 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i List pointConfigs = getPointConfigsWithCache(siteId, deviceId, deviceCategory); if (!CollectionUtils.isEmpty(pointConfigs)) { for (EmsPointConfig pointConfig : pointConfigs) { - if (isCalcPoint(pointConfig)) { + if (isComputedPoint(pointConfig)) { continue; } String dataKey = StringUtils.defaultString(pointConfig.getDataKey()).trim().toUpperCase(); @@ -438,8 +439,12 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return StringUtils.isBlank(normalized) ? null : normalized; } - private boolean isCalcPoint(EmsPointConfig pointConfig) { - return pointConfig != null && "calc".equalsIgnoreCase(pointConfig.getPointType()); + private boolean isComputedPoint(EmsPointConfig pointConfig) { + if (pointConfig == null) { + return false; + } + String pointType = StringUtils.defaultString(pointConfig.getPointType()).trim().toLowerCase(); + return "calc".equals(pointType); } private void enqueuePointData(String siteId, String deviceId, String pointKey, BigDecimal pointValue, Date dataTime) { @@ -521,14 +526,17 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i String calcDataKey = entry.getKey(); EmsPointConfig calcPointConfig = entry.getValue(); try { - BigDecimal calcValue = evaluateCalcExpression(calcPointConfig.getCalcExpression(), contextValues); - contextValues.put(calcDataKey, calcValue); - putPointValueToContext(calcPointConfig, calcValue, contextValues); + ExpressionValue calcValue = evaluateCalcExpression(calcPointConfig.getCalcExpression(), contextValues); + if (calcValue.isNumber()) { + contextValues.put(calcDataKey, calcValue.asNumber()); + putPointValueToContext(calcPointConfig, calcValue.asNumber(), contextValues); + } // 计算点按站点维度统一落库,不再按配置中的 device_id 分流 String pointId = resolveInfluxPointKey(calcPointConfig); if (StringUtils.isNotBlank(pointId)) { - enqueuePointData(siteId, deviceId, pointId, calcValue, dataUpdateTime); - calcPointIdValueMap.put(pointId, calcValue); + BigDecimal storedValue = calcValue.asNumber(); + enqueuePointData(siteId, deviceId, pointId, storedValue, dataUpdateTime); + calcPointIdValueMap.put(pointId, storedValue); } finishedKeys.add(calcDataKey); progressed = true; @@ -584,213 +592,19 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return StringUtils.isNotBlank(pointId) ? pointId : null; } - private BigDecimal evaluateCalcExpression(String expression, Map contextValues) { + private ExpressionValue evaluateCalcExpression(String expression, Map contextValues) { if (StringUtils.isBlank(expression)) { throw new IllegalArgumentException("计算表达式为空"); } - List rpnTokens = calcExpressionCache.computeIfAbsent(expression, this::compileExpressionToRpn); - return evaluateRpnTokens(rpnTokens, contextValues == null ? Collections.emptyMap() : contextValues); + if (!SIMPLE_CALC_EXPRESSION_PATTERN.matcher(expression).matches()) { + throw new IllegalArgumentException("计算表达式仅支持四则运算"); + } + CompiledExpression compiledExpression = calcExpressionCache.computeIfAbsent(expression, this::compileExpression); + return compiledExpression.evaluate(contextValues == null ? Collections.emptyMap() : contextValues); } - private List compileExpressionToRpn(String expression) { - if (StringUtils.isBlank(expression)) { - throw new IllegalArgumentException("计算表达式为空"); - } - List output = new ArrayList<>(); - Deque operators = new ArrayDeque<>(); - int index = 0; - ExpressionToken previous = null; - - while (index < expression.length()) { - char ch = expression.charAt(index); - if (Character.isWhitespace(ch)) { - index++; - continue; - } - if (Character.isDigit(ch) || ch == '.') { - int start = index; - boolean hasDot = ch == '.'; - index++; - while (index < expression.length()) { - char next = expression.charAt(index); - if (Character.isDigit(next)) { - index++; - continue; - } - if (next == '.' && !hasDot) { - hasDot = true; - index++; - continue; - } - break; - } - String numberText = expression.substring(start, index); - try { - output.add(ExpressionToken.number(new BigDecimal(numberText))); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("数值格式错误: " + numberText); - } - previous = output.get(output.size() - 1); - continue; - } - if (Character.isLetter(ch) || ch == '_') { - int start = index; - index++; - while (index < expression.length()) { - char next = expression.charAt(index); - if (Character.isLetterOrDigit(next) || next == '_') { - index++; - continue; - } - break; - } - String variable = expression.substring(start, index).trim().toUpperCase(); - output.add(ExpressionToken.variable(variable)); - previous = output.get(output.size() - 1); - continue; - } - if (ch == '(') { - operators.push(ExpressionToken.leftParen()); - previous = ExpressionToken.leftParen(); - index++; - continue; - } - if (ch == ')') { - boolean matched = false; - while (!operators.isEmpty()) { - ExpressionToken token = operators.pop(); - if (token.getType() == ExpressionTokenType.LEFT_PAREN) { - matched = true; - break; - } - output.add(token); - } - if (!matched) { - throw new IllegalArgumentException("括号不匹配"); - } - previous = ExpressionToken.rightParen(); - index++; - continue; - } - if (isOperator(ch)) { - boolean unaryMinus = ch == '-' && (previous == null - || previous.getType() == ExpressionTokenType.OPERATOR - || previous.getType() == ExpressionTokenType.LEFT_PAREN); - String operatorText = unaryMinus ? "~" : String.valueOf(ch); - ExpressionToken currentOperator = ExpressionToken.operator(operatorText); - while (!operators.isEmpty() && operators.peek().getType() == ExpressionTokenType.OPERATOR) { - ExpressionToken top = operators.peek(); - if (shouldPopOperator(currentOperator, top)) { - output.add(operators.pop()); - } else { - break; - } - } - operators.push(currentOperator); - previous = currentOperator; - index++; - continue; - } - throw new IllegalArgumentException("表达式包含非法字符: " + ch); - } - - while (!operators.isEmpty()) { - ExpressionToken token = operators.pop(); - if (token.getType() == ExpressionTokenType.LEFT_PAREN) { - throw new IllegalArgumentException("括号不匹配"); - } - output.add(token); - } - return output; - } - - private BigDecimal evaluateRpnTokens(List rpnTokens, Map contextValues) { - Deque values = new ArrayDeque<>(); - for (ExpressionToken token : rpnTokens) { - if (token.getType() == ExpressionTokenType.NUMBER) { - values.push(token.getNumber()); - continue; - } - if (token.getType() == ExpressionTokenType.VARIABLE) { - BigDecimal variableValue = contextValues.get(token.getText()); - if (variableValue == null) { - throw new MissingVariableException(token.getText()); - } - values.push(variableValue); - continue; - } - if (token.getType() != ExpressionTokenType.OPERATOR) { - throw new IllegalArgumentException("表达式令牌类型不合法: " + token.getType()); - } - if ("~".equals(token.getText())) { - if (values.isEmpty()) { - throw new IllegalArgumentException("表达式不合法,缺少操作数"); - } - values.push(values.pop().negate()); - continue; - } - if (values.size() < 2) { - throw new IllegalArgumentException("表达式不合法,缺少操作数"); - } - BigDecimal right = values.pop(); - BigDecimal left = values.pop(); - values.push(applyOperator(left, right, token.getText())); - } - if (values.size() != 1) { - throw new IllegalArgumentException("表达式不合法,无法归约到单值"); - } - return values.pop(); - } - - private BigDecimal applyOperator(BigDecimal left, BigDecimal right, String operator) { - switch (operator) { - case "+": - return left.add(right); - case "-": - return left.subtract(right); - case "*": - return left.multiply(right); - case "/": - if (BigDecimal.ZERO.compareTo(right) == 0) { - throw new IllegalArgumentException("除数不能为0"); - } - return left.divide(right, 10, RoundingMode.HALF_UP); - default: - throw new IllegalArgumentException("不支持的操作符: " + operator); - } - } - - private boolean shouldPopOperator(ExpressionToken currentOperator, ExpressionToken stackOperator) { - if (currentOperator == null || stackOperator == null) { - return false; - } - int currentPriority = getOperatorPriority(currentOperator.getText()); - int stackPriority = getOperatorPriority(stackOperator.getText()); - if (isRightAssociative(currentOperator.getText())) { - return stackPriority > currentPriority; - } - return stackPriority >= currentPriority; - } - - private int getOperatorPriority(String operator) { - if ("~".equals(operator)) { - return 3; - } - if ("*".equals(operator) || "/".equals(operator)) { - return 2; - } - if ("+".equals(operator) || "-".equals(operator)) { - return 1; - } - return 0; - } - - private boolean isRightAssociative(String operator) { - return "~".equals(operator); - } - - private boolean isOperator(char ch) { - return ch == '+' || ch == '-' || ch == '*' || ch == '/'; + private CompiledExpression compileExpression(String expression) { + return new CompiledExpression(expression); } private void flushPointDataSafely() { @@ -864,7 +678,7 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i } Map uniqueByPointId = new LinkedHashMap<>(); for (EmsPointConfig calcPoint : calcPoints) { - if (!isCalcPoint(calcPoint)) { + if (!isComputedPoint(calcPoint)) { continue; } String calcKey = resolvePointContextKey(calcPoint); @@ -925,56 +739,622 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i } } - private enum ExpressionTokenType { + private enum ExprTokenType { NUMBER, - VARIABLE, + STRING, + IDENTIFIER, OPERATOR, LEFT_PAREN, - RIGHT_PAREN + RIGHT_PAREN, + COMMA, + QUESTION, + COLON, + EOF } - private static class ExpressionToken { - private final ExpressionTokenType type; + private static class ExprToken { + private final ExprTokenType type; private final String text; private final BigDecimal number; + private final String stringValue; - private ExpressionToken(ExpressionTokenType type, String text, BigDecimal number) { + private ExprToken(ExprTokenType type, String text, BigDecimal number, String stringValue) { this.type = type; this.text = text; this.number = number; + this.stringValue = stringValue; } - private static ExpressionToken number(BigDecimal value) { - return new ExpressionToken(ExpressionTokenType.NUMBER, null, value); + private static ExprToken number(BigDecimal value) { + return new ExprToken(ExprTokenType.NUMBER, null, value, null); } - private static ExpressionToken variable(String variable) { - return new ExpressionToken(ExpressionTokenType.VARIABLE, variable, null); + private static ExprToken string(String value) { + return new ExprToken(ExprTokenType.STRING, null, null, value); } - private static ExpressionToken operator(String operator) { - return new ExpressionToken(ExpressionTokenType.OPERATOR, operator, null); + private static ExprToken identifier(String value) { + return new ExprToken(ExprTokenType.IDENTIFIER, value, null, null); } - private static ExpressionToken leftParen() { - return new ExpressionToken(ExpressionTokenType.LEFT_PAREN, "(", null); + private static ExprToken operator(String value) { + return new ExprToken(ExprTokenType.OPERATOR, value, null, null); } - private static ExpressionToken rightParen() { - return new ExpressionToken(ExpressionTokenType.RIGHT_PAREN, ")", null); + private static ExprToken symbol(ExprTokenType type, String text) { + return new ExprToken(type, text, null, null); + } + } + + private static class ExpressionValue { + private final BigDecimal numberValue; + private final String textValue; + + private ExpressionValue(BigDecimal numberValue, String textValue) { + this.numberValue = numberValue; + this.textValue = textValue; } - private ExpressionTokenType getType() { - return type; + private static ExpressionValue ofNumber(BigDecimal numberValue) { + return new ExpressionValue(numberValue, null); } - private String getText() { - return text; + private static ExpressionValue ofText(String textValue) { + return new ExpressionValue(null, textValue == null ? "" : textValue); } - private BigDecimal getNumber() { - return number; + private boolean isNumber() { + return numberValue != null; } + + private BigDecimal asNumber() { + if (numberValue == null) { + throw new IllegalArgumentException("表达式值不是数值类型: " + textValue); + } + return numberValue; + } + + private String asText() { + return numberValue != null ? numberValue.stripTrailingZeros().toPlainString() : textValue; + } + + private boolean asBoolean() { + return isNumber() ? BigDecimal.ZERO.compareTo(numberValue) != 0 : StringUtils.isNotBlank(textValue); + } + + } + + private interface ExpressionNode { + ExpressionValue evaluate(Map contextValues); + } + + private static class NumberNode implements ExpressionNode { + private final BigDecimal value; + + private NumberNode(BigDecimal value) { + this.value = value; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + return ExpressionValue.ofNumber(value); + } + } + + private static class StringNode implements ExpressionNode { + private final String value; + + private StringNode(String value) { + this.value = value; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + return ExpressionValue.ofText(value); + } + } + + private static class VariableNode implements ExpressionNode { + private final String name; + + private VariableNode(String name) { + this.name = name; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + BigDecimal value = contextValues.get(name); + if (value == null) { + throw new MissingVariableException(name); + } + return ExpressionValue.ofNumber(value); + } + } + + private static class UnaryNode implements ExpressionNode { + private final String operator; + private final ExpressionNode node; + + private UnaryNode(String operator, ExpressionNode node) { + this.operator = operator; + this.node = node; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + ExpressionValue value = node.evaluate(contextValues); + switch (operator) { + case "+": + return value; + case "-": + return ExpressionValue.ofNumber(value.asNumber().negate()); + case "!": + return value.asBoolean() ? ExpressionValue.ofNumber(BigDecimal.ZERO) : ExpressionValue.ofNumber(BigDecimal.ONE); + default: + throw new IllegalArgumentException("不支持的一元操作符: " + operator); + } + } + } + + private static class BinaryNode implements ExpressionNode { + private final String operator; + private final ExpressionNode left; + private final ExpressionNode right; + + private BinaryNode(String operator, ExpressionNode left, ExpressionNode right) { + this.operator = operator; + this.left = left; + this.right = right; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + if ("&&".equals(operator)) { + ExpressionValue leftValue = left.evaluate(contextValues); + if (!leftValue.asBoolean()) { + return ExpressionValue.ofNumber(BigDecimal.ZERO); + } + return right.evaluate(contextValues).asBoolean() + ? ExpressionValue.ofNumber(BigDecimal.ONE) + : ExpressionValue.ofNumber(BigDecimal.ZERO); + } + if ("||".equals(operator)) { + ExpressionValue leftValue = left.evaluate(contextValues); + if (leftValue.asBoolean()) { + return ExpressionValue.ofNumber(BigDecimal.ONE); + } + return right.evaluate(contextValues).asBoolean() + ? ExpressionValue.ofNumber(BigDecimal.ONE) + : ExpressionValue.ofNumber(BigDecimal.ZERO); + } + + ExpressionValue leftValue = left.evaluate(contextValues); + ExpressionValue rightValue = right.evaluate(contextValues); + switch (operator) { + case "+": + if (!leftValue.isNumber() || !rightValue.isNumber()) { + return ExpressionValue.ofText(leftValue.asText() + rightValue.asText()); + } + return ExpressionValue.ofNumber(leftValue.asNumber().add(rightValue.asNumber())); + case "-": + return ExpressionValue.ofNumber(leftValue.asNumber().subtract(rightValue.asNumber())); + case "*": + return ExpressionValue.ofNumber(leftValue.asNumber().multiply(rightValue.asNumber())); + case "/": + if (BigDecimal.ZERO.compareTo(rightValue.asNumber()) == 0) { + throw new IllegalArgumentException("除数不能为0"); + } + return ExpressionValue.ofNumber(leftValue.asNumber().divide(rightValue.asNumber(), 10, RoundingMode.HALF_UP)); + case ">": + return leftValue.asNumber().compareTo(rightValue.asNumber()) > 0 + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + case ">=": + return leftValue.asNumber().compareTo(rightValue.asNumber()) >= 0 + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + case "<": + return leftValue.asNumber().compareTo(rightValue.asNumber()) < 0 + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + case "<=": + return leftValue.asNumber().compareTo(rightValue.asNumber()) <= 0 + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + case "==": + if (leftValue.isNumber() && rightValue.isNumber()) { + return leftValue.asNumber().compareTo(rightValue.asNumber()) == 0 + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + } + return Objects.equals(leftValue.asText(), rightValue.asText()) + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + case "!=": + if (leftValue.isNumber() && rightValue.isNumber()) { + return leftValue.asNumber().compareTo(rightValue.asNumber()) != 0 + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + } + return !Objects.equals(leftValue.asText(), rightValue.asText()) + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + default: + throw new IllegalArgumentException("不支持的操作符: " + operator); + } + } + } + + private static class TernaryNode implements ExpressionNode { + private final ExpressionNode condition; + private final ExpressionNode trueNode; + private final ExpressionNode falseNode; + + private TernaryNode(ExpressionNode condition, ExpressionNode trueNode, ExpressionNode falseNode) { + this.condition = condition; + this.trueNode = trueNode; + this.falseNode = falseNode; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + return condition.evaluate(contextValues).asBoolean() + ? trueNode.evaluate(contextValues) + : falseNode.evaluate(contextValues); + } + } + + private static class CompiledExpression { + private final ExpressionNode root; + + private CompiledExpression(String expression) { + List tokens = tokenizeExpression(expression); + ExpressionParser parser = new ExpressionParser(tokens); + this.root = parser.parseExpression(); + if (!parser.isEnd()) { + ExprToken token = parser.peek(); + throw new IllegalArgumentException("表达式尾部有多余内容: " + token.text); + } + } + + private ExpressionValue evaluate(Map contextValues) { + return root.evaluate(contextValues); + } + } + + private static class ExpressionParser { + private final List tokens; + private int index; + + private ExpressionParser(List tokens) { + this.tokens = tokens; + this.index = 0; + } + + private ExpressionNode parseExpression() { + return parseTernary(); + } + + private ExpressionNode parseTernary() { + ExpressionNode condition = parseOr(); + if (match(ExprTokenType.QUESTION)) { + ExpressionNode trueNode = parseTernary(); + expect(ExprTokenType.COLON, "三元表达式缺少 ':'"); + ExpressionNode falseNode = parseTernary(); + return new TernaryNode(condition, trueNode, falseNode); + } + return condition; + } + + private ExpressionNode parseOr() { + ExpressionNode left = parseAnd(); + while (matchOperator("||")) { + left = new BinaryNode("||", left, parseAnd()); + } + return left; + } + + private ExpressionNode parseAnd() { + ExpressionNode left = parseEquality(); + while (matchOperator("&&")) { + left = new BinaryNode("&&", left, parseEquality()); + } + return left; + } + + private ExpressionNode parseEquality() { + ExpressionNode left = parseComparison(); + while (true) { + if (matchOperator("==")) { + left = new BinaryNode("==", left, parseComparison()); + continue; + } + if (matchOperator("!=")) { + left = new BinaryNode("!=", left, parseComparison()); + continue; + } + break; + } + return left; + } + + private ExpressionNode parseComparison() { + ExpressionNode left = parseAddSub(); + while (true) { + if (matchOperator(">=")) { + left = new BinaryNode(">=", left, parseAddSub()); + continue; + } + if (matchOperator("<=")) { + left = new BinaryNode("<=", left, parseAddSub()); + continue; + } + if (matchOperator(">")) { + left = new BinaryNode(">", left, parseAddSub()); + continue; + } + if (matchOperator("<")) { + left = new BinaryNode("<", left, parseAddSub()); + continue; + } + break; + } + return left; + } + + private ExpressionNode parseAddSub() { + ExpressionNode left = parseMulDiv(); + while (true) { + if (matchOperator("+")) { + left = new BinaryNode("+", left, parseMulDiv()); + continue; + } + if (matchOperator("-")) { + left = new BinaryNode("-", left, parseMulDiv()); + continue; + } + break; + } + return left; + } + + private ExpressionNode parseMulDiv() { + ExpressionNode left = parseUnary(); + while (true) { + if (matchOperator("*")) { + left = new BinaryNode("*", left, parseUnary()); + continue; + } + if (matchOperator("/")) { + left = new BinaryNode("/", left, parseUnary()); + continue; + } + break; + } + return left; + } + + private ExpressionNode parseUnary() { + if (matchOperator("+")) { + return new UnaryNode("+", parseUnary()); + } + if (matchOperator("-")) { + return new UnaryNode("-", parseUnary()); + } + if (matchOperator("!")) { + return new UnaryNode("!", parseUnary()); + } + return parsePrimary(); + } + + private ExpressionNode parsePrimary() { + ExprToken token = peek(); + if (match(ExprTokenType.NUMBER)) { + return new NumberNode(token.number); + } + if (match(ExprTokenType.STRING)) { + return new StringNode(token.stringValue); + } + if (match(ExprTokenType.IDENTIFIER)) { + String identifier = token.text; + if (match(ExprTokenType.LEFT_PAREN)) { + if (!"IF".equalsIgnoreCase(identifier)) { + throw new IllegalArgumentException("不支持的函数: " + identifier); + } + ExpressionNode condition = parseExpression(); + expect(ExprTokenType.COMMA, "IF函数参数格式错误,缺少第1个逗号"); + ExpressionNode trueNode = parseExpression(); + expect(ExprTokenType.COMMA, "IF函数参数格式错误,缺少第2个逗号"); + ExpressionNode falseNode = parseExpression(); + expect(ExprTokenType.RIGHT_PAREN, "IF函数缺少右括号"); + return new TernaryNode(condition, trueNode, falseNode); + } + return new VariableNode(identifier.toUpperCase()); + } + if (match(ExprTokenType.LEFT_PAREN)) { + ExpressionNode nested = parseExpression(); + expect(ExprTokenType.RIGHT_PAREN, "括号不匹配,缺少右括号"); + return nested; + } + throw new IllegalArgumentException("表达式语法错误,当前位置: " + token.text); + } + + private ExprToken peek() { + if (index >= tokens.size()) { + return ExprToken.symbol(ExprTokenType.EOF, ""); + } + return tokens.get(index); + } + + private boolean isEnd() { + return peek().type == ExprTokenType.EOF; + } + + private boolean match(ExprTokenType type) { + if (peek().type != type) { + return false; + } + index++; + return true; + } + + private boolean matchOperator(String operator) { + ExprToken token = peek(); + if (token.type != ExprTokenType.OPERATOR || !operator.equals(token.text)) { + return false; + } + index++; + return true; + } + + private void expect(ExprTokenType type, String message) { + if (!match(type)) { + throw new IllegalArgumentException(message); + } + } + } + + private static List tokenizeExpression(String expression) { + if (StringUtils.isBlank(expression)) { + throw new IllegalArgumentException("计算表达式为空"); + } + List tokens = new ArrayList<>(); + int index = 0; + while (index < expression.length()) { + char ch = expression.charAt(index); + if (Character.isWhitespace(ch)) { + index++; + continue; + } + if (Character.isDigit(ch) || ch == '.') { + int start = index; + boolean hasDot = ch == '.'; + index++; + while (index < expression.length()) { + char next = expression.charAt(index); + if (Character.isDigit(next)) { + index++; + continue; + } + if (next == '.' && !hasDot) { + hasDot = true; + index++; + continue; + } + break; + } + String text = expression.substring(start, index); + try { + tokens.add(ExprToken.number(new BigDecimal(text))); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("数值格式错误: " + text); + } + continue; + } + if (Character.isLetter(ch) || ch == '_') { + int start = index; + index++; + while (index < expression.length()) { + char next = expression.charAt(index); + if (Character.isLetterOrDigit(next) || next == '_') { + index++; + continue; + } + break; + } + String text = expression.substring(start, index).trim().toUpperCase(); + tokens.add(ExprToken.identifier(text)); + continue; + } + if (ch == '\'' || ch == '"') { + char quote = ch; + index++; + StringBuilder sb = new StringBuilder(); + boolean escaped = false; + while (index < expression.length()) { + char current = expression.charAt(index++); + if (escaped) { + switch (current) { + case 'n': + sb.append('\n'); + break; + case 't': + sb.append('\t'); + break; + case 'r': + sb.append('\r'); + break; + case '\\': + sb.append('\\'); + break; + case '\'': + sb.append('\''); + break; + case '"': + sb.append('"'); + break; + default: + sb.append(current); + break; + } + escaped = false; + continue; + } + if (current == '\\') { + escaped = true; + continue; + } + if (current == quote) { + break; + } + sb.append(current); + } + if (escaped || index > expression.length() || expression.charAt(index - 1) != quote) { + throw new IllegalArgumentException("字符串字面量未闭合"); + } + tokens.add(ExprToken.string(sb.toString())); + continue; + } + if (ch == '(') { + tokens.add(ExprToken.symbol(ExprTokenType.LEFT_PAREN, "(")); + index++; + continue; + } + if (ch == ')') { + tokens.add(ExprToken.symbol(ExprTokenType.RIGHT_PAREN, ")")); + index++; + continue; + } + if (ch == ',') { + tokens.add(ExprToken.symbol(ExprTokenType.COMMA, ",")); + index++; + continue; + } + if (ch == '?') { + tokens.add(ExprToken.symbol(ExprTokenType.QUESTION, "?")); + index++; + continue; + } + if (ch == ':') { + tokens.add(ExprToken.symbol(ExprTokenType.COLON, ":")); + index++; + continue; + } + if (index + 1 < expression.length()) { + String twoChars = expression.substring(index, index + 2); + if ("&&".equals(twoChars) + || "||".equals(twoChars) + || ">=".equals(twoChars) + || "<=".equals(twoChars) + || "==".equals(twoChars) + || "!=".equals(twoChars)) { + tokens.add(ExprToken.operator(twoChars)); + index += 2; + continue; + } + } + if (ch == '+' || ch == '-' || ch == '*' || ch == '/' + || ch == '>' || ch == '<' || ch == '!') { + tokens.add(ExprToken.operator(String.valueOf(ch))); + index++; + continue; + } + throw new IllegalArgumentException("表达式包含非法字符: " + ch); + } + tokens.add(ExprToken.symbol(ExprTokenType.EOF, "")); + return tokens; } private JSONArray parseJsonData(String message) { diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java index 71b0323..3b9d351 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java @@ -17,6 +17,7 @@ import com.xzzn.common.exception.ServiceException; import com.xzzn.common.utils.DateUtils; import com.xzzn.common.utils.StringUtils; import com.xzzn.ems.domain.EmsDevicesSetting; +import com.xzzn.ems.domain.EmsPointEnumMatch; import com.xzzn.ems.domain.EmsPointConfig; import com.xzzn.ems.domain.EmsPointMatch; import com.xzzn.ems.domain.EmsPcsSetting; @@ -31,15 +32,19 @@ import com.xzzn.ems.domain.vo.SiteMonitorDataSaveRequest; import com.xzzn.ems.domain.vo.SiteMonitorProjectDisplayVo; import com.xzzn.ems.domain.vo.SiteMonitorProjectPointMappingSaveRequest; import com.xzzn.ems.domain.vo.SiteMonitorProjectPointMappingVo; +import com.xzzn.ems.domain.vo.WorkStatusEnumMappingVo; import com.xzzn.ems.mapper.EmsBatteryDataMinutesMapper; import com.xzzn.ems.mapper.EmsDevicesSettingMapper; import com.xzzn.ems.mapper.EmsPcsSettingMapper; import com.xzzn.ems.mapper.EmsPointConfigMapper; +import com.xzzn.ems.mapper.EmsPointEnumMatchMapper; import com.xzzn.ems.mapper.EmsPointMatchMapper; import com.xzzn.ems.mapper.EmsSiteMonitorDataMapper; import com.xzzn.ems.mapper.EmsSiteMonitorItemMapper; import com.xzzn.ems.mapper.EmsSiteMonitorPointMatchMapper; import com.xzzn.ems.service.IEmsDeviceSettingService; +import com.xzzn.ems.service.InfluxPointDataWriter; +import com.xzzn.ems.utils.DevicePointMatchDataProcessor; import java.math.BigDecimal; import java.util.ArrayList; @@ -48,10 +53,13 @@ import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.Collections; @@ -61,6 +69,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; /** * 站点信息 服务层实现 @@ -75,12 +84,39 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService private static final String MODULE_HOME = "HOME"; private static final String MODULE_SBJK = "SBJK"; private static final String MODULE_TJBB = "TJBB"; + private static final String MENU_SBJK_EMS = "SBJK_EMS"; private static final Integer USE_FIXED_DISPLAY_YES = 1; + private static final String FIELD_CURVE_PCS_ACTIVE_POWER = "SBJK_SSYX__curvePcsActivePower"; + private static final String FIELD_CURVE_PCS_REACTIVE_POWER = "SBJK_SSYX__curvePcsReactivePower"; private static final String HISTORY_TABLE_HOME = "ems_site_monitor_data_home_his"; private static final String HISTORY_TABLE_SBJK = "ems_site_monitor_data_sbjk_his"; private static final String HISTORY_TABLE_TJBB = "ems_site_monitor_data_tjbb_his"; private static final String DELETED_FIELD_MARK = "__DELETED__"; + private static final String PCS_MATCH_DEVICE_CATEGORY = "PCS"; + private static final String STACK_MATCH_DEVICE_CATEGORY = "STACK"; + private static final String CLUSTER_MATCH_DEVICE_CATEGORY = "CLUSTER"; + private static final String MATCH_FIELD_WORK_STATUS = "workStatus"; + private static final String MATCH_FIELD_GRID_STATUS = "gridStatus"; + private static final String MATCH_FIELD_DEVICE_STATUS = "deviceStatus"; + private static final String MATCH_FIELD_CONTROL_MODE = "controlMode"; + private static final String MATCH_FIELD_PCS_COMMUNICATION_STATUS = "pcsCommunicationStatus"; + private static final String MATCH_FIELD_EMS_COMMUNICATION_STATUS = "emsCommunicationStatus"; + private static final String SITE_LEVEL_CALC_DEVICE_ID = "SITE_CALC"; + private static final Set DEVICE_DIMENSION_MENU_CODES = new HashSet<>(Arrays.asList( + "SBJK_EMS", "SBJK_PCS", "SBJK_BMSZL", "SBJK_BMSDCC", "SBJK_DTDC", "SBJK_DB", "SBJK_YL", "SBJK_DH", "SBJK_XF" + )); + private static final Set DEVICE_DIMENSION_FIELD_CODES = new HashSet<>(Arrays.asList( + FIELD_CURVE_PCS_ACTIVE_POWER, + FIELD_CURVE_PCS_REACTIVE_POWER, + "SBJK_SSYX__curvePcsMaxTemp", + "SBJK_SSYX__curveBatteryAveSoc", + "SBJK_SSYX__curveBatteryAveTemp" + )); + private static final Map MENU_DEVICE_CATEGORY_MAP = buildMenuDeviceCategoryMap(); private static final long MONITOR_ITEM_CACHE_TTL_MS = 60_000L; + private static final long PROJECT_DISPLAY_CACHE_TTL_MS = 15_000L; + private static final int MONITOR_POINT_MATCH_REDIS_TTL_SECONDS = 300; + private static final int DISPLAY_DEBUG_SAMPLE_SIZE = 20; @Autowired private EmsDevicesSettingMapper emsDevicesMapper; @Autowired @@ -90,6 +126,8 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService @Autowired private EmsPointConfigMapper emsPointConfigMapper; @Autowired + private EmsPointEnumMatchMapper emsPointEnumMatchMapper; + @Autowired private RedisCache redisCache; @Autowired private EmsBatteryDataMinutesMapper emsBatteryDataMinutesMapper; @@ -103,9 +141,44 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService private EmsSiteMonitorPointMatchMapper emsSiteMonitorPointMatchMapper; @Autowired private EmsSiteMonitorDataMapper emsSiteMonitorDataMapper; + @Autowired + private InfluxPointDataWriter influxPointDataWriter; private volatile List monitorItemCache = Collections.emptyList(); private volatile long monitorItemCacheExpireAt = 0L; + private final ConcurrentMap projectDisplayCache = new ConcurrentHashMap<>(); + private static final Map ENUM_SCOPE_NAME_MAP = buildEnumScopeNameMap(); + private static final Set MANAGED_ENUM_SCOPE_KEYS = ENUM_SCOPE_NAME_MAP.keySet(); + + private static Map buildEnumScopeNameMap() { + Map result = new LinkedHashMap<>(); + result.put(buildEnumScopeKey(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS), "PCS-工作状态"); + result.put(buildEnumScopeKey(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_GRID_STATUS), "PCS-并网状态"); + result.put(buildEnumScopeKey(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_DEVICE_STATUS), "PCS-设备状态"); + result.put(buildEnumScopeKey(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_CONTROL_MODE), "PCS-控制模式"); + result.put(buildEnumScopeKey(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS), "BMS总览-工作状态"); + result.put(buildEnumScopeKey(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_PCS_COMMUNICATION_STATUS), "BMS总览-与PCS通信"); + result.put(buildEnumScopeKey(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_EMS_COMMUNICATION_STATUS), "BMS总览-与EMS通信"); + result.put(buildEnumScopeKey(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS), "BMS电池簇-工作状态"); + result.put(buildEnumScopeKey(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_PCS_COMMUNICATION_STATUS), "BMS电池簇-与PCS通信"); + result.put(buildEnumScopeKey(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_EMS_COMMUNICATION_STATUS), "BMS电池簇-与EMS通信"); + return result; + } + + private static Map buildMenuDeviceCategoryMap() { + Map map = new HashMap<>(); + map.put("SBJK_SSYX", DeviceCategory.PCS.getCode()); + map.put("SBJK_EMS", DeviceCategory.EMS.getCode()); + map.put("SBJK_PCS", DeviceCategory.PCS.getCode()); + map.put("SBJK_BMSZL", DeviceCategory.STACK.getCode()); + map.put("SBJK_BMSDCC", DeviceCategory.CLUSTER.getCode()); + map.put("SBJK_DTDC", DeviceCategory.BATTERY.getCode()); + map.put("SBJK_DB", DeviceCategory.AMMETER.getCode()); + map.put("SBJK_YL", DeviceCategory.COOLING.getCode()); + map.put("SBJK_DH", DeviceCategory.DH.getCode()); + map.put("SBJK_XF", DeviceCategory.XF.getCode()); + return map; + } /** * 获取设备详细信息 @@ -422,38 +495,40 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService if (itemList == null || itemList.isEmpty()) { return result; } - List mappingList = emsSiteMonitorPointMatchMapper.selectBySiteId(siteId); + List mappingList = getPointMatchesBySiteId(siteId); Set deletedFieldCodeSet = mappingList.stream() .filter(item -> StringUtils.isNotBlank(item.getFieldCode())) .filter(item -> DELETED_FIELD_MARK.equals(item.getDataPoint())) .map(EmsSiteMonitorPointMatch::getFieldCode) .collect(Collectors.toSet()); - Map mappingByFieldCode = mappingList.stream() + Map mappingByFieldAndDevice = mappingList.stream() .filter(item -> StringUtils.isNotBlank(item.getFieldCode())) .filter(item -> !DELETED_FIELD_MARK.equals(item.getDataPoint())) - .collect(Collectors.toMap(EmsSiteMonitorPointMatch::getFieldCode, item -> item, (a, b) -> b)); - Map pointMap = mappingList.stream() - .filter(item -> StringUtils.isNotBlank(item.getFieldCode())) - .filter(item -> !DELETED_FIELD_MARK.equals(item.getDataPoint())) - .collect(Collectors.toMap(EmsSiteMonitorPointMatch::getFieldCode, EmsSiteMonitorPointMatch::getDataPoint, (a, b) -> b)); + .collect(Collectors.toMap( + item -> buildMatchKey(item.getFieldCode(), item.getDeviceId()), + item -> item, + (a, b) -> b + )); + Map> deviceMetaByMenu = buildDeviceMetaByMenu(siteId, itemList); itemList.forEach(item -> { if (deletedFieldCodeSet.contains(item.getFieldCode())) { return; } - EmsSiteMonitorPointMatch pointMatch = mappingByFieldCode.get(item.getFieldCode()); - SiteMonitorProjectPointMappingVo vo = new SiteMonitorProjectPointMappingVo(); - vo.setModuleCode(item.getModuleCode()); - vo.setModuleName(item.getModuleName()); - vo.setMenuCode(item.getMenuCode()); - vo.setMenuName(item.getMenuName()); - vo.setSectionName(item.getSectionName()); - vo.setFieldCode(item.getFieldCode()); - vo.setFieldName(item.getFieldName()); - vo.setDataPoint(pointMap.getOrDefault(item.getFieldCode(), "")); - vo.setFixedDataPoint(pointMatch == null ? "" : pointMatch.getFixedDataPoint()); - vo.setUseFixedDisplay(pointMatch == null ? 0 : (pointMatch.getUseFixedDisplay() == null ? 0 : pointMatch.getUseFixedDisplay())); - result.add(vo); + if (isDeviceDimensionItem(item)) { + List deviceMetaList = deviceMetaByMenu.getOrDefault(item.getMenuCode(), Collections.emptyList()); + if (deviceMetaList.isEmpty()) { + result.add(buildMappingVo(item, null, null, null)); + return; + } + for (DeviceMeta deviceMeta : deviceMetaList) { + EmsSiteMonitorPointMatch pointMatch = getMatchWithFallback(mappingByFieldAndDevice, item.getFieldCode(), deviceMeta.getDeviceId()); + result.add(buildMappingVo(item, pointMatch, deviceMeta.getDeviceId(), deviceMeta.getDeviceName())); + } + return; + } + EmsSiteMonitorPointMatch pointMatch = getMatchWithFallback(mappingByFieldAndDevice, item.getFieldCode(), null); + result.add(buildMappingVo(item, pointMatch, null, null)); }); return result; } @@ -477,6 +552,7 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService .filter(fieldCodeSet::contains) .forEach(deletedFieldCodeSet::add); } + validatePcsCurvePointMappings(siteId, request.getMappings(), deletedFieldCodeSet); int deletedRows = emsSiteMonitorPointMatchMapper.deleteBySiteId(siteId); List saveList = new ArrayList<>(); @@ -489,6 +565,10 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService if (!fieldCodeSet.contains(fieldCode) || deletedFieldCodeSet.contains(fieldCode)) { continue; } + String deviceId = StringUtils.isBlank(mapping.getDeviceId()) ? "" : mapping.getDeviceId().trim(); + if (isDeviceDimensionField(fieldCode) && StringUtils.isBlank(deviceId)) { + continue; + } String dataPoint = StringUtils.isBlank(mapping.getDataPoint()) ? "" : mapping.getDataPoint().trim(); String fixedDataPoint = StringUtils.isBlank(mapping.getFixedDataPoint()) ? null : mapping.getFixedDataPoint().trim(); Integer useFixedDisplay = mapping.getUseFixedDisplay() == null ? 0 : mapping.getUseFixedDisplay(); @@ -499,6 +579,7 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService EmsSiteMonitorPointMatch pointMatch = new EmsSiteMonitorPointMatch(); pointMatch.setSiteId(siteId); pointMatch.setFieldCode(fieldCode); + pointMatch.setDeviceId(deviceId); pointMatch.setDataPoint(dataPoint); pointMatch.setFixedDataPoint(fixedDataPoint); pointMatch.setUseFixedDisplay(useFixedDisplay); @@ -512,6 +593,7 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService EmsSiteMonitorPointMatch pointMatch = new EmsSiteMonitorPointMatch(); pointMatch.setSiteId(siteId); pointMatch.setFieldCode(deletedFieldCode); + pointMatch.setDeviceId(""); pointMatch.setDataPoint(DELETED_FIELD_MARK); pointMatch.setFixedDataPoint(null); pointMatch.setUseFixedDisplay(0); @@ -522,6 +604,7 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService // 点位映射变更后清理“单站监控最新值”缓存,避免页面回退读取到旧快照 clearSiteMonitorLatestCache(siteId); + redisCache.deleteObject(buildSiteMonitorPointMatchRedisKey(StringUtils.trim(siteId))); if (saveList.isEmpty()) { return deletedRows; @@ -530,9 +613,353 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService return deletedRows + insertedRows; } + @Override + public List getSiteWorkStatusEnumMappings(String siteId) { + List defaults = buildDefaultEnumMappings(); + if (StringUtils.isBlank(siteId)) { + return defaults; + } + String normalizedSiteId = siteId.trim(); + List result = new ArrayList<>(); + for (String scopeKey : MANAGED_ENUM_SCOPE_KEYS) { + String[] scope = parseEnumScopeKey(scopeKey); + List dbList = emsPointEnumMatchMapper.selectList(normalizedSiteId, scope[0], scope[1]); + if (CollectionUtils.isEmpty(dbList)) { + result.addAll(defaults.stream() + .filter(item -> scopeKey.equals(buildEnumScopeKey(item.getDeviceCategory(), item.getMatchField()))) + .collect(Collectors.toList())); + continue; + } + for (EmsPointEnumMatch match : dbList) { + if (match == null) { + continue; + } + WorkStatusEnumMappingVo vo = new WorkStatusEnumMappingVo(); + vo.setDeviceCategory(scope[0]); + vo.setMatchField(scope[1]); + vo.setMatchFieldName(getScopeName(scope[0], scope[1])); + vo.setEnumCode(StringUtils.nvl(match.getEnumCode(), "")); + vo.setEnumName(StringUtils.nvl(match.getEnumName(), "")); + vo.setEnumDesc(StringUtils.nvl(match.getEnumDesc(), "")); + vo.setDataEnumCode(StringUtils.nvl(match.getDataEnumCode(), "")); + result.add(vo); + } + } + return result; + } + + @Override + public int saveSiteWorkStatusEnumMappings(String siteId, List mappings, String operName) { + if (StringUtils.isBlank(siteId)) { + throw new ServiceException("站点ID不能为空"); + } + String normalizedSiteId = siteId.trim(); + int deletedRows = 0; + for (String scopeKey : MANAGED_ENUM_SCOPE_KEYS) { + String[] scope = parseEnumScopeKey(scopeKey); + deletedRows += emsPointEnumMatchMapper.deleteByScope(normalizedSiteId, scope[0], scope[1]); + } + int insertedRows = 0; + if (mappings != null) { + for (WorkStatusEnumMappingVo item : mappings) { + if (item == null) { + continue; + } + String deviceCategory = StringUtils.trim(item.getDeviceCategory()); + String matchField = StringUtils.trim(item.getMatchField()); + if (!isManagedEnumScope(deviceCategory, matchField)) { + continue; + } + String enumCode = StringUtils.trim(item.getEnumCode()); + String enumName = StringUtils.trim(item.getEnumName()); + String enumDesc = StringUtils.trim(item.getEnumDesc()); + String dataEnumCode = StringUtils.trim(item.getDataEnumCode()); + if (StringUtils.isBlank(enumCode) && StringUtils.isBlank(enumName) && StringUtils.isBlank(dataEnumCode)) { + continue; + } + if (StringUtils.isBlank(enumCode)) { + throw new ServiceException(String.format("%s 枚举编码不能为空", getScopeName(deviceCategory, matchField))); + } + EmsPointEnumMatch insertItem = new EmsPointEnumMatch(); + insertItem.setSiteId(normalizedSiteId); + insertItem.setDeviceCategory(deviceCategory); + insertItem.setMatchField(matchField); + insertItem.setEnumCode(enumCode); + insertItem.setEnumName(StringUtils.defaultString(enumName)); + insertItem.setEnumDesc(StringUtils.defaultString(enumDesc)); + insertItem.setDataEnumCode(StringUtils.defaultString(dataEnumCode)); + insertItem.setCreateBy(operName); + insertItem.setUpdateBy(operName); + insertedRows += emsPointEnumMatchMapper.insertEmsPointEnumMatch(insertItem); + } + } + syncPointEnumMatchToRedis(normalizedSiteId, PCS_MATCH_DEVICE_CATEGORY); + syncPointEnumMatchToRedis(normalizedSiteId, STACK_MATCH_DEVICE_CATEGORY); + syncPointEnumMatchToRedis(normalizedSiteId, CLUSTER_MATCH_DEVICE_CATEGORY); + projectDisplayCache.remove(normalizedSiteId); + return deletedRows + insertedRows; + } + + private void syncPointEnumMatchToRedis(String siteId, String deviceCategory) { + if (StringUtils.isAnyBlank(siteId, deviceCategory)) { + return; + } + String cacheKey = DevicePointMatchDataProcessor.getPointEnumMacthCacheKey(siteId, deviceCategory); + redisCache.deleteObject(cacheKey); + List pointEnumMatchList = emsPointEnumMatchMapper.selectList(siteId, deviceCategory, null); + if (!CollectionUtils.isEmpty(pointEnumMatchList)) { + redisCache.setCacheList(cacheKey, pointEnumMatchList); + } + } + + private List buildDefaultEnumMappings() { + List result = new ArrayList<>(); + result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "0", "运行")); + result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "1", "停机")); + result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "2", "故障")); + result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "3", "待机")); + result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "4", "充电")); + result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "5", "放电")); + + result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_GRID_STATUS, "0", "并网")); + result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_GRID_STATUS, "1", "未并网")); + result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_DEVICE_STATUS, "0", "离线")); + result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_DEVICE_STATUS, "1", "在线")); + result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_CONTROL_MODE, "0", "远程")); + result.add(buildEnumMapping(PCS_MATCH_DEVICE_CATEGORY, MATCH_FIELD_CONTROL_MODE, "1", "本地")); + + result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "0", "静置")); + result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "1", "充电")); + result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "2", "放电")); + result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "3", "浮充")); + result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "4", "待机")); + result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "5", "运行")); + result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "9", "故障")); + result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_PCS_COMMUNICATION_STATUS, "0", "正常")); + result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_PCS_COMMUNICATION_STATUS, "1", "通讯中断")); + result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_PCS_COMMUNICATION_STATUS, "2", "异常")); + result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_EMS_COMMUNICATION_STATUS, "0", "正常")); + result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_EMS_COMMUNICATION_STATUS, "1", "通讯中断")); + result.add(buildEnumMapping(STACK_MATCH_DEVICE_CATEGORY, MATCH_FIELD_EMS_COMMUNICATION_STATUS, "2", "异常")); + + result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "0", "静置")); + result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "1", "充电")); + result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "2", "放电")); + result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "3", "待机")); + result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "5", "运行")); + result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_WORK_STATUS, "9", "故障")); + result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_PCS_COMMUNICATION_STATUS, "0", "正常")); + result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_PCS_COMMUNICATION_STATUS, "1", "通讯中断")); + result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_PCS_COMMUNICATION_STATUS, "2", "异常")); + result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_EMS_COMMUNICATION_STATUS, "0", "正常")); + result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_EMS_COMMUNICATION_STATUS, "1", "通讯中断")); + result.add(buildEnumMapping(CLUSTER_MATCH_DEVICE_CATEGORY, MATCH_FIELD_EMS_COMMUNICATION_STATUS, "2", "异常")); + return result; + } + + private WorkStatusEnumMappingVo buildEnumMapping(String deviceCategory, String matchField, String enumCode, String enumName) { + WorkStatusEnumMappingVo vo = new WorkStatusEnumMappingVo(); + vo.setDeviceCategory(deviceCategory); + vo.setMatchField(matchField); + vo.setMatchFieldName(getScopeName(deviceCategory, matchField)); + vo.setEnumCode(enumCode); + vo.setEnumName(enumName); + vo.setEnumDesc(""); + vo.setDataEnumCode(""); + return vo; + } + + private boolean isManagedEnumScope(String deviceCategory, String matchField) { + return MANAGED_ENUM_SCOPE_KEYS.contains(buildEnumScopeKey(deviceCategory, matchField)); + } + + private static String buildEnumScopeKey(String deviceCategory, String matchField) { + return StringUtils.defaultString(deviceCategory).trim() + "|" + StringUtils.defaultString(matchField).trim(); + } + + private static String[] parseEnumScopeKey(String scopeKey) { + String[] values = StringUtils.defaultString(scopeKey).split("\\|", 2); + if (values.length < 2) { + return new String[]{"", ""}; + } + return values; + } + + private String getScopeName(String deviceCategory, String matchField) { + String name = ENUM_SCOPE_NAME_MAP.get(buildEnumScopeKey(deviceCategory, matchField)); + if (StringUtils.isNotBlank(name)) { + return name; + } + return StringUtils.defaultString(deviceCategory) + "-" + StringUtils.defaultString(matchField); + } + + private void validatePcsCurvePointMappings(String siteId, List mappings, Set deletedFieldCodeSet) { + List> pcsDevices = emsDevicesMapper.getDeviceInfosBySiteIdAndCategory(siteId, DeviceCategory.PCS.getCode()); + Set pcsDeviceIdSet = new HashSet<>(); + if (pcsDevices != null) { + pcsDevices.stream() + .filter(Objects::nonNull) + .map(item -> item.get("id")) + .filter(Objects::nonNull) + .map(String::valueOf) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .forEach(pcsDeviceIdSet::add); + } + validateSingleCurveFieldMapping(FIELD_CURVE_PCS_ACTIVE_POWER, "PCS有功功率曲线点位", pcsDeviceIdSet, mappings, deletedFieldCodeSet); + validateSingleCurveFieldMapping(FIELD_CURVE_PCS_REACTIVE_POWER, "PCS无功功率曲线点位", pcsDeviceIdSet, mappings, deletedFieldCodeSet); + } + + private void validateSingleCurveFieldMapping(String fieldCode, String fieldName, Set pcsDeviceIdSet, + List mappings, Set deletedFieldCodeSet) { + if (deletedFieldCodeSet != null && deletedFieldCodeSet.contains(fieldCode) && !pcsDeviceIdSet.isEmpty()) { + throw new ServiceException(fieldName + "不能删除,且配置数量必须与PCS设备数量一致"); + } + Set configuredDeviceIdSet = new HashSet<>(); + if (mappings != null) { + mappings.stream() + .filter(Objects::nonNull) + .filter(item -> fieldCode.equals(StringUtils.trim(item.getFieldCode()))) + .filter(item -> StringUtils.isNotBlank(item.getDataPoint())) + .map(SiteMonitorProjectPointMappingVo::getDeviceId) + .map(StringUtils::trim) + .filter(StringUtils::isNotBlank) + .forEach(configuredDeviceIdSet::add); + } + + if (configuredDeviceIdSet.size() != pcsDeviceIdSet.size() || !configuredDeviceIdSet.equals(pcsDeviceIdSet)) { + throw new ServiceException(String.format("%s数量需与PCS设备数量一致:PCS设备%d个,已配置%d个", fieldName, pcsDeviceIdSet.size(), configuredDeviceIdSet.size())); + } + } + + private SiteMonitorProjectPointMappingVo buildMappingVo(EmsSiteMonitorItem item, EmsSiteMonitorPointMatch pointMatch, + String deviceId, String deviceName) { + SiteMonitorProjectPointMappingVo vo = new SiteMonitorProjectPointMappingVo(); + vo.setModuleCode(item.getModuleCode()); + vo.setModuleName(item.getModuleName()); + vo.setMenuCode(item.getMenuCode()); + vo.setMenuName(item.getMenuName()); + vo.setSectionName(item.getSectionName()); + vo.setFieldCode(item.getFieldCode()); + vo.setFieldName(item.getFieldName()); + vo.setDeviceId(deviceId); + vo.setDeviceName(deviceName); + vo.setDataPoint(pointMatch == null ? "" : StringUtils.nvl(pointMatch.getDataPoint(), "")); + vo.setFixedDataPoint(pointMatch == null ? "" : StringUtils.nvl(pointMatch.getFixedDataPoint(), "")); + vo.setUseFixedDisplay(pointMatch == null ? 0 : (pointMatch.getUseFixedDisplay() == null ? 0 : pointMatch.getUseFixedDisplay())); + return vo; + } + + private EmsSiteMonitorPointMatch getMatchWithFallback(Map mappingByFieldAndDevice, + String fieldCode, String deviceId) { + EmsSiteMonitorPointMatch exact = mappingByFieldAndDevice.get(buildMatchKey(fieldCode, deviceId)); + if (exact != null) { + return exact; + } + return mappingByFieldAndDevice.get(buildMatchKey(fieldCode, null)); + } + + private String buildMatchKey(String fieldCode, String deviceId) { + return StringUtils.nvl(fieldCode, "") + "|" + StringUtils.nvl(deviceId, ""); + } + + private Map> buildDeviceMetaByMenu(String siteId, List itemList) { + Map> result = new HashMap<>(); + if (StringUtils.isBlank(siteId) || itemList == null || itemList.isEmpty()) { + return result; + } + Set menuCodeSet = itemList.stream() + .filter(this::isDeviceDimensionItem) + .map(EmsSiteMonitorItem::getMenuCode) + .collect(Collectors.toSet()); + for (String menuCode : menuCodeSet) { + String deviceCategory = MENU_DEVICE_CATEGORY_MAP.get(menuCode); + if (StringUtils.isBlank(deviceCategory)) { + continue; + } + List> deviceList = emsDevicesMapper.getDeviceInfosBySiteIdAndCategory(siteId, deviceCategory); + if (deviceList == null || deviceList.isEmpty()) { + continue; + } + List deviceMetas = new ArrayList<>(); + for (Map item : deviceList) { + if (item == null || item.get("id") == null) { + continue; + } + String deviceId = String.valueOf(item.get("id")); + if (StringUtils.isBlank(deviceId)) { + continue; + } + String deviceName = item.get("deviceName") == null ? deviceId : String.valueOf(item.get("deviceName")); + deviceMetas.add(new DeviceMeta(deviceId, deviceName)); + } + deviceMetas.sort(Comparator.comparing(DeviceMeta::getDeviceName, Comparator.nullsLast(String::compareTo)) + .thenComparing(DeviceMeta::getDeviceId, Comparator.nullsLast(String::compareTo))); + result.put(menuCode, deviceMetas); + } + return result; + } + + private boolean isDeviceDimensionField(String fieldCode) { + if (StringUtils.isBlank(fieldCode)) { + return false; + } + String normalizedFieldCode = fieldCode.trim(); + if (DEVICE_DIMENSION_FIELD_CODES.contains(normalizedFieldCode)) { + return true; + } + if (!normalizedFieldCode.contains("__")) { + return false; + } + String menuCode = normalizedFieldCode.substring(0, normalizedFieldCode.indexOf("__")); + return isDeviceDimensionMenu(menuCode); + } + + private boolean isDeviceDimensionItem(EmsSiteMonitorItem item) { + if (item == null) { + return false; + } + if (isDeviceDimensionField(item.getFieldCode())) { + return true; + } + return isDeviceDimensionMenu(item.getMenuCode()); + } + + private boolean isDeviceDimensionMenu(String menuCode) { + return StringUtils.isNotBlank(menuCode) && DEVICE_DIMENSION_MENU_CODES.contains(menuCode); + } + + private static class DeviceMeta { + private final String deviceId; + private final String deviceName; + + private DeviceMeta(String deviceId, String deviceName) { + this.deviceId = deviceId; + this.deviceName = deviceName; + } + + public String getDeviceId() { + return deviceId; + } + + public String getDeviceName() { + return deviceName; + } + } + @Override public List getSiteMonitorProjectDisplay(String siteId) { - List mappingList = getSiteMonitorProjectPointMapping(siteId); + String normalizedSiteId = StringUtils.trim(siteId); + if (StringUtils.isBlank(normalizedSiteId)) { + return new ArrayList<>(); + } + DisplayCacheEntry cacheEntry = projectDisplayCache.get(normalizedSiteId); + long now = System.currentTimeMillis(); + if (cacheEntry != null && cacheEntry.getExpireAt() > now) { + return cloneDisplayList(cacheEntry.getData()); + } + + List mappingList = getSiteMonitorProjectPointMapping(normalizedSiteId); if (mappingList.isEmpty()) { return new ArrayList<>(); } @@ -541,26 +968,64 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService .filter(StringUtils::isNotBlank) .map(item -> item.trim().toUpperCase()) .collect(Collectors.toSet()); - Map pointConfigByPointId = buildPointConfigByPointId(siteId, pointIds); - Map homeLatestMap = safeRedisMap(redisCache.getCacheMap(buildSiteMonitorLatestRedisKey(siteId, MODULE_HOME))); - Map sbjkLatestMap = safeRedisMap(redisCache.getCacheMap(buildSiteMonitorLatestRedisKey(siteId, MODULE_SBJK))); - Map tjbbLatestMap = safeRedisMap(redisCache.getCacheMap(buildSiteMonitorLatestRedisKey(siteId, MODULE_TJBB))); + Map pointConfigByPointId = buildPointConfigByPointId(normalizedSiteId, pointIds); + Map homeLatestMap = safeRedisMap(redisCache.getCacheMap(buildSiteMonitorLatestRedisKey(normalizedSiteId, MODULE_HOME))); + Map sbjkLatestMap = safeRedisMap(redisCache.getCacheMap(buildSiteMonitorLatestRedisKey(normalizedSiteId, MODULE_SBJK))); + Map tjbbLatestMap = safeRedisMap(redisCache.getCacheMap(buildSiteMonitorLatestRedisKey(normalizedSiteId, MODULE_TJBB))); + Map> enumDataCodeMapByScope = buildEnumDataCodeMapByScope(normalizedSiteId); + Map pointSnapshotCache = new HashMap<>(); List result = new ArrayList<>(); + int totalCount = 0; + int fixedHitCount = 0; + int pointHitCount = 0; + int cacheHitCount = 0; + int emptyCount = 0; + List emptySamples = new ArrayList<>(); + List pointHitSamples = new ArrayList<>(); + List cacheHitSamples = new ArrayList<>(); for (SiteMonitorProjectPointMappingVo mapping : mappingList) { + totalCount++; SiteMonitorProjectDisplayVo vo = new SiteMonitorProjectDisplayVo(); BeanUtils.copyProperties(mapping, vo); if (USE_FIXED_DISPLAY_YES.equals(mapping.getUseFixedDisplay()) && StringUtils.isNotBlank(mapping.getFixedDataPoint())) { vo.setFieldValue(mapping.getFixedDataPoint()); vo.setValueTime(null); + fixedHitCount++; result.add(vo); continue; } // 与“点位配置列表最新值”一致:按 pointId -> 点位配置(dataKey/deviceId) -> MQTT 最新报文读取 - PointLatestSnapshot latestSnapshot = getLatestSnapshotByPointId(siteId, mapping.getDataPoint(), pointConfigByPointId); + PointLatestSnapshot latestSnapshot = null; + String dataPoint = StringUtils.trim(mapping.getDataPoint()); + if (StringUtils.isNotBlank(dataPoint)) { + String pointCacheKey = dataPoint.toUpperCase(); + if (pointSnapshotCache.containsKey(pointCacheKey)) { + latestSnapshot = pointSnapshotCache.get(pointCacheKey); + } else { + latestSnapshot = getLatestSnapshotByPointId(normalizedSiteId, dataPoint, pointConfigByPointId); + pointSnapshotCache.put(pointCacheKey, latestSnapshot); + } + } if (latestSnapshot != null && latestSnapshot.getFieldValue() != null) { vo.setFieldValue(latestSnapshot.getFieldValue()); vo.setValueTime(latestSnapshot.getValueTime()); + applyEnumMappingIfNecessary(mapping, vo, enumDataCodeMapByScope); + pointHitCount++; + if (pointHitSamples.size() < DISPLAY_DEBUG_SAMPLE_SIZE) { + pointHitSamples.add(mapping.getFieldCode() + "->" + StringUtils.defaultString(mapping.getDataPoint())); + } + result.add(vo); + continue; + } + if (isSbjkEmsMenu(mapping)) { + if (StringUtils.isBlank(vo.getFieldValue())) { + emptyCount++; + if (emptySamples.size() < DISPLAY_DEBUG_SAMPLE_SIZE) { + emptySamples.add(mapping.getFieldCode() + "->" + StringUtils.defaultString(mapping.getDataPoint())); + } + } + applyEnumMappingIfNecessary(mapping, vo, enumDataCodeMapByScope); result.add(vo); continue; } @@ -576,12 +1041,139 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService JSONObject snapshot = parseFieldSnapshot(cacheObj); vo.setFieldValue(snapshot.getString("fieldValue")); vo.setValueTime(parseValueTime(snapshot.get("valueTime"))); + if (StringUtils.isNotBlank(vo.getFieldValue())) { + cacheHitCount++; + if (cacheHitSamples.size() < DISPLAY_DEBUG_SAMPLE_SIZE) { + cacheHitSamples.add(mapping.getFieldCode() + "->cache"); + } + } } + if (StringUtils.isBlank(vo.getFieldValue())) { + emptyCount++; + if (emptySamples.size() < DISPLAY_DEBUG_SAMPLE_SIZE) { + emptySamples.add(mapping.getFieldCode() + "->" + StringUtils.defaultString(mapping.getDataPoint())); + } + } + applyEnumMappingIfNecessary(mapping, vo, enumDataCodeMapByScope); result.add(vo); } + log.info("SiteMonitorDisplayDebug siteId={}, total={}, fixedHit={}, pointHit={}, cacheHit={}, empty={}, emptySamples={}, pointHitSamples={}, cacheHitSamples={}", + normalizedSiteId, totalCount, fixedHitCount, pointHitCount, cacheHitCount, emptyCount, + emptySamples, pointHitSamples, cacheHitSamples); + projectDisplayCache.put(normalizedSiteId, new DisplayCacheEntry(cloneDisplayList(result), now + PROJECT_DISPLAY_CACHE_TTL_MS)); return result; } + private Map> buildEnumDataCodeMapByScope(String siteId) { + Map> result = new HashMap<>(); + if (StringUtils.isBlank(siteId)) { + return result; + } + List categories = Arrays.asList(PCS_MATCH_DEVICE_CATEGORY, STACK_MATCH_DEVICE_CATEGORY, CLUSTER_MATCH_DEVICE_CATEGORY); + for (String deviceCategory : categories) { + String cacheKey = DevicePointMatchDataProcessor.getPointEnumMacthCacheKey(siteId, deviceCategory); + List enumList = redisCache.getCacheList(cacheKey); + if (CollectionUtils.isEmpty(enumList)) { + enumList = emsPointEnumMatchMapper.selectList(siteId, deviceCategory, null); + } + if (CollectionUtils.isEmpty(enumList)) { + continue; + } + for (EmsPointEnumMatch item : enumList) { + if (item == null) { + continue; + } + String matchField = StringUtils.trim(item.getMatchField()); + String enumCode = StringUtils.trim(item.getEnumCode()); + String dataEnumCode = normalizeEnumDataValue(item.getDataEnumCode()); + if (StringUtils.isAnyBlank(matchField, enumCode, dataEnumCode)) { + continue; + } + String scopeKey = buildEnumScopeKey(deviceCategory, matchField); + if (!MANAGED_ENUM_SCOPE_KEYS.contains(scopeKey)) { + continue; + } + result.computeIfAbsent(scopeKey, key -> new HashMap<>()).put(dataEnumCode, enumCode); + } + } + return result; + } + + private void applyEnumMappingIfNecessary(SiteMonitorProjectPointMappingVo mapping, + SiteMonitorProjectDisplayVo vo, + Map> enumDataCodeMapByScope) { + if (mapping == null || vo == null || CollectionUtils.isEmpty(enumDataCodeMapByScope)) { + return; + } + String rawFieldValue = StringUtils.trim(vo.getFieldValue()); + if (StringUtils.isBlank(rawFieldValue)) { + return; + } + String scopeKey = resolveEnumScopeKey(mapping.getMenuCode(), mapping.getFieldCode()); + if (StringUtils.isBlank(scopeKey)) { + return; + } + Map enumMap = enumDataCodeMapByScope.get(scopeKey); + if (CollectionUtils.isEmpty(enumMap)) { + return; + } + String mappedValue = enumMap.get(normalizeEnumDataValue(rawFieldValue)); + if (StringUtils.isNotBlank(mappedValue)) { + vo.setFieldValue(mappedValue); + } + } + + private String resolveEnumScopeKey(String menuCode, String fieldCode) { + String normalizedMenuCode = StringUtils.trim(menuCode); + String deviceCategory = StringUtils.trim(MENU_DEVICE_CATEGORY_MAP.get(normalizedMenuCode)); + if (StringUtils.isBlank(deviceCategory)) { + return null; + } + String matchField = null; + String normalizedFieldCode = StringUtils.trim(fieldCode); + if (StringUtils.isNotBlank(normalizedFieldCode)) { + int splitIndex = normalizedFieldCode.lastIndexOf("__"); + matchField = splitIndex >= 0 ? normalizedFieldCode.substring(splitIndex + 2) : normalizedFieldCode; + } + matchField = StringUtils.trim(matchField); + if (StringUtils.isBlank(matchField)) { + return null; + } + String scopeKey = buildEnumScopeKey(deviceCategory, matchField); + return MANAGED_ENUM_SCOPE_KEYS.contains(scopeKey) ? scopeKey : null; + } + + private String normalizeEnumDataValue(String value) { + String normalized = StringUtils.trim(value); + if (StringUtils.isBlank(normalized)) { + return normalized; + } + if (normalized.matches("^-?\\d+\\.0+$")) { + return normalized.substring(0, normalized.indexOf('.')); + } + return normalized; + } + + private boolean isSbjkEmsMenu(SiteMonitorProjectPointMappingVo mapping) { + return mapping != null && MENU_SBJK_EMS.equals(mapping.getMenuCode()); + } + + private List cloneDisplayList(List source) { + if (source == null || source.isEmpty()) { + return new ArrayList<>(); + } + List copy = new ArrayList<>(source.size()); + for (SiteMonitorProjectDisplayVo item : source) { + if (item == null) { + continue; + } + SiteMonitorProjectDisplayVo target = new SiteMonitorProjectDisplayVo(); + BeanUtils.copyProperties(item, target); + copy.add(target); + } + return copy; + } + private Map buildPointConfigByPointId(String siteId, Set pointIds) { Map result = new HashMap<>(); if (StringUtils.isBlank(siteId) || pointIds == null || pointIds.isEmpty()) { @@ -609,31 +1201,66 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService return null; } EmsPointConfig pointConfig = pointConfigByPointId.get(pointId.trim().toUpperCase()); - if (pointConfig == null || StringUtils.isAnyBlank(pointConfig.getDeviceId(), pointConfig.getDataKey())) { + // 直接按 pointId 拉取最新值(pointId 全局唯一),不再依赖 deviceId 维度 + PointLatestSnapshot curveSnapshot = getLatestSnapshotFromCurve(siteId, pointId, pointConfig); + if (curveSnapshot != null) { + return curveSnapshot; + } + + // 兼容旧逻辑:Influx 无数据时,再按点位配置回退到 MQTT 原始报文解析 + if (pointConfig != null && StringUtils.isNoneBlank(pointConfig.getDeviceId(), pointConfig.getDataKey())) { + String redisKey = RedisKeyConstants.ORIGINAL_MQTT_DATA + siteId + "_" + pointConfig.getDeviceId().trim(); + Object raw = redisCache.getCacheObject(redisKey); + if (raw != null) { + JSONObject root = toJsonObject(raw); + if (root != null) { + JSONObject dataObject = extractDataObject(root); + if (dataObject != null) { + Object rawValue = getValueIgnoreCase(dataObject, pointConfig.getDataKey()); + BigDecimal sourceValue = StringUtils.getBigDecimal(rawValue); + if (sourceValue != null) { + BigDecimal pointValue = convertPointValue(sourceValue, pointConfig); + Long timestamp = root.getLong("timestamp"); + Date valueTime = timestamp == null ? null : DateUtils.convertUpdateTime(timestamp); + return new PointLatestSnapshot(pointValue.stripTrailingZeros().toPlainString(), valueTime); + } + } + } + } + } + return null; + } + + private PointLatestSnapshot getLatestSnapshotFromCurve(String siteId, String pointId, EmsPointConfig pointConfig) { + if (StringUtils.isAnyBlank(siteId, pointId)) { return null; } - String redisKey = RedisKeyConstants.ORIGINAL_MQTT_DATA + siteId + "_" + pointConfig.getDeviceId().trim(); - Object raw = redisCache.getCacheObject(redisKey); - if (raw == null) { + String configPointId = pointConfig == null ? null : pointConfig.getPointId(); + String influxPointKey = StringUtils.isNotBlank(configPointId) + ? configPointId.trim() + : pointId.trim(); + Date endTime = DateUtils.getNowDate(); + // 优先近24小时快速取最新值,查不到再回退到7天窗口 + Date fastStartTime = DateUtils.addDays(endTime, -1); + InfluxPointDataWriter.PointValue latest = influxPointDataWriter.queryLatestPointValueByPointKey( + siteId.trim(), + influxPointKey, + fastStartTime, + endTime + ); + if (latest == null) { + Date fallbackStartTime = DateUtils.addDays(endTime, -7); + latest = influxPointDataWriter.queryLatestPointValueByPointKey( + siteId.trim(), + influxPointKey, + fallbackStartTime, + endTime + ); + } + if (latest == null || latest.getPointValue() == null) { return null; } - JSONObject root = toJsonObject(raw); - if (root == null) { - return null; - } - JSONObject dataObject = extractDataObject(root); - if (dataObject == null) { - return null; - } - Object rawValue = getValueIgnoreCase(dataObject, pointConfig.getDataKey()); - BigDecimal sourceValue = StringUtils.getBigDecimal(rawValue); - if (sourceValue == null) { - return null; - } - BigDecimal pointValue = convertPointValue(sourceValue, pointConfig); - Long timestamp = root.getLong("timestamp"); - Date valueTime = timestamp == null ? null : DateUtils.convertUpdateTime(timestamp); - return new PointLatestSnapshot(pointValue.stripTrailingZeros().toPlainString(), valueTime); + return new PointLatestSnapshot(latest.getPointValue().stripTrailingZeros().toPlainString(), latest.getDataTime()); } private JSONObject toJsonObject(Object raw) { @@ -715,6 +1342,24 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService } } + private static class DisplayCacheEntry { + private final List data; + private final long expireAt; + + private DisplayCacheEntry(List data, long expireAt) { + this.data = data; + this.expireAt = expireAt; + } + + public List getData() { + return data; + } + + public long getExpireAt() { + return expireAt; + } + } + @Override public int saveSiteMonitorProjectData(SiteMonitorDataSaveRequest request, String operName) { if (request == null || StringUtils.isBlank(request.getSiteId())) { @@ -781,6 +1426,7 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService upsertLatestToRedis(request.getSiteId(), MODULE_TJBB, tjbbLatestUpdates); rows += upsertHistoryByMinute(HISTORY_TABLE_TJBB, request.getSiteId(), tjbbHistoryByMinute, operName); } + projectDisplayCache.remove(StringUtils.trim(request.getSiteId())); return rows; } @@ -800,7 +1446,7 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService } }); - List mappingList = emsSiteMonitorPointMatchMapper.selectBySiteId(siteId); + List mappingList = getPointMatchesBySiteId(siteId); if (mappingList == null || mappingList.isEmpty()) { return 0; } @@ -820,6 +1466,9 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService Map homeHistoryByMinute = new HashMap<>(); Map sbjkHistoryByMinute = new HashMap<>(); Map tjbbHistoryByMinute = new HashMap<>(); + int matchedCount = 0; + int missCount = 0; + List missSamples = new ArrayList<>(); for (EmsSiteMonitorPointMatch mapping : mappingList) { if (mapping == null || StringUtils.isBlank(mapping.getFieldCode())) { @@ -828,6 +1477,7 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService if (DELETED_FIELD_MARK.equals(mapping.getDataPoint())) { continue; } + // pointId 在系统内唯一,按 pointId 匹配即可,不再按 deviceId 过滤 EmsSiteMonitorItem itemDef = itemMap.get(mapping.getFieldCode()); if (itemDef == null || StringUtils.isBlank(itemDef.getModuleCode())) { continue; @@ -845,8 +1495,13 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService value = upperDataMap.get((deviceId + point).toUpperCase()); } if (value == null) { + missCount++; + if (missSamples.size() < DISPLAY_DEBUG_SAMPLE_SIZE) { + missSamples.add(mapping.getFieldCode() + "->" + point); + } continue; } + matchedCount++; JSONObject snapshot = buildFieldSnapshot(String.valueOf(value), actualValueTime); @@ -875,6 +1530,9 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService upsertLatestToRedis(siteId, MODULE_TJBB, tjbbLatestUpdates); rows += upsertHistoryByMinute(HISTORY_TABLE_TJBB, siteId, tjbbHistoryByMinute, "mqtt"); } + log.info("SiteMonitorSyncDebug siteId={}, deviceId={}, mappingTotal={}, matched={}, miss={}, homeUpdates={}, sbjkUpdates={}, tjbbUpdates={}, missSamples={}", + siteId, deviceId, mappingList.size(), matchedCount, missCount, + homeLatestUpdates.size(), sbjkLatestUpdates.size(), tjbbLatestUpdates.size(), missSamples); return rows; } @@ -883,18 +1541,12 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService for (Map.Entry entry : minuteSnapshotMap.entrySet()) { Date statisMinute = entry.getKey(); JSONObject merged = mergeHistoryRecord(tableName, siteId, statisMinute, entry.getValue()); + Map hotColumns = extractHotColumns(merged); rows += emsSiteMonitorDataMapper.upsertHistoryJsonByMinute( tableName, siteId, statisMinute, merged.toJSONString(), - operName - ); - Map hotColumns = extractHotColumns(merged); - rows += emsSiteMonitorDataMapper.updateHistoryHotColumns( - tableName, - siteId, - statisMinute, hotColumns.get("hotSoc"), hotColumns.get("hotTotalActivePower"), hotColumns.get("hotTotalReactivePower"), @@ -959,6 +1611,51 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService } } + private List getPointMatchesBySiteId(String siteId) { + if (StringUtils.isBlank(siteId)) { + return Collections.emptyList(); + } + String normalizedSiteId = siteId.trim(); + String redisKey = buildSiteMonitorPointMatchRedisKey(normalizedSiteId); + Object cacheObj = redisCache.getCacheObject(redisKey); + List cached = parsePointMatchCache(cacheObj); + if (cached != null) { + return cached; + } + List latest = emsSiteMonitorPointMatchMapper.selectBySiteId(normalizedSiteId); + if (latest == null) { + latest = Collections.emptyList(); + } + redisCache.setCacheObject(redisKey, latest, MONITOR_POINT_MATCH_REDIS_TTL_SECONDS, TimeUnit.SECONDS); + return latest; + } + + private List parsePointMatchCache(Object cacheObj) { + if (cacheObj == null) { + return null; + } + try { + if (cacheObj instanceof String) { + return JSON.parseArray((String) cacheObj, EmsSiteMonitorPointMatch.class); + } + if (cacheObj instanceof List) { + List cacheList = (List) cacheObj; + if (cacheList.isEmpty()) { + return Collections.emptyList(); + } + if (cacheList.get(0) instanceof EmsSiteMonitorPointMatch) { + return (List) cacheList; + } + return JSON.parseArray(JSON.toJSONString(cacheObj), EmsSiteMonitorPointMatch.class); + } + return JSON.parseArray(JSON.toJSONString(cacheObj), EmsSiteMonitorPointMatch.class); + } catch (Exception ex) { + log.warn("解析单站监控点位映射缓存失败,key类型={}, err={}", + cacheObj.getClass().getName(), ex.getMessage()); + return null; + } + } + private Map extractHotColumns(JSONObject merged) { Map result = new HashMap<>(); result.put("hotSoc", extractFieldValue(merged, @@ -1077,6 +1774,10 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService return RedisKeyConstants.SITE_MONITOR_LATEST + siteId + "_" + moduleCode; } + private String buildSiteMonitorPointMatchRedisKey(String siteId) { + return RedisKeyConstants.SITE_MONITOR_POINT_MATCH + siteId; + } + private void clearSiteMonitorLatestCache(String siteId) { if (StringUtils.isBlank(siteId)) { return; diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java index 1b9b939..bc019e8 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java @@ -50,7 +50,6 @@ import java.util.stream.Collectors; public class EmsPointConfigServiceImpl implements IEmsPointConfigService { private static final Logger log = LoggerFactory.getLogger(EmsPointConfigServiceImpl.class); private static final String TEMPLATE_SITE_ID = "DEFAULT"; - private static final String SITE_LEVEL_CALC_DEVICE_ID = "SITE_CALC"; private static final int CSV_IMPORT_BATCH_SIZE = 200; private static final Pattern DB_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$"); private static final Pattern INTEGER_PATTERN = Pattern.compile("^-?\\d+$"); @@ -249,19 +248,13 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { return new ArrayList<>(); } String siteId = StringUtils.trim(request.getSiteId()); - String deviceId = StringUtils.trim(request.getDeviceId()); String pointId = StringUtils.trim(request.getPointId()); if (StringUtils.isAnyBlank(siteId, pointId)) { return new ArrayList<>(); } EmsPointConfig pointConfig = resolvePointConfigForCurve(siteId, pointId); - String pointType = resolvePointTypeForCurve(request, pointConfig); - String queryDeviceId = resolveCurveDeviceId(pointType, deviceId, pointConfig); - if (StringUtils.isBlank(queryDeviceId)) { - return new ArrayList<>(); - } Date[] range = resolveTimeRange(request); - return queryCurveDataFromInflux(siteId, queryDeviceId, pointId, pointConfig, range[0], range[1]); + return queryCurveDataFromInflux(siteId, pointId, pointConfig, range[0], range[1]); } private PointConfigLatestValueVo queryLatestValueFromRedis(PointConfigLatestValueItemVo item, @@ -296,13 +289,13 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { return vo; } - private List queryCurveDataFromInflux(String siteId, String deviceId, String pointId, + private List queryCurveDataFromInflux(String siteId, String pointId, EmsPointConfig pointConfig, Date startTime, Date endTime) { String influxPointKey = resolveInfluxPointKey(pointConfig, pointId); if (StringUtils.isBlank(influxPointKey)) { return new ArrayList<>(); } - List values = influxPointDataWriter.queryCurveData(siteId, deviceId, influxPointKey, startTime, endTime); + List values = influxPointDataWriter.queryCurveDataByPointKey(siteId, influxPointKey, startTime, endTime); if (values == null || values.isEmpty()) { return new ArrayList<>(); } @@ -518,29 +511,6 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { return pointConfigs.get(0); } - private String resolvePointTypeForCurve(PointConfigCurveRequest request, EmsPointConfig pointConfig) { - if (pointConfig != null && StringUtils.isNotBlank(pointConfig.getPointType())) { - return StringUtils.trim(pointConfig.getPointType()).toLowerCase(Locale.ROOT); - } - if (request == null || StringUtils.isBlank(request.getPointType())) { - return "data"; - } - return StringUtils.trim(request.getPointType()).toLowerCase(Locale.ROOT); - } - - private String resolveCurveDeviceId(String pointType, String requestDeviceId, EmsPointConfig pointConfig) { - if ("calc".equalsIgnoreCase(StringUtils.defaultString(pointType))) { - return SITE_LEVEL_CALC_DEVICE_ID; - } - if (StringUtils.isNotBlank(requestDeviceId)) { - return StringUtils.trim(requestDeviceId); - } - if (pointConfig != null && StringUtils.isNotBlank(pointConfig.getDeviceId())) { - return StringUtils.trim(pointConfig.getDeviceId()); - } - return null; - } - private BigDecimal convertPointValue(BigDecimal sourceValue, EmsPointConfig pointConfig) { if (sourceValue == null || pointConfig == null) { return sourceValue; diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStrategyRuntimeConfigServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStrategyRuntimeConfigServiceImpl.java index 2f19806..280e7be 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStrategyRuntimeConfigServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStrategyRuntimeConfigServiceImpl.java @@ -26,6 +26,12 @@ public class EmsStrategyRuntimeConfigServiceImpl implements IEmsStrategyRuntimeC private static final BigDecimal DEFAULT_ANTI_REVERSE_UP = new BigDecimal("100"); private static final BigDecimal DEFAULT_ANTI_REVERSE_POWER_DOWN_PERCENT = new BigDecimal("10"); private static final BigDecimal DEFAULT_ANTI_REVERSE_HARD_STOP_THRESHOLD = new BigDecimal("20"); + private static final BigDecimal DEFAULT_POWER_SET_MULTIPLIER = new BigDecimal("10"); + private static final Integer DEFAULT_PROTECT_INTERVENE_ENABLE = 1; + private static final BigDecimal DEFAULT_PROTECT_L1_DERATE_PERCENT = new BigDecimal("50"); + private static final Integer DEFAULT_PROTECT_RECOVERY_STABLE_SECONDS = 5; + private static final Integer DEFAULT_PROTECT_L3_LATCH_ENABLE = 1; + private static final String DEFAULT_PROTECT_CONFLICT_POLICY = "MAX_LEVEL_WIN"; @Autowired private EmsStrategyRuntimeConfigMapper runtimeConfigMapper; @@ -83,5 +89,23 @@ public class EmsStrategyRuntimeConfigServiceImpl implements IEmsStrategyRuntimeC if (config.getAntiReverseHardStopThreshold() == null) { config.setAntiReverseHardStopThreshold(DEFAULT_ANTI_REVERSE_HARD_STOP_THRESHOLD); } + if (config.getPowerSetMultiplier() == null || config.getPowerSetMultiplier().compareTo(BigDecimal.ZERO) <= 0) { + config.setPowerSetMultiplier(DEFAULT_POWER_SET_MULTIPLIER); + } + if (config.getProtectInterveneEnable() == null) { + config.setProtectInterveneEnable(DEFAULT_PROTECT_INTERVENE_ENABLE); + } + if (config.getProtectL1DeratePercent() == null || config.getProtectL1DeratePercent().compareTo(BigDecimal.ZERO) < 0) { + config.setProtectL1DeratePercent(DEFAULT_PROTECT_L1_DERATE_PERCENT); + } + if (config.getProtectRecoveryStableSeconds() == null || config.getProtectRecoveryStableSeconds() < 0) { + config.setProtectRecoveryStableSeconds(DEFAULT_PROTECT_RECOVERY_STABLE_SECONDS); + } + if (config.getProtectL3LatchEnable() == null) { + config.setProtectL3LatchEnable(DEFAULT_PROTECT_L3_LATCH_ENABLE); + } + if (StringUtils.isEmpty(config.getProtectConflictPolicy())) { + config.setProtectConflictPolicy(DEFAULT_PROTECT_CONFLICT_POLICY); + } } } diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/GeneralQueryServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/GeneralQueryServiceImpl.java index 7f66cf2..990be5c 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/GeneralQueryServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/GeneralQueryServiceImpl.java @@ -49,15 +49,12 @@ public class GeneralQueryServiceImpl implements IGeneralQueryService if (siteIds == null || siteIds.isEmpty()) { return Collections.emptyList(); } - - String deviceCategory = request.getDeviceCategory(); - String deviceId = request.getDeviceId(); - if ((deviceCategory == null || "".equals(deviceCategory.trim())) - && (deviceId == null || "".equals(deviceId.trim()))) { - return Collections.emptyList(); - } - - return emsPointConfigMapper.getPointNameList(siteIds, deviceCategory, deviceId, request.getPointName()); + return emsPointConfigMapper.getPointNameList( + siteIds, + request.getDeviceCategory(), + request.getDeviceId(), + request.getPointName() + ); } @Override @@ -94,15 +91,10 @@ public class GeneralQueryServiceImpl implements IGeneralQueryService } String deviceCategory = request.getDeviceCategory(); - String requestDeviceId = request.getDeviceId(); - if ((deviceCategory == null || "".equals(deviceCategory.trim())) - && (requestDeviceId == null || "".equals(requestDeviceId.trim())) - ) { - return Collections.emptyList(); - } + List pointIds = resolvePointIds(request); List pointNames = resolvePointNames(request); - if (pointNames.isEmpty()) { + if (pointIds.isEmpty() && pointNames.isEmpty()) { return Collections.emptyList(); } @@ -114,17 +106,19 @@ public class GeneralQueryServiceImpl implements IGeneralQueryService endDate = DateUtils.adjustToEndOfDay(request.getEndDate()); } - List selectedDeviceIds = resolveSelectedDeviceIds(request); + List selectedDeviceIds = pointIds.isEmpty() ? resolveSelectedDeviceIds(request) : Collections.emptyList(); List pointConfigs = emsPointConfigMapper.getConfigListForGeneralQuery( - siteIds, deviceCategory, pointNames, selectedDeviceIds + siteIds, deviceCategory, pointIds, pointNames, selectedDeviceIds ); if (pointConfigs == null || pointConfigs.isEmpty()) { return Collections.emptyList(); } + Map selectedPointNameById = buildSelectedPointNameById(request); List dataVoList = new ArrayList<>(); for (EmsPointConfig pointConfig : pointConfigs) { - dataVoList.addAll(queryPointCurve(pointConfig, request.getDataUnit(), startDate, endDate)); + String selectedPointName = selectedPointNameById.get(resolveInfluxPointKey(pointConfig)); + dataVoList.addAll(queryPointCurve(pointConfig, request.getDataUnit(), startDate, endDate, selectedPointName)); } if (dataVoList.isEmpty()) { @@ -154,6 +148,19 @@ public class GeneralQueryServiceImpl implements IGeneralQueryService return names.stream().distinct().collect(Collectors.toList()); } + private List resolvePointIds(PointNameRequest request) { + List ids = new ArrayList<>(); + if (request.getPointIds() != null && !request.getPointIds().isEmpty()) { + ids.addAll(request.getPointIds()); + } else if (request.getPointId() != null && !"".equals(request.getPointId().trim())) { + ids.addAll(Arrays.stream(request.getPointId().split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList())); + } + return ids.stream().distinct().collect(Collectors.toList()); + } + private List resolveSelectedDeviceIds(PointNameRequest request) { List selected = new ArrayList<>(); if (request.getDeviceId() != null && !"".equals(request.getDeviceId().trim())) { @@ -175,16 +182,18 @@ public class GeneralQueryServiceImpl implements IGeneralQueryService return selected.stream().distinct().collect(Collectors.toList()); } - private List queryPointCurve(EmsPointConfig config, int dataUnit, Date startDate, Date endDate) { - if (config == null || config.getSiteId() == null || config.getDeviceId() == null) { + private List queryPointCurve(EmsPointConfig config, int dataUnit, Date startDate, Date endDate, + String selectedPointName) { + if (config == null || config.getSiteId() == null) { return Collections.emptyList(); } String influxPointKey = resolveInfluxPointKey(config); if (influxPointKey == null) { return Collections.emptyList(); } - List values = influxPointDataWriter.queryCurveData( - config.getSiteId(), config.getDeviceId(), influxPointKey, startDate, endDate + // 与点位列表曲线保持一致:按 siteId + pointKey 查询,避免 deviceId 维度导致综合查询漏数 + List values = influxPointDataWriter.queryCurveDataByPointKey( + config.getSiteId(), influxPointKey, startDate, endDate ); if (values == null || values.isEmpty()) { return Collections.emptyList(); @@ -197,7 +206,7 @@ public class GeneralQueryServiceImpl implements IGeneralQueryService } List result = new ArrayList<>(); - String displayDeviceId = buildDisplayDeviceId(config); + String displayDeviceId = buildDisplayDeviceId(config, selectedPointName); for (Map.Entry entry : latestByBucket.entrySet()) { GeneralQueryDataVo vo = new GeneralQueryDataVo(); vo.setSiteId(config.getSiteId()); @@ -219,10 +228,32 @@ public class GeneralQueryServiceImpl implements IGeneralQueryService return null; } - private String buildDisplayDeviceId(EmsPointConfig config) { + private String buildDisplayDeviceId(EmsPointConfig config, String selectedPointName) { + if (selectedPointName != null && !"".equals(selectedPointName.trim())) { + return selectedPointName.trim(); + } String pointName = config.getPointName() == null || "".equals(config.getPointName().trim()) ? config.getDataKey() : config.getPointName().trim(); - return config.getDeviceId() + "-" + pointName; + return pointName; + } + + private Map buildSelectedPointNameById(PointNameRequest request) { + Map selectedNameById = new HashMap<>(); + if (request == null || request.getPointIds() == null || request.getPointNames() == null) { + return selectedNameById; + } + List pointIds = request.getPointIds(); + List pointNames = request.getPointNames(); + int size = Math.min(pointIds.size(), pointNames.size()); + for (int i = 0; i < size; i++) { + String pointId = pointIds.get(i); + String pointName = pointNames.get(i); + if (pointId == null || "".equals(pointId.trim()) || pointName == null || "".equals(pointName.trim())) { + continue; + } + selectedNameById.put(pointId.trim(), pointName.trim()); + } + return selectedNameById; } private String formatByDataUnit(Date dataTime, int dataUnit) { diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/SingleSiteServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/SingleSiteServiceImpl.java index 937bea0..f1618a1 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/SingleSiteServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/SingleSiteServiceImpl.java @@ -14,7 +14,7 @@ import com.xzzn.ems.domain.EmsCoolingData; import com.xzzn.ems.domain.EmsDhData; import com.xzzn.ems.domain.EmsEmsData; import com.xzzn.ems.domain.EmsPcsBranchData; -import com.xzzn.ems.domain.EmsPcsData; +import com.xzzn.ems.domain.EmsSiteMonitorPointMatch; import com.xzzn.ems.domain.EmsStrategyTemp; import com.xzzn.ems.domain.EmsXfData; import com.xzzn.ems.domain.vo.*; @@ -28,12 +28,15 @@ import com.xzzn.ems.mapper.EmsDevicesSettingMapper; import com.xzzn.ems.mapper.EmsEnergyPriceConfigMapper; import com.xzzn.ems.mapper.EmsPcsDataMapper; import com.xzzn.ems.mapper.EmsPointMatchMapper; +import com.xzzn.ems.mapper.EmsSiteMonitorPointMatchMapper; import com.xzzn.ems.mapper.EmsStrategyRunningMapper; import com.xzzn.ems.mapper.EmsStrategyTempMapper; import com.xzzn.ems.service.IEmsEnergyPriceConfigService; import com.xzzn.ems.service.ISingleSiteService; +import com.xzzn.ems.service.InfluxPointDataWriter; import com.xzzn.ems.utils.DevicePointMatchDataProcessor; +import java.beans.PropertyDescriptor; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; @@ -42,6 +45,7 @@ import java.time.LocalTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -53,7 +57,9 @@ import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeanWrapperImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; @@ -65,6 +71,25 @@ import org.springframework.util.CollectionUtils; public class SingleSiteServiceImpl implements ISingleSiteService { private static final Logger log = LoggerFactory.getLogger(SingleSiteServiceImpl.class); + private static final String RUNNING_GRAPH_DEBUG = "RunningGraphDebug"; + private static final String FIELD_CURVE_PCS_ACTIVE_POWER = "SBJK_SSYX__curvePcsActivePower"; + private static final String FIELD_CURVE_PCS_REACTIVE_POWER = "SBJK_SSYX__curvePcsReactivePower"; + private static final String FIELD_CURVE_PCS_MAX_TEMP = "SBJK_SSYX__curvePcsMaxTemp"; + private static final String FIELD_CURVE_BATTERY_AVE_SOC = "SBJK_SSYX__curveBatteryAveSoc"; + private static final String FIELD_CURVE_BATTERY_AVE_TEMP = "SBJK_SSYX__curveBatteryAveTemp"; + private static final String FIELD_TOTAL_ACTIVE_POWER = "SBJK_SSYX__totalActivePower"; + private static final String FIELD_TOTAL_REACTIVE_POWER = "SBJK_SSYX__totalReactivePower"; + private static final String FIELD_SOC = "SBJK_SSYX__soc"; + private static final String FIELD_HOME_AVG_TEMP = "HOME__avgTemp"; + private static final String RUNNING_GRAPH_DEFAULT_DEVICE = "SITE"; + private static final int USE_FIXED_DISPLAY_YES = 1; + private static final String DEVICE_INFO_ID = "id"; + private static final String DEVICE_INFO_NAME = "deviceName"; + private static final String DEVICE_INFO_COMM_STATUS = "communicationStatus"; + private static final String DEVICE_INFO_DEVICE_STATUS = "deviceStatus"; + private static final Set PCS_DETAIL_META_FIELDS = new HashSet<>(Arrays.asList( + "siteId", "deviceId", "deviceName", "alarmNum", "pcsBranchInfoList", "dataUpdateTime" + )); private static final String CLUSTER_DATA_TEP = "温度"; @@ -90,6 +115,10 @@ public class SingleSiteServiceImpl implements ISingleSiteService { private EmsDevicesSettingMapper emsDevicesSettingMapper; @Autowired private EmsPointMatchMapper emsPointMatchMapper; + @Autowired + private EmsSiteMonitorPointMatchMapper emsSiteMonitorPointMatchMapper; + @Autowired + private InfluxPointDataWriter influxPointDataWriter; @Autowired private RedisCache redisCache; @@ -217,46 +246,61 @@ public class SingleSiteServiceImpl implements ISingleSiteService { public SiteMonitorRuningInfoVo getRunningGraphStorage(RunningGraphRequest request) { SiteMonitorRuningInfoVo siteMonitorRuningInfoVo = new SiteMonitorRuningInfoVo(); if (Objects.isNull(request) || StringUtils.isEmpty(request.getSiteId())) { + log.info("{} storage skip, request invalid, request={}", RUNNING_GRAPH_DEBUG, request); return siteMonitorRuningInfoVo; } -// // 时间暂定今日+昨日 -// Date today = DateUtils.getNowDate(); -// Date yesterday = DateUtils.addDays(today, -1); - Date startDate = request.getStartDate(); - Date endDate = request.getEndDate(); + String siteId = request.getSiteId(); + Date[] dateRange = normalizeRunningGraphDateRange(request.getStartDate(), request.getEndDate()); + Date startDate = dateRange[0]; + Date endDate = dateRange[1]; + List mappingList = emsSiteMonitorPointMatchMapper.selectBySiteId(siteId); + Map mappingByFieldAndDevice = buildMonitorPointMappingByFieldAndDevice(mappingList); + List pcsDeviceList = getDeviceMetaListByCategory(siteId, DeviceCategory.PCS.getCode()); + if (CollectionUtils.isEmpty(pcsDeviceList)) { + pcsDeviceList = Collections.singletonList(new DeviceMeta(RUNNING_GRAPH_DEFAULT_DEVICE, RUNNING_GRAPH_DEFAULT_DEVICE)); + } - //pcs有功无功 List pcsPowerList = new ArrayList<>(); - List energyStoragePowList = emsPcsDataMapper.getStoragePowerList(request.getSiteId(), startDate, endDate); + for (DeviceMeta pcsDevice : pcsDeviceList) { + String deviceId = StringUtils.defaultString(pcsDevice.getDeviceId()); + String activePointId = firstNonBlankPointByDevice(mappingByFieldAndDevice, deviceId, FIELD_CURVE_PCS_ACTIVE_POWER); + String reactivePointId = firstNonBlankPointByDevice(mappingByFieldAndDevice, deviceId, FIELD_CURVE_PCS_REACTIVE_POWER); - // List -> 按pcs的deviceId分组转成List - if (!CollectionUtils.isEmpty(energyStoragePowList)) { - Map> dataMap = energyStoragePowList.stream() - .collect(Collectors.groupingBy( - EnergyStoragePowVo::getDeviceId, - Collectors.toList())); + List activeValues = queryInfluxPointValues(siteId, activePointId, startDate, endDate); + List reactiveValues = queryInfluxPointValues(siteId, reactivePointId, startDate, endDate); + Map reactiveByTs = reactiveValues.stream() + .filter(v -> v != null && v.getDataTime() != null && v.getPointValue() != null) + .collect(Collectors.toMap(v -> v.getDataTime().getTime(), InfluxPointDataWriter.PointValue::getPointValue, (a, b) -> b)); - pcsPowerList = dataMap.entrySet().stream() - .map(entry -> { - PcsPowerList pcdData = new PcsPowerList(); - pcdData.setDeviceId(entry.getKey()); - pcdData.setEnergyStoragePowList(entry.getValue()); - return pcdData; - }).collect(Collectors.toList()); + List energyStoragePowList = new ArrayList<>(); + for (InfluxPointDataWriter.PointValue activeValue : activeValues) { + if (activeValue == null || activeValue.getDataTime() == null || activeValue.getPointValue() == null) { + continue; + } + Date dataTime = activeValue.getDataTime(); + EnergyStoragePowVo vo = new EnergyStoragePowVo(); + vo.setDeviceId(deviceId); + vo.setDateDay(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, dataTime)); + vo.setCreateDate(DateUtils.parseDateToStr("HH:mm:00", dataTime)); + vo.setGroupTime(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, dataTime)); + vo.setPcsTotalActPower(activeValue.getPointValue()); + vo.setPcsTotalReactivePower(reactiveByTs.get(dataTime.getTime())); + energyStoragePowList.add(vo); + } -// // 生成时间列表(每5分钟一个) -// List targetMinutes = new ArrayList<>(12); -// LocalDateTime startLocalDate = DateUtils.toLocalDateTime(startDate).truncatedTo(ChronoUnit.DAYS); -// LocalDateTime endLocalDate = DateUtils.toLocalDateTime(endDate).with(LocalDateTime.now().toLocalTime()); -// while (startLocalDate.isBefore(endLocalDate)) { -// targetMinutes.add(startLocalDate); -// startLocalDate = startLocalDate.plusMinutes(5); // 递增5分钟 -// } -// // 根据时间列表填充数据 -// pcsPowerList = fullFillData(pcsPowerList, targetMinutes); + PcsPowerList pcdData = new PcsPowerList(); + pcdData.setDeviceId(deviceId); + pcdData.setEnergyStoragePowList(energyStoragePowList); + pcsPowerList.add(pcdData); } siteMonitorRuningInfoVo.setPcsPowerList(pcsPowerList); + int pointCount = pcsPowerList.stream() + .filter(item -> item != null && item.getEnergyStoragePowList() != null) + .mapToInt(item -> item.getEnergyStoragePowList().size()) + .sum(); + log.info("{} storage, siteId={}, startDate={}, endDate={}, deviceCount={}, pointCount={}", + RUNNING_GRAPH_DEBUG, siteId, startDate, endDate, pcsPowerList.size(), pointCount); return siteMonitorRuningInfoVo; } @@ -316,49 +360,52 @@ public class SingleSiteServiceImpl implements ISingleSiteService { @Override public SiteMonitorRuningInfoVo getRunningGraphPcsMaxTemp(RunningGraphRequest request) { SiteMonitorRuningInfoVo siteMonitorRuningInfoVo = new SiteMonitorRuningInfoVo(); - List pcsMaxTempList = new ArrayList<>(); -// // 时间暂定今日+昨日 -// Date today = new Date(); -// Date yesterday = DateUtils.addDays(today, -1); + if (Objects.isNull(request) || StringUtils.isEmpty(request.getSiteId())) { + log.info("{} pcsMaxTemp skip, request invalid, request={}", RUNNING_GRAPH_DEBUG, request); + return siteMonitorRuningInfoVo; + } String siteId = request.getSiteId(); - Date startDate = request.getStartDate(); - Date endDate = request.getEndDate(); - //PCS最高温度list - List pcsMaxTempVos = emsPcsDataMapper.getPcsMaxTemp(siteId, startDate, endDate); -// if (SiteEnum.FX.getCode().equals(siteId)) { -// pcsMaxTempVos = emsPcsDataMapper.getFXMaxTemp(siteId, startDate, endDate); -// } else if (SiteEnum.DDS.getCode().equals(siteId)) { -// pcsMaxTempVos = emsPcsDataMapper.getDDSMaxTemp(siteId, startDate, endDate); -// } + Date[] dateRange = normalizeRunningGraphDateRange(request.getStartDate(), request.getEndDate()); + Date startDate = dateRange[0]; + Date endDate = dateRange[1]; + List mappingList = emsSiteMonitorPointMatchMapper.selectBySiteId(siteId); + Map mappingByFieldAndDevice = buildMonitorPointMappingByFieldAndDevice(mappingList); + List pcsDeviceList = getDeviceMetaListByCategory(siteId, DeviceCategory.PCS.getCode()); + if (CollectionUtils.isEmpty(pcsDeviceList)) { + pcsDeviceList = Collections.singletonList(new DeviceMeta(RUNNING_GRAPH_DEFAULT_DEVICE, RUNNING_GRAPH_DEFAULT_DEVICE)); + } - // List -> 按pcs的deviceId分组转成List - if (!CollectionUtils.isEmpty(pcsMaxTempVos)) { - Map> dataMap = pcsMaxTempVos.stream() - .collect(Collectors.groupingBy( - PcsMaxTempVo::getDeviceId, - Collectors.toList())); - - pcsMaxTempList = dataMap.entrySet().stream() - .map(entry -> { - PcsMaxTempList pcdData = new PcsMaxTempList(); - pcdData.setDeviceId(entry.getKey()); - pcdData.setMaxTempVoList(entry.getValue()); - return pcdData; - }).collect(Collectors.toList()); - -// // 生成时间列表(每小时一个) -// List targetHours = new ArrayList<>(60); -// LocalDateTime startDate = DateUtils.toLocalDateTime(yesterday).truncatedTo(ChronoUnit.DAYS); -// LocalDateTime endDate = DateUtils.toLocalDateTime(today); -// while (startDate.isBefore(endDate)) { -// targetHours.add(startDate); -// startDate = startDate.plusHours(1); // 递增1小时 -// } -// // 根据时间列表填充数据 -// pcsMaxTempList = fullFillMaxTempData(pcsMaxTempList,targetHours); + List pcsMaxTempList = new ArrayList<>(); + for (DeviceMeta pcsDevice : pcsDeviceList) { + String deviceId = StringUtils.defaultString(pcsDevice.getDeviceId()); + String pointId = firstNonBlankPointByDevice(mappingByFieldAndDevice, deviceId, FIELD_CURVE_PCS_MAX_TEMP); + List values = queryInfluxPointValues(siteId, pointId, startDate, endDate); + List pcsMaxTempVos = new ArrayList<>(); + for (InfluxPointDataWriter.PointValue value : values) { + if (value == null || value.getDataTime() == null || value.getPointValue() == null) { + continue; + } + Date dataTime = value.getDataTime(); + PcsMaxTempVo vo = new PcsMaxTempVo(); + vo.setDeviceId(deviceId); + vo.setDateDay(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, dataTime)); + vo.setCreateDate(DateUtils.parseDateToStr("HH:mm:00", dataTime)); + vo.setTemp(value.getPointValue()); + pcsMaxTempVos.add(vo); + } + PcsMaxTempList pcdData = new PcsMaxTempList(); + pcdData.setDeviceId(deviceId); + pcdData.setMaxTempVoList(pcsMaxTempVos); + pcsMaxTempList.add(pcdData); } siteMonitorRuningInfoVo.setPcsMaxTempList(pcsMaxTempList); + int pointCount = pcsMaxTempList.stream() + .filter(item -> item != null && item.getMaxTempVoList() != null) + .mapToInt(item -> item.getMaxTempVoList().size()) + .sum(); + log.info("{} pcsMaxTemp, siteId={}, startDate={}, endDate={}, deviceCount={}, pointCount={}", + RUNNING_GRAPH_DEBUG, siteId, startDate, endDate, pcsMaxTempList.size(), pointCount); return siteMonitorRuningInfoVo; } private List fullFillMaxTempData(List pcsMaxTempList, List targetHours) { @@ -415,14 +462,32 @@ public class SingleSiteServiceImpl implements ISingleSiteService { @Override public SiteMonitorRuningInfoVo getRunningGraphBatterySoc(RunningGraphRequest request) { SiteMonitorRuningInfoVo siteMonitorRuningInfoVo = new SiteMonitorRuningInfoVo(); - if (!StringUtils.isEmpty(request.getSiteId())) { -// // 时间暂定今日+昨日 -// Date today = new Date(); -// Date yesterday = DateUtils.addDays(today, -1); - //电池平均soclist - List batteryAveSOCList = emsBatteryStackMapper.getAveSocList(request.getSiteId(), request.getStartDate(), request.getEndDate()); - siteMonitorRuningInfoVo.setBatteryAveSOCList(batteryAveSOCList); + if (Objects.isNull(request) || StringUtils.isEmpty(request.getSiteId())) { + log.info("{} batteryAveSoc skip, request invalid, request={}", RUNNING_GRAPH_DEBUG, request); + return siteMonitorRuningInfoVo; } + Date[] dateRange = normalizeRunningGraphDateRange(request.getStartDate(), request.getEndDate()); + Date startDate = dateRange[0]; + Date endDate = dateRange[1]; + Map mappingByField = getMonitorPointMappingByField(request.getSiteId()); + String pointId = firstNonBlankPoint(mappingByField, FIELD_CURVE_BATTERY_AVE_SOC, FIELD_SOC); + List values = queryInfluxPointValues(request.getSiteId(), pointId, startDate, endDate); + List batteryAveSOCList = new ArrayList<>(); + for (InfluxPointDataWriter.PointValue value : values) { + if (value == null || value.getDataTime() == null || value.getPointValue() == null) { + continue; + } + Date dataTime = value.getDataTime(); + BatteryAveSOCVo vo = new BatteryAveSOCVo(); + vo.setDateDay(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, dataTime)); + vo.setCreateDate(DateUtils.parseDateToStr("HH:mm:00", dataTime)); + vo.setBatterySOC(value.getPointValue()); + batteryAveSOCList.add(vo); + } + siteMonitorRuningInfoVo.setBatteryAveSOCList(batteryAveSOCList); + int pointCount = CollectionUtils.isEmpty(batteryAveSOCList) ? 0 : batteryAveSOCList.size(); + log.info("{} batteryAveSoc, siteId={}, startDate={}, endDate={}, pointId={}, pointCount={}", + RUNNING_GRAPH_DEBUG, request.getSiteId(), startDate, endDate, pointId, pointCount); return siteMonitorRuningInfoVo; } // 获取单站监控实时运行-电池平均温度 @@ -430,40 +495,178 @@ public class SingleSiteServiceImpl implements ISingleSiteService { public SiteMonitorRuningInfoVo getRunningGraphBatteryTemp(RunningGraphRequest request) { SiteMonitorRuningInfoVo siteMonitorRuningInfoVo = new SiteMonitorRuningInfoVo(); if (Objects.isNull(request) || StringUtils.isEmpty(request.getSiteId())) { + log.info("{} batteryAveTemp skip, request invalid, request={}", RUNNING_GRAPH_DEBUG, request); return siteMonitorRuningInfoVo; } String siteId = request.getSiteId(); - Date startDate = request.getStartDate(); - Date endDate = request.getEndDate(); - //电池平均温度list,优先从电池堆取,电池堆没有的话再从电池簇取 + Date[] dateRange = normalizeRunningGraphDateRange(request.getStartDate(), request.getEndDate()); + Date startDate = dateRange[0]; + Date endDate = dateRange[1]; + Map mappingByField = getMonitorPointMappingByField(siteId); + String pointId = firstNonBlankPoint(mappingByField, FIELD_CURVE_BATTERY_AVE_TEMP, FIELD_HOME_AVG_TEMP); + List values = queryInfluxPointValues(siteId, pointId, startDate, endDate); List batteryAveTempList = new ArrayList<>(); - batteryAveTempList = emsBatteryStackMapper.getBatteryAveTempList(siteId, startDate, endDate); - // 电池堆暂无数据,从电池簇取 - if (CollectionUtils.isEmpty(batteryAveTempList)) { - batteryAveTempList = emsBatteryClusterMapper.getBatteryAveTempList(siteId, startDate, endDate); + for (InfluxPointDataWriter.PointValue value : values) { + if (value == null || value.getDataTime() == null || value.getPointValue() == null) { + continue; + } + Date dataTime = value.getDataTime(); + BatteryAveTempVo vo = new BatteryAveTempVo(); + vo.setDateDay(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, dataTime)); + vo.setCreateDate(DateUtils.parseDateToStr("HH:mm:00", dataTime)); + vo.setBatteryTemp(value.getPointValue()); + batteryAveTempList.add(vo); } -// if (SiteEnum.FX.getCode().equals(siteId)) { -// batteryAveTempList = emsBatteryClusterMapper.getBatteryAveTempList(siteId, startDate, endDate); -// } else if (SiteEnum.DDS.getCode().equals(siteId)) { -// batteryAveTempList = emsBatteryStackMapper.getBatteryAveTempList(siteId, startDate, endDate); -// } siteMonitorRuningInfoVo.setBatteryAveTempList(batteryAveTempList); -// if (!StringUtils.isEmpty(siteId)) { -// // 时间暂定今日+昨日 -// Date today = new Date(); -// Date yesterday = DateUtils.addDays(today, -1); -// //电池平均温度list -// List batteryAveTempList = new ArrayList<>(); -// if (SiteEnum.FX.getCode().equals(siteId)) { -// batteryAveTempList = emsBatteryClusterMapper.getBatteryAveTempList(siteId, yesterday, today); -// } else if (SiteEnum.DDS.getCode().equals(siteId)) { -// batteryAveTempList = emsBatteryStackMapper.getBatteryAveTempList(siteId, yesterday, today); -// } -// siteMonitorRuningInfoVo.setBatteryAveTempList(batteryAveTempList); -// } + int pointCount = CollectionUtils.isEmpty(batteryAveTempList) ? 0 : batteryAveTempList.size(); + log.info("{} batteryAveTemp, siteId={}, startDate={}, endDate={}, pointId={}, pointCount={}", + RUNNING_GRAPH_DEBUG, siteId, startDate, endDate, pointId, pointCount); return siteMonitorRuningInfoVo; } + private Date[] normalizeRunningGraphDateRange(Date startDate, Date endDate) { + Date normalizedStart = startDate; + Date normalizedEnd = endDate; + if (normalizedStart == null || normalizedEnd == null) { + Date today = DateUtils.getNowDate(); + Date yesterday = DateUtils.addDays(today, -1); + normalizedStart = normalizedStart == null ? yesterday : normalizedStart; + normalizedEnd = normalizedEnd == null ? today : normalizedEnd; + } + LocalDate startDay = DateUtils.toLocalDateTime(normalizedStart).toLocalDate(); + LocalDate endDay = DateUtils.toLocalDateTime(normalizedEnd).toLocalDate(); + Date dayStart = DateUtils.toDate(startDay.atStartOfDay()); + Date dayEnd = DateUtils.toDate(endDay.plusDays(1).atStartOfDay().minusNanos(1_000_000)); + return new Date[]{dayStart, dayEnd}; + } + + private Map getMonitorPointMappingByField(String siteId) { + if (StringUtils.isBlank(siteId)) { + return Collections.emptyMap(); + } + List mappingList = emsSiteMonitorPointMatchMapper.selectBySiteId(siteId.trim()); + if (CollectionUtils.isEmpty(mappingList)) { + return Collections.emptyMap(); + } + return mappingList.stream() + .filter(item -> item != null && StringUtils.isNotBlank(item.getFieldCode())) + .collect(Collectors.toMap( + item -> item.getFieldCode().trim(), + item -> item, + (a, b) -> b + )); + } + + private Map buildMonitorPointMappingByFieldAndDevice(List mappingList) { + if (CollectionUtils.isEmpty(mappingList)) { + return Collections.emptyMap(); + } + return mappingList.stream() + .filter(item -> item != null && StringUtils.isNotBlank(item.getFieldCode())) + .collect(Collectors.toMap( + item -> buildFieldDeviceKey(item.getFieldCode(), item.getDeviceId()), + item -> item, + (a, b) -> b + )); + } + + private String buildFieldDeviceKey(String fieldCode, String deviceId) { + return StringUtils.defaultString(fieldCode).trim() + "|" + StringUtils.defaultString(deviceId).trim(); + } + + private String firstNonBlankPointByDevice(Map mappingByFieldAndDevice, + String deviceId, String... fieldCodes) { + if (mappingByFieldAndDevice == null || fieldCodes == null) { + return null; + } + String normalizedDeviceId = StringUtils.defaultString(deviceId).trim(); + for (String fieldCode : fieldCodes) { + if (StringUtils.isBlank(fieldCode)) { + continue; + } + EmsSiteMonitorPointMatch exact = mappingByFieldAndDevice.get(buildFieldDeviceKey(fieldCode, normalizedDeviceId)); + if (exact != null && StringUtils.isNotBlank(exact.getDataPoint())) { + return exact.getDataPoint().trim(); + } + EmsSiteMonitorPointMatch fallback = mappingByFieldAndDevice.get(buildFieldDeviceKey(fieldCode, "")); + if (fallback != null && StringUtils.isNotBlank(fallback.getDataPoint())) { + return fallback.getDataPoint().trim(); + } + } + return null; + } + + private List getDeviceMetaListByCategory(String siteId, String deviceCategory) { + if (StringUtils.isBlank(siteId) || StringUtils.isBlank(deviceCategory)) { + return Collections.emptyList(); + } + List> deviceList = emsDevicesSettingMapper.getDeviceInfosBySiteIdAndCategory(siteId, deviceCategory); + if (CollectionUtils.isEmpty(deviceList)) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (Map item : deviceList) { + if (item == null || item.get("id") == null) { + continue; + } + String deviceId = String.valueOf(item.get("id")).trim(); + if (StringUtils.isBlank(deviceId)) { + continue; + } + String deviceName = item.get("deviceName") == null ? deviceId : String.valueOf(item.get("deviceName")).trim(); + result.add(new DeviceMeta(deviceId, deviceName)); + } + result.sort(Comparator.comparing(DeviceMeta::getDeviceName, Comparator.nullsLast(String::compareTo)) + .thenComparing(DeviceMeta::getDeviceId, Comparator.nullsLast(String::compareTo))); + return result; + } + + private static class DeviceMeta { + private final String deviceId; + private final String deviceName; + + private DeviceMeta(String deviceId, String deviceName) { + this.deviceId = deviceId; + this.deviceName = deviceName; + } + + public String getDeviceId() { + return deviceId; + } + + public String getDeviceName() { + return deviceName; + } + } + + private String firstNonBlankPoint(Map mappingByField, String... fieldCodes) { + if (mappingByField == null || fieldCodes == null) { + return null; + } + for (String fieldCode : fieldCodes) { + if (StringUtils.isBlank(fieldCode)) { + continue; + } + EmsSiteMonitorPointMatch match = mappingByField.get(fieldCode); + if (match != null && StringUtils.isNotBlank(match.getDataPoint())) { + return match.getDataPoint().trim(); + } + } + return null; + } + + private List queryInfluxPointValues(String siteId, String pointId, Date startDate, Date endDate) { + if (StringUtils.isBlank(siteId) || StringUtils.isBlank(pointId) || startDate == null || endDate == null) { + return Collections.emptyList(); + } + List values = influxPointDataWriter.queryCurveDataByPointKey(siteId, pointId, startDate, endDate); + if (CollectionUtils.isEmpty(values)) { + return Collections.emptyList(); + } + values.sort(Comparator.comparing(InfluxPointDataWriter.PointValue::getDataTime, Comparator.nullsLast(Date::compareTo))); + return values; + } + // 根据site_id获取pcs详细数据+支路数据 @Override public List getPcsDetailInfo(String siteId) { @@ -472,17 +675,27 @@ public class SingleSiteServiceImpl implements ISingleSiteService { if (!StringUtils.isEmpty(siteId)) { // 获取该设备下所有pcs的id List> pcsIds = emsDevicesSettingMapper.getDeviceInfosBySiteIdAndCategory(siteId, DeviceCategory.PCS.getCode()); + List mappingList = emsSiteMonitorPointMatchMapper.selectBySiteId(siteId); + Map mappingByFieldAndDevice = buildMonitorPointMappingByFieldAndDevice(mappingList); + Map latestPointCache = new HashMap<>(); for (Map pcsDevice : pcsIds) { PcsDetailInfoVo pcsDetailInfoVo = new PcsDetailInfoVo(); - pcsDetailInfoVo.setDeviceName(pcsDevice.get("deviceName").toString()); - pcsDetailInfoVo.setCommunicationStatus(pcsDevice.get("communicationStatus") == null ? - "" :pcsDevice.get("communicationStatus").toString()); - // 从redis取pcs单个详细数据 - String pcsId = pcsDevice.get("id").toString(); - EmsPcsData pcsData = redisCache.getCacheObject(RedisKeyConstants.PCS +siteId+"_"+pcsId); - if (pcsData != null) { - BeanUtils.copyProperties(pcsData, pcsDetailInfoVo); + String pcsId = String.valueOf(pcsDevice.get(DEVICE_INFO_ID)); + pcsDetailInfoVo.setSiteId(siteId); + pcsDetailInfoVo.setDeviceId(pcsId); + pcsDetailInfoVo.setDeviceName(String.valueOf(pcsDevice.get(DEVICE_INFO_NAME))); + + fillPcsDetailByLatestPointMapping(siteId, pcsId, pcsDetailInfoVo, mappingByFieldAndDevice, latestPointCache); + if (StringUtils.isBlank(pcsDetailInfoVo.getCommunicationStatus())) { + pcsDetailInfoVo.setCommunicationStatus(pcsDevice.get(DEVICE_INFO_COMM_STATUS) == null + ? "" + : pcsDevice.get(DEVICE_INFO_COMM_STATUS).toString()); + } + if (StringUtils.isBlank(pcsDetailInfoVo.getDeviceStatus())) { + pcsDetailInfoVo.setDeviceStatus(pcsDevice.get(DEVICE_INFO_DEVICE_STATUS) == null + ? "" + : pcsDevice.get(DEVICE_INFO_DEVICE_STATUS).toString()); } // 支路信息数据 List pcsBranchInfoList = new ArrayList<>(); @@ -493,7 +706,6 @@ public class SingleSiteServiceImpl implements ISingleSiteService { // // 告警设备点位个数 // int alarmNum = emsPointMatchMapper.getDevicePointAlarmNum(siteId, pcsId, DeviceCategory.PCS.getCode()); pcsDetailInfoVo.setAlarmNum(alarmNum); - pcsDetailInfoVo.setDeviceStatus(pcsDevice.get("deviceStatus") == null ? "" : pcsDevice.get("deviceStatus").toString()); // 处理枚举匹配字段 devicePointMatchDataProcessor.convertFieldValueToEnumMatch(siteId, DeviceCategory.PCS.getCode(), pcsDetailInfoVo); @@ -504,6 +716,114 @@ public class SingleSiteServiceImpl implements ISingleSiteService { return pcsDetailInfoVoList; } + private void fillPcsDetailByLatestPointMapping(String siteId, + String deviceId, + PcsDetailInfoVo target, + Map mappingByFieldAndDevice, + Map latestPointCache) { + if (StringUtils.isAnyBlank(siteId, deviceId) || target == null || mappingByFieldAndDevice == null) { + return; + } + BeanWrapper beanWrapper = new BeanWrapperImpl(target); + Date latestDataTime = null; + for (PropertyDescriptor pd : beanWrapper.getPropertyDescriptors()) { + if (pd == null || StringUtils.isBlank(pd.getName()) || pd.getWriteMethod() == null) { + continue; + } + String fieldCode = pd.getName(); + if (PCS_DETAIL_META_FIELDS.contains(fieldCode)) { + continue; + } + EmsSiteMonitorPointMatch pointMatch = resolvePointMatchByFieldAndDevice(mappingByFieldAndDevice, fieldCode, deviceId); + if (pointMatch == null) { + continue; + } + Object fieldValue; + Date valueTime = null; + if (USE_FIXED_DISPLAY_YES == (pointMatch.getUseFixedDisplay() == null ? 0 : pointMatch.getUseFixedDisplay()) + && StringUtils.isNotBlank(pointMatch.getFixedDataPoint())) { + fieldValue = pointMatch.getFixedDataPoint().trim(); + } else { + InfluxPointDataWriter.PointValue latestValue = getLatestPointValueByPointId(siteId, pointMatch.getDataPoint(), latestPointCache); + if (latestValue == null || latestValue.getPointValue() == null) { + continue; + } + fieldValue = latestValue.getPointValue(); + valueTime = latestValue.getDataTime(); + } + Object convertedValue = convertFieldValueByType(fieldValue, pd.getPropertyType()); + if (convertedValue == null) { + continue; + } + beanWrapper.setPropertyValue(fieldCode, convertedValue); + if (valueTime != null && (latestDataTime == null || valueTime.after(latestDataTime))) { + latestDataTime = valueTime; + } + } + if (latestDataTime != null) { + target.setDataUpdateTime(latestDataTime); + } + } + + private EmsSiteMonitorPointMatch resolvePointMatchByFieldAndDevice(Map mappingByFieldAndDevice, + String fieldCode, + String deviceId) { + if (mappingByFieldAndDevice == null || StringUtils.isBlank(fieldCode)) { + return null; + } + String normalizedField = fieldCode.trim(); + String normalizedDeviceId = StringUtils.defaultString(deviceId).trim(); + EmsSiteMonitorPointMatch exact = mappingByFieldAndDevice.get(buildFieldDeviceKey(normalizedField, normalizedDeviceId)); + if (exact != null) { + return exact; + } + return mappingByFieldAndDevice.get(buildFieldDeviceKey(normalizedField, "")); + } + + private InfluxPointDataWriter.PointValue getLatestPointValueByPointId(String siteId, + String pointId, + Map latestPointCache) { + if (StringUtils.isAnyBlank(siteId, pointId)) { + return null; + } + String pointCacheKey = pointId.trim().toUpperCase(); + if (latestPointCache.containsKey(pointCacheKey)) { + return latestPointCache.get(pointCacheKey); + } + Date endTime = DateUtils.getNowDate(); + Date fastStartTime = DateUtils.addDays(endTime, -1); + InfluxPointDataWriter.PointValue latest = influxPointDataWriter.queryLatestPointValueByPointKey( + siteId.trim(), pointId.trim(), fastStartTime, endTime + ); + if (latest == null) { + Date fallbackStartTime = DateUtils.addDays(endTime, -7); + latest = influxPointDataWriter.queryLatestPointValueByPointKey( + siteId.trim(), pointId.trim(), fallbackStartTime, endTime + ); + } + latestPointCache.put(pointCacheKey, latest); + return latest; + } + + private Object convertFieldValueByType(Object rawValue, Class targetType) { + if (rawValue == null || targetType == null) { + return null; + } + if (BigDecimal.class.equals(targetType)) { + return StringUtils.getBigDecimal(rawValue); + } + if (String.class.equals(targetType)) { + if (rawValue instanceof BigDecimal) { + return ((BigDecimal) rawValue).stripTrailingZeros().toPlainString(); + } + return String.valueOf(rawValue); + } + if (Date.class.equals(targetType) && rawValue instanceof Date) { + return rawValue; + } + return null; + } + private void processBranchDataInfo(String siteId, String pcsId, List pcsBranchInfoList) { if (!StringUtils.isEmpty(pcsId)) { List pcsBranchData = redisCache.getCacheObject(RedisKeyConstants.BRANCH +siteId+"_"+pcsId); diff --git a/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml index bd43748..741f34d 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml @@ -213,7 +213,8 @@ - select id, site_id, field_code, data_point, fixed_data_point, use_fixed_display, create_by, create_time, update_by, update_time, remark + select id, site_id, field_code, device_id, data_point, fixed_data_point, use_fixed_display, create_by, create_time, update_by, update_time, remark from ems_site_monitor_point_match where site_id = #{siteId} order by id asc @@ -32,10 +33,10 @@ insert into ems_site_monitor_point_match - (site_id, field_code, data_point, fixed_data_point, use_fixed_display, create_by, create_time, update_by, update_time) + (site_id, field_code, device_id, data_point, fixed_data_point, use_fixed_display, create_by, create_time, update_by, update_time) values - (#{item.siteId}, #{item.fieldCode}, #{item.dataPoint}, #{item.fixedDataPoint}, #{item.useFixedDisplay}, #{item.createBy}, now(), #{item.updateBy}, now()) + (#{item.siteId}, #{item.fieldCode}, #{item.deviceId}, #{item.dataPoint}, #{item.fixedDataPoint}, #{item.useFixedDisplay}, #{item.createBy}, now(), #{item.updateBy}, now()) diff --git a/ems-system/src/main/resources/mapper/ems/EmsSiteSettingMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsSiteSettingMapper.xml index c7e0fa9..9a85642 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsSiteSettingMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsSiteSettingMapper.xml @@ -134,17 +134,17 @@ select distinct site_id from ems_site_setting - \ No newline at end of file + diff --git a/ems-system/src/main/resources/mapper/ems/EmsStrategyRuntimeConfigMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsStrategyRuntimeConfigMapper.xml index 89bc89e..cacdc13 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsStrategyRuntimeConfigMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsStrategyRuntimeConfigMapper.xml @@ -14,6 +14,12 @@ + + + + + + @@ -31,6 +37,12 @@ anti_reverse_up, anti_reverse_power_down_percent, anti_reverse_hard_stop_threshold, + power_set_multiplier, + protect_intervene_enable, + protect_l1_derate_percent, + protect_recovery_stable_seconds, + protect_l3_latch_enable, + protect_conflict_policy, create_by, create_time, update_by, @@ -56,6 +68,12 @@ anti_reverse_up, anti_reverse_power_down_percent, anti_reverse_hard_stop_threshold, + power_set_multiplier, + protect_intervene_enable, + protect_l1_derate_percent, + protect_recovery_stable_seconds, + protect_l3_latch_enable, + protect_conflict_policy, create_by, create_time, update_by, @@ -71,6 +89,12 @@ #{antiReverseUp}, #{antiReversePowerDownPercent}, #{antiReverseHardStopThreshold}, + #{powerSetMultiplier}, + #{protectInterveneEnable}, + #{protectL1DeratePercent}, + #{protectRecoveryStableSeconds}, + #{protectL3LatchEnable}, + #{protectConflictPolicy}, #{createBy}, #{createTime}, #{updateBy}, @@ -89,6 +113,12 @@ anti_reverse_up = #{antiReverseUp}, anti_reverse_power_down_percent = #{antiReversePowerDownPercent}, anti_reverse_hard_stop_threshold = #{antiReverseHardStopThreshold}, + power_set_multiplier = #{powerSetMultiplier}, + protect_intervene_enable = #{protectInterveneEnable}, + protect_l1_derate_percent = #{protectL1DeratePercent}, + protect_recovery_stable_seconds = #{protectRecoveryStableSeconds}, + protect_l3_latch_enable = #{protectL3LatchEnable}, + protect_conflict_policy = #{protectConflictPolicy}, update_by = #{updateBy}, update_time = #{updateTime}, remark = #{remark}, -- 2.49.0 From 88064730803fef542341ad779218f53c4bf9486a Mon Sep 17 00:00:00 2001 From: dashixiong Date: Mon, 16 Feb 2026 13:41:35 +0800 Subject: [PATCH 05/10] =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ems/EmsSiteMonitorController.java | 4 +- .../xzzn/quartz/task/ProtectionPlanTask.java | 158 ++++++++- .../com/xzzn/quartz/task/StrategyPoller.java | 3 - .../ems/domain/vo/BatteryDataStatsListVo.java | 44 +++ .../ems/domain/vo/ProtectionConstraintVo.java | 114 +++++++ .../domain/vo/ProtectionSettingsGroupVo.java | 38 +++ .../EmsSiteMonitorPointMatchMapper.java | 2 + .../impl/EmsDeviceSettingServiceImpl.java | 140 +++++++- .../service/impl/SingleSiteServiceImpl.java | 317 ++++++++++++++++-- .../mapper/ems/EmsDevicesSettingMapper.xml | 10 +- .../ems/EmsSiteMonitorPointMatchMapper.xml | 6 + 11 files changed, 783 insertions(+), 53 deletions(-) create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/vo/ProtectionConstraintVo.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/vo/ProtectionSettingsGroupVo.java diff --git a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java index b788985..1bd3fbe 100644 --- a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java +++ b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java @@ -208,9 +208,7 @@ public class EmsSiteMonitorController extends BaseController{ { startPage(); SiteBatteryDataList siteBatteryDataList = new SiteBatteryDataList(); - // 簇最大最小单体id数据 - List clusterBatteryDataList = iSingleSiteService.getClusterBatteryList(siteId,stackDeviceId,clusterDeviceId); - siteBatteryDataList.setClusterList(clusterBatteryDataList); + // 单体电池数据 List List = iSingleSiteService.getClusterDataInfoList(clusterDeviceId,siteId,stackDeviceId,batteryId); // 对batteryList进行分页处理 diff --git a/ems-quartz/src/main/java/com/xzzn/quartz/task/ProtectionPlanTask.java b/ems-quartz/src/main/java/com/xzzn/quartz/task/ProtectionPlanTask.java index 65f0ec2..ebd3baf 100644 --- a/ems-quartz/src/main/java/com/xzzn/quartz/task/ProtectionPlanTask.java +++ b/ems-quartz/src/main/java/com/xzzn/quartz/task/ProtectionPlanTask.java @@ -14,7 +14,9 @@ import com.xzzn.common.utils.StringUtils; import com.xzzn.ems.domain.EmsAlarmRecords; import com.xzzn.ems.domain.EmsFaultProtectionPlan; import com.xzzn.ems.domain.vo.ProtectionConstraintVo; +import com.xzzn.ems.domain.vo.ProtectionPlanVo; import com.xzzn.ems.domain.vo.ProtectionSettingVo; +import com.xzzn.ems.domain.vo.ProtectionSettingsGroupVo; import com.xzzn.ems.mapper.EmsAlarmRecordsMapper; import com.xzzn.ems.mapper.EmsFaultProtectionPlanMapper; import com.xzzn.ems.service.IEmsFaultProtectionPlanService; @@ -75,11 +77,12 @@ public class ProtectionPlanTask { if (StringUtils.isEmpty(siteId)) { continue; } - List protSettings = parseProtectionSettings(plan.getProtectionSettings()); - if (CollectionUtils.isEmpty(protSettings)) { + ProtectionSettingsGroupVo settingGroup = parseProtectionSettings(plan.getProtectionSettings()); + if (CollectionUtils.isEmpty(settingGroup.getFaultSettings()) + && CollectionUtils.isEmpty(settingGroup.getReleaseSettings())) { continue; } - dealWithProtectionPlan(plan, protSettings); + dealWithProtectionPlan(plan, settingGroup); } refreshProtectionConstraintCache(planList); } catch (Exception e) { @@ -88,20 +91,22 @@ public class ProtectionPlanTask { } @SyncAfterInsert - private void dealWithProtectionPlan(EmsFaultProtectionPlan plan, List protSettings) { + private void dealWithProtectionPlan(EmsFaultProtectionPlan plan, ProtectionSettingsGroupVo settingGroup) { logger.info("<轮询保护方案> 站点:{},方案ID:{}", plan.getSiteId(), plan.getId()); String siteId = plan.getSiteId(); Integer isAlertAlarm = plan.getIsAlert(); + List faultSettings = settingGroup.getFaultSettings(); + List releaseSettings = settingGroup.getReleaseSettings(); Long status = plan.getStatus(); if (status == null) { status = ProtPlanStatus.STOP.getCode(); } if (Objects.equals(status, ProtPlanStatus.STOP.getCode())) { - if (checkIsNeedIssuedPlan(protSettings, siteId)) { + if (checkIsNeedIssuedPlan(faultSettings, siteId)) { int faultDelay = safeDelaySeconds(plan.getFaultDelaySeconds(), 0); scheduledExecutorService.schedule(() -> { - if (!checkIsNeedIssuedPlan(protSettings, siteId)) { + if (!checkIsNeedIssuedPlan(faultSettings, siteId)) { return; } if (Integer.valueOf(1).equals(isAlertAlarm)) { @@ -117,7 +122,7 @@ public class ProtectionPlanTask { return; } - if (checkIsNeedCancelPlan(protSettings, siteId)) { + if (checkIsNeedCancelPlan(releaseSettings, siteId)) { int releaseDelay = safeDelaySeconds(plan.getReleaseDelaySeconds(), 0); scheduledExecutorService.schedule(() -> { if (Integer.valueOf(1).equals(isAlertAlarm)) { @@ -146,14 +151,58 @@ public class ProtectionPlanTask { return delay.intValue(); } - private List parseProtectionSettings(String settingsJson) { + private ProtectionSettingsGroupVo parseProtectionSettings(String settingsJson) { if (StringUtils.isEmpty(settingsJson)) { + return ProtectionSettingsGroupVo.empty(); + } + try { + if (settingsJson.trim().startsWith("[")) { + List legacy = objectMapper.readValue( + settingsJson, + new TypeReference>() {} + ); + ProtectionSettingsGroupVo groupVo = ProtectionSettingsGroupVo.empty(); + groupVo.setFaultSettings(legacy); + groupVo.setReleaseSettings(legacy); + return groupVo; + } + ProtectionSettingsGroupVo groupVo = objectMapper.readValue(settingsJson, ProtectionSettingsGroupVo.class); + if (groupVo == null) { + return ProtectionSettingsGroupVo.empty(); + } + if (groupVo.getFaultSettings() == null) { + groupVo.setFaultSettings(new ArrayList<>()); + } + if (groupVo.getReleaseSettings() == null) { + groupVo.setReleaseSettings(new ArrayList<>()); + } + return groupVo; + } catch (Exception e) { + logger.error("解析保护前提失败,json:{}", settingsJson, e); + return ProtectionSettingsGroupVo.empty(); + } + } + + private List parseProtectionPlan(String planJson) { + if (StringUtils.isEmpty(planJson)) { return new ArrayList<>(); } try { - return objectMapper.readValue(settingsJson, new TypeReference>() {}); + if (planJson.trim().startsWith("[")) { + List plans = objectMapper.readValue( + planJson, + new TypeReference>() {} + ); + return plans == null ? new ArrayList<>() : plans; + } + ProtectionPlanVo plan = objectMapper.readValue(planJson, ProtectionPlanVo.class); + List plans = new ArrayList<>(); + if (plan != null) { + plans.add(plan); + } + return plans; } catch (Exception e) { - logger.error("解析保护前提失败,json:{}", settingsJson, e); + logger.error("解析执行保护失败,json:{}", planJson, e); return new ArrayList<>(); } } @@ -256,9 +305,98 @@ public class ProtectionPlanTask { vo.setPowerLimitRatio(BigDecimal.ZERO); } + // 执行保护配置优先于描述文本配置 + List protectionPlans = parseProtectionPlan(plan.getProtectionPlan()); + applyCapabilityByProtectionPlan(vo, protectionPlans); + return vo; } + private void applyCapabilityByProtectionPlan(ProtectionConstraintVo vo, List protectionPlans) { + if (CollectionUtils.isEmpty(protectionPlans)) { + return; + } + for (ProtectionPlanVo item : protectionPlans) { + if (item == null) { + continue; + } + String marker = ((item.getPointName() == null ? "" : item.getPointName()) + " " + + (item.getPoint() == null ? "" : item.getPoint())).toLowerCase(); + if (StringUtils.isEmpty(marker)) { + continue; + } + + if (containsAny(marker, "降功率", "derate", "power_limit", "powerlimit")) { + BigDecimal ratio = parseDerateRatioByPlan(item); + if (ratio != null) { + vo.setPowerLimitRatio(minRatio(vo.getPowerLimitRatio(), ratio)); + } + } + + if (!isCapabilityEnabled(item.getValue())) { + continue; + } + + if (containsAny(marker, "禁止充放电", "forbid_charge_discharge", "disable_charge_discharge")) { + vo.setAllowCharge(false); + vo.setAllowDischarge(false); + continue; + } + if (containsAny(marker, "禁止充电", "forbid_charge", "disable_charge")) { + vo.setAllowCharge(false); + } + if (containsAny(marker, "禁止放电", "forbid_discharge", "disable_discharge")) { + vo.setAllowDischarge(false); + } + if (containsAny(marker, "允许充电", "allow_charge")) { + vo.setAllowCharge(true); + } + if (containsAny(marker, "允许放电", "allow_discharge")) { + vo.setAllowDischarge(true); + } + if (containsAny(marker, "待机", "standby")) { + vo.setForceStandby(true); + } + if (containsAny(marker, "关机", "停机", "切断", "shutdown", "stop")) { + vo.setForceStop(true); + vo.setForceStandby(true); + vo.setAllowCharge(false); + vo.setAllowDischarge(false); + vo.setPowerLimitRatio(BigDecimal.ZERO); + } + } + } + + private BigDecimal parseDerateRatioByPlan(ProtectionPlanVo planVo) { + BigDecimal value = planVo.getValue(); + if (value == null || value.compareTo(BigDecimal.ZERO) < 0) { + return null; + } + if (value.compareTo(BigDecimal.ONE) <= 0) { + return value; + } + if (value.compareTo(new BigDecimal("100")) <= 0) { + return value.divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP); + } + return null; + } + + private boolean isCapabilityEnabled(BigDecimal value) { + return value == null || value.compareTo(BigDecimal.ZERO) != 0; + } + + private boolean containsAny(String text, String... markers) { + if (StringUtils.isEmpty(text) || markers == null) { + return false; + } + for (String marker : markers) { + if (!StringUtils.isEmpty(marker) && text.contains(marker)) { + return true; + } + } + return false; + } + private BigDecimal parseDerateRatio(String text) { if (StringUtils.isEmpty(text)) { return null; diff --git a/ems-quartz/src/main/java/com/xzzn/quartz/task/StrategyPoller.java b/ems-quartz/src/main/java/com/xzzn/quartz/task/StrategyPoller.java index da7f50b..311feda 100644 --- a/ems-quartz/src/main/java/com/xzzn/quartz/task/StrategyPoller.java +++ b/ems-quartz/src/main/java/com/xzzn/quartz/task/StrategyPoller.java @@ -575,9 +575,6 @@ public class StrategyPoller { ChargeStatus targetStatus, EmsStrategyRuntimeConfig runtimeConfig, ProtectionConstraintVo constraint) { - if (!Integer.valueOf(1).equals(runtimeConfig.getProtectInterveneEnable())) { - return new StrategyCommandDecision(targetStatus, safePower(targetPower)); - } if (constraint == null || nullSafeInt(constraint.getLevel()) <= 0) { return new StrategyCommandDecision(targetStatus, safePower(targetPower)); } diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/BatteryDataStatsListVo.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/BatteryDataStatsListVo.java index 95cacf5..f323c09 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/vo/BatteryDataStatsListVo.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/BatteryDataStatsListVo.java @@ -24,6 +24,18 @@ public class BatteryDataStatsListVo { /** SOH (%) */ private BigDecimal soh; + + /** 电压映射点位ID */ + private String voltagePointId; + + /** 温度映射点位ID */ + private String temperaturePointId; + + /** SOC映射点位ID */ + private String socPointId; + + /** SOH映射点位ID */ + private String sohPointId; @JsonFormat(pattern = "yyyy-MM-dd") private Date dataTimestamp; @@ -71,6 +83,38 @@ public class BatteryDataStatsListVo { this.soh = soh; } + public String getVoltagePointId() { + return voltagePointId; + } + + public void setVoltagePointId(String voltagePointId) { + this.voltagePointId = voltagePointId; + } + + public String getTemperaturePointId() { + return temperaturePointId; + } + + public void setTemperaturePointId(String temperaturePointId) { + this.temperaturePointId = temperaturePointId; + } + + public String getSocPointId() { + return socPointId; + } + + public void setSocPointId(String socPointId) { + this.socPointId = socPointId; + } + + public String getSohPointId() { + return sohPointId; + } + + public void setSohPointId(String sohPointId) { + this.sohPointId = sohPointId; + } + public Date getDataTimestamp() { return dataTimestamp; } diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/ProtectionConstraintVo.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/ProtectionConstraintVo.java new file mode 100644 index 0000000..1499d27 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/ProtectionConstraintVo.java @@ -0,0 +1,114 @@ +package com.xzzn.ems.domain.vo; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * 站点保护约束(供策略轮询仲裁) + */ +public class ProtectionConstraintVo implements Serializable { + private static final long serialVersionUID = 1L; + + /** 最高保护等级:0-无保护,1/2/3 对应故障等级 */ + private Integer level; + + /** 是否允许充电 */ + private Boolean allowCharge; + + /** 是否允许放电 */ + private Boolean allowDischarge; + + /** 功率上限比例(0~1) */ + private BigDecimal powerLimitRatio; + + /** 是否强制待机 */ + private Boolean forceStandby; + + /** 是否强制停机 */ + private Boolean forceStop; + + /** 生效的方案ID列表 */ + private List sourcePlanIds; + + /** 更新时间戳(毫秒) */ + private Long updateAt; + + public Integer getLevel() { + return level; + } + + public void setLevel(Integer level) { + this.level = level; + } + + public Boolean getAllowCharge() { + return allowCharge; + } + + public void setAllowCharge(Boolean allowCharge) { + this.allowCharge = allowCharge; + } + + public Boolean getAllowDischarge() { + return allowDischarge; + } + + public void setAllowDischarge(Boolean allowDischarge) { + this.allowDischarge = allowDischarge; + } + + public BigDecimal getPowerLimitRatio() { + return powerLimitRatio; + } + + public void setPowerLimitRatio(BigDecimal powerLimitRatio) { + this.powerLimitRatio = powerLimitRatio; + } + + public Boolean getForceStandby() { + return forceStandby; + } + + public void setForceStandby(Boolean forceStandby) { + this.forceStandby = forceStandby; + } + + public Boolean getForceStop() { + return forceStop; + } + + public void setForceStop(Boolean forceStop) { + this.forceStop = forceStop; + } + + public List getSourcePlanIds() { + return sourcePlanIds; + } + + public void setSourcePlanIds(List sourcePlanIds) { + this.sourcePlanIds = sourcePlanIds; + } + + public Long getUpdateAt() { + return updateAt; + } + + public void setUpdateAt(Long updateAt) { + this.updateAt = updateAt; + } + + public static ProtectionConstraintVo empty() { + ProtectionConstraintVo vo = new ProtectionConstraintVo(); + vo.setLevel(0); + vo.setAllowCharge(true); + vo.setAllowDischarge(true); + vo.setPowerLimitRatio(BigDecimal.ONE); + vo.setForceStandby(false); + vo.setForceStop(false); + vo.setSourcePlanIds(new ArrayList<>()); + vo.setUpdateAt(System.currentTimeMillis()); + return vo; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/ProtectionSettingsGroupVo.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/ProtectionSettingsGroupVo.java new file mode 100644 index 0000000..3c48d55 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/ProtectionSettingsGroupVo.java @@ -0,0 +1,38 @@ +package com.xzzn.ems.domain.vo; + +import java.util.ArrayList; +import java.util.List; + +/** + * 告警保护方案-保护前提分组 + */ +public class ProtectionSettingsGroupVo { + /** 故障保护前提 */ + private List faultSettings; + + /** 释放保护前提 */ + private List releaseSettings; + + public static ProtectionSettingsGroupVo empty() { + ProtectionSettingsGroupVo vo = new ProtectionSettingsGroupVo(); + vo.setFaultSettings(new ArrayList<>()); + vo.setReleaseSettings(new ArrayList<>()); + return vo; + } + + public List getFaultSettings() { + return faultSettings; + } + + public void setFaultSettings(List faultSettings) { + this.faultSettings = faultSettings; + } + + public List getReleaseSettings() { + return releaseSettings; + } + + public void setReleaseSettings(List releaseSettings) { + this.releaseSettings = releaseSettings; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorPointMatchMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorPointMatchMapper.java index 0f001d6..47c5a94 100644 --- a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorPointMatchMapper.java +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorPointMatchMapper.java @@ -13,5 +13,7 @@ public interface EmsSiteMonitorPointMatchMapper { int deleteBySiteId(@Param("siteId") String siteId); + int deleteBySiteIdAndDeviceId(@Param("siteId") String siteId, @Param("deviceId") String deviceId); + int insertBatch(@Param("list") List list); } diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java index 3b9d351..38b0539 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java @@ -69,6 +69,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; /** @@ -215,9 +216,127 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService pcsSetting.setDeviceSettingId(devicesSetting.getId()); emsPcsSettingMapper.insertEmsPcsSetting(pcsSetting); } + initSiteMonitorPointMappingsForNewDevice(devicesSetting); return result; } + private void initSiteMonitorPointMappingsForNewDevice(DevicesSettingVo devicesSetting) { + if (devicesSetting == null) { + return; + } + String siteId = StringUtils.trim(devicesSetting.getSiteId()); + String deviceId = StringUtils.trim(devicesSetting.getDeviceId()); + String deviceCategory = StringUtils.trim(devicesSetting.getDeviceCategory()); + if (StringUtils.isAnyBlank(siteId, deviceId, deviceCategory)) { + return; + } + + List itemList = getEnabledMonitorItems(); + if (CollectionUtils.isEmpty(itemList)) { + return; + } + Set targetFieldCodes = itemList.stream() + .filter(Objects::nonNull) + .filter(item -> isDeviceDimensionItem(item)) + .filter(item -> StringUtils.equals(deviceCategory, resolveDeviceCategoryByItem(item))) + .map(EmsSiteMonitorItem::getFieldCode) + .filter(StringUtils::isNotBlank) + .map(String::trim) + .collect(Collectors.toSet()); + if (targetFieldCodes.isEmpty()) { + return; + } + + List existMappings = emsSiteMonitorPointMatchMapper.selectBySiteId(siteId); + if (existMappings == null) { + existMappings = Collections.emptyList(); + } + + Set deletedFieldCodes = existMappings.stream() + .filter(Objects::nonNull) + .filter(item -> DELETED_FIELD_MARK.equals(item.getDataPoint())) + .map(EmsSiteMonitorPointMatch::getFieldCode) + .filter(StringUtils::isNotBlank) + .map(String::trim) + .collect(Collectors.toSet()); + targetFieldCodes.removeAll(deletedFieldCodes); + if (targetFieldCodes.isEmpty()) { + return; + } + + Set existsFieldAndDevice = existMappings.stream() + .filter(Objects::nonNull) + .map(item -> buildMatchKey(StringUtils.trim(item.getFieldCode()), StringUtils.trim(item.getDeviceId()))) + .collect(Collectors.toSet()); + + String operName = StringUtils.defaultIfBlank(StringUtils.trim(devicesSetting.getCreateBy()), "system"); + List insertList = new ArrayList<>(); + for (String fieldCode : targetFieldCodes) { + String key = buildMatchKey(fieldCode, deviceId); + if (existsFieldAndDevice.contains(key)) { + continue; + } + EmsSiteMonitorPointMatch template = findTemplateMappingForNewDevice(existMappings, fieldCode); + EmsSiteMonitorPointMatch insertItem = new EmsSiteMonitorPointMatch(); + insertItem.setSiteId(siteId); + insertItem.setFieldCode(fieldCode); + insertItem.setDeviceId(deviceId); + insertItem.setDataPoint(template == null ? "" : StringUtils.defaultString(template.getDataPoint())); + insertItem.setFixedDataPoint(template == null ? null : StringUtils.trimToNull(template.getFixedDataPoint())); + insertItem.setUseFixedDisplay(template == null ? 0 : (template.getUseFixedDisplay() == null ? 0 : template.getUseFixedDisplay())); + insertItem.setCreateBy(operName); + insertItem.setUpdateBy(operName); + insertList.add(insertItem); + } + + if (insertList.isEmpty()) { + return; + } + emsSiteMonitorPointMatchMapper.insertBatch(insertList); + redisCache.deleteObject(buildSiteMonitorPointMatchRedisKey(siteId)); + clearSiteMonitorLatestCache(siteId); + projectDisplayCache.remove(siteId); + } + + private EmsSiteMonitorPointMatch findTemplateMappingForNewDevice(List mappings, String fieldCode) { + if (CollectionUtils.isEmpty(mappings) || StringUtils.isBlank(fieldCode)) { + return null; + } + String normalizedFieldCode = fieldCode.trim(); + EmsSiteMonitorPointMatch fallback = null; + for (EmsSiteMonitorPointMatch item : mappings) { + if (item == null || !normalizedFieldCode.equals(StringUtils.trim(item.getFieldCode()))) { + continue; + } + if (DELETED_FIELD_MARK.equals(item.getDataPoint())) { + continue; + } + if (StringUtils.isNotBlank(item.getDeviceId())) { + return item; + } + if (fallback == null) { + fallback = item; + } + } + return fallback; + } + + private String resolveDeviceCategoryByItem(EmsSiteMonitorItem item) { + if (item == null) { + return null; + } + String byMenu = StringUtils.trim(MENU_DEVICE_CATEGORY_MAP.get(StringUtils.trim(item.getMenuCode()))); + if (StringUtils.isNotBlank(byMenu)) { + return byMenu; + } + String fieldCode = StringUtils.trim(item.getFieldCode()); + if (StringUtils.isBlank(fieldCode) || !fieldCode.contains("__")) { + return null; + } + String menuCode = fieldCode.substring(0, fieldCode.indexOf("__")); + return StringUtils.trim(MENU_DEVICE_CATEGORY_MAP.get(StringUtils.trim(menuCode))); + } + /** * 更新设备 * @param devicesSetting @@ -277,9 +396,26 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService * @return 结果 */ @Override + @Transactional(rollbackFor = Exception.class) public int deleteEmsDevicesSettingById(Long id){ - - return emsDevicesMapper.deleteEmsDevicesSettingById(id); + if (id == null) { + return 0; + } + EmsDevicesSetting existDevice = emsDevicesMapper.selectEmsDevicesSettingById(id); + int rows = emsDevicesMapper.deleteEmsDevicesSettingById(id); + if (rows <= 0 || existDevice == null) { + return rows; + } + String siteId = StringUtils.trim(existDevice.getSiteId()); + String deviceId = StringUtils.trim(existDevice.getDeviceId()); + if (StringUtils.isAnyBlank(siteId, deviceId)) { + return rows; + } + emsSiteMonitorPointMatchMapper.deleteBySiteIdAndDeviceId(siteId, deviceId); + redisCache.deleteObject(buildSiteMonitorPointMatchRedisKey(siteId)); + clearSiteMonitorLatestCache(siteId); + projectDisplayCache.remove(siteId); + return rows; } @Override diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/SingleSiteServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/SingleSiteServiceImpl.java index f1618a1..2d8a8f1 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/SingleSiteServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/SingleSiteServiceImpl.java @@ -12,6 +12,7 @@ import com.xzzn.ems.domain.EmsBatteryData; import com.xzzn.ems.domain.EmsBatteryStack; import com.xzzn.ems.domain.EmsCoolingData; import com.xzzn.ems.domain.EmsDhData; +import com.xzzn.ems.domain.EmsDevicesSetting; import com.xzzn.ems.domain.EmsEmsData; import com.xzzn.ems.domain.EmsPcsBranchData; import com.xzzn.ems.domain.EmsSiteMonitorPointMatch; @@ -32,6 +33,7 @@ import com.xzzn.ems.mapper.EmsSiteMonitorPointMatchMapper; import com.xzzn.ems.mapper.EmsStrategyRunningMapper; import com.xzzn.ems.mapper.EmsStrategyTempMapper; import com.xzzn.ems.service.IEmsEnergyPriceConfigService; +import com.xzzn.ems.service.IEmsDeviceSettingService; import com.xzzn.ems.service.ISingleSiteService; import com.xzzn.ems.service.InfluxPointDataWriter; import com.xzzn.ems.utils.DevicePointMatchDataProcessor; @@ -90,6 +92,10 @@ public class SingleSiteServiceImpl implements ISingleSiteService { private static final Set PCS_DETAIL_META_FIELDS = new HashSet<>(Arrays.asList( "siteId", "deviceId", "deviceName", "alarmNum", "pcsBranchInfoList", "dataUpdateTime" )); + private static final Set BATTERY_DETAIL_META_FIELDS = new HashSet<>(Arrays.asList( + "deviceId", "clusterDeviceId", "dataTimestamp" + )); + private static final String MENU_SBJK_DTDC = "SBJK_DTDC"; private static final String CLUSTER_DATA_TEP = "温度"; @@ -135,6 +141,8 @@ public class SingleSiteServiceImpl implements ISingleSiteService { @Autowired private IEmsEnergyPriceConfigService iEmsEnergyPriceConfigService; @Autowired + private IEmsDeviceSettingService iEmsDeviceSettingService; + @Autowired private DevicePointMatchDataProcessor devicePointMatchDataProcessor; @Override @@ -1007,45 +1015,292 @@ public class SingleSiteServiceImpl implements ISingleSiteService { public List getClusterDataInfoList(String clusterDeviceId,String siteId, String stackDeviceId, String batteryId) { List batteryDataStatsListVo = new ArrayList<>(); + log.info("getClusterDataInfoList start, siteId={}, stackDeviceId={}, clusterDeviceId={}, batteryId={}", + siteId, stackDeviceId, clusterDeviceId, batteryId); + String targetStackDeviceId = StringUtils.trimToEmpty(stackDeviceId); + String targetClusterDeviceId = StringUtils.trimToEmpty(clusterDeviceId); + String targetBatteryId = StringUtils.trimToEmpty(batteryId); + + if ("undefined".equalsIgnoreCase(targetStackDeviceId) || "null".equalsIgnoreCase(targetStackDeviceId)) { + targetStackDeviceId = ""; + } + if ("undefined".equalsIgnoreCase(targetClusterDeviceId) || "null".equalsIgnoreCase(targetClusterDeviceId)) { + targetClusterDeviceId = ""; + } + if ("undefined".equalsIgnoreCase(targetBatteryId) || "null".equalsIgnoreCase(targetBatteryId)) { + targetBatteryId = ""; + } + log.info("getClusterDataInfoList normalized params, siteId={}, stackDeviceId={}, clusterDeviceId={}, batteryId={}", + siteId, targetStackDeviceId, targetClusterDeviceId, targetBatteryId); + + if (StringUtils.isEmpty(siteId)) { + log.warn("getClusterDataInfoList early return, siteId is empty"); + return batteryDataStatsListVo; + } + Map> batteryDisplayMap = + buildBatteryDisplayMap(iEmsDeviceSettingService.getSiteMonitorProjectDisplay(siteId)); + if (batteryDisplayMap.isEmpty()) { + log.warn("getClusterDataInfoList no battery display data from projectDisplay, siteId={}", siteId); + } + log.info("getClusterDataInfoList battery list source projectDisplay, siteId={}, batteryCount={}", + siteId, batteryDisplayMap.size()); + + Map batteryDeviceCache = new HashMap<>(); + Map clusterDeviceCache = new HashMap<>(); + List sortedBatteryIds = new ArrayList<>(batteryDisplayMap.keySet()); + Collections.sort(sortedBatteryIds); + + for (String batteryDeviceId : sortedBatteryIds) { + if (StringUtils.isEmpty(batteryDeviceId)) { + continue; + } + if (StringUtils.isNotEmpty(targetBatteryId) && !batteryDeviceId.equals(targetBatteryId)) { + continue; + } + + EmsDevicesSetting batterySetting = batteryDeviceCache.computeIfAbsent( + batteryDeviceId, key -> emsDevicesSettingMapper.getDeviceBySiteAndDeviceId(key, siteId)); + String currentClusterDeviceId = batterySetting == null ? "" : StringUtils.trimToEmpty(batterySetting.getParentId()); + + if (StringUtils.isNotEmpty(targetClusterDeviceId) && !targetClusterDeviceId.equals(currentClusterDeviceId)) { + continue; + } + if (StringUtils.isNotEmpty(targetStackDeviceId)) { + if (StringUtils.isEmpty(currentClusterDeviceId)) { + continue; + } + EmsDevicesSetting clusterSetting = clusterDeviceCache.computeIfAbsent( + currentClusterDeviceId, key -> emsDevicesSettingMapper.getDeviceBySiteAndDeviceId(key, siteId)); + String parentStackDeviceId = clusterSetting == null ? "" : StringUtils.trimToEmpty(clusterSetting.getParentId()); + if (!targetStackDeviceId.equals(parentStackDeviceId)) { + continue; + } + } + + BatteryDataStatsListVo batteryDataStatsVo = new BatteryDataStatsListVo(); + fillBatteryValueFromProjectDisplay(batteryDataStatsVo, batteryDisplayMap.get(batteryDeviceId)); + batteryDataStatsVo.setDeviceId(batteryDeviceId); + batteryDataStatsVo.setClusterDeviceId(currentClusterDeviceId); + batteryDataStatsListVo.add(batteryDataStatsVo); + log.info("getClusterDataInfoList battery from projectDisplay list, siteId={}, batteryDeviceId={}, clusterDeviceId={}, voltage={}, temperature={}, soc={}, soh={}, voltagePointId={}, temperaturePointId={}, socPointId={}, sohPointId={}, dataTimestamp={}", + siteId, batteryDeviceId, currentClusterDeviceId, batteryDataStatsVo.getVoltage(), + batteryDataStatsVo.getTemperature(), batteryDataStatsVo.getSoc(), + batteryDataStatsVo.getSoh(), batteryDataStatsVo.getVoltagePointId(), + batteryDataStatsVo.getTemperaturePointId(), batteryDataStatsVo.getSocPointId(), + batteryDataStatsVo.getSohPointId(), batteryDataStatsVo.getDataTimestamp()); + } + + if (batteryDataStatsListVo.isEmpty()) { + log.info("getClusterDataInfoList fallback to realtime cache, siteId={}, stackDeviceId={}, clusterDeviceId={}, batteryId={}", + siteId, targetStackDeviceId, targetClusterDeviceId, targetBatteryId); + appendBatteryFromRealtimeCache(siteId, targetStackDeviceId, targetClusterDeviceId, targetBatteryId, + batteryDisplayMap, batteryDataStatsListVo); + } + + log.info("getClusterDataInfoList end, siteId={}, stackDeviceId={}, clusterDeviceId={}, batteryId={}, total={}", + siteId, targetStackDeviceId, targetClusterDeviceId, targetBatteryId, batteryDataStatsListVo.size()); + return batteryDataStatsListVo; + } + + private void appendBatteryFromRealtimeCache(String siteId, + String targetStackDeviceId, + String targetClusterDeviceId, + String targetBatteryId, + Map> batteryDisplayMap, + List targetList) { + if (StringUtils.isBlank(siteId) || targetList == null) { + return; + } List> clusterIds = new ArrayList<>(); - if (StringUtils.isEmpty(clusterDeviceId)) { - clusterIds = emsDevicesSettingMapper.getClusterIdsByFuzzyQuery(siteId, DeviceCategory.CLUSTER.getCode(),stackDeviceId); - } else { + if (StringUtils.isNotBlank(targetClusterDeviceId)) { Map map = new HashMap<>(); - map.put("id", clusterDeviceId); + map.put("id", targetClusterDeviceId); clusterIds.add(map); + } else { + clusterIds = emsDevicesSettingMapper.getClusterIdsByFuzzyQuery(siteId, + DeviceCategory.CLUSTER.getCode(), StringUtils.trimToEmpty(targetStackDeviceId)); + } + if (CollectionUtils.isEmpty(clusterIds)) { + return; } for (Map clusterDevice : clusterIds) { - // 从redis取单个簇详细数据 - String clusterId = clusterDevice.get("id").toString(); + if (clusterDevice == null || clusterDevice.get("id") == null) { + continue; + } + String clusterId = StringUtils.trimToEmpty(String.valueOf(clusterDevice.get("id"))); + if (StringUtils.isBlank(clusterId)) { + continue; + } List batteryDataList = redisCache.getCacheList(RedisKeyConstants.BATTERY + siteId + "_" + clusterId); - if (batteryDataList != null) { - for (EmsBatteryData batteryData : batteryDataList) { - // 判断是否需要筛选:batteryId不为空时才进行匹配 - if (batteryId == null || batteryId.trim().isEmpty()) { - // 空值情况:直接添加所有数据 - BatteryDataStatsListVo batteryDataStatsVo = new BatteryDataStatsListVo(); - BeanUtils.copyProperties(batteryData, batteryDataStatsVo); - batteryDataStatsListVo.add(batteryDataStatsVo); - } else { - // 有值情况:只添加匹配的数据 - if (batteryId.equals(batteryData.getDeviceId())) { - BatteryDataStatsListVo batteryDataStatsVo = new BatteryDataStatsListVo(); - BeanUtils.copyProperties(batteryData, batteryDataStatsVo); - batteryDataStatsListVo.add(batteryDataStatsVo); - // 找到匹配项后可提前退出当前簇的循环 - break; - } - } - } + if (CollectionUtils.isEmpty(batteryDataList)) { + continue; + } + for (EmsBatteryData batteryData : batteryDataList) { + if (batteryData == null) { + continue; + } + String currentBatteryId = StringUtils.trimToEmpty(batteryData.getDeviceId()); + if (StringUtils.isNotEmpty(targetBatteryId) && !targetBatteryId.equals(currentBatteryId)) { + continue; + } + BatteryDataStatsListVo batteryDataStatsVo = new BatteryDataStatsListVo(); + BeanUtils.copyProperties(batteryData, batteryDataStatsVo); + if (StringUtils.isBlank(batteryDataStatsVo.getClusterDeviceId())) { + batteryDataStatsVo.setClusterDeviceId(clusterId); + } + fillBatteryPointIdFromProjectDisplay(batteryDataStatsVo, batteryDisplayMap == null ? null : batteryDisplayMap.get(currentBatteryId)); + targetList.add(batteryDataStatsVo); } } - //TODO 临时删除排序 - // 排序 -// List sortedList = batteryDataStatsListVo.stream() -// .sorted((u1, u2) -> Integer.parseInt(u1.getClusterDeviceId()) - Integer.parseInt(u2.getClusterDeviceId())) -// .collect(Collectors.toList()); - return batteryDataStatsListVo; + } + + private void fillBatteryDetailByLatestPointMapping(String siteId, + String deviceId, + BatteryDataStatsListVo target, + Map mappingByFieldAndDevice, + Map latestPointCache) { + if (StringUtils.isAnyBlank(siteId, deviceId) || target == null || mappingByFieldAndDevice == null) { + return; + } + BeanWrapper beanWrapper = new BeanWrapperImpl(target); + Date latestDataTime = null; + for (PropertyDescriptor pd : beanWrapper.getPropertyDescriptors()) { + if (pd == null || StringUtils.isBlank(pd.getName()) || pd.getWriteMethod() == null) { + continue; + } + String fieldCode = pd.getName(); + if (BATTERY_DETAIL_META_FIELDS.contains(fieldCode)) { + continue; + } + EmsSiteMonitorPointMatch pointMatch = resolvePointMatchByFieldAndDevice(mappingByFieldAndDevice, fieldCode, deviceId); + if (pointMatch == null) { + continue; + } + Object fieldValue; + Date valueTime = null; + if (USE_FIXED_DISPLAY_YES == (pointMatch.getUseFixedDisplay() == null ? 0 : pointMatch.getUseFixedDisplay()) + && StringUtils.isNotBlank(pointMatch.getFixedDataPoint())) { + fieldValue = pointMatch.getFixedDataPoint().trim(); + } else { + InfluxPointDataWriter.PointValue latestValue = getLatestPointValueByPointId(siteId, pointMatch.getDataPoint(), latestPointCache); + if (latestValue == null || latestValue.getPointValue() == null) { + continue; + } + fieldValue = latestValue.getPointValue(); + valueTime = latestValue.getDataTime(); + } + Object convertedValue = convertFieldValueByType(fieldValue, pd.getPropertyType()); + if (convertedValue == null) { + continue; + } + beanWrapper.setPropertyValue(fieldCode, convertedValue); + if (valueTime != null && (latestDataTime == null || valueTime.after(latestDataTime))) { + latestDataTime = valueTime; + } + } + if (latestDataTime != null) { + target.setDataTimestamp(latestDataTime); + } + } + + private Map> buildBatteryDisplayMap(List displayList) { + Map> result = new HashMap<>(); + if (CollectionUtils.isEmpty(displayList)) { + return result; + } + for (SiteMonitorProjectDisplayVo item : displayList) { + if (item == null) { + continue; + } + String menuCode = StringUtils.trimToEmpty(item.getMenuCode()); + String deviceId = StringUtils.trimToEmpty(item.getDeviceId()); + String fieldCode = StringUtils.trimToEmpty(item.getFieldCode()); + if (!MENU_SBJK_DTDC.equals(menuCode) || StringUtils.isEmpty(deviceId) || StringUtils.isEmpty(fieldCode)) { + continue; + } + Map fieldMap = result.computeIfAbsent(deviceId, key -> new HashMap<>()); + fieldMap.put(fieldCode, item); + String normalizedFieldCode = normalizeBatteryFieldCode(fieldCode); + if (StringUtils.isNotBlank(normalizedFieldCode)) { + fieldMap.put(normalizedFieldCode, item); + } + } + return result; + } + + private String normalizeBatteryFieldCode(String fieldCode) { + String normalized = StringUtils.trimToEmpty(fieldCode); + if (StringUtils.isBlank(normalized)) { + return normalized; + } + int splitIndex = normalized.lastIndexOf("__"); + if (splitIndex >= 0 && splitIndex + 2 < normalized.length()) { + normalized = normalized.substring(splitIndex + 2); + } + return normalized.toLowerCase(); + } + + private void fillBatteryValueFromProjectDisplay(BatteryDataStatsListVo target, + Map fieldMap) { + if (target == null || fieldMap == null || fieldMap.isEmpty()) { + return; + } + SiteMonitorProjectDisplayVo voltageVo = fieldMap.get("voltage"); + SiteMonitorProjectDisplayVo temperatureVo = fieldMap.get("temperature"); + SiteMonitorProjectDisplayVo socVo = fieldMap.get("soc"); + SiteMonitorProjectDisplayVo sohVo = fieldMap.get("soh"); + + target.setVoltagePointId(voltageVo == null ? null : StringUtils.trimToNull(voltageVo.getDataPoint())); + target.setTemperaturePointId(temperatureVo == null ? null : StringUtils.trimToNull(temperatureVo.getDataPoint())); + target.setSocPointId(socVo == null ? null : StringUtils.trimToNull(socVo.getDataPoint())); + target.setSohPointId(sohVo == null ? null : StringUtils.trimToNull(sohVo.getDataPoint())); + + if (voltageVo != null && StringUtils.isNotBlank(voltageVo.getFieldValue())) { + target.setVoltage(StringUtils.getBigDecimal(voltageVo.getFieldValue())); + } + if (temperatureVo != null && StringUtils.isNotBlank(temperatureVo.getFieldValue())) { + target.setTemperature(StringUtils.getBigDecimal(temperatureVo.getFieldValue())); + } + if (socVo != null && StringUtils.isNotBlank(socVo.getFieldValue())) { + target.setSoc(StringUtils.getBigDecimal(socVo.getFieldValue())); + } + if (sohVo != null && StringUtils.isNotBlank(sohVo.getFieldValue())) { + target.setSoh(StringUtils.getBigDecimal(sohVo.getFieldValue())); + } + + Date latest = null; + latest = pickLaterTime(latest, voltageVo == null ? null : voltageVo.getValueTime()); + latest = pickLaterTime(latest, temperatureVo == null ? null : temperatureVo.getValueTime()); + latest = pickLaterTime(latest, socVo == null ? null : socVo.getValueTime()); + latest = pickLaterTime(latest, sohVo == null ? null : sohVo.getValueTime()); + if (latest != null) { + target.setDataTimestamp(latest); + } + } + + private void fillBatteryPointIdFromProjectDisplay(BatteryDataStatsListVo target, + Map fieldMap) { + if (target == null || fieldMap == null || fieldMap.isEmpty()) { + return; + } + SiteMonitorProjectDisplayVo voltageVo = fieldMap.get("voltage"); + SiteMonitorProjectDisplayVo temperatureVo = fieldMap.get("temperature"); + SiteMonitorProjectDisplayVo socVo = fieldMap.get("soc"); + SiteMonitorProjectDisplayVo sohVo = fieldMap.get("soh"); + target.setVoltagePointId(voltageVo == null ? null : StringUtils.trimToNull(voltageVo.getDataPoint())); + target.setTemperaturePointId(temperatureVo == null ? null : StringUtils.trimToNull(temperatureVo.getDataPoint())); + target.setSocPointId(socVo == null ? null : StringUtils.trimToNull(socVo.getDataPoint())); + target.setSohPointId(sohVo == null ? null : StringUtils.trimToNull(sohVo.getDataPoint())); + } + + private Date pickLaterTime(Date left, Date right) { + if (left == null) { + return right; + } + if (right == null) { + return left; + } + return right.after(left) ? right : left; } // 获取单站的最大最小温度和电压单体数据id diff --git a/ems-system/src/main/resources/mapper/ems/EmsDevicesSettingMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsDevicesSettingMapper.xml index 5557599..e1ac098 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsDevicesSettingMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsDevicesSettingMapper.xml @@ -175,6 +175,7 @@ from ems_devices_setting t where t.site_id = #{siteId} and t.parent_id = #{parentId} + order by t.device_id - where device_id = #{deviceId} - AND site_id = #{siteId} - limit 1 + where site_id = #{siteId} + + AND device_id = #{deviceId} + - \ No newline at end of file + diff --git a/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml index 14538db..daac50a 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml @@ -31,6 +31,12 @@ where site_id = #{siteId} + + delete from ems_site_monitor_point_match + where site_id = #{siteId} + and device_id = #{deviceId} + + insert into ems_site_monitor_point_match (site_id, field_code, device_id, data_point, fixed_data_point, use_fixed_display, create_by, create_time, update_by, update_time) -- 2.49.0 From 49ed5f218a6695930fac7d3fe71bda1d7f4a611b Mon Sep 17 00:00:00 2001 From: dashixiong Date: Mon, 16 Feb 2026 14:46:27 +0800 Subject: [PATCH 06/10] =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ems/EmsAlarmRecordsController.java | 17 +++++++++++++++ .../ems/service/IEmsAlarmRecordsService.java | 9 ++++++++ .../impl/EmsAlarmRecordsServiceImpl.java | 21 +++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsAlarmRecordsController.java b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsAlarmRecordsController.java index e3fc301..5ac7ffa 100644 --- a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsAlarmRecordsController.java +++ b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsAlarmRecordsController.java @@ -55,4 +55,21 @@ public class EmsAlarmRecordsController extends BaseController } } + /** + * 关闭告警 + */ + @PostMapping("/closeAlarm") + public AjaxResult closeAlarm(@RequestBody EmsAlarmRecords emsAlarmRecords) + { + Long id = emsAlarmRecords.getId(); + if (id == null) { + return error("告警id不能为空"); + } + String result = iEmsAlarmRecordsService.closeAlarm(id, getUserId()); + if ("success".equals(result) || "告警已关闭".equals(result)) { + return AjaxResult.success("操作成功"); + } + return error(result); + } + } diff --git a/ems-system/src/main/java/com/xzzn/ems/service/IEmsAlarmRecordsService.java b/ems-system/src/main/java/com/xzzn/ems/service/IEmsAlarmRecordsService.java index ba7d36f..c44d15b 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/IEmsAlarmRecordsService.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/IEmsAlarmRecordsService.java @@ -77,6 +77,15 @@ public interface IEmsAlarmRecordsService * @return */ public String createTicketNo(Long id, Long userId); + + /** + * 关闭告警并设置为已处理 + * + * @param id 告警ID + * @param userId 用户ID + * @return 处理结果 + */ + public String closeAlarm(Long id, Long userId); // 订阅失败-增加告警 public void addSubFailedAlarmRecord(String topic); // 订阅成功-处理告警 diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsAlarmRecordsServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsAlarmRecordsServiceImpl.java index a679c7e..c3a4acd 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsAlarmRecordsServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsAlarmRecordsServiceImpl.java @@ -174,6 +174,27 @@ public class EmsAlarmRecordsServiceImpl implements IEmsAlarmRecordsService return ticketNo; } + @Override + public String closeAlarm(Long id, Long userId) { + EmsAlarmRecords emsAlarmRecords = emsAlarmRecordsMapper.selectEmsAlarmRecordsById(id); + if (emsAlarmRecords == null) { + return "告警记录不存在"; + } + if (AlarmStatus.DONE.getCode().equals(emsAlarmRecords.getStatus())) { + return "告警已关闭"; + } + emsAlarmRecords.setStatus(AlarmStatus.DONE.getCode()); + emsAlarmRecords.setAlarmEndTime(DateUtils.getNowDate()); + emsAlarmRecords.setUpdateTime(DateUtils.getNowDate()); + if (userId == null) { + userId = 1L; + } + SysUser user = sysUserMapper.selectUserById(userId); + emsAlarmRecords.setUpdateBy(user != null ? user.getUserName() : "system"); + emsAlarmRecordsMapper.updateEmsAlarmRecords(emsAlarmRecords); + return "success"; + } + // 订阅失败-增加告警 @Override public void addSubFailedAlarmRecord(String topic) { -- 2.49.0 From 4e7d387edf24e4015d3047ff26c5d6a50ba108b5 Mon Sep 17 00:00:00 2001 From: dashixiong Date: Tue, 17 Feb 2026 21:42:59 +0800 Subject: [PATCH 07/10] =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/xzzn/ems/domain/EmsSiteSetting.java | 15 +++++++++++++++ .../ems/utils/DevicePointMatchDataProcessor.java | 12 +++++++++++- .../resources/mapper/ems/EmsSiteSettingMapper.xml | 7 ++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteSetting.java b/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteSetting.java index e67d0d2..189b531 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteSetting.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/EmsSiteSetting.java @@ -25,6 +25,10 @@ public class EmsSiteSetting extends BaseEntity @Excel(name = "站点名称") private String siteName; + /** 站点简称 */ + @Excel(name = "站点简称") + private String siteShortName; + /** 站点地址 */ @Excel(name = "站点地址") private String siteAddress; @@ -77,6 +81,16 @@ public class EmsSiteSetting extends BaseEntity return siteName; } + public void setSiteShortName(String siteShortName) + { + this.siteShortName = siteShortName; + } + + public String getSiteShortName() + { + return siteShortName; + } + public void setSiteAddress(String siteAddress) { this.siteAddress = siteAddress; @@ -162,6 +176,7 @@ public class EmsSiteSetting extends BaseEntity return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) .append("id", getId()) .append("siteName", getSiteName()) + .append("siteShortName", getSiteShortName()) .append("siteAddress", getSiteAddress()) .append("runningTime", getRunningTime()) .append("latitude", getLatitude()) diff --git a/ems-system/src/main/java/com/xzzn/ems/utils/DevicePointMatchDataProcessor.java b/ems-system/src/main/java/com/xzzn/ems/utils/DevicePointMatchDataProcessor.java index a34ff2c..0a91a33 100644 --- a/ems-system/src/main/java/com/xzzn/ems/utils/DevicePointMatchDataProcessor.java +++ b/ems-system/src/main/java/com/xzzn/ems/utils/DevicePointMatchDataProcessor.java @@ -135,6 +135,8 @@ public class DevicePointMatchDataProcessor { return new HashMap<>(); } return pointEnumMatchList.stream() + .filter(Objects::nonNull) + .filter(data -> StringUtils.isNotEmpty(data.getMatchField())) .collect(Collectors.groupingBy(data -> StringUtils.toCamelCase(data.getMatchField()))); } @@ -237,8 +239,14 @@ public class DevicePointMatchDataProcessor { * 转换字段值为配置枚举值 */ public void convertFieldValueToEnumMatch(String siteId, String deviceCategory, Object entity) { + if (entity == null) { + return; + } Map> pointEnumMatchMap = this.getPointEnumMatchMap(siteId, deviceCategory); + if (pointEnumMatchMap == null || pointEnumMatchMap.isEmpty()) { + return; + } Field[] fields = entity.getClass().getDeclaredFields(); for (Field field : fields) { String fieldName = field.getName(); @@ -252,7 +260,9 @@ public class DevicePointMatchDataProcessor { if (CollectionUtils.isNotEmpty(pointEnumMatchList) && matchValue != null) { String finalMatchValue = String.valueOf(matchValue).replace(".0", ""); Optional enumMatch = pointEnumMatchList.stream() - .filter(data -> data.getDataEnumCode().equals(finalMatchValue)).findFirst(); + .filter(Objects::nonNull) + .filter(data -> Objects.equals(data.getDataEnumCode(), finalMatchValue)) + .findFirst(); if (enumMatch.isPresent()) { matchValue = enumMatch.get().getEnumCode(); } diff --git a/ems-system/src/main/resources/mapper/ems/EmsSiteSettingMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsSiteSettingMapper.xml index 9a85642..35df233 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsSiteSettingMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsSiteSettingMapper.xml @@ -7,6 +7,7 @@ + @@ -22,13 +23,14 @@ - select id, site_name, site_address, running_time, latitude, longitude, install_capacity, install_power, remark, create_by, update_by, create_time, update_time, site_id from ems_site_setting + select id, site_name, site_short_name, site_address, running_time, latitude, longitude, install_capacity, install_power, remark, create_by, update_by, create_time, update_time, site_id from ems_site_setting WITH ranked AS ( SELECT - *, - DATE_FORMAT(DATE_ADD(DATE_FORMAT(p.update_time, '%Y-%m-%d %H:00:00'), INTERVAL CEIL(MINUTE(p.update_time) / 5) * 5 MINUTE) - , '%Y-%m-%d %H:%i:%s') AS group_time, - ROW_NUMBER() OVER (PARTITION BY p.device_id, date_format(p.update_time, '%Y-%m-%d %H:00:00'), CEIL(MINUTE(p.update_time) / 5) ORDER BY p.data_update_time DESC) as rn + p.site_id, + p.device_id, + p.stack_soc, + p.stack_soh, + p.avg_temperature, + FROM_UNIXTIME(((UNIX_TIMESTAMP(p.update_time) + 299) DIV 300) * 300) AS group_time, + ROW_NUMBER() OVER ( + PARTITION BY p.device_id, ((UNIX_TIMESTAMP(p.update_time) + 299) DIV 300) + ORDER BY p.data_update_time DESC + ) as rn FROM ems_battery_stack p @@ -528,16 +534,27 @@ avg(t.avg_temperature) as avgTemp FROM ranked as t + where t.rn = 1 GROUP BY t.site_id, t.group_time - \ No newline at end of file + diff --git a/ems-system/src/main/resources/mapper/ems/EmsDailyEnergyDataMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsDailyEnergyDataMapper.xml index 3b34ab5..fe40a83 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsDailyEnergyDataMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsDailyEnergyDataMapper.xml @@ -236,26 +236,40 @@ - \ No newline at end of file + diff --git a/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml index 741f34d..b4e3bd6 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml @@ -22,6 +22,12 @@ + + + + + + @@ -32,6 +38,7 @@ select id, point_id, site_id, device_category, device_id, point_name, data_key, point_desc, register_address, data_unit, data_a, data_k, data_b, data_bit, is_alarm, point_type, calc_expression, + collect_enabled, collect_source, modbus_register_type, modbus_data_type, modbus_read_order, modbus_group, create_by, create_time, update_by, update_time, remark from ems_point_config @@ -51,6 +58,10 @@ and data_key like concat('%', #{dataKey}, '%') and point_desc like concat('%', #{pointDesc}, '%') and point_type = #{pointType} + and collect_enabled = #{collectEnabled} + and collect_source = #{collectSource} + and modbus_register_type = #{modbusRegisterType} + and modbus_data_type = #{modbusDataType} and register_address = #{registerAddress} order by update_time desc, id desc @@ -75,6 +86,12 @@ is_alarm, point_type, calc_expression, + collect_enabled, + collect_source, + modbus_register_type, + modbus_data_type, + modbus_read_order, + modbus_group, create_by, create_time, update_by, @@ -98,6 +115,12 @@ #{isAlarm}, #{pointType}, #{calcExpression}, + #{collectEnabled}, + #{collectSource}, + #{modbusRegisterType}, + #{modbusDataType}, + #{modbusReadOrder}, + #{modbusGroup}, #{createBy}, #{createTime}, #{updateBy}, @@ -110,6 +133,7 @@ insert into ems_point_config ( point_id, site_id, device_category, device_id, point_name, data_key, point_desc, register_address, data_unit, data_a, data_k, data_b, data_bit, is_alarm, point_type, calc_expression, + collect_enabled, collect_source, modbus_register_type, modbus_data_type, modbus_read_order, modbus_group, create_by, create_time, update_by, update_time, remark ) values @@ -131,6 +155,12 @@ #{item.isAlarm}, #{item.pointType}, #{item.calcExpression}, + #{item.collectEnabled}, + #{item.collectSource}, + #{item.modbusRegisterType}, + #{item.modbusDataType}, + #{item.modbusReadOrder}, + #{item.modbusGroup}, #{item.createBy}, now(), #{item.updateBy}, @@ -159,6 +189,12 @@ is_alarm = #{isAlarm}, point_type = #{pointType}, calc_expression = #{calcExpression}, + collect_enabled = #{collectEnabled}, + collect_source = #{collectSource}, + modbus_register_type = #{modbusRegisterType}, + modbus_data_type = #{modbusDataType}, + modbus_read_order = #{modbusReadOrder}, + modbus_group = #{modbusGroup}, update_by = #{updateBy}, update_time = #{updateTime}, remark = #{remark}, @@ -192,11 +228,13 @@ insert into ems_point_config ( point_id, site_id, device_category, device_id, point_name, data_key, point_desc, register_address, data_unit, data_a, data_k, data_b, data_bit, is_alarm, point_type, calc_expression, + collect_enabled, collect_source, modbus_register_type, modbus_data_type, modbus_read_order, modbus_group, create_by, create_time, update_by, update_time, remark ) select concat('PT_', replace(uuid(), '-', '')), #{targetSiteId}, device_category, device_id, point_name, data_key, point_desc, register_address, data_unit, data_a, data_k, data_b, data_bit, is_alarm, point_type, calc_expression, + collect_enabled, collect_source, modbus_register_type, modbus_data_type, modbus_read_order, modbus_group, #{operName}, now(), #{operName}, now(), remark from ems_point_config where site_id = #{templateSiteId} @@ -287,4 +325,19 @@ + + diff --git a/ems-system/src/main/resources/mapper/ems/EmsSiteWeatherDayMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsSiteWeatherDayMapper.xml new file mode 100644 index 0000000..93acc0f --- /dev/null +++ b/ems-system/src/main/resources/mapper/ems/EmsSiteWeatherDayMapper.xml @@ -0,0 +1,28 @@ + + + + + + update ems_site_weather_day + set weather_desc = #{weatherDesc}, + weather_code = #{weatherCode}, + update_time = now() + where site_id = #{siteId} + and calendar_date = #{calendarDate} + + + + + + insert into ems_site_weather_day(site_id, calendar_date, weather_desc, weather_code, source, create_time, update_time) + values (#{siteId}, #{calendarDate}, #{weatherDesc}, #{weatherCode}, #{source}, now(), now()) + + + -- 2.49.0 From e4cfd15cb453e4f65476dbf70ea18b4edaa29088 Mon Sep 17 00:00:00 2001 From: dashixiong Date: Wed, 18 Mar 2026 10:06:42 +0800 Subject: [PATCH 09/10] =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ems/EmsPointConfigController.java | 7 + .../ems/EmsSiteMonitorController.java | 9 + .../ems/EmsStatisticalReportController.java | 22 + .../xzzn/ems/domain/EmsDailyChargeData.java | 45 +- .../xzzn/ems/domain/EmsDailyEnergyData.java | 32 +- .../vo/PointConfigLatestValueItemVo.java | 9 + .../domain/vo/PointConfigLatestValueVo.java | 9 + .../ems/mapper/EmsDailyChargeDataMapper.java | 11 + .../ems/mapper/EmsDailyEnergyDataMapper.java | 6 + .../xzzn/ems/mapper/EmsPointConfigMapper.java | 2 + .../EmsSiteMonitorPointMatchMapper.java | 2 + .../ems/service/IEmsPointConfigService.java | 3 + .../xzzn/ems/service/ISingleSiteService.java | 2 + .../ems/service/InfluxPointDataWriter.java | 19 +- .../impl/DeviceDataProcessServiceImpl.java | 1800 +++++++++++------ .../impl/EmsDeviceSettingServiceImpl.java | 153 +- .../impl/EmsEnergyPriceConfigServiceImpl.java | 22 +- .../impl/EmsPointCalcConfigServiceImpl.java | 4 +- .../impl/EmsPointConfigServiceImpl.java | 1350 ++++++++++++- .../impl/EmsStatsReportServiceImpl.java | 9 +- .../service/impl/SingleSiteServiceImpl.java | 55 + .../mapper/ems/EmsDailyChageDataMapper.xml | 48 +- .../mapper/ems/EmsDailyEnergyDataMapper.xml | 172 +- .../mapper/ems/EmsPointConfigMapper.xml | 40 +- .../ems/EmsSiteMonitorPointMatchMapper.xml | 8 + 25 files changed, 2948 insertions(+), 891 deletions(-) diff --git a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointConfigController.java b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointConfigController.java index f9d530e..71a2347 100644 --- a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointConfigController.java +++ b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointConfigController.java @@ -8,6 +8,7 @@ import com.xzzn.common.enums.BusinessType; import com.xzzn.ems.domain.EmsPointConfig; import com.xzzn.ems.domain.vo.ImportPointTemplateRequest; import com.xzzn.ems.domain.vo.PointConfigCurveRequest; +import com.xzzn.ems.domain.vo.PointConfigGenerateRecentRequest; import com.xzzn.ems.domain.vo.PointConfigLatestValueRequest; import com.xzzn.ems.service.IEmsPointConfigService; import org.springframework.beans.factory.annotation.Autowired; @@ -93,4 +94,10 @@ public class EmsPointConfigController extends BaseController { public AjaxResult curve(@RequestBody PointConfigCurveRequest request) { return success(pointConfigService.getCurveData(request)); } + + @Log(title = "点位配置", businessType = BusinessType.INSERT) + @PostMapping("/generateRecent7Days") + public AjaxResult generateRecent7Days(@Valid @RequestBody PointConfigGenerateRecentRequest request) { + return success(pointConfigService.generateRecent7DaysData(request)); + } } diff --git a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java index 1bd3fbe..079ad52 100644 --- a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java +++ b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java @@ -52,6 +52,15 @@ public class EmsSiteMonitorController extends BaseController{ return success(iSingleSiteService.getSiteMonitorDataVo(siteId)); } + /** + * 获取单站首页总累计运行数据(基于日表) + */ + @GetMapping("/homeTotalView") + public AjaxResult getSingleSiteHomeTotalView(@RequestParam String siteId) + { + return success(iSingleSiteService.getSiteMonitorTotalDataVo(siteId)); + } + /** * 单站监控-设备监控-实时运行头部数据 */ diff --git a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsStatisticalReportController.java b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsStatisticalReportController.java index d18840b..c82a3d3 100644 --- a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsStatisticalReportController.java +++ b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsStatisticalReportController.java @@ -134,6 +134,17 @@ public class EmsStatisticalReportController extends BaseController return getDataTable(dataList); } + /** + * 统计报表-电表报表(直接基于 ems_daily_energy_data) + */ + @GetMapping("/getAmmeterDataFromDaily") + public TableDataInfo getAmmeterDataFromDaily(StatisAmmeterDateRequest requestVo) + { + startPage(); + List dataList = ieEmsStatsReportService.getAmmeterDataResult(requestVo); + return getDataTable(dataList); + } + /** * 导出电表报表 */ @@ -145,6 +156,17 @@ public class EmsStatisticalReportController extends BaseController ieEmsStatsReportService.exportAmmeterData(response, requestVo); } + /** + * 导出电表报表(直接基于 ems_daily_energy_data) + */ + @PreAuthorize("@ss.hasPermi('system:ammeterData:export')") + @Log(title = "电表报表", businessType = BusinessType.EXPORT) + @PostMapping("/exportAmmeterDataFromDaily") + public void exportAmmeterDataFromDaily(HttpServletResponse response, StatisAmmeterDateRequest requestVo) + { + ieEmsStatsReportService.exportAmmeterData(response, requestVo); + } + /** * 概率统计-电表收益报表 */ diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/EmsDailyChargeData.java b/ems-system/src/main/java/com/xzzn/ems/domain/EmsDailyChargeData.java index 05e5898..bea8886 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/EmsDailyChargeData.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/EmsDailyChargeData.java @@ -25,10 +25,6 @@ public class EmsDailyChargeData extends BaseEntity @Excel(name = "站点id") private String siteId; - /** 设备唯一标识符 */ - @Excel(name = "设备唯一标识符") - private String deviceId; - /** 数据日期:yyyy-MM-dd */ @JsonFormat(pattern = "yyyy-MM-dd") @Excel(name = "数据日期:yyyy-MM-dd", width = 30, dateFormat = "yyyy-MM-dd") @@ -50,6 +46,14 @@ public class EmsDailyChargeData extends BaseEntity @Excel(name = "当日放电量") private BigDecimal dischargeData; + /** 总收入 */ + @Excel(name = "总收入") + private BigDecimal totalRevenue; + + /** 当日实时收入 */ + @Excel(name = "当日实时收入") + private BigDecimal dayRevenue; + public void setId(Long id) { this.id = id; @@ -70,16 +74,6 @@ public class EmsDailyChargeData extends BaseEntity return siteId; } - public void setDeviceId(String deviceId) - { - this.deviceId = deviceId; - } - - public String getDeviceId() - { - return deviceId; - } - public void setDateTime(Date dateTime) { this.dateTime = dateTime; @@ -130,17 +124,38 @@ public class EmsDailyChargeData extends BaseEntity return dischargeData; } + public void setTotalRevenue(BigDecimal totalRevenue) + { + this.totalRevenue = totalRevenue; + } + + public BigDecimal getTotalRevenue() + { + return totalRevenue; + } + + public void setDayRevenue(BigDecimal dayRevenue) + { + this.dayRevenue = dayRevenue; + } + + public BigDecimal getDayRevenue() + { + return dayRevenue; + } + @Override public String toString() { return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) .append("id", getId()) .append("siteId", getSiteId()) - .append("deviceId", getDeviceId()) .append("dateTime", getDateTime()) .append("totalChargeData", getTotalChargeData()) .append("totalDischargeData", getTotalDischargeData()) .append("chargeData", getChargeData()) .append("dischargeData", getDischargeData()) + .append("totalRevenue", getTotalRevenue()) + .append("dayRevenue", getDayRevenue()) .append("createBy", getCreateBy()) .append("createTime", getCreateTime()) .append("updateBy", getUpdateBy()) diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/EmsDailyEnergyData.java b/ems-system/src/main/java/com/xzzn/ems/domain/EmsDailyEnergyData.java index 91f1630..cf3d897 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/EmsDailyEnergyData.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/EmsDailyEnergyData.java @@ -30,13 +30,9 @@ public class EmsDailyEnergyData extends BaseEntity @Excel(name = "数据日期:yyyy-MM-dd", width = 30, dateFormat = "yyyy-MM-dd") private Date dataDate; - /** 总收入 */ - @Excel(name = "总收入") - private BigDecimal totalRevenue; - - /** 当日实时收入 */ - @Excel(name = "当日实时收入") - private BigDecimal dayRevenue; + /** 数据小时(0-23) */ + @Excel(name = "数据小时(0-23)") + private Integer dataHour; /** 尖峰时段充电差值 */ @Excel(name = "尖峰时段充电差值") @@ -71,6 +67,7 @@ public class EmsDailyEnergyData extends BaseEntity private BigDecimal valleyDischargeDiff; /** 差值计算时间(如2025-10-10 23:59:00) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @Excel(name = "差值计算时间", readConverterExp = "如=2025-10-10,2=3:59:00") private Date calcTime; @@ -104,24 +101,14 @@ public class EmsDailyEnergyData extends BaseEntity return dataDate; } - public void setTotalRevenue(BigDecimal totalRevenue) + public void setDataHour(Integer dataHour) { - this.totalRevenue = totalRevenue; + this.dataHour = dataHour; } - public BigDecimal getTotalRevenue() + public Integer getDataHour() { - return totalRevenue; - } - - public void setDayRevenue(BigDecimal dayRevenue) - { - this.dayRevenue = dayRevenue; - } - - public BigDecimal getDayRevenue() - { - return dayRevenue; + return dataHour; } public void setPeakChargeDiff(BigDecimal peakChargeDiff) @@ -220,8 +207,7 @@ public class EmsDailyEnergyData extends BaseEntity .append("id", getId()) .append("siteId", getSiteId()) .append("dataDate", getDataDate()) - .append("totalRevenue", getTotalRevenue()) - .append("dayRevenue", getDayRevenue()) + .append("dataHour", getDataHour()) .append("peakChargeDiff", getPeakChargeDiff()) .append("peakDischargeDiff", getPeakDischargeDiff()) .append("highChargeDiff", getHighChargeDiff()) diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueItemVo.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueItemVo.java index 4f8016d..af5fded 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueItemVo.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueItemVo.java @@ -2,6 +2,7 @@ package com.xzzn.ems.domain.vo; public class PointConfigLatestValueItemVo { private String siteId; + private String pointId; private String deviceId; private String dataKey; @@ -13,6 +14,14 @@ public class PointConfigLatestValueItemVo { this.siteId = siteId; } + public String getPointId() { + return pointId; + } + + public void setPointId(String pointId) { + this.pointId = pointId; + } + public String getDeviceId() { return deviceId; } diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueVo.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueVo.java index 76831c2..af322c3 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueVo.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueVo.java @@ -4,6 +4,7 @@ import java.util.Date; public class PointConfigLatestValueVo { private String siteId; + private String pointId; private String deviceId; private String dataKey; private Object pointValue; @@ -17,6 +18,14 @@ public class PointConfigLatestValueVo { this.siteId = siteId; } + public String getPointId() { + return pointId; + } + + public void setPointId(String pointId) { + this.pointId = pointId; + } + public String getDeviceId() { return deviceId; } diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsDailyChargeDataMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsDailyChargeDataMapper.java index 27c3864..44372b2 100644 --- a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsDailyChargeDataMapper.java +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsDailyChargeDataMapper.java @@ -69,6 +69,17 @@ public interface EmsDailyChargeDataMapper // 插入或更新站点每日充放电数据 public void insertOrUpdateData(EmsDailyChargeData emsDailyChargeData); + // 按站点+日期(天)查询当日已存在记录 + public EmsDailyChargeData selectBySiteIdAndDateTime(@Param("siteId") String siteId, + @Param("dateTime") Date dateTime); + + // 按站点+日期(天)更新收入字段 + public int updateRevenueBySiteAndDate(@Param("siteId") String siteId, + @Param("dateTime") Date dateTime, + @Param("totalRevenue") BigDecimal totalRevenue, + @Param("dayRevenue") BigDecimal dayRevenue, + @Param("updateBy") String updateBy); + // 获取所有站点总充总放 public Map getAllSiteChargeData(@Param("nowData")String nowData, @Param("siteId")String siteId); diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsDailyEnergyDataMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsDailyEnergyDataMapper.java index 536f73d..ff7c2eb 100644 --- a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsDailyEnergyDataMapper.java +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsDailyEnergyDataMapper.java @@ -68,6 +68,12 @@ public interface EmsDailyEnergyDataMapper // 获取站点某日电表数据 public EmsDailyEnergyData getDataByDate(@Param("siteId")String siteId,@Param("today") String today); + + // 按站点+日期+小时查询当小时已存在记录 + public EmsDailyEnergyData selectBySiteIdAndDateHour(@Param("siteId") String siteId, + @Param("dataDate") java.util.Date dataDate, + @Param("dataHour") Integer dataHour); + // 插入或更新每日尖峰平谷差值 public void insertOrUpdateData(EmsDailyEnergyData energyData); // 电表报表 diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java index 324e5ed..2eb97d5 100644 --- a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java @@ -17,6 +17,8 @@ public interface EmsPointConfigMapper { int updateEmsPointConfig(EmsPointConfig emsPointConfig); + int updateEmsPointConfigForImport(EmsPointConfig emsPointConfig); + int deleteEmsPointConfigById(Long id); int deleteEmsPointConfigByIds(Long[] ids); diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorPointMatchMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorPointMatchMapper.java index 47c5a94..b9fa099 100644 --- a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorPointMatchMapper.java +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorPointMatchMapper.java @@ -9,6 +9,8 @@ import java.util.List; * 单站监控字段点位映射 Mapper */ public interface EmsSiteMonitorPointMatchMapper { + List selectDistinctSiteIds(); + List selectBySiteId(@Param("siteId") String siteId); int deleteBySiteId(@Param("siteId") String siteId); diff --git a/ems-system/src/main/java/com/xzzn/ems/service/IEmsPointConfigService.java b/ems-system/src/main/java/com/xzzn/ems/service/IEmsPointConfigService.java index 04abb2a..4cad426 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/IEmsPointConfigService.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/IEmsPointConfigService.java @@ -4,6 +4,7 @@ import com.xzzn.ems.domain.EmsPointConfig; import com.xzzn.ems.domain.vo.ImportPointTemplateRequest; import com.xzzn.ems.domain.vo.PointConfigCurveRequest; import com.xzzn.ems.domain.vo.PointConfigCurveValueVo; +import com.xzzn.ems.domain.vo.PointConfigGenerateRecentRequest; import com.xzzn.ems.domain.vo.PointConfigLatestValueRequest; import com.xzzn.ems.domain.vo.PointConfigLatestValueVo; import org.springframework.web.multipart.MultipartFile; @@ -30,4 +31,6 @@ public interface IEmsPointConfigService { List getLatestValues(PointConfigLatestValueRequest request); List getCurveData(PointConfigCurveRequest request); + + String generateRecent7DaysData(PointConfigGenerateRecentRequest request); } diff --git a/ems-system/src/main/java/com/xzzn/ems/service/ISingleSiteService.java b/ems-system/src/main/java/com/xzzn/ems/service/ISingleSiteService.java index 926d1b7..1ea2971 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/ISingleSiteService.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/ISingleSiteService.java @@ -13,6 +13,8 @@ public interface ISingleSiteService public SiteMonitorHomeVo getSiteMonitorDataVo(String siteId); + public SiteMonitorHomeVo getSiteMonitorTotalDataVo(String siteId); + public SiteMonitorRunningHeadInfoVo getSiteRunningHeadInfo(String siteId); diff --git a/ems-system/src/main/java/com/xzzn/ems/service/InfluxPointDataWriter.java b/ems-system/src/main/java/com/xzzn/ems/service/InfluxPointDataWriter.java index 747c37a..4aed2c3 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/InfluxPointDataWriter.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/InfluxPointDataWriter.java @@ -204,24 +204,7 @@ public class InfluxPointDataWriter { try { String queryUrl = buildQueryUrl(influxQl); - List values = parseInfluxQlResponse(executeRequestWithResponse(methodOrDefault(readMethod, "GET"), queryUrl)); - if (!values.isEmpty()) { - return values; - } - - // 兼容 pointId 大小写差异 - String regexQuery = String.format( - "SELECT \"value\" FROM \"%s\" WHERE \"site_id\" = '%s' AND \"point_key\" =~ /(?i)^%s$/ " + - "AND time >= %dms AND time <= %dms ORDER BY time ASC", - measurement, - escapeTagValue(normalizedSiteId), - escapeRegex(normalizedPointKey), - startTime.getTime(), - endTime.getTime() - ); - return parseInfluxQlResponse( - executeRequestWithResponse(methodOrDefault(readMethod, "GET"), buildQueryUrl(regexQuery)) - ); + return parseInfluxQlResponse(executeRequestWithResponse(methodOrDefault(readMethod, "GET"), queryUrl)); } catch (Exception e) { log.warn("按 pointKey 查询 InfluxDB 曲线失败: {}", e.getMessage()); return Collections.emptyList(); diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java index a5c2b19..d41a2fa 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java @@ -5,7 +5,6 @@ import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONException; import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.TypeReference; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.xzzn.common.constant.Constants; @@ -28,6 +27,7 @@ import com.xzzn.common.utils.DateUtils; import com.xzzn.common.utils.MapUtils; import com.xzzn.common.utils.StringUtils; import com.xzzn.ems.domain.*; +import com.xzzn.ems.domain.vo.AmmeterRevenueStatisListVo; import com.xzzn.ems.domain.vo.EnergyPriceTimeRange; import com.xzzn.ems.domain.vo.EnergyPriceVo; import com.xzzn.ems.enums.DeviceMatchTable; @@ -65,6 +65,8 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.YearMonth; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.ArrayDeque; @@ -111,7 +113,10 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i private static final Pattern DTDC_PATTERN = Pattern.compile("DTDC(\\d+)([A-Za-z]*)"); private static final Pattern DB_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$"); private static final Pattern VARIABLE_SAFE_PATTERN = Pattern.compile("[^A-Za-z0-9_]"); - private static final Pattern SIMPLE_CALC_EXPRESSION_PATTERN = Pattern.compile("^[0-9A-Za-z_+\\-*/().\\s]+$"); + private static final Pattern SIMPLE_CALC_EXPRESSION_PATTERN = Pattern.compile("^[0-9A-Za-z_+\\-*/().,?:<>=!&|\\s]+$"); + private static final Pattern AUTO_DAY_DIFF_FUNCTION_PATTERN = Pattern.compile("(?i)DAY_DIFF\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\)"); + private static final Pattern AUTO_MONTH_DIFF_FUNCTION_PATTERN = Pattern.compile("(?i)MONTH_DIFF\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\)"); + private static final Pattern AUTO_HOUR_DIFF_FUNCTION_PATTERN = Pattern.compile("(?i)HOUR_DIFF\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\)"); private static final int POINT_QUEUE_CAPACITY = 100000; private static final int POINT_FLUSH_BATCH_SIZE = 2000; private static final int POINT_FLUSH_MAX_DRAIN_PER_RUN = 20000; @@ -121,15 +126,23 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i private static final String SITE_LEVEL_CALC_DEVICE_ID = "SITE_CALC"; private static final int MONITOR_POINT_MATCH_REDIS_TTL_SECONDS = 300; private static final String DELETED_FIELD_MARK = "__DELETED__"; - private static final String DAILY_ENERGY_RAW_CACHE_PREFIX = "DAILY_ENERGY_RAW_"; - private static final String ENERGY_METRIC_PEAK_CHARGE = "peakCharge"; - private static final String ENERGY_METRIC_PEAK_DISCHARGE = "peakDischarge"; - private static final String ENERGY_METRIC_HIGH_CHARGE = "highCharge"; - private static final String ENERGY_METRIC_HIGH_DISCHARGE = "highDischarge"; - private static final String ENERGY_METRIC_FLAT_CHARGE = "flatCharge"; - private static final String ENERGY_METRIC_FLAT_DISCHARGE = "flatDischarge"; - private static final String ENERGY_METRIC_VALLEY_CHARGE = "valleyCharge"; - private static final String ENERGY_METRIC_VALLEY_DISCHARGE = "valleyDischarge"; + private static final String AUTO_DAY_DIFF_CONTEXT_PREFIX = "__AUTO_DAY_DIFF__"; + private static final String AUTO_MONTH_DIFF_CONTEXT_PREFIX = "__AUTO_MONTH_DIFF__"; + private static final String AUTO_HOUR_DIFF_CONTEXT_PREFIX = "__AUTO_HOUR_DIFF__"; + private static final String MONITOR_FIELD_PREFIX_HOME = "home__"; + private static final String MONITOR_FIELD_PREFIX_TJBB_DBBB = "tjbb_dbbb__"; + private static final String[] FIELD_SUFFIX_TOTAL_CHARGE = { + "activeTotalKwh", "totalChargeData", "totalCharge", "totalChargedCap", "chargedCap" + }; + private static final String[] FIELD_SUFFIX_TOTAL_DISCHARGE = { + "reActiveTotalKwh", "totalDischargeData", "totalDischarge", "totalDischargedCap", "disChargedCap" + }; + private static final String[] FIELD_SUFFIX_DAY_CHARGE = { + "dayChargedCap", "dayChargeData", "dailyChargeData", "dailyChargedCap" + }; + private static final String[] FIELD_SUFFIX_DAY_DISCHARGE = { + "dayDisChargedCap", "dayDischargeData", "dailyDischargeData", "dailyDisChargedCap" + }; private static final String[] FIELD_SUFFIX_PEAK_CHARGE = {"activePeakKwh", "peakChargeDiff", "peakCharge"}; private static final String[] FIELD_SUFFIX_PEAK_DISCHARGE = {"reActivePeakKwh", "peakDischargeDiff", "peakDischarge"}; private static final String[] FIELD_SUFFIX_HIGH_CHARGE = {"activeHighKwh", "highChargeDiff", "highCharge"}; @@ -138,6 +151,8 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i private static final String[] FIELD_SUFFIX_FLAT_DISCHARGE = {"reActiveFlatKwh", "flatDischargeDiff", "flatDischarge"}; private static final String[] FIELD_SUFFIX_VALLEY_CHARGE = {"activeValleyKwh", "valleyChargeDiff", "valleyCharge"}; private static final String[] FIELD_SUFFIX_VALLEY_DISCHARGE = {"reActiveValleyKwh", "valleyDischargeDiff", "valleyDischarge"}; + private static final long DAILY_CHARGE_INFLUX_QUERY_WINDOW_MS = TimeUnit.DAYS.toMillis(2); + private static final long CALC_POINT_INFLUX_QUERY_WINDOW_MS = TimeUnit.DAYS.toMillis(2); @Autowired private EmsBatteryClusterMapper emsBatteryClusterMapper; @@ -233,19 +248,13 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i log.info("开始处理设备数据, siteId: {}, messageSize: {}", siteId, arraylist.size()); Set deviceIds = new LinkedHashSet<>(); Map deviceCategoryMap = buildDeviceCategoryMap(siteId, arraylist); - Map batchContextBySite = buildBatchContextBySite(siteId, arraylist, deviceCategoryMap); - Date latestDataUpdateTime = null; - for (int i = 0; i < arraylist.size(); i++) { JSONObject obj = JSONObject.parseObject(arraylist.get(i).toString()); String deviceId = obj.getString("Device"); - String jsonData = obj.getString("Data"); + String jsonData = resolveJsonDataWithRedisFallback(siteId, deviceId, obj.getString("Data")); Long timestamp = obj.getLong("timestamp"); Date dataUpdateTime = DateUtils.convertUpdateTime(timestamp); - if (dataUpdateTime != null && (latestDataUpdateTime == null || dataUpdateTime.after(latestDataUpdateTime))) { - latestDataUpdateTime = dataUpdateTime; - } if (StringUtils.isNotBlank(deviceId)) { deviceIds.add(deviceId); @@ -264,20 +273,16 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i String deviceCategory = resolveDeviceCategory(siteId, deviceId, deviceCategoryMap); // 旧设备表落库链路已下线,MQTT 数据仅走点位映射与站点监控数据同步。 Map pointIdValueMap = processPointConfigData(siteId, deviceId, deviceCategory, mergedJsonData, dataUpdateTime); + List pointIdSamples = pointIdValueMap.keySet().stream().limit(10).collect(Collectors.toList()); + log.info("MQTT点位映射完成,siteId: {}, deviceId: {}, deviceCategory: {}, pointCount: {}, pointIdSamples: {}", + siteId, deviceId, deviceCategory, pointIdValueMap.size(), pointIdSamples); + cacheLatestPointValuesByPointId(siteId, deviceId, pointIdValueMap, dataUpdateTime); iEmsDeviceSettingService.syncSiteMonitorDataByMqtt(siteId, deviceId, JSON.toJSONString(pointIdValueMap), dataUpdateTime); } catch (Exception e) { log.warn("设备数据处理失败,siteId: {}, deviceId: {}, err: {}", siteId, deviceId, e.getMessage(), e); } } - Map calcPointIdValueMap = executeCalcPointConfigs( - siteId, SITE_LEVEL_CALC_DEVICE_ID, getSiteCalcPointConfigs(siteId), batchContextBySite, latestDataUpdateTime - ); - if (!calcPointIdValueMap.isEmpty()) { - iEmsDeviceSettingService.syncSiteMonitorDataByMqtt( - siteId, SITE_LEVEL_CALC_DEVICE_ID, JSON.toJSONString(calcPointIdValueMap), latestDataUpdateTime - ); - } log.info("结束处理设备数据, siteId: {}, deviceCount: {}, deviceIds: {}, costMs: {}", siteId, deviceIds.size(), String.join(",", deviceIds), System.currentTimeMillis() - startMs); } @@ -343,10 +348,576 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i enqueuePointData(siteId, deviceId, pointId, convertedValue, dataUpdateTime); pointIdValueMap.put(pointId, convertedValue); } - updateDailyEnergyDataByMqtt(siteId, deviceId, deviceCategory, pointIdValueMap, dataUpdateTime); return pointIdValueMap; } + private void updateDailyChargeDataByQuartz(String siteId, + Map pointIdValueMap, + Date dataUpdateTime) { + if (StringUtils.isBlank(siteId) + || org.apache.commons.collections4.MapUtils.isEmpty(pointIdValueMap)) { + return; + } + + List mappingList = getPointMatchesBySiteId(siteId); + if (CollectionUtils.isEmpty(mappingList)) { + return; + } + + Map normalizedPointValueMap = new HashMap<>(); + for (Map.Entry entry : pointIdValueMap.entrySet()) { + if (StringUtils.isBlank(entry.getKey()) || entry.getValue() == null) { + continue; + } + normalizedPointValueMap.put(entry.getKey().trim().toUpperCase(), entry.getValue()); + } + if (normalizedPointValueMap.isEmpty()) { + return; + } + + EmsSiteMonitorPointMatch totalChargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_TOTAL_CHARGE + ); + EmsSiteMonitorPointMatch totalDischargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_TOTAL_DISCHARGE + ); + EmsSiteMonitorPointMatch dayChargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_DAY_CHARGE + ); + EmsSiteMonitorPointMatch dayDischargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_DAY_DISCHARGE + ); + String totalChargePointId = totalChargeMatch == null ? null : StringUtils.trimToNull(totalChargeMatch.getDataPoint()); + String totalDischargePointId = totalDischargeMatch == null ? null : StringUtils.trimToNull(totalDischargeMatch.getDataPoint()); + String dayChargePointId = dayChargeMatch == null ? null : StringUtils.trimToNull(dayChargeMatch.getDataPoint()); + String dayDischargePointId = dayDischargeMatch == null ? null : StringUtils.trimToNull(dayDischargeMatch.getDataPoint()); + log.info("日充放电自检, siteId: {}, totalCharge: {}->{} , totalDischarge: {}->{} , dayCharge: {}->{} , dayDischarge: {}->{}", + siteId, + totalChargeMatch == null ? "null" : StringUtils.defaultString(totalChargeMatch.getFieldCode()), + totalChargePointId, + totalDischargeMatch == null ? "null" : StringUtils.defaultString(totalDischargeMatch.getFieldCode()), + totalDischargePointId, + dayChargeMatch == null ? "null" : StringUtils.defaultString(dayChargeMatch.getFieldCode()), + dayChargePointId, + dayDischargeMatch == null ? "null" : StringUtils.defaultString(dayDischargeMatch.getFieldCode()), + dayDischargePointId); + BigDecimal nowTotalCharge = getPointValueById(normalizedPointValueMap, totalChargePointId); + BigDecimal nowTotalDischarge = getPointValueById(normalizedPointValueMap, totalDischargePointId); + BigDecimal nowDayCharge = getLatestPointValueFromRedis(dayChargePointId); + BigDecimal nowDayDischarge = getLatestPointValueFromRedis(dayDischargePointId); + if (nowTotalCharge == null || nowTotalDischarge == null) { + log.info("日充放电累计未匹配到有效点位值, siteId: {}, totalChargePointId: {}, totalDischargePointId: {}, pointKeys: {}", + siteId, totalChargePointId, totalDischargePointId, normalizedPointValueMap.keySet()); + return; + } + + EmsDailyChargeData dailyChargeData = new EmsDailyChargeData(); + Date recordDateTime = dataUpdateTime == null ? DateUtils.getNowDate() : dataUpdateTime; + dailyChargeData.setSiteId(siteId); + dailyChargeData.setDateTime(recordDateTime); + dailyChargeData.setTotalChargeData(nowTotalCharge); + dailyChargeData.setTotalDischargeData(nowTotalDischarge); + dailyChargeData.setChargeData(nowDayCharge); + dailyChargeData.setDischargeData(nowDayDischarge); + + EmsDailyChargeData existedDailyData = emsDailyChargeDataMapper.selectBySiteIdAndDateTime(siteId, recordDateTime); + if (existedDailyData != null && existedDailyData.getId() != null) { + dailyChargeData.setId(existedDailyData.getId()); + dailyChargeData.setDateTime(existedDailyData.getDateTime()); + } + + Date now = DateUtils.getNowDate(); + dailyChargeData.setCreateBy("system"); + dailyChargeData.setCreateTime(now); + dailyChargeData.setUpdateBy("system"); + dailyChargeData.setUpdateTime(now); + emsDailyChargeDataMapper.insertOrUpdateData(dailyChargeData); + + log.info("日充放电累计落库完成, siteId: {}, total[充:{} 放:{}], daily[充:{} 放:{}], points[总充:{} 总放:{} 日充:{} 日放:{}]", + siteId, + dailyChargeData.getTotalChargeData(), dailyChargeData.getTotalDischargeData(), + dailyChargeData.getChargeData(), dailyChargeData.getDischargeData(), + totalChargePointId, totalDischargePointId, dayChargePointId, dayDischargePointId); + } + + public void syncDailyChargeDataFromInfluxByQuartz() { + syncDailyChargeDataFromInfluxByQuartz(null); + } + + public void syncDailyChargeDataFromInfluxByQuartz(String siteId) { + List siteIds; + if (StringUtils.isNotBlank(siteId)) { + siteIds = Collections.singletonList(siteId.trim()); + } else { + siteIds = emsSiteMonitorPointMatchMapper.selectDistinctSiteIds(); + } + if (CollectionUtils.isEmpty(siteIds)) { + log.info("Quartz同步日充放电跳过,未找到站点列表, siteIdParam: {}", siteId); + return; + } + for (String currentSiteId : siteIds) { + if (StringUtils.isBlank(currentSiteId)) { + continue; + } + try { + syncSingleSiteDailyChargeFromInflux(currentSiteId.trim()); + } catch (Exception ex) { + log.error("Quartz同步日充放电失败, siteId: {}, err: {}", currentSiteId, ex.getMessage(), ex); + } + } + } + + private void syncSingleSiteDailyChargeFromInflux(String siteId) { + List mappingList = getPointMatchesBySiteId(siteId); + if (CollectionUtils.isEmpty(mappingList)) { + log.info("Quartz同步日充放电跳过,未找到点位映射, siteId: {}", siteId); + return; + } + + Date queryEndTime = DateUtils.getNowDate(); + Date queryStartTime = new Date(queryEndTime.getTime() - DAILY_CHARGE_INFLUX_QUERY_WINDOW_MS); + EmsSiteMonitorPointMatch totalChargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_TOTAL_CHARGE + ); + EmsSiteMonitorPointMatch totalDischargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_TOTAL_DISCHARGE + ); + String totalChargePointId = totalChargeMatch == null ? null : StringUtils.trimToNull(totalChargeMatch.getDataPoint()); + String totalDischargePointId = totalDischargeMatch == null ? null : StringUtils.trimToNull(totalDischargeMatch.getDataPoint()); + log.info("Quartz同步日充放电自检, siteId: {}, totalCharge: {}->{} , totalDischarge: {}->{}", + siteId, + totalChargeMatch == null ? "null" : StringUtils.defaultString(totalChargeMatch.getFieldCode()), + totalChargePointId, + totalDischargeMatch == null ? "null" : StringUtils.defaultString(totalDischargeMatch.getFieldCode()), + totalDischargePointId); + if (StringUtils.isBlank(totalChargePointId) && StringUtils.isBlank(totalDischargePointId)) { + log.info("Quartz同步日充放电跳过,未找到总充/总放点位, siteId: {}", siteId); + return; + } + + Map pointIdValueMap = new HashMap<>(); + Date dataUpdateTime = null; + if (StringUtils.isNotBlank(totalChargePointId)) { + InfluxPointDataWriter.PointValue totalChargePoint = influxPointDataWriter.queryLatestPointValueByPointKey( + siteId, totalChargePointId, queryStartTime, queryEndTime + ); + if (totalChargePoint != null && totalChargePoint.getPointValue() != null) { + pointIdValueMap.put(totalChargePointId.trim().toUpperCase(), totalChargePoint.getPointValue()); + dataUpdateTime = totalChargePoint.getDataTime(); + } else { + log.info("Quartz同步日充放电未获取到总充点最新值, siteId: {}, pointId: {}, range: {} ~ {}", + siteId, totalChargePointId, + DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, queryStartTime), + DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, queryEndTime)); + } + } + if (StringUtils.isNotBlank(totalDischargePointId)) { + InfluxPointDataWriter.PointValue totalDischargePoint = influxPointDataWriter.queryLatestPointValueByPointKey( + siteId, totalDischargePointId, queryStartTime, queryEndTime + ); + if (totalDischargePoint != null && totalDischargePoint.getPointValue() != null) { + pointIdValueMap.put(totalDischargePointId.trim().toUpperCase(), totalDischargePoint.getPointValue()); + if (dataUpdateTime == null + || (totalDischargePoint.getDataTime() != null + && totalDischargePoint.getDataTime().after(dataUpdateTime))) { + dataUpdateTime = totalDischargePoint.getDataTime(); + } + } else { + log.info("Quartz同步日充放电未获取到总放点最新值, siteId: {}, pointId: {}, range: {} ~ {}", + siteId, totalDischargePointId, + DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, queryStartTime), + DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, queryEndTime)); + } + } + + if (org.apache.commons.collections4.MapUtils.isEmpty(pointIdValueMap)) { + log.info("Quartz同步日充放电跳过,点位最新值为空, siteId: {}", siteId); + return; + } + if (dataUpdateTime == null) { + log.info("Quartz同步日充放电点位值存在但时间为空, siteId: {}, pointIds: {}", + siteId, pointIdValueMap.keySet()); + } else { + log.info("Quartz同步日充放电准备落库, siteId: {}, dataUpdateTime: {}, pointIds: {}", + siteId, DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, dataUpdateTime), + pointIdValueMap.keySet()); + } + updateDailyChargeDataByQuartz(siteId, pointIdValueMap, dataUpdateTime); + updateDailyEnergyHourlyDataByQuartz(siteId, dataUpdateTime); + syncDailyChargeRevenueByQuartz(siteId, dataUpdateTime); + } + + private void syncDailyChargeRevenueByQuartz(String siteId, Date dataUpdateTime) { + if (StringUtils.isBlank(siteId)) { + return; + } + Date revenueDate = dataUpdateTime == null ? DateUtils.getNowDate() : dataUpdateTime; + String day = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, revenueDate); + List revenueRows = emsDailyEnergyDataMapper.getRevenueDataBySiteId(siteId, day, day); + + BigDecimal dayRevenue = BigDecimal.ZERO; + if (CollectionUtils.isNotEmpty(revenueRows)) { + AmmeterRevenueStatisListVo revenueRow = revenueRows.get(0); + BigDecimal activeTotalPrice = safeBigDecimal(revenueRow.getActivePeakPrice()) + .add(safeBigDecimal(revenueRow.getActiveHighPrice())) + .add(safeBigDecimal(revenueRow.getActiveFlatPrice())) + .add(safeBigDecimal(revenueRow.getActiveValleyPrice())); + BigDecimal reActiveTotalPrice = safeBigDecimal(revenueRow.getReActivePeakPrice()) + .add(safeBigDecimal(revenueRow.getReActiveHighPrice())) + .add(safeBigDecimal(revenueRow.getReActiveFlatPrice())) + .add(safeBigDecimal(revenueRow.getReActiveValleyPrice())); + dayRevenue = reActiveTotalPrice.subtract(activeTotalPrice); + } + BigDecimal totalRevenue = getYestLastData(siteId).add(dayRevenue); + + syncDailyRevenueToChargeData(siteId, revenueDate, totalRevenue, dayRevenue); + + log.info("Quartz同步日收益到充放电表完成, siteId: {}, date: {}, dayRevenue: {}, totalRevenue: {}", + siteId, day, dayRevenue, totalRevenue); + } + + private void updateDailyEnergyHourlyDataByQuartz(String siteId, Date dataUpdateTime) { + log.info("Quartz日电量小时落库入口, siteId: {}, dataTime: {}", + siteId, + dataUpdateTime == null ? "null" : DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, dataUpdateTime)); + if (StringUtils.isBlank(siteId)) { + return; + } + + List mappingList = getPointMatchesBySiteId(siteId); + if (CollectionUtils.isEmpty(mappingList)) { + return; + } + + Date recordDateTime = dataUpdateTime == null ? DateUtils.getNowDate() : dataUpdateTime; + Integer dataHour = resolveHourOfDay(recordDateTime); + + String priceKey = RedisKeyConstants.ENERGY_PRICE_TIME + siteId + "_" + LocalDate.now().getYear() + LocalDate.now().getMonthValue(); + EnergyPriceVo priceVo = redisCache.getCacheObject(priceKey); + if (priceVo == null) { + priceVo = emsEnergyPriceConfigService.getCurrentMonthPrice(siteId); + redisCache.setCacheObject(priceKey, priceVo, 31, TimeUnit.DAYS); + } + if (priceVo == null || CollectionUtils.isEmpty(priceVo.getRange())) { + log.info("Quartz日电量小时落库跳过,未找到有效电价时段配置, siteId: {}", siteId); + return; + } + + String costType = null; + for (EnergyPriceTimeRange timeRange : priceVo.getRange()) { + if (timeRange == null || StringUtils.isBlank(timeRange.getCostType())) { + continue; + } + if (isInPriceTimeRange(timeRange.getStartTime(), timeRange.getEndTime(), recordDateTime)) { + costType = timeRange.getCostType(); + break; + } + } + if (StringUtils.isBlank(costType)) { + log.info("Quartz日电量小时落库跳过,未命中电价时段, siteId: {}, dataTime: {}", + siteId, DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, recordDateTime)); + return; + } + + EmsSiteMonitorPointMatch peakChargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_PEAK_CHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB + ); + EmsSiteMonitorPointMatch peakDischargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_PEAK_DISCHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB + ); + EmsSiteMonitorPointMatch highChargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_HIGH_CHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB + ); + EmsSiteMonitorPointMatch highDischargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_HIGH_DISCHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB + ); + EmsSiteMonitorPointMatch flatChargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_FLAT_CHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB + ); + EmsSiteMonitorPointMatch flatDischargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_FLAT_DISCHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB + ); + EmsSiteMonitorPointMatch valleyChargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_VALLEY_CHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB + ); + EmsSiteMonitorPointMatch valleyDischargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_VALLEY_DISCHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB + ); + String peakChargePointId = peakChargeMatch == null ? null : StringUtils.trimToNull(peakChargeMatch.getDataPoint()); + String peakDischargePointId = peakDischargeMatch == null ? null : StringUtils.trimToNull(peakDischargeMatch.getDataPoint()); + String highChargePointId = highChargeMatch == null ? null : StringUtils.trimToNull(highChargeMatch.getDataPoint()); + String highDischargePointId = highDischargeMatch == null ? null : StringUtils.trimToNull(highDischargeMatch.getDataPoint()); + String flatChargePointId = flatChargeMatch == null ? null : StringUtils.trimToNull(flatChargeMatch.getDataPoint()); + String flatDischargePointId = flatDischargeMatch == null ? null : StringUtils.trimToNull(flatDischargeMatch.getDataPoint()); + String valleyChargePointId = valleyChargeMatch == null ? null : StringUtils.trimToNull(valleyChargeMatch.getDataPoint()); + String valleyDischargePointId = valleyDischargeMatch == null ? null : StringUtils.trimToNull(valleyDischargeMatch.getDataPoint()); + log.info("Quartz日电量小时自检, siteId: {}, peak: {}->{} / {}->{} , high: {}->{} / {}->{} , flat: {}->{} / {}->{} , valley: {}->{} / {}->{}", + siteId, + peakChargeMatch == null ? "null" : StringUtils.defaultString(peakChargeMatch.getFieldCode()), + peakChargePointId, + peakDischargeMatch == null ? "null" : StringUtils.defaultString(peakDischargeMatch.getFieldCode()), + peakDischargePointId, + highChargeMatch == null ? "null" : StringUtils.defaultString(highChargeMatch.getFieldCode()), + highChargePointId, + highDischargeMatch == null ? "null" : StringUtils.defaultString(highDischargeMatch.getFieldCode()), + highDischargePointId, + flatChargeMatch == null ? "null" : StringUtils.defaultString(flatChargeMatch.getFieldCode()), + flatChargePointId, + flatDischargeMatch == null ? "null" : StringUtils.defaultString(flatDischargeMatch.getFieldCode()), + flatDischargePointId, + valleyChargeMatch == null ? "null" : StringUtils.defaultString(valleyChargeMatch.getFieldCode()), + valleyChargePointId, + valleyDischargeMatch == null ? "null" : StringUtils.defaultString(valleyDischargeMatch.getFieldCode()), + valleyDischargePointId); + String currentChargePointId; + String currentDischargePointId; + BigDecimal currentCharge; + BigDecimal currentDischarge; + switch (costType) { + case "peak": + currentChargePointId = peakChargePointId; + currentDischargePointId = peakDischargePointId; + currentCharge = getLatestPointValueFromRedis(peakChargePointId); + currentDischarge = getLatestPointValueFromRedis(peakDischargePointId); + break; + case "high": + currentChargePointId = highChargePointId; + currentDischargePointId = highDischargePointId; + currentCharge = getLatestPointValueFromRedis(highChargePointId); + currentDischarge = getLatestPointValueFromRedis(highDischargePointId); + break; + case "flat": + currentChargePointId = flatChargePointId; + currentDischargePointId = flatDischargePointId; + currentCharge = getLatestPointValueFromRedis(flatChargePointId); + currentDischarge = getLatestPointValueFromRedis(flatDischargePointId); + break; + case "valley": + currentChargePointId = valleyChargePointId; + currentDischargePointId = valleyDischargePointId; + currentCharge = getLatestPointValueFromRedis(valleyChargePointId); + currentDischarge = getLatestPointValueFromRedis(valleyDischargePointId); + break; + default: + log.info("Quartz日电量小时落库跳过,未知电价时段类型, siteId: {}, costType: {}", siteId, costType); + return; + } + + if (currentCharge == null && currentDischarge == null) { + log.info("Quartz日电量小时值未匹配到有效Redis点位值, siteId: {}, costType: {}, chargePoint: {}, dischargePoint: {}", + siteId, costType, currentChargePointId, currentDischargePointId); + return; + } + + EmsDailyEnergyData energyData = new EmsDailyEnergyData(); + energyData.setSiteId(siteId); + energyData.setDataDate(recordDateTime); + energyData.setDataHour(dataHour); + if ("peak".equals(costType)) { + energyData.setPeakChargeDiff(currentCharge); + energyData.setPeakDischargeDiff(currentDischarge); + } else if ("high".equals(costType)) { + energyData.setHighChargeDiff(currentCharge); + energyData.setHighDischargeDiff(currentDischarge); + } else if ("flat".equals(costType)) { + energyData.setFlatChargeDiff(currentCharge); + energyData.setFlatDischargeDiff(currentDischarge); + } else if ("valley".equals(costType)) { + energyData.setValleyChargeDiff(currentCharge); + energyData.setValleyDischargeDiff(currentDischarge); + } + energyData.setCalcTime(recordDateTime); + + EmsDailyEnergyData existedEnergyData = emsDailyEnergyDataMapper.selectBySiteIdAndDateHour(siteId, recordDateTime, dataHour); + if (existedEnergyData != null && existedEnergyData.getId() != null) { + energyData.setId(existedEnergyData.getId()); + energyData.setDataDate(existedEnergyData.getDataDate()); + energyData.setDataHour(existedEnergyData.getDataHour()); + } + + Date now = DateUtils.getNowDate(); + energyData.setCreateBy("system"); + energyData.setCreateTime(now); + energyData.setUpdateBy("system"); + energyData.setUpdateTime(now); + emsDailyEnergyDataMapper.insertOrUpdateData(energyData); + + log.info("Quartz日电量小时落库完成, siteId: {}, hour: {}, costType: {}, charge: {}, discharge: {}, chargePoint: {}, dischargePoint: {}", + siteId, dataHour, costType, currentCharge, currentDischarge, currentChargePointId, currentDischargePointId); + } + + private boolean isInPriceTimeRange(String startTime, String endTime, Date dataUpdateTime) { + if (StringUtils.isAnyBlank(startTime, endTime) || dataUpdateTime == null) { + return false; + } + try { + LocalTime current = LocalTime.parse(DateUtils.parseDateToStr("HH:mm", dataUpdateTime)); + LocalTime start = LocalTime.parse(startTime); + LocalTime end = LocalTime.parse(endTime); + if (start.equals(end)) { + return true; + } + if (start.isBefore(end)) { + return !current.isBefore(start) && current.isBefore(end); + } + return !current.isBefore(start) || current.isBefore(end); + } catch (Exception ex) { + log.warn("解析电价时段失败, start: {}, end: {}, err: {}", startTime, endTime, ex.getMessage()); + return false; + } + } + + private Integer resolveHourOfDay(Date dateTime) { + Date targetTime = dateTime == null ? DateUtils.getNowDate() : dateTime; + String hourText = DateUtils.parseDateToStr("HH", targetTime); + try { + return Integer.parseInt(hourText); + } catch (NumberFormatException ex) { + return LocalDateTime.now().getHour(); + } + } + + public void syncCalcPointDataFromInfluxByQuartz() { + syncCalcPointDataFromInfluxByQuartz(null); + } + + public void syncCalcPointDataFromInfluxByQuartz(String siteId) { + List siteIds; + if (StringUtils.isNotBlank(siteId)) { + siteIds = Collections.singletonList(siteId.trim()); + } else { + siteIds = emsSiteMonitorPointMatchMapper.selectDistinctSiteIds(); + } + if (CollectionUtils.isEmpty(siteIds)) { + return; + } + for (String currentSiteId : siteIds) { + if (StringUtils.isBlank(currentSiteId)) { + continue; + } + try { + syncSingleSiteCalcPointFromInflux(currentSiteId.trim()); + } catch (Exception ex) { + log.error("Quartz同步计算点失败, siteId: {}, err: {}", currentSiteId, ex.getMessage(), ex); + } + } + } + + private void syncSingleSiteCalcPointFromInflux(String siteId) { + List calcPointConfigs = getSiteCalcPointConfigs(siteId); + if (CollectionUtils.isEmpty(calcPointConfigs)) { + return; + } + List sourcePointConfigs = getSiteSourcePointConfigs(siteId); + if (CollectionUtils.isEmpty(sourcePointConfigs)) { + log.info("Quartz同步计算点跳过,未找到源点位配置, siteId: {}", siteId); + return; + } + + Date queryEndTime = DateUtils.getNowDate(); + Date queryStartTime = new Date(queryEndTime.getTime() - CALC_POINT_INFLUX_QUERY_WINDOW_MS); + Map contextValues = new HashMap<>(); + Date latestDataTime = null; + for (EmsPointConfig sourcePointConfig : sourcePointConfigs) { + if (sourcePointConfig == null) { + continue; + } + String pointId = resolveInfluxPointKey(sourcePointConfig); + if (StringUtils.isBlank(pointId)) { + continue; + } + InfluxPointDataWriter.PointValue latestPointValue = influxPointDataWriter.queryLatestPointValueByPointKey( + siteId, pointId, queryStartTime, queryEndTime + ); + if (latestPointValue == null || latestPointValue.getPointValue() == null) { + continue; + } + putSourcePointValueToContext(sourcePointConfig, latestPointValue.getPointValue(), contextValues); + if (latestDataTime == null + || (latestPointValue.getDataTime() != null && latestPointValue.getDataTime().after(latestDataTime))) { + latestDataTime = latestPointValue.getDataTime(); + } + } + + if (org.apache.commons.collections4.MapUtils.isEmpty(contextValues)) { + log.info("Quartz同步计算点跳过,InfluxDB未查询到源点位最新值, siteId: {}", siteId); + return; + } + Date calcTime = latestDataTime == null ? queryEndTime : latestDataTime; + Map calcPointIdValueMap = executeCalcPointConfigs( + siteId, SITE_LEVEL_CALC_DEVICE_ID, calcPointConfigs, contextValues, calcTime + ); + if (!calcPointIdValueMap.isEmpty()) { + cacheLatestPointValuesByPointId(siteId, SITE_LEVEL_CALC_DEVICE_ID, calcPointIdValueMap, calcTime); + iEmsDeviceSettingService.syncSiteMonitorDataByMqtt( + siteId, SITE_LEVEL_CALC_DEVICE_ID, JSON.toJSONString(calcPointIdValueMap), calcTime + ); + } + } + + private String firstNonBlankPointByFieldSuffixIgnoreDevice(List mappingList, + String[] fieldSuffixes, + String... fieldPrefixes) { + EmsSiteMonitorPointMatch match = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, fieldSuffixes, fieldPrefixes + ); + return match == null ? null : StringUtils.trimToNull(match.getDataPoint()); + } + + private EmsSiteMonitorPointMatch firstNonBlankMatchByFieldSuffixIgnoreDevice(List mappingList, + String[] fieldSuffixes, + String... fieldPrefixes) { + if (CollectionUtils.isEmpty(mappingList) || fieldSuffixes == null || fieldSuffixes.length == 0) { + return null; + } + List normalizedPrefixes = new ArrayList<>(); + if (fieldPrefixes != null && fieldPrefixes.length > 0) { + for (String prefix : fieldPrefixes) { + String normalizedPrefix = StringUtils.trimToNull(prefix); + if (normalizedPrefix != null) { + normalizedPrefixes.add(normalizedPrefix.toLowerCase()); + } + } + } + for (String suffix : fieldSuffixes) { + if (StringUtils.isBlank(suffix)) { + continue; + } + String normalizedSuffix = suffix.trim().toLowerCase(); + for (EmsSiteMonitorPointMatch mapping : mappingList) { + if (mapping == null || StringUtils.isBlank(mapping.getFieldCode())) { + continue; + } + String dataPoint = StringUtils.defaultString(mapping.getDataPoint()).trim(); + if (StringUtils.isBlank(dataPoint) || DELETED_FIELD_MARK.equalsIgnoreCase(dataPoint)) { + continue; + } + String fieldCode = mapping.getFieldCode().trim().toLowerCase(); + if (!normalizedPrefixes.isEmpty()) { + if (fieldCode.equals(normalizedSuffix)) { + return mapping; + } + boolean matchedPrefix = false; + for (String prefix : normalizedPrefixes) { + if (fieldCode.startsWith(prefix)) { + matchedPrefix = true; + String stripped = fieldCode.substring(prefix.length()); + if (stripped.equals(normalizedSuffix)) { + return mapping; + } + } + } + if (matchedPrefix) { + continue; + } + } else if (fieldCode.equals(normalizedSuffix)) { + return mapping; + } + } + } + return null; + } + private String mergeWithLatestRedisData(String siteId, String deviceId, String currentJsonData) { Map currentDataMap = parseDataJsonToMap(currentJsonData); if (StringUtils.isAnyBlank(siteId, deviceId)) { @@ -368,6 +939,22 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return JSON.toJSONString(merged); } + private String resolveJsonDataWithRedisFallback(String siteId, String deviceId, String jsonData) { + if (!checkJsonDataEmpty(jsonData)) { + return jsonData; + } + if (StringUtils.isAnyBlank(siteId, deviceId)) { + return jsonData; + } + String redisKey = RedisKeyConstants.ORIGINAL_MQTT_DATA + siteId + "_" + deviceId; + Object latestObj = redisCache.getCacheObject(redisKey); + Map latestDataMap = extractDataMapFromLatest(latestObj); + if (latestDataMap.isEmpty()) { + return jsonData; + } + return JSON.toJSONString(latestDataMap); + } + private Map extractDataMapFromLatest(Object latestObj) { if (latestObj == null) { return new LinkedHashMap<>(); @@ -410,238 +997,6 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return parsed == null ? new LinkedHashMap<>() : new LinkedHashMap<>(parsed); } - private void updateDailyEnergyDataByMqtt(String siteId, String deviceId, String deviceCategory, - Map pointIdValueMap, - Date dataUpdateTime) { - if (StringUtils.isBlank(siteId) || StringUtils.isBlank(deviceId) - || org.apache.commons.collections4.MapUtils.isEmpty(pointIdValueMap)) { - return; - } - // 仅处理储能电表,避免其它设备误触发日报计算 - if (!SiteDevice.METE.name().equalsIgnoreCase(deviceId) - && !DeviceCategory.AMMETER.getCode().equalsIgnoreCase(StringUtils.defaultString(deviceCategory))) { - return; - } - - Map mappingByFieldAndDevice = getMonitorPointMappingByFieldAndDevice(siteId); - if (mappingByFieldAndDevice.isEmpty()) { - return; - } - - Map normalizedPointValueMap = new HashMap<>(); - for (Map.Entry entry : pointIdValueMap.entrySet()) { - if (StringUtils.isBlank(entry.getKey()) || entry.getValue() == null) { - continue; - } - normalizedPointValueMap.put(entry.getKey().trim().toUpperCase(), entry.getValue()); - } - if (normalizedPointValueMap.isEmpty()) { - return; - } - - String peakChargePointId = firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, FIELD_SUFFIX_PEAK_CHARGE); - String peakDischargePointId = firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, FIELD_SUFFIX_PEAK_DISCHARGE); - String highChargePointId = firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, FIELD_SUFFIX_HIGH_CHARGE); - String highDischargePointId = firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, FIELD_SUFFIX_HIGH_DISCHARGE); - String flatChargePointId = firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, FIELD_SUFFIX_FLAT_CHARGE); - String flatDischargePointId = firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, FIELD_SUFFIX_FLAT_DISCHARGE); - String valleyChargePointId = firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, FIELD_SUFFIX_VALLEY_CHARGE); - String valleyDischargePointId = firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, FIELD_SUFFIX_VALLEY_DISCHARGE); - - Map currentTotals = new HashMap<>(); - currentTotals.put(ENERGY_METRIC_PEAK_CHARGE, getPointValueById(normalizedPointValueMap, peakChargePointId)); - currentTotals.put(ENERGY_METRIC_PEAK_DISCHARGE, getPointValueById(normalizedPointValueMap, peakDischargePointId)); - currentTotals.put(ENERGY_METRIC_HIGH_CHARGE, getPointValueById(normalizedPointValueMap, highChargePointId)); - currentTotals.put(ENERGY_METRIC_HIGH_DISCHARGE, getPointValueById(normalizedPointValueMap, highDischargePointId)); - currentTotals.put(ENERGY_METRIC_FLAT_CHARGE, getPointValueById(normalizedPointValueMap, flatChargePointId)); - currentTotals.put(ENERGY_METRIC_FLAT_DISCHARGE, getPointValueById(normalizedPointValueMap, flatDischargePointId)); - currentTotals.put(ENERGY_METRIC_VALLEY_CHARGE, getPointValueById(normalizedPointValueMap, valleyChargePointId)); - currentTotals.put(ENERGY_METRIC_VALLEY_DISCHARGE, getPointValueById(normalizedPointValueMap, valleyDischargePointId)); - log.info("日电量分时映射命中, siteId: {}, deviceId: {}, points[尖充:{} 尖放:{} 峰充:{} 峰放:{} 平充:{} 平放:{} 谷充:{} 谷放:{}]", - siteId, deviceId, - peakChargePointId, peakDischargePointId, - highChargePointId, highDischargePointId, - flatChargePointId, flatDischargePointId, - valleyChargePointId, valleyDischargePointId); - boolean hasAnyCurrentTotal = currentTotals.values().stream().anyMatch(Objects::nonNull); - if (!hasAnyCurrentTotal) { - log.info("日电量累计未匹配到有效点位值, siteId: {}, deviceId: {}, pointKeys: {}", - siteId, deviceId, normalizedPointValueMap.keySet()); - return; - } - log.info("日电量累计当前值, siteId: {}, deviceId: {}, currentTotals: {}", - siteId, deviceId, currentTotals); - - Map lastTotals = getCachedDailyEnergyRawTotals(siteId, deviceId); - if (org.apache.commons.collections4.MapUtils.isEmpty(lastTotals)) { - lastTotals = loadYesterdayAmmeterTotals(siteId, deviceId); - if (!org.apache.commons.collections4.MapUtils.isEmpty(lastTotals)) { - log.info("日电量累计使用昨日末值作为基线, siteId: {}, deviceId: {}, lastTotals: {}", - siteId, deviceId, lastTotals); - } - } - if (org.apache.commons.collections4.MapUtils.isEmpty(lastTotals)) { - log.info("日电量累计缺少上一条基线, 仅缓存当前值等待下一次计算, siteId: {}, deviceId: {}, currentTotals: {}", - siteId, deviceId, currentTotals); - String redisKey = buildDailyEnergyRawCacheKey(siteId, deviceId); - redisCache.setCacheObject(redisKey, currentTotals, 2, TimeUnit.DAYS); - return; - } - EmsDailyEnergyData energyData = initEnergyData(siteId); - accumulateDailyEnergyDiff(energyData, currentTotals, lastTotals, siteId, deviceId); - recalculateDailyEnergyRevenue(siteId, energyData); - energyData.setCalcTime(dataUpdateTime == null ? DateUtils.getNowDate() : dataUpdateTime); - emsDailyEnergyDataMapper.insertOrUpdateData(energyData); - log.info("日电量累计落库完成, siteId: {}, deviceId: {}, diff[尖充:{} 尖放:{} 峰充:{} 峰放:{} 平充:{} 平放:{} 谷充:{} 谷放:{}], dayRevenue: {}, totalRevenue: {}", - siteId, deviceId, - energyData.getPeakChargeDiff(), energyData.getPeakDischargeDiff(), - energyData.getHighChargeDiff(), energyData.getHighDischargeDiff(), - energyData.getFlatChargeDiff(), energyData.getFlatDischargeDiff(), - energyData.getValleyChargeDiff(), energyData.getValleyDischargeDiff(), - energyData.getDayRevenue(), energyData.getTotalRevenue()); - - String redisKey = buildDailyEnergyRawCacheKey(siteId, deviceId); - redisCache.setCacheObject(redisKey, currentTotals, 2, TimeUnit.DAYS); - } - - private Map loadYesterdayAmmeterTotals(String siteId, String deviceId) { - if (StringUtils.isAnyBlank(siteId, deviceId)) { - return Collections.emptyMap(); - } - String yestDate = DateUtils.getYesterdayDate(); - EmsAmmeterData yestData = emsAmmeterDataMapper.getYestLatestDate(siteId, deviceId, yestDate); - if (yestData == null) { - return Collections.emptyMap(); - } - Map lastTotals = new HashMap<>(); - lastTotals.put(ENERGY_METRIC_PEAK_CHARGE, safeBigDecimal(yestData.getCurrentForwardActivePeak())); - lastTotals.put(ENERGY_METRIC_PEAK_DISCHARGE, safeBigDecimal(yestData.getCurrentReverseActivePeak())); - lastTotals.put(ENERGY_METRIC_HIGH_CHARGE, safeBigDecimal(yestData.getCurrentForwardActiveHigh())); - lastTotals.put(ENERGY_METRIC_HIGH_DISCHARGE, safeBigDecimal(yestData.getCurrentReverseActiveHigh())); - lastTotals.put(ENERGY_METRIC_FLAT_CHARGE, safeBigDecimal(yestData.getCurrentForwardActiveFlat())); - lastTotals.put(ENERGY_METRIC_FLAT_DISCHARGE, safeBigDecimal(yestData.getCurrentReverseActiveFlat())); - lastTotals.put(ENERGY_METRIC_VALLEY_CHARGE, safeBigDecimal(yestData.getCurrentForwardActiveValley())); - lastTotals.put(ENERGY_METRIC_VALLEY_DISCHARGE, safeBigDecimal(yestData.getCurrentReverseActiveValley())); - return lastTotals; - } - - private void accumulateDailyEnergyDiff(EmsDailyEnergyData energyData, - Map currentTotals, - Map lastTotals, - String siteId, - String deviceId) { - if (energyData == null || org.apache.commons.collections4.MapUtils.isEmpty(currentTotals)) { - return; - } - addMetricIncrement(energyData, ENERGY_METRIC_PEAK_CHARGE, currentTotals, lastTotals, siteId, deviceId); - addMetricIncrement(energyData, ENERGY_METRIC_PEAK_DISCHARGE, currentTotals, lastTotals, siteId, deviceId); - addMetricIncrement(energyData, ENERGY_METRIC_HIGH_CHARGE, currentTotals, lastTotals, siteId, deviceId); - addMetricIncrement(energyData, ENERGY_METRIC_HIGH_DISCHARGE, currentTotals, lastTotals, siteId, deviceId); - addMetricIncrement(energyData, ENERGY_METRIC_FLAT_CHARGE, currentTotals, lastTotals, siteId, deviceId); - addMetricIncrement(energyData, ENERGY_METRIC_FLAT_DISCHARGE, currentTotals, lastTotals, siteId, deviceId); - addMetricIncrement(energyData, ENERGY_METRIC_VALLEY_CHARGE, currentTotals, lastTotals, siteId, deviceId); - addMetricIncrement(energyData, ENERGY_METRIC_VALLEY_DISCHARGE, currentTotals, lastTotals, siteId, deviceId); - } - - private void addMetricIncrement(EmsDailyEnergyData energyData, - String metric, - Map currentTotals, - Map lastTotals, - String siteId, - String deviceId) { - BigDecimal current = currentTotals.get(metric); - if (current == null) { - return; - } - BigDecimal last = lastTotals == null ? null : lastTotals.get(metric); - if (last == null) { - return; - } - BigDecimal delta = current.subtract(last); - if (delta.compareTo(BigDecimal.ZERO) < 0) { - log.warn("日电量累计值回退,跳过本次增量,siteId: {}, deviceId: {}, metric: {}, current: {}, last: {}", - siteId, deviceId, metric, current, last); - return; - } - - if (ENERGY_METRIC_PEAK_CHARGE.equals(metric)) { - energyData.setPeakChargeDiff(safeBigDecimal(energyData.getPeakChargeDiff()).add(delta)); - return; - } - if (ENERGY_METRIC_PEAK_DISCHARGE.equals(metric)) { - energyData.setPeakDischargeDiff(safeBigDecimal(energyData.getPeakDischargeDiff()).add(delta)); - return; - } - if (ENERGY_METRIC_HIGH_CHARGE.equals(metric)) { - energyData.setHighChargeDiff(safeBigDecimal(energyData.getHighChargeDiff()).add(delta)); - return; - } - if (ENERGY_METRIC_HIGH_DISCHARGE.equals(metric)) { - energyData.setHighDischargeDiff(safeBigDecimal(energyData.getHighDischargeDiff()).add(delta)); - return; - } - if (ENERGY_METRIC_FLAT_CHARGE.equals(metric)) { - energyData.setFlatChargeDiff(safeBigDecimal(energyData.getFlatChargeDiff()).add(delta)); - return; - } - if (ENERGY_METRIC_FLAT_DISCHARGE.equals(metric)) { - energyData.setFlatDischargeDiff(safeBigDecimal(energyData.getFlatDischargeDiff()).add(delta)); - return; - } - if (ENERGY_METRIC_VALLEY_CHARGE.equals(metric)) { - energyData.setValleyChargeDiff(safeBigDecimal(energyData.getValleyChargeDiff()).add(delta)); - return; - } - if (ENERGY_METRIC_VALLEY_DISCHARGE.equals(metric)) { - energyData.setValleyDischargeDiff(safeBigDecimal(energyData.getValleyDischargeDiff()).add(delta)); - } - } - - private void recalculateDailyEnergyRevenue(String siteId, EmsDailyEnergyData energyData) { - if (StringUtils.isBlank(siteId) || energyData == null) { - return; - } - String priceKey = RedisKeyConstants.ENERGY_PRICE_TIME + siteId + "_" + LocalDate.now().getYear() + LocalDate.now().getMonthValue(); - EnergyPriceVo priceVo = redisCache.getCacheObject(priceKey); - if (priceVo == null) { - priceVo = emsEnergyPriceConfigService.getCurrentMonthPrice(siteId); - redisCache.setCacheObject(priceKey, priceVo, 31, TimeUnit.DAYS); - } - if (priceVo == null) { - energyData.setDayRevenue(BigDecimal.ZERO); - energyData.setTotalRevenue(getYestLastData(siteId)); - return; - } - - BigDecimal peakRevenue = safeBigDecimal(energyData.getPeakDischargeDiff()) - .subtract(safeBigDecimal(energyData.getPeakChargeDiff())) - .multiply(safeBigDecimal(priceVo.getPeak())); - BigDecimal highRevenue = safeBigDecimal(energyData.getHighDischargeDiff()) - .subtract(safeBigDecimal(energyData.getHighChargeDiff())) - .multiply(safeBigDecimal(priceVo.getHigh())); - BigDecimal flatRevenue = safeBigDecimal(energyData.getFlatDischargeDiff()) - .subtract(safeBigDecimal(energyData.getFlatChargeDiff())) - .multiply(safeBigDecimal(priceVo.getFlat())); - BigDecimal valleyRevenue = safeBigDecimal(energyData.getValleyDischargeDiff()) - .subtract(safeBigDecimal(energyData.getValleyChargeDiff())) - .multiply(safeBigDecimal(priceVo.getValley())); - - BigDecimal dayRevenue = peakRevenue.add(highRevenue).add(flatRevenue).add(valleyRevenue); - energyData.setDayRevenue(dayRevenue); - energyData.setTotalRevenue(getYestLastData(siteId).add(dayRevenue)); - } - - private BigDecimal resolveEnergyMetricValue(Map mappingByFieldAndDevice, - String deviceId, - Map pointIdValueMap, - String[] fieldSuffixes) { - String pointId = firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, fieldSuffixes); - if (StringUtils.isBlank(pointId)) { - return null; - } - return pointIdValueMap.get(pointId.trim().toUpperCase()); - } - private BigDecimal getPointValueById(Map pointIdValueMap, String pointId) { if (pointIdValueMap == null || pointIdValueMap.isEmpty() || StringUtils.isBlank(pointId)) { return null; @@ -649,9 +1004,82 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return pointIdValueMap.get(pointId.trim().toUpperCase()); } + private BigDecimal getLatestPointValueFromRedis(String pointId) { + String normalizedPointId = StringUtils.trimToNull(pointId); + if (normalizedPointId == null) { + return null; + } + List candidates = new ArrayList<>(); + candidates.add(normalizedPointId); + String upperPointId = normalizedPointId.toUpperCase(); + if (!StringUtils.equals(upperPointId, normalizedPointId)) { + candidates.add(upperPointId); + } + String lowerPointId = normalizedPointId.toLowerCase(); + if (!StringUtils.equals(lowerPointId, normalizedPointId) + && !StringUtils.equals(lowerPointId, upperPointId)) { + candidates.add(lowerPointId); + } + for (String candidate : candidates) { + Object raw = redisCache.getCacheObject(candidate); + if (raw == null) { + continue; + } + try { + JSONObject valueObj = raw instanceof JSONObject + ? (JSONObject) raw + : JSON.parseObject(JSON.toJSONString(raw)); + if (valueObj == null) { + continue; + } + BigDecimal pointValue = StringUtils.getBigDecimal(valueObj.get("pointValue")); + if (pointValue == null) { + pointValue = readBigDecimalField(raw, "pointValue"); + } + if (pointValue != null) { + return pointValue; + } + } catch (Exception ex) { + log.warn("读取点位Redis值失败, pointId: {}, redisKey: {}, err: {}", normalizedPointId, candidate, ex.getMessage()); + } + } + return null; + } + + private BigDecimal readBigDecimalField(Object source, String fieldName) { + Object fieldValue = readFieldValue(source, fieldName); + return StringUtils.getBigDecimal(fieldValue); + } + + private Object readFieldValue(Object source, String fieldName) { + if (source == null || StringUtils.isBlank(fieldName)) { + return null; + } + Class current = source.getClass(); + while (current != null) { + try { + Field field = current.getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(source); + } catch (NoSuchFieldException ex) { + current = current.getSuperclass(); + } catch (Exception ex) { + return null; + } + } + return null; + } + private String firstNonBlankPointByFieldSuffix(Map mappingByFieldAndDevice, String deviceId, String[] fieldSuffixes) { + return firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, fieldSuffixes, MONITOR_FIELD_PREFIX_HOME); + } + + private String firstNonBlankPointByFieldSuffix(Map mappingByFieldAndDevice, + String deviceId, + String[] fieldSuffixes, + String... fieldPrefixes) { if (mappingByFieldAndDevice == null || fieldSuffixes == null || fieldSuffixes.length == 0) { return null; } @@ -661,11 +1089,15 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i continue; } String normalizedSuffix = suffix.trim().toLowerCase(); - EmsSiteMonitorPointMatch exactMatch = resolvePointMatchByFieldSuffix(mappingByFieldAndDevice, normalizedDeviceId, normalizedSuffix); + EmsSiteMonitorPointMatch exactMatch = resolvePointMatchByFieldSuffix( + mappingByFieldAndDevice, normalizedDeviceId, normalizedSuffix, fieldPrefixes + ); if (exactMatch != null && StringUtils.isNotBlank(exactMatch.getDataPoint())) { return exactMatch.getDataPoint().trim(); } - EmsSiteMonitorPointMatch fallbackMatch = resolvePointMatchByFieldSuffix(mappingByFieldAndDevice, "", normalizedSuffix); + EmsSiteMonitorPointMatch fallbackMatch = resolvePointMatchByFieldSuffix( + mappingByFieldAndDevice, "", normalizedSuffix, fieldPrefixes + ); if (fallbackMatch != null && StringUtils.isNotBlank(fallbackMatch.getDataPoint())) { return fallbackMatch.getDataPoint().trim(); } @@ -675,10 +1107,20 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i private EmsSiteMonitorPointMatch resolvePointMatchByFieldSuffix(Map mappingByFieldAndDevice, String deviceId, - String suffixLowerCase) { + String suffixLowerCase, + String... fieldPrefixes) { if (mappingByFieldAndDevice == null || StringUtils.isBlank(suffixLowerCase)) { return null; } + List normalizedPrefixes = new ArrayList<>(); + if (fieldPrefixes != null && fieldPrefixes.length > 0) { + for (String prefix : fieldPrefixes) { + String normalizedPrefix = StringUtils.trimToNull(prefix); + if (normalizedPrefix != null) { + normalizedPrefixes.add(normalizedPrefix.toLowerCase()); + } + } + } for (Map.Entry entry : mappingByFieldAndDevice.entrySet()) { EmsSiteMonitorPointMatch mapping = entry.getValue(); if (mapping == null || StringUtils.isBlank(mapping.getFieldCode())) { @@ -693,7 +1135,10 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i continue; } String fieldCode = mapping.getFieldCode().trim().toLowerCase(); - if (fieldCode.endsWith(suffixLowerCase)) { + if (!normalizedPrefixes.isEmpty() && normalizedPrefixes.stream().noneMatch(fieldCode::startsWith)) { + continue; + } + if (fieldCode.equals(suffixLowerCase)) { return mapping; } } @@ -763,44 +1208,6 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return StringUtils.defaultString(fieldCode).trim() + "|" + StringUtils.defaultString(deviceId).trim(); } - private Map getCachedDailyEnergyRawTotals(String siteId, String deviceId) { - String redisKey = buildDailyEnergyRawCacheKey(siteId, deviceId); - Object cacheObj = redisCache.getCacheObject(redisKey); - if (cacheObj == null) { - return Collections.emptyMap(); - } - try { - if (cacheObj instanceof Map) { - Map mapObj = (Map) cacheObj; - Map parsed = new HashMap<>(); - for (Map.Entry entry : mapObj.entrySet()) { - if (entry.getKey() == null || entry.getValue() == null) { - continue; - } - String key = String.valueOf(entry.getKey()).trim(); - if (StringUtils.isBlank(key)) { - continue; - } - BigDecimal value = StringUtils.getBigDecimal(entry.getValue()); - if (value != null) { - parsed.put(key, value); - } - } - return parsed; - } - return JSON.parseObject(JSON.toJSONString(cacheObj), new TypeReference>() { - }); - } catch (Exception e) { - log.warn("解析日电量原始累计缓存失败,siteId: {}, deviceId: {}, err: {}", - siteId, deviceId, e.getMessage()); - return Collections.emptyMap(); - } - } - - private String buildDailyEnergyRawCacheKey(String siteId, String deviceId) { - return DAILY_ENERGY_RAW_CACHE_PREFIX + StringUtils.defaultString(siteId).trim() + "_" + StringUtils.defaultString(deviceId).trim(); - } - private BigDecimal safeBigDecimal(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } @@ -855,7 +1262,7 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i String deviceId = item.getString("Device"); String devicePrefix = normalizeVariablePart(deviceId); String deviceCategory = resolveDeviceCategory(siteId, deviceId, deviceCategoryMap); - String jsonData = item.getString("Data"); + String jsonData = resolveJsonDataWithRedisFallback(siteId, deviceId, item.getString("Data")); if (checkJsonDataEmpty(jsonData)) { continue; } @@ -1010,7 +1417,8 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i String calcDataKey = entry.getKey(); EmsPointConfig calcPointConfig = entry.getValue(); try { - ExpressionValue calcValue = evaluateCalcExpression(calcPointConfig.getCalcExpression(), contextValues); + ExpressionValue calcValue = evaluateCalcExpression( + calcPointConfig.getCalcExpression(), contextValues, siteId, dataUpdateTime); if (calcValue.isNumber()) { contextValues.put(calcDataKey, calcValue.asNumber()); putPointValueToContext(calcPointConfig, calcValue.asNumber(), contextValues); @@ -1076,15 +1484,207 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return StringUtils.isNotBlank(pointId) ? pointId : null; } - private ExpressionValue evaluateCalcExpression(String expression, Map contextValues) { + private ExpressionValue evaluateCalcExpression(String expression, Map contextValues, + String siteId, Date dataUpdateTime) { if (StringUtils.isBlank(expression)) { throw new IllegalArgumentException("计算表达式为空"); } if (!SIMPLE_CALC_EXPRESSION_PATTERN.matcher(expression).matches()) { - throw new IllegalArgumentException("计算表达式仅支持四则运算"); + throw new IllegalArgumentException("计算表达式仅支持数字、字母、下划线、空格、运算符和函数语法"); } + Map evaluateContext = contextValues == null + ? new HashMap<>() + : new HashMap<>(contextValues); + prepareAutoPeriodDiffContextValues(expression, evaluateContext, siteId, dataUpdateTime); CompiledExpression compiledExpression = calcExpressionCache.computeIfAbsent(expression, this::compileExpression); - return compiledExpression.evaluate(contextValues == null ? Collections.emptyMap() : contextValues); + return compiledExpression.evaluate(evaluateContext); + } + + private void prepareAutoPeriodDiffContextValues(String expression, Map contextValues, + String siteId, Date dataUpdateTime) { + if (StringUtils.isBlank(expression) || contextValues == null || contextValues.isEmpty()) { + return; + } + Date effectiveTime = dataUpdateTime == null ? DateUtils.getNowDate() : dataUpdateTime; + prepareSingleAutoDiffContext(expression, contextValues, siteId, effectiveTime, "DAY_DIFF", + AUTO_DAY_DIFF_FUNCTION_PATTERN); + prepareSingleAutoDiffContext(expression, contextValues, siteId, effectiveTime, "MONTH_DIFF", + AUTO_MONTH_DIFF_FUNCTION_PATTERN); + prepareSingleAutoDiffContext(expression, contextValues, siteId, effectiveTime, "HOUR_DIFF", + AUTO_HOUR_DIFF_FUNCTION_PATTERN); + } + + private void prepareSingleAutoDiffContext(String expression, Map contextValues, String siteId, + Date effectiveTime, String functionName, Pattern functionPattern) { + Set variables = extractAutoDiffVariables(expression, functionPattern); + if (variables.isEmpty()) { + return; + } + for (String variable : variables) { + BigDecimal currentValue = contextValues.get(variable); + if (currentValue == null) { + continue; + } + BigDecimal diffValue = calculateAutoPeriodDiffValue(siteId, functionName, variable, currentValue, effectiveTime); + contextValues.put(resolveAutoDiffContextKey(functionName, variable), diffValue); + } + } + + private Set extractAutoDiffVariables(String expression, Pattern functionPattern) { + Set variables = new HashSet<>(); + Matcher matcher = functionPattern.matcher(expression); + while (matcher.find()) { + String variable = StringUtils.defaultString(matcher.group(1)).trim().toUpperCase(); + if (StringUtils.isNotBlank(variable)) { + variables.add(variable); + } + } + return variables; + } + + private BigDecimal calculateAutoPeriodDiffValue(String siteId, String functionName, String variable, + BigDecimal currentValue, Date effectiveTime) { + if (currentValue == null || StringUtils.isAnyBlank(siteId, functionName, variable) || effectiveTime == null) { + return BigDecimal.ZERO; + } + if ("DAY_DIFF".equalsIgnoreCase(functionName)) { + return calculateDayDiffValueFromInflux(siteId, variable, currentValue, effectiveTime); + } + if ("MONTH_DIFF".equalsIgnoreCase(functionName)) { + return calculateMonthDiffValueFromInflux(siteId, variable, currentValue, effectiveTime); + } + if ("HOUR_DIFF".equalsIgnoreCase(functionName)) { + return calculateHourDiffValueFromInflux(siteId, variable, currentValue, effectiveTime); + } + return BigDecimal.ZERO; + } + + private BigDecimal calculateDayDiffValueFromInflux(String siteId, String variable, + BigDecimal currentValue, Date effectiveTime) { + LocalDateTime currentDateTime = DateUtils.toLocalDateTime(effectiveTime); + if (currentDateTime == null) { + return BigDecimal.ZERO; + } + LocalDate currentDate = currentDateTime.toLocalDate(); + Date currentDayStart = DateUtils.toDate(currentDate); + Date previousDayStart = DateUtils.toDate(currentDate.minusDays(1)); + Date previousDayEnd = new Date(currentDayStart.getTime() - 1); + return calculateDiffByPreviousPeriodLastValue(siteId, variable, currentValue, previousDayStart, previousDayEnd); + } + + private BigDecimal calculateMonthDiffValueFromInflux(String siteId, String variable, + BigDecimal currentValue, Date effectiveTime) { + LocalDateTime currentDateTime = DateUtils.toLocalDateTime(effectiveTime); + if (currentDateTime == null) { + return BigDecimal.ZERO; + } + YearMonth currentMonth = YearMonth.from(currentDateTime); + LocalDate currentMonthStartDate = currentMonth.atDay(1); + Date currentMonthStart = DateUtils.toDate(currentMonthStartDate); + YearMonth previousMonth = currentMonth.minusMonths(1); + Date previousMonthStart = DateUtils.toDate(previousMonth.atDay(1)); + Date previousMonthEnd = new Date(currentMonthStart.getTime() - 1); + return calculateDiffByPreviousPeriodLastValue(siteId, variable, currentValue, previousMonthStart, previousMonthEnd); + } + + private BigDecimal calculateHourDiffValueFromInflux(String siteId, String variable, + BigDecimal currentValue, Date effectiveTime) { + LocalDateTime currentDateTime = DateUtils.toLocalDateTime(effectiveTime); + if (currentDateTime == null) { + return BigDecimal.ZERO; + } + LocalDateTime currentHourStartTime = currentDateTime.withMinute(0).withSecond(0).withNano(0); + Date currentHourStart = DateUtils.toDate(currentHourStartTime); + Date previousHourStart = DateUtils.toDate(currentHourStartTime.minusHours(1)); + Date previousHourEnd = new Date(currentHourStart.getTime() - 1); + return calculateDiffByPreviousPeriodLastValue(siteId, variable, currentValue, previousHourStart, previousHourEnd); + } + + private BigDecimal calculateDiffByPreviousPeriodLastValue(String siteId, String variable, + BigDecimal currentValue, Date previousPeriodStart, + Date previousPeriodEnd) { + if (StringUtils.isAnyBlank(siteId, variable) + || currentValue == null + || previousPeriodStart == null + || previousPeriodEnd == null + || previousPeriodEnd.before(previousPeriodStart)) { + return BigDecimal.ZERO; + } + String pointKey = resolveInfluxPointKeyByVariable(siteId, variable); + if (StringUtils.isBlank(pointKey)) { + return BigDecimal.ZERO; + } + InfluxPointDataWriter.PointValue previousPeriodLastPoint = influxPointDataWriter.queryLatestPointValueByPointKey( + siteId, pointKey, previousPeriodStart, previousPeriodEnd + ); + if (previousPeriodLastPoint == null || previousPeriodLastPoint.getPointValue() == null) { + return BigDecimal.ZERO; + } + return currentValue.subtract(previousPeriodLastPoint.getPointValue()); + } + + private String resolveInfluxPointKeyByVariable(String siteId, String variable) { + if (StringUtils.isAnyBlank(siteId, variable)) { + return null; + } + String normalizedVariable = normalizeVariablePart(variable); + String upperVariable = variable.trim().toUpperCase(); + List sourcePointConfigs = getSiteSourcePointConfigs(siteId); + for (EmsPointConfig sourcePointConfig : sourcePointConfigs) { + if (sourcePointConfig == null) { + continue; + } + String pointKey = resolveInfluxPointKey(sourcePointConfig); + if (StringUtils.isBlank(pointKey)) { + continue; + } + + String pointIdKey = resolvePointContextKey(sourcePointConfig); + if (StringUtils.isNotBlank(pointIdKey) && upperVariable.equals(pointIdKey)) { + return pointKey; + } + + String normalizedDataKey = normalizeVariablePart(sourcePointConfig.getDataKey()); + if (StringUtils.isNotBlank(normalizedVariable) + && StringUtils.isNotBlank(normalizedDataKey) + && normalizedVariable.equals(normalizedDataKey)) { + return pointKey; + } + + String devicePrefix = normalizeVariablePart(sourcePointConfig.getDeviceId()); + if (StringUtils.isNotBlank(devicePrefix) + && StringUtils.isNotBlank(normalizedDataKey) + && StringUtils.isNotBlank(normalizedVariable) + && normalizedVariable.equals(devicePrefix + "_" + normalizedDataKey)) { + return pointKey; + } + } + return null; + } + + private static String resolveAutoDayDiffContextKey(String variable) { + return AUTO_DAY_DIFF_CONTEXT_PREFIX + StringUtils.defaultString(variable).trim().toUpperCase(); + } + + private static String resolveAutoMonthDiffContextKey(String variable) { + return AUTO_MONTH_DIFF_CONTEXT_PREFIX + StringUtils.defaultString(variable).trim().toUpperCase(); + } + + private static String resolveAutoHourDiffContextKey(String variable) { + return AUTO_HOUR_DIFF_CONTEXT_PREFIX + StringUtils.defaultString(variable).trim().toUpperCase(); + } + + private static String resolveAutoDiffContextKey(String functionName, String variable) { + if ("DAY_DIFF".equalsIgnoreCase(functionName)) { + return resolveAutoDayDiffContextKey(variable); + } + if ("MONTH_DIFF".equalsIgnoreCase(functionName)) { + return resolveAutoMonthDiffContextKey(variable); + } + if ("HOUR_DIFF".equalsIgnoreCase(functionName)) { + return resolveAutoHourDiffContextKey(variable); + } + return null; } private CompiledExpression compileExpression(String expression) { @@ -1174,6 +1774,84 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return new ArrayList<>(uniqueByPointId.values()); } + private List getSiteSourcePointConfigs(String siteId) { + if (StringUtils.isBlank(siteId)) { + return Collections.emptyList(); + } + EmsPointConfig query = new EmsPointConfig(); + query.setSiteId(siteId); + List pointConfigs = emsPointConfigMapper.selectEmsPointConfigList(query); + if (CollectionUtils.isEmpty(pointConfigs)) { + return Collections.emptyList(); + } + Map uniqueByPointId = new LinkedHashMap<>(); + for (EmsPointConfig pointConfig : pointConfigs) { + if (pointConfig == null || isComputedPoint(pointConfig)) { + continue; + } + String pointId = resolveInfluxPointKey(pointConfig); + if (StringUtils.isBlank(pointId)) { + continue; + } + uniqueByPointId.putIfAbsent(pointId.trim().toUpperCase(), pointConfig); + } + return new ArrayList<>(uniqueByPointId.values()); + } + + private void putSourcePointValueToContext(EmsPointConfig pointConfig, BigDecimal pointValue, Map contextValues) { + if (pointConfig == null || pointValue == null || contextValues == null) { + return; + } + String pointContextKey = resolvePointContextKey(pointConfig); + if (StringUtils.isNotBlank(pointContextKey)) { + contextValues.put(pointContextKey, pointValue); + } + String normalizedDataKey = normalizeVariablePart(pointConfig.getDataKey()); + if (StringUtils.isNotBlank(normalizedDataKey)) { + contextValues.put(normalizedDataKey, pointValue); + String devicePrefix = normalizeVariablePart(pointConfig.getDeviceId()); + if (StringUtils.isNotBlank(devicePrefix)) { + contextValues.put(devicePrefix + "_" + normalizedDataKey, pointValue); + } + } + } + + private void cacheLatestPointValuesByPointId(String siteId, String deviceId, + Map pointIdValueMap, + Date dataTime) { + if (StringUtils.isBlank(siteId) || org.apache.commons.collections4.MapUtils.isEmpty(pointIdValueMap)) { + log.warn("MQTT点位写Redis跳过,siteId: {}, deviceId: {}, pointMapEmpty: {}", + siteId, deviceId, org.apache.commons.collections4.MapUtils.isEmpty(pointIdValueMap)); + return; + } + Date latestTime = dataTime == null ? DateUtils.getNowDate() : dataTime; + int writeCount = 0; + List writeSamples = new ArrayList<>(); + for (Map.Entry entry : pointIdValueMap.entrySet()) { + String pointId = StringUtils.trimToNull(entry.getKey()); + BigDecimal pointValue = entry.getValue(); + if (StringUtils.isBlank(pointId) || pointValue == null) { + log.warn("MQTT点位写Redis忽略无效值,siteId: {}, deviceId: {}, rawPointId: {}, pointValue: {}", + siteId, deviceId, entry.getKey(), pointValue); + continue; + } + JSONObject cacheValue = new JSONObject(); + cacheValue.put("siteId", siteId); + cacheValue.put("deviceId", deviceId); + cacheValue.put("pointId", pointId); + cacheValue.put("pointValue", pointValue); + cacheValue.put("dataTime", latestTime); + cacheValue.put("timestamp", latestTime == null ? null : latestTime.getTime()); + redisCache.setCacheObject(pointId, cacheValue); + writeCount++; + if (writeSamples.size() < 10) { + writeSamples.add(pointId); + } + } + log.info("MQTT点位写Redis完成,siteId: {}, deviceId: {}, writeCount: {}, writeSamples: {}, dataTime: {}", + siteId, deviceId, writeCount, writeSamples, latestTime); + } + private static class PointDataRecord { private final String siteId; private final String deviceId; @@ -1210,6 +1888,54 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i } } + private static class PointLatestCacheValue { + private String siteId; + private String deviceId; + private String pointId; + private BigDecimal pointValue; + private Date dataTime; + + private String getSiteId() { + return siteId; + } + + private void setSiteId(String siteId) { + this.siteId = siteId; + } + + private String getDeviceId() { + return deviceId; + } + + private void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + private String getPointId() { + return pointId; + } + + private void setPointId(String pointId) { + this.pointId = pointId; + } + + private BigDecimal getPointValue() { + return pointValue; + } + + private void setPointValue(BigDecimal pointValue) { + this.pointValue = pointValue; + } + + private Date getDataTime() { + return dataTime; + } + + private void setDataTime(Date dataTime) { + this.dataTime = dataTime; + } + } + private static class MissingVariableException extends IllegalArgumentException { private final String variable; @@ -1353,6 +2079,10 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i } return ExpressionValue.ofNumber(value); } + + private String getName() { + return name; + } } private static class UnaryNode implements ExpressionNode { @@ -1480,6 +2210,40 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i } } + private static class FunctionNode implements ExpressionNode { + private final String functionName; + private final List arguments; + + private FunctionNode(String functionName, List arguments) { + this.functionName = StringUtils.defaultString(functionName).trim().toUpperCase(); + this.arguments = arguments == null ? Collections.emptyList() : arguments; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + if (("DAY_DIFF".equals(functionName) + || "MONTH_DIFF".equals(functionName) + || "HOUR_DIFF".equals(functionName)) && arguments.size() == 1) { + ExpressionNode argument = arguments.get(0); + if (!(argument instanceof VariableNode)) { + throw new IllegalArgumentException(functionName + "单参数模式仅支持变量参数"); + } + String variable = ((VariableNode) argument).getName(); + BigDecimal autoDiffValue = contextValues.get(resolveAutoDiffContextKey(functionName, variable)); + if (autoDiffValue == null) { + throw new MissingVariableException(variable); + } + return ExpressionValue.ofNumber(autoDiffValue); + } + if ("DAY_DIFF".equals(functionName) + || "MONTH_DIFF".equals(functionName) + || "HOUR_DIFF".equals(functionName)) { + throw new IllegalArgumentException(functionName + "函数参数数量错误,需1个参数"); + } + throw new IllegalArgumentException("不支持的函数: " + functionName); + } + } + private static class CompiledExpression { private final ExpressionNode root; @@ -1634,16 +2398,7 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i if (match(ExprTokenType.IDENTIFIER)) { String identifier = token.text; if (match(ExprTokenType.LEFT_PAREN)) { - if (!"IF".equalsIgnoreCase(identifier)) { - throw new IllegalArgumentException("不支持的函数: " + identifier); - } - ExpressionNode condition = parseExpression(); - expect(ExprTokenType.COMMA, "IF函数参数格式错误,缺少第1个逗号"); - ExpressionNode trueNode = parseExpression(); - expect(ExprTokenType.COMMA, "IF函数参数格式错误,缺少第2个逗号"); - ExpressionNode falseNode = parseExpression(); - expect(ExprTokenType.RIGHT_PAREN, "IF函数缺少右括号"); - return new TernaryNode(condition, trueNode, falseNode); + return parseFunction(identifier); } return new VariableNode(identifier.toUpperCase()); } @@ -1655,6 +2410,55 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i throw new IllegalArgumentException("表达式语法错误,当前位置: " + token.text); } + private ExpressionNode parseFunction(String identifier) { + if ("IF".equalsIgnoreCase(identifier)) { + ExpressionNode condition = parseExpression(); + expect(ExprTokenType.COMMA, "IF函数参数格式错误,缺少第1个逗号"); + ExpressionNode trueNode = parseExpression(); + expect(ExprTokenType.COMMA, "IF函数参数格式错误,缺少第2个逗号"); + ExpressionNode falseNode = parseExpression(); + expect(ExprTokenType.RIGHT_PAREN, "IF函数缺少右括号"); + return new TernaryNode(condition, trueNode, falseNode); + } + if ("DAY_DIFF".equalsIgnoreCase(identifier)) { + List arguments = parseFunctionArguments(identifier); + if (arguments.size() != 1) { + throw new IllegalArgumentException("DAY_DIFF函数参数数量错误,需1个参数"); + } + return new FunctionNode(identifier, arguments); + } + if ("MONTH_DIFF".equalsIgnoreCase(identifier)) { + List arguments = parseFunctionArguments(identifier); + if (arguments.size() != 1) { + throw new IllegalArgumentException("MONTH_DIFF函数参数数量错误,需1个参数"); + } + return new FunctionNode(identifier, arguments); + } + if ("HOUR_DIFF".equalsIgnoreCase(identifier)) { + List arguments = parseFunctionArguments(identifier); + if (arguments.size() != 1) { + throw new IllegalArgumentException("HOUR_DIFF函数参数数量错误,需1个参数"); + } + return new FunctionNode(identifier, arguments); + } + throw new IllegalArgumentException("不支持的函数: " + identifier); + } + + private List parseFunctionArguments(String functionName) { + List arguments = new ArrayList<>(); + if (match(ExprTokenType.RIGHT_PAREN)) { + return arguments; + } + while (true) { + arguments.add(parseExpression()); + if (match(ExprTokenType.COMMA)) { + continue; + } + expect(ExprTokenType.RIGHT_PAREN, functionName + "函数缺少右括号"); + return arguments; + } + } + private ExprToken peek() { if (index >= tokens.size()) { return ExprToken.symbol(ExprTokenType.EOF, ""); @@ -2750,7 +3554,6 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i // 初始化当日数据 EmsDailyChargeData emsDailyChargeData = new EmsDailyChargeData(); emsDailyChargeData.setSiteId(siteId); - emsDailyChargeData.setDeviceId(deviceId); emsDailyChargeData.setDateTime(DateUtils.getNowDate()); emsDailyChargeData.setTotalChargeData(pcsData.getTotalAcChargeEnergy()); emsDailyChargeData.setTotalDischargeData(pcsData.getTotalAcDischargeEnergy()); @@ -2780,9 +3583,6 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return; } - // 获取上次数据,便于后面计算差值均无则默认0 - EmsAmmeterData lastAmmeterData = getLastAmmeterData(siteId, deviceId); - EmsAmmeterData dataMete = new EmsAmmeterData(); // 更新时间 dataMete.setDataUpdateTime(dataUpdateTime); @@ -2804,80 +3604,14 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i // 处理储能电表-METE每日充放电数据 dealDailyChargeDate(siteId, deviceId, dataMete); - // 处理储能电表-METE每日数据(尖、峰、平、谷差值) -// if (SiteEnum.FX.getCode().equals(siteId)) { - dealAmmeterDailyDate(siteId, dataMete, dataUpdateTime, lastAmmeterData); -// } else if (SiteEnum.DDS.getCode().equals(siteId)) { -// dealDailyEnergyData(siteId, dataMete, yestData); -// } + // ems_daily_energy_data 仅保留页面修正与Quartz写入 } } - private EmsAmmeterData getLastAmmeterData(String siteId, String deviceId) { - // 先从redis取,取不到查数据 - EmsAmmeterData lastData = redisCache.getCacheObject(RedisKeyConstants.AMMETER + siteId + "_" + deviceId); - if (lastData == null) { - lastData = emsAmmeterDataMapper.getLastData(siteId, deviceId); - if (lastData == null) { - lastData = new EmsAmmeterData(); - lastData.setSiteId(siteId); - lastData.setDeviceId(deviceId); - lastData.setCurrentForwardActiveTotal(BigDecimal.ZERO); - lastData.setCurrentReverseActiveTotal(BigDecimal.ZERO); - } - } - return lastData; - } - - private void dealDDSDailyChargeDate(String siteId, EmsAmmeterData currentData, String deviceId) { - // 初始化今日充放电 - BigDecimal dailyDisChargeDate = new BigDecimal(0); - BigDecimal dailyChargeDate = new BigDecimal(0); - -// BigDecimal nowTotalDisChargeDate = StringUtils.getBigDecimal(obj.get("DQFXZYGDN")); -// BigDecimal nowTotalChargeDate = StringUtils.getBigDecimal(obj.get("DQZXZYGDN")); - BigDecimal nowTotalDisChargeDate = currentData.getCurrentReverseActiveTotal(); - BigDecimal nowTotalChargeDate = currentData.getCurrentForwardActiveTotal(); - // 初始化当日数据-总的 - EmsDailyChargeData emsDailyChargeData = initDailyChargeData(siteId, deviceId, nowTotalChargeDate, nowTotalDisChargeDate); - // 获取redis存放昨日最晚数据 - String yestDate = DateUtils.getYesterdayDate(); - String yestDateRedisKey = RedisKeyConstants.AMMETER + siteId + "_" + deviceId + "_" + yestDate; - EmsAmmeterData yestData = redisCache.getCacheObject(yestDateRedisKey); - if (yestData == null) { - // redis没有这查电表总数据表取截止到昨日最新第一条数据 - yestData = emsAmmeterDataMapper.getYestLatestDate(siteId, deviceId, yestDate); - // 数据存redis-有效期1天 - redisCache.setCacheObject(yestDateRedisKey, yestData, Constants.DATE_VALID_TIME, TimeUnit.DAYS); - } - if (yestData != null) { - // 今日总数据-昨日总数据=今日充放电 - BigDecimal yestTotalDisChargeDate = yestData.getCurrentReverseActiveTotal(); - BigDecimal yestTotalChargeDate = yestData.getCurrentForwardActiveTotal(); - - dailyChargeDate = nowTotalChargeDate.subtract(yestTotalChargeDate); - dailyDisChargeDate = nowTotalDisChargeDate.subtract(yestTotalDisChargeDate); - emsDailyChargeData.setChargeData(dailyChargeDate); - emsDailyChargeData.setDischargeData(dailyDisChargeDate); - } - - // 插入或更新每日充放电数据表 - emsDailyChargeDataMapper.insertOrUpdateData(emsDailyChargeData); - - // 初始化数据-尖峰平谷 - EmsDailyEnergyData energyData = initEnergyData(siteId); - // 计算尖峰平谷差值,更新表 - calcEnergyDiffAndRevenue(siteId, energyData, currentData, yestData); - energyData.setCalcTime(DateUtils.getNowDate()); - // 插入或更新电表每日差值数据表 - emsDailyEnergyDataMapper.insertOrUpdateData(energyData); - } - private EmsDailyChargeData initDailyChargeData(String siteId, String deviceId, BigDecimal nowTotalChargeDate, BigDecimal nowTotalDisChargeDate) { EmsDailyChargeData emsDailyChargeData = new EmsDailyChargeData(); emsDailyChargeData.setSiteId(siteId); - emsDailyChargeData.setDeviceId(deviceId); emsDailyChargeData.setDateTime(DateUtils.getNowDate()); emsDailyChargeData.setTotalChargeData(nowTotalChargeDate); emsDailyChargeData.setTotalDischargeData(nowTotalDisChargeDate); @@ -2888,15 +3622,7 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return emsDailyChargeData; } - private void dealDailyEnergyData(String siteId, EmsAmmeterData currentData, EmsAmmeterData yestData) { - // 初始化数据-尖峰平谷 - EmsDailyEnergyData energyData = initEnergyData(siteId); - // 计算尖峰平谷差值,更新表 - calcEnergyDiffAndRevenue(siteId, energyData, currentData, yestData); - energyData.setCalcTime(DateUtils.getNowDate()); - // 插入或更新电表每日差值数据表 - emsDailyEnergyDataMapper.insertOrUpdateData(energyData); - } + private EmsAmmeterData dealDailyChargeDate(String siteId, String deviceId, EmsAmmeterData currentData) { // 初始化今日充放电 BigDecimal dailyDisChargeDate = new BigDecimal(0); @@ -2933,83 +3659,6 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return yestData; } - private void calcEnergyDiffAndRevenue(String siteId, EmsDailyEnergyData energyData, EmsAmmeterData currentData, EmsAmmeterData yestData) { - - // 获取当月电价 - String key = RedisKeyConstants.ENERGY_PRICE_TIME + siteId + "_" + LocalDate.now().getYear() + LocalDate.now().getMonthValue(); - EnergyPriceVo priceVo = redisCache.getCacheObject(key); - - // 计算尖峰平谷差值 - // 正反向-尖 -// BigDecimal peakChargeDiff = StringUtils.getBigDecimal(obj.get("DQZXYGJDN")) -// .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentForwardActivePeak()); - BigDecimal peakChargeDiff = currentData.getCurrentForwardActivePeak() - .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentForwardActivePeak()); - energyData.setPeakChargeDiff(peakChargeDiff); -// BigDecimal peakDischargeDiff = StringUtils.getBigDecimal(obj.get("DQFXYGJDN")) -// .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentReverseActivePeak()); - BigDecimal peakDischargeDiff = currentData.getCurrentReverseActivePeak() - .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentReverseActivePeak()); - energyData.setPeakDischargeDiff(peakDischargeDiff); - // 正反向-峰 -// BigDecimal highChargeDiff = StringUtils.getBigDecimal(obj.get("DQZXYGFDN")) -// .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentForwardActiveHigh()); - BigDecimal highChargeDiff = currentData.getCurrentForwardActiveHigh() - .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentForwardActiveHigh()); - energyData.setHighChargeDiff(highChargeDiff); -// BigDecimal highDischargeDiff = StringUtils.getBigDecimal(obj.get("DQFXYGFDN")) -// .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentReverseActiveHigh()); - BigDecimal highDischargeDiff = currentData.getCurrentReverseActiveHigh() - .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentReverseActiveHigh()); - energyData.setHighDischargeDiff(highDischargeDiff); - // 正反向-平 -// BigDecimal flatChargeDiff = StringUtils.getBigDecimal(obj.get("DQZXYGPDN")) -// .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentForwardActiveFlat()); - BigDecimal flatChargeDiff = currentData.getCurrentForwardActiveFlat() - .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentForwardActiveFlat()); - energyData.setFlatChargeDiff(flatChargeDiff); -// BigDecimal flatDisChargeDiff = StringUtils.getBigDecimal(obj.get("DQFXYGPDN")) -// .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentReverseActiveFlat()); - BigDecimal flatDisChargeDiff = currentData.getCurrentReverseActiveFlat() - .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentReverseActiveFlat()); - energyData.setFlatDischargeDiff(flatDisChargeDiff); - // 正反向-谷 -// BigDecimal valleyChargeDiff = StringUtils.getBigDecimal(obj.get("DQZXYGGDN")) -// .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentForwardActiveValley()); - BigDecimal valleyChargeDiff = currentData.getCurrentForwardActiveValley() - .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentForwardActiveValley()); - energyData.setValleyChargeDiff(valleyChargeDiff); -// BigDecimal valleyDisChargeDiff = StringUtils.getBigDecimal(obj.get("DQFXYGGDN")) -// .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentReverseActiveValley()); - BigDecimal valleyDisChargeDiff = currentData.getCurrentReverseActiveValley() - .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentReverseActiveValley()); - energyData.setValleyDischargeDiff(valleyDisChargeDiff); - - - BigDecimal totalRevenue = getYestLastData(siteId); - BigDecimal dayRevenue = BigDecimal.ZERO; - BigDecimal price = BigDecimal.ZERO; - // 计算当日收益,尖峰平谷收益累加,(放电量-充电量)*电价 - if (priceVo != null) { - price = priceVo.getPeak() == null ? BigDecimal.ZERO : priceVo.getPeak(); - BigDecimal peakRevenue = peakDischargeDiff.subtract(peakChargeDiff).multiply(price); - dayRevenue = dayRevenue.add(peakRevenue); - price = priceVo.getHigh() == null ? BigDecimal.ZERO : priceVo.getHigh(); - BigDecimal highRevenue = highDischargeDiff.subtract(highChargeDiff).multiply(price); - dayRevenue = dayRevenue.add(highRevenue); - price = priceVo.getFlat() == null ? BigDecimal.ZERO : priceVo.getFlat(); - BigDecimal flatRevenue = flatDisChargeDiff.subtract(flatChargeDiff).multiply(price); - dayRevenue = dayRevenue.add(flatRevenue); - price = priceVo.getValley() == null ? BigDecimal.ZERO : priceVo.getValley(); - BigDecimal valleyRevenue = valleyDisChargeDiff.subtract(valleyChargeDiff).multiply(price); - dayRevenue = dayRevenue.add(valleyRevenue); - energyData.setDayRevenue(dayRevenue); - } - // 总收益 = 昨日总收益+今日实时收益 - totalRevenue = totalRevenue.add(dayRevenue); - energyData.setTotalRevenue(totalRevenue); - } - private BigDecimal getYestLastData(String siteId) { // dds存的是累计到昨日总收益 String yestDate = DateUtils.getYesterdayDayString(); @@ -3029,175 +3678,39 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return yestLastTotalRevenue; } - private void dealAmmeterDailyDate(String siteId, EmsAmmeterData currentData, Date dataUpdateTime, EmsAmmeterData lastData) { - EmsDailyEnergyData energyData = initEnergyData(siteId); - energyData.setCalcTime(DateUtils.getNowDate()); - - // 先获取当月电价配置,redis没有这查数据库,都没有则返回 - String priceKey = RedisKeyConstants.ENERGY_PRICE_TIME + siteId + "_" + LocalDate.now().getYear() + LocalDate.now().getMonthValue(); - EnergyPriceVo priceVo = redisCache.getCacheObject(priceKey); - if (priceVo == null) { - priceVo = emsEnergyPriceConfigService.getCurrentMonthPrice(siteId); - redisCache.setCacheObject(priceKey, priceVo, 31, TimeUnit.DAYS); - if (priceVo == null) { - emsDailyEnergyDataMapper.insertOrUpdateData(energyData); - return; - } + private void syncDailyRevenueToChargeData(String siteId, Date revenueDate, BigDecimal totalRevenue, BigDecimal dayRevenue) { + if (StringUtils.isBlank(siteId)) { + return; } - List timeRanges = priceVo.getRange(); - if (timeRanges == null) { - emsDailyEnergyDataMapper.insertOrUpdateData(energyData); + Date persistDate = revenueDate == null ? DateUtils.getNowDate() : revenueDate; + BigDecimal persistTotalRevenue = safeBigDecimal(totalRevenue); + BigDecimal persistDayRevenue = safeBigDecimal(dayRevenue); + + int updated = emsDailyChargeDataMapper.updateRevenueBySiteAndDate(siteId, persistDate, persistTotalRevenue, persistDayRevenue, "system"); + if (updated > 0) { return; } - // 根据时间范围判断数据类型(尖峰平谷),无法确定数据类型则不处理 - String costType = ""; - String startTime = ""; - for (EnergyPriceTimeRange timeRange : timeRanges) { - startTime = timeRange.getStartTime(); - if (isInPriceTimeRange(startTime, timeRange.getEndTime(), dataUpdateTime)) { - costType = timeRange.getCostType(); - break; - } - } - if (StringUtils.isEmpty(costType)) { - emsDailyEnergyDataMapper.insertOrUpdateData(energyData); + EmsDailyChargeData existedData = emsDailyChargeDataMapper.selectBySiteIdAndDateTime(siteId, persistDate); + if (existedData != null) { + existedData.setTotalRevenue(persistTotalRevenue); + existedData.setDayRevenue(persistDayRevenue); + existedData.setUpdateBy("system"); + existedData.setUpdateTime(DateUtils.getNowDate()); + emsDailyChargeDataMapper.updateEmsDailyChargeData(existedData); return; } - // 根据 costType,计算本次与上次数据差值,累加到对应的数据类型里面 - setDiffByCostType(siteId, costType, energyData, lastData, currentData, priceVo); - - // 插入或更新电表每日差值数据表 - emsDailyEnergyDataMapper.insertOrUpdateData(energyData); - } - - private void setDiffByCostType(String siteId, String costType, EmsDailyEnergyData energyData, EmsAmmeterData lastData, - EmsAmmeterData currentData, EnergyPriceVo priceVo) { -// BigDecimal currentChargeData = StringUtils.getBigDecimal(obj.get("ZXYGDN")); -// BigDecimal currentDischargeData = StringUtils.getBigDecimal(obj.get("FXYGDN")); - BigDecimal currentChargeData = currentData.getCurrentForwardActiveTotal(); - BigDecimal currentDischargeData = currentData.getCurrentReverseActiveTotal(); - currentChargeData = currentChargeData != null ? currentChargeData : BigDecimal.ZERO; - currentDischargeData = currentDischargeData != null ? currentDischargeData : BigDecimal.ZERO; - - // 获取上次实时总收益+当日实时总收益,初始化电价 - Map revenueMap = getRealTimeData(siteId); - BigDecimal totalRevenue = revenueMap.get("totalRevenue") == null ? BigDecimal.ZERO : revenueMap.get("totalRevenue"); - BigDecimal dayRevenue = revenueMap.get("dayRevenue") == null ? BigDecimal.ZERO : revenueMap.get("dayRevenue"); - BigDecimal price = BigDecimal.ZERO; - // 计算时段差值,按照数据类型累加 - // 计算当日累加收益,尖峰平谷收益在当日原基础累加,(放电量-充电量)*电价 - BigDecimal chargeDiffData = currentChargeData.subtract( - lastData.getCurrentForwardActiveTotal() == null ? BigDecimal.ZERO : lastData.getCurrentForwardActiveTotal()); - BigDecimal disChargeDiffData = currentDischargeData.subtract( - lastData.getCurrentReverseActiveTotal() == null ? BigDecimal.ZERO : lastData.getCurrentReverseActiveTotal() - ); - switch (costType) { - case "peak": - // 增加电量 - BigDecimal peakCharge = energyData.getPeakChargeDiff() == null ? BigDecimal.ZERO : energyData.getPeakChargeDiff(); - energyData.setPeakChargeDiff(peakCharge.add(chargeDiffData)); - BigDecimal peakDischarge = energyData.getPeakDischargeDiff() == null ? BigDecimal.ZERO : energyData.getPeakDischargeDiff(); - energyData.setPeakDischargeDiff(peakDischarge.add(disChargeDiffData)); - - // 电价 - price = priceVo.getPeak() == null ? BigDecimal.ZERO : priceVo.getPeak(); - break; - case "high": - // 增加电量 - BigDecimal highCharge = energyData.getHighChargeDiff() == null ? BigDecimal.ZERO : energyData.getHighChargeDiff(); - energyData.setHighChargeDiff(highCharge.add(chargeDiffData)); - BigDecimal highDischarge = energyData.getHighDischargeDiff() == null ? BigDecimal.ZERO : energyData.getHighDischargeDiff(); - energyData.setHighDischargeDiff(highDischarge.add(disChargeDiffData)); - - // 电价 - price = priceVo.getHigh() == null ? BigDecimal.ZERO : priceVo.getHigh(); - break; - case "flat": - // 增加电量 - BigDecimal flatCharge = energyData.getFlatChargeDiff() == null ? BigDecimal.ZERO : energyData.getFlatChargeDiff(); - energyData.setFlatChargeDiff(flatCharge.add(chargeDiffData)); - BigDecimal flatDischarge = energyData.getFlatDischargeDiff() == null ? BigDecimal.ZERO : energyData.getFlatDischargeDiff(); - energyData.setFlatDischargeDiff(flatDischarge.add(disChargeDiffData)); - - // 电价 - price = priceVo.getFlat() == null ? BigDecimal.ZERO : priceVo.getFlat(); - break; - case "valley": - // 增加电量 - BigDecimal valleyCharge = energyData.getValleyChargeDiff() == null ? BigDecimal.ZERO : energyData.getValleyChargeDiff(); - energyData.setValleyChargeDiff(valleyCharge.add(chargeDiffData)); - BigDecimal valleyDischarge = energyData.getValleyDischargeDiff() == null ? BigDecimal.ZERO : energyData.getValleyDischargeDiff(); - energyData.setValleyDischargeDiff(valleyDischarge.add(disChargeDiffData)); - - // 电价 - price = priceVo.getValley() == null ? BigDecimal.ZERO : priceVo.getValley(); - break; - default: - return; - } - - // 计算本次累加收益 - BigDecimal addRevenue = disChargeDiffData.subtract(chargeDiffData).multiply(price); - dayRevenue = dayRevenue.add(addRevenue); - energyData.setDayRevenue(dayRevenue); - // 总收益 = 上次实时总收益+今日实时增加的收益 - totalRevenue = totalRevenue.add(addRevenue); - energyData.setTotalRevenue(totalRevenue); - - // 存redis便于下次取用 - String today = DateUtils.getDate(); - String redisKey = RedisKeyConstants.FXX_REALTIME_REVENUE + siteId + "_" + today; - Map realTimeRevenue = new HashMap<>(); - realTimeRevenue.put("totalRevenue", totalRevenue); - realTimeRevenue.put("dayRevenue", dayRevenue); - redisCache.setCacheObject(redisKey, realTimeRevenue, 1, TimeUnit.DAYS); - } - - private Map getRealTimeData(String siteId) { - // fx取实时总收益和当天实时收益 - String today = DateUtils.getDate(); - String redisKey = RedisKeyConstants.FXX_REALTIME_REVENUE + siteId + "_" + today; - Map realTimeRevenue = redisCache.getCacheObject(redisKey); - if (realTimeRevenue == null) { - // 查数据库 - realTimeRevenue = emsEnergyPriceConfigService.getDayRevenueMap(siteId); - if (realTimeRevenue == null) { - realTimeRevenue = new HashMap<>(); - realTimeRevenue.put("totalRevenue", BigDecimal.ZERO); - realTimeRevenue.put("dayRevenue", BigDecimal.ZERO); - } - redisCache.setCacheObject(redisKey, realTimeRevenue, 1, TimeUnit.DAYS); - } - return realTimeRevenue; - } - - private boolean isInPriceTimeRange(String startTime, String endTime, Date dataUpdateTime) { - if (startTime == null || endTime == null || dataUpdateTime == null) { - return false; - } - LocalDateTime time = DateUtils.toLocalDateTime(dataUpdateTime); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm"); - String dataTimeStr = time.format(formatter); - // 比较时间范围 - return dataTimeStr.compareTo(startTime) >= 0 - && dataTimeStr.compareTo(endTime) < 0; - } - - private EmsDailyEnergyData initEnergyData(String siteId) { - // 先获取数据库当天数据,存在则更新时间,不存在则初始化 - EmsDailyEnergyData energyData = emsDailyEnergyDataMapper.getDataByDate(siteId, DateUtils.getDate()); - if (energyData == null) { - energyData = new EmsDailyEnergyData(); - energyData.setSiteId(siteId); - energyData.setDataDate(DateUtils.getNowDate()); - energyData.setCreateBy("system"); - energyData.setCreateTime(DateUtils.getNowDate()); - } - energyData.setUpdateBy("system"); - energyData.setCalcTime(DateUtils.getNowDate()); - return energyData; + EmsDailyChargeData chargeData = new EmsDailyChargeData(); + chargeData.setSiteId(siteId); + chargeData.setDateTime(persistDate); + chargeData.setTotalRevenue(persistTotalRevenue); + chargeData.setDayRevenue(persistDayRevenue); + chargeData.setCreateBy("system"); + chargeData.setCreateTime(DateUtils.getNowDate()); + chargeData.setUpdateBy("system"); + chargeData.setUpdateTime(DateUtils.getNowDate()); + emsDailyChargeDataMapper.insertEmsDailyChargeData(chargeData); } // 数据分组处理 @@ -3564,19 +4077,16 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i // 空数据不处理 private boolean checkJsonDataEmpty(String jsonData) { - boolean flag = false; + if (StringUtils.isEmpty(jsonData)) { + return true; + } try { - if (StringUtils.isEmpty(jsonData)) { - flag = true; - } JsonNode jsonNode = objectMapper.readTree(jsonData); // 判断是否为空对象({}) - if (jsonNode.isObject() && jsonNode.isEmpty()) { - flag = true; - } - } catch (JsonProcessingException e) { - throw new RuntimeException(e); + return jsonNode.isObject() && jsonNode.isEmpty(); + } catch (Exception e) { + log.warn("设备数据Data字段解析失败,按空数据处理,data: {}, err: {}", jsonData, e.getMessage()); + return true; } - return flag; } } diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java index aa9bd62..4336a71 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java @@ -19,6 +19,7 @@ import com.xzzn.common.enums.WorkStatus; import com.xzzn.common.exception.ServiceException; import com.xzzn.common.utils.DateUtils; import com.xzzn.common.utils.StringUtils; +import com.xzzn.ems.domain.EmsDailyChargeData; import com.xzzn.ems.domain.EmsDevicesSetting; import com.xzzn.ems.domain.EmsPointEnumMatch; import com.xzzn.ems.domain.EmsPointConfig; @@ -32,11 +33,14 @@ import com.xzzn.ems.domain.vo.PointDataRequest; import com.xzzn.ems.domain.vo.PointQueryResponse; import com.xzzn.ems.domain.vo.SiteMonitorDataSaveItemVo; import com.xzzn.ems.domain.vo.SiteMonitorDataSaveRequest; +import com.xzzn.ems.domain.vo.SiteMonitorDataVo; import com.xzzn.ems.domain.vo.SiteMonitorProjectDisplayVo; import com.xzzn.ems.domain.vo.SiteMonitorProjectPointMappingSaveRequest; import com.xzzn.ems.domain.vo.SiteMonitorProjectPointMappingVo; import com.xzzn.ems.domain.vo.WorkStatusEnumMappingVo; import com.xzzn.ems.mapper.EmsBatteryDataMinutesMapper; +import com.xzzn.ems.mapper.EmsDailyChargeDataMapper; +import com.xzzn.ems.mapper.EmsDailyEnergyDataMapper; import com.xzzn.ems.mapper.EmsDevicesSettingMapper; import com.xzzn.ems.mapper.EmsPcsSettingMapper; import com.xzzn.ems.mapper.EmsPointConfigMapper; @@ -52,6 +56,7 @@ import com.xzzn.system.domain.SysOperLog; import com.xzzn.system.service.ISysOperLogService; import java.math.BigDecimal; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -123,6 +128,12 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService private static final long PROJECT_DISPLAY_CACHE_TTL_MS = 15_000L; private static final int MONITOR_POINT_MATCH_REDIS_TTL_SECONDS = 300; private static final int DISPLAY_DEBUG_SAMPLE_SIZE = 20; + private static final Set HOME_CHARGE_FIELD_SET = new HashSet<>(Arrays.asList( + "dayChargedCap", "dayDisChargedCap", "yesterdayChargedCap", "yesterdayDisChargedCap", "totalChargedCap", "totalDischargedCap" + )); + private static final Set HOME_REVENUE_FIELD_SET = new HashSet<>(Arrays.asList( + "totalRevenue", "dayRevenue", "yesterdayRevenue" + )); @Autowired private EmsDevicesSettingMapper emsDevicesMapper; @Autowired @@ -138,6 +149,10 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService @Autowired private EmsBatteryDataMinutesMapper emsBatteryDataMinutesMapper; @Autowired + private EmsDailyChargeDataMapper emsDailyChargeDataMapper; + @Autowired + private EmsDailyEnergyDataMapper emsDailyEnergyDataMapper; + @Autowired private EmsBatteryClusterServiceImpl emsBatteryClusterServiceImpl; @Autowired private ModbusProcessor modbusProcessor; @@ -695,7 +710,6 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService .filter(fieldCodeSet::contains) .forEach(deletedFieldCodeSet::add); } - validatePcsCurvePointMappings(siteId, request.getMappings(), deletedFieldCodeSet); int deletedRows = emsSiteMonitorPointMatchMapper.deleteBySiteId(siteId); List saveList = new ArrayList<>(); @@ -936,45 +950,6 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService return StringUtils.defaultString(deviceCategory) + "-" + StringUtils.defaultString(matchField); } - private void validatePcsCurvePointMappings(String siteId, List mappings, Set deletedFieldCodeSet) { - List> pcsDevices = emsDevicesMapper.getDeviceInfosBySiteIdAndCategory(siteId, DeviceCategory.PCS.getCode()); - Set pcsDeviceIdSet = new HashSet<>(); - if (pcsDevices != null) { - pcsDevices.stream() - .filter(Objects::nonNull) - .map(item -> item.get("id")) - .filter(Objects::nonNull) - .map(String::valueOf) - .map(String::trim) - .filter(StringUtils::isNotBlank) - .forEach(pcsDeviceIdSet::add); - } - validateSingleCurveFieldMapping(FIELD_CURVE_PCS_ACTIVE_POWER, "PCS有功功率曲线点位", pcsDeviceIdSet, mappings, deletedFieldCodeSet); - validateSingleCurveFieldMapping(FIELD_CURVE_PCS_REACTIVE_POWER, "PCS无功功率曲线点位", pcsDeviceIdSet, mappings, deletedFieldCodeSet); - } - - private void validateSingleCurveFieldMapping(String fieldCode, String fieldName, Set pcsDeviceIdSet, - List mappings, Set deletedFieldCodeSet) { - if (deletedFieldCodeSet != null && deletedFieldCodeSet.contains(fieldCode) && !pcsDeviceIdSet.isEmpty()) { - throw new ServiceException(fieldName + "不能删除,且配置数量必须与PCS设备数量一致"); - } - Set configuredDeviceIdSet = new HashSet<>(); - if (mappings != null) { - mappings.stream() - .filter(Objects::nonNull) - .filter(item -> fieldCode.equals(StringUtils.trim(item.getFieldCode()))) - .filter(item -> StringUtils.isNotBlank(item.getDataPoint())) - .map(SiteMonitorProjectPointMappingVo::getDeviceId) - .map(StringUtils::trim) - .filter(StringUtils::isNotBlank) - .forEach(configuredDeviceIdSet::add); - } - - if (configuredDeviceIdSet.size() != pcsDeviceIdSet.size() || !configuredDeviceIdSet.equals(pcsDeviceIdSet)) { - throw new ServiceException(String.format("%s数量需与PCS设备数量一致:PCS设备%d个,已配置%d个", fieldName, pcsDeviceIdSet.size(), configuredDeviceIdSet.size())); - } - } - private SiteMonitorProjectPointMappingVo buildMappingVo(EmsSiteMonitorItem item, EmsSiteMonitorPointMatch pointMatch, String deviceId, String deviceName) { SiteMonitorProjectPointMappingVo vo = new SiteMonitorProjectPointMappingVo(); @@ -1117,6 +1092,7 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService Map tjbbLatestMap = safeRedisMap(redisCache.getCacheMap(buildSiteMonitorLatestRedisKey(normalizedSiteId, MODULE_TJBB))); Map> enumDataCodeMapByScope = buildEnumDataCodeMapByScope(normalizedSiteId); Map pointSnapshotCache = new HashMap<>(); + HomeRunningSnapshot homeRunningSnapshot = buildHomeRunningSnapshot(normalizedSiteId); List result = new ArrayList<>(); int totalCount = 0; @@ -1138,6 +1114,18 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService result.add(vo); continue; } + if (MODULE_HOME.equals(mapping.getModuleCode()) && homeRunningSnapshot != null) { + String homeFieldKey = resolveHomeFieldKey(mapping.getFieldCode()); + if (StringUtils.isNotBlank(homeFieldKey)) { + String homeFieldValue = homeRunningSnapshot.getValue(homeFieldKey); + if (homeFieldValue != null) { + vo.setFieldValue(homeFieldValue); + vo.setValueTime(homeRunningSnapshot.getValueTime()); + result.add(vo); + continue; + } + } + } // 与“点位配置列表最新值”一致:按 pointId -> 点位配置(dataKey/deviceId) -> MQTT 最新报文读取 PointLatestSnapshot latestSnapshot = null; String dataPoint = StringUtils.trim(mapping.getDataPoint()); @@ -1207,6 +1195,91 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService return result; } + private HomeRunningSnapshot buildHomeRunningSnapshot(String siteId) { + if (StringUtils.isBlank(siteId)) { + return null; + } + HomeRunningSnapshot snapshot = new HomeRunningSnapshot(); + String today = DateUtils.getDate(); + Map totalChargeData = emsDailyChargeDataMapper.getAllSiteChargeData(today, siteId); + if (totalChargeData != null) { + snapshot.put("totalChargedCap", totalChargeData.get("totalChargedCap")); + snapshot.put("totalDischargedCap", totalChargeData.get("totalDischargedCap")); + } + + LocalDate now = LocalDate.now(); + LocalDate yesterday = now.minusDays(1); + List chargeList = emsDailyChargeDataMapper.getSingleSiteChargeData( + siteId, + DateUtils.toDate(yesterday), + DateUtils.toDate(now) + ); + if (!CollectionUtils.isEmpty(chargeList)) { + for (SiteMonitorDataVo item : chargeList) { + if (item == null || StringUtils.isBlank(item.getAmmeterDate())) { + continue; + } + if (today.equals(item.getAmmeterDate())) { + snapshot.put("dayChargedCap", item.getChargedCap()); + snapshot.put("dayDisChargedCap", item.getDisChargedCap()); + } else if (DateUtils.getYesterdayDayString().equals(item.getAmmeterDate())) { + snapshot.put("yesterdayChargedCap", item.getChargedCap()); + snapshot.put("yesterdayDisChargedCap", item.getDisChargedCap()); + } + } + } + + EmsDailyChargeData todayChargeData = emsDailyChargeDataMapper.selectBySiteIdAndDateTime(siteId, DateUtils.toDate(now)); + EmsDailyChargeData yesterdayChargeData = emsDailyChargeDataMapper.selectBySiteIdAndDateTime(siteId, DateUtils.toDate(yesterday)); + if (todayChargeData != null) { + snapshot.put("totalRevenue", todayChargeData.getTotalRevenue()); + snapshot.put("dayRevenue", todayChargeData.getDayRevenue()); + } else { + Map latestRevenueData = emsDailyEnergyDataMapper.getRealTimeRevenue(siteId); + if (latestRevenueData != null) { + snapshot.put("totalRevenue", latestRevenueData.get("totalRevenue")); + snapshot.put("dayRevenue", latestRevenueData.get("dayRevenue")); + } + } + if (yesterdayChargeData != null) { + snapshot.put("yesterdayRevenue", yesterdayChargeData.getDayRevenue()); + } + return snapshot; + } + + private String resolveHomeFieldKey(String fieldCode) { + String normalizedFieldCode = StringUtils.trim(fieldCode); + if (StringUtils.isBlank(normalizedFieldCode)) { + return null; + } + int splitIndex = normalizedFieldCode.lastIndexOf("__"); + String fieldKey = splitIndex >= 0 ? normalizedFieldCode.substring(splitIndex + 2) : normalizedFieldCode; + if (HOME_CHARGE_FIELD_SET.contains(fieldKey) || HOME_REVENUE_FIELD_SET.contains(fieldKey)) { + return fieldKey; + } + return null; + } + + private static class HomeRunningSnapshot { + private final Map valueMap = new HashMap<>(); + private final Date valueTime = new Date(); + + public void put(String key, BigDecimal value) { + if (StringUtils.isBlank(key) || value == null) { + return; + } + valueMap.put(key, value.stripTrailingZeros().toPlainString()); + } + + public String getValue(String key) { + return valueMap.get(key); + } + + public Date getValueTime() { + return valueTime; + } + } + private Map> buildEnumDataCodeMapByScope(String siteId) { Map> result = new HashMap<>(); if (StringUtils.isBlank(siteId)) { diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsEnergyPriceConfigServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsEnergyPriceConfigServiceImpl.java index 682282a..8276e26 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsEnergyPriceConfigServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsEnergyPriceConfigServiceImpl.java @@ -4,11 +4,13 @@ import com.xzzn.common.constant.RedisKeyConstants; import com.xzzn.common.core.redis.RedisCache; import com.xzzn.common.utils.DateUtils; import com.xzzn.common.utils.StringUtils; +import com.xzzn.ems.domain.EmsDailyChargeData; import com.xzzn.ems.domain.EmsDailyEnergyData; import com.xzzn.ems.domain.EmsEnergyPriceConfig; import com.xzzn.ems.domain.EmsPriceTimeConfig; import com.xzzn.ems.domain.vo.EnergyPriceTimeRange; import com.xzzn.ems.domain.vo.EnergyPriceVo; +import com.xzzn.ems.mapper.EmsDailyChargeDataMapper; import com.xzzn.ems.mapper.EmsDailyEnergyDataMapper; import com.xzzn.ems.mapper.EmsEnergyPriceConfigMapper; import com.xzzn.ems.mapper.EmsPriceTimeConfigMapper; @@ -17,6 +19,7 @@ import com.xzzn.ems.service.IEmsEnergyPriceConfigService; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; +import java.util.Date; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -45,6 +48,8 @@ public class EmsEnergyPriceConfigServiceImpl implements IEmsEnergyPriceConfigSer @Autowired private RedisCache redisCache; @Autowired + private EmsDailyChargeDataMapper emsDailyChargeDataMapper; + @Autowired private EmsDailyEnergyDataMapper emsDailyEnergyDataMapper; /** @@ -280,22 +285,21 @@ public class EmsEnergyPriceConfigServiceImpl implements IEmsEnergyPriceConfigSer BigDecimal dayRevenue = BigDecimal.ZERO; BigDecimal yesterdayRevenue = BigDecimal.ZERO; // 获取昨日数据 - String yesterday = DateUtils.getYesterdayDayString(); - EmsDailyEnergyData yesterdayData = emsDailyEnergyDataMapper.getDataByDate(siteId, yesterday); + Date yesterday = DateUtils.toDate(LocalDate.now().minusDays(1)); + EmsDailyChargeData yesterdayData = emsDailyChargeDataMapper.selectBySiteIdAndDateTime(siteId, yesterday); if (yesterdayData != null) { yesterdayRevenue = yesterdayData.getDayRevenue() == null ? BigDecimal.ZERO : yesterdayData.getDayRevenue(); } // 当日实时数据 String today = DateUtils.getDate(); + Map lastData = emsDailyEnergyDataMapper.getRealTimeRevenue(siteId); + if (lastData != null) { + totalRevenue = lastData.get("totalRevenue") == null ? BigDecimal.ZERO : lastData.get("totalRevenue"); + } + EmsDailyEnergyData todayData = emsDailyEnergyDataMapper.getDataByDate(siteId,today); - if (todayData == null) { - Map lastData = emsDailyEnergyDataMapper.getRealTimeRevenue(siteId); - if (lastData != null) { - totalRevenue = lastData.get("totalRevenue") == null ? BigDecimal.ZERO : lastData.get("totalRevenue"); - } - } else { - totalRevenue = todayData.getTotalRevenue() == null ? BigDecimal.ZERO : todayData.getTotalRevenue(); + if (todayData != null) { // 获取当月电价 int currentMonth = LocalDate.now().getMonthValue(); int currentYear = LocalDate.now().getYear(); diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointCalcConfigServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointCalcConfigServiceImpl.java index 8d1b753..5fe7d81 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointCalcConfigServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointCalcConfigServiceImpl.java @@ -16,7 +16,7 @@ import java.util.stream.Collectors; @Service public class EmsPointCalcConfigServiceImpl implements IEmsPointCalcConfigService { - private static final Pattern CALC_EXPRESSION_PATTERN = Pattern.compile("^[0-9A-Za-z_+\\-*/().\\s]+$"); + private static final Pattern CALC_EXPRESSION_PATTERN = Pattern.compile("^[0-9A-Za-z_+\\-*/().,?:<>=!&|\\s]+$"); private static final String CALC_POINT_TYPE = "calc"; @Autowired @@ -91,7 +91,7 @@ public class EmsPointCalcConfigServiceImpl implements IEmsPointCalcConfigService throw new ServiceException("计算表达式不能为空"); } if (!CALC_EXPRESSION_PATTERN.matcher(expression).matches()) { - throw new ServiceException("计算表达式仅支持数字、字母、下划线、空格和四则运算符"); + throw new ServiceException("计算表达式仅支持数字、字母、下划线、空格、运算符和函数语法"); } } diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java index 21178d7..09e9515 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java @@ -11,6 +11,7 @@ import com.xzzn.ems.domain.EmsPointConfig; import com.xzzn.ems.domain.vo.ImportPointTemplateRequest; import com.xzzn.ems.domain.vo.PointConfigCurveRequest; import com.xzzn.ems.domain.vo.PointConfigCurveValueVo; +import com.xzzn.ems.domain.vo.PointConfigGenerateRecentRequest; import com.xzzn.ems.domain.vo.PointConfigLatestValueItemVo; import com.xzzn.ems.domain.vo.PointConfigLatestValueRequest; import com.xzzn.ems.domain.vo.PointConfigLatestValueVo; @@ -30,7 +31,9 @@ import org.springframework.web.multipart.MultipartFile; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; +import java.lang.reflect.Field; import java.math.BigDecimal; +import java.math.RoundingMode; import java.nio.charset.StandardCharsets; import java.sql.Timestamp; import java.time.LocalDateTime; @@ -38,11 +41,16 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Locale; +import java.util.Objects; +import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -54,7 +62,13 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { private static final Pattern DB_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$"); private static final Pattern INTEGER_PATTERN = Pattern.compile("^-?\\d+$"); private static final Pattern DECIMAL_PATTERN = Pattern.compile("^-?\\d+(\\.\\d+)?$"); - private static final Pattern CALC_EXPRESSION_PATTERN = Pattern.compile("^[0-9A-Za-z_+\\-*/().\\s]+$"); + private static final Pattern CALC_EXPRESSION_PATTERN = Pattern.compile("^[0-9A-Za-z_+\\-*/().,?:<>=!&|\\s]+$"); + private static final Pattern AUTO_DAY_DIFF_FUNCTION_PATTERN = Pattern.compile("(?i)DAY_DIFF\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\)"); + private static final Pattern AUTO_MONTH_DIFF_FUNCTION_PATTERN = Pattern.compile("(?i)MONTH_DIFF\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\)"); + private static final Pattern AUTO_HOUR_DIFF_FUNCTION_PATTERN = Pattern.compile("(?i)HOUR_DIFF\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\)"); + private static final String AUTO_DAY_DIFF_CONTEXT_PREFIX = "__AUTO_DAY_DIFF__"; + private static final String AUTO_MONTH_DIFF_CONTEXT_PREFIX = "__AUTO_MONTH_DIFF__"; + private static final String AUTO_HOUR_DIFF_CONTEXT_PREFIX = "__AUTO_HOUR_DIFF__"; private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @Autowired @@ -80,6 +94,7 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { throw new ServiceException("站点ID不能为空"); } normalizeAndValidatePointConfig(pointConfig); + applyCollectDefaultsForInsert(pointConfig); pointConfig.setCreateBy(operName); pointConfig.setUpdateBy(operName); int rows; @@ -177,43 +192,67 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { throw new ServiceException("仅支持上传CSV文件"); } - boolean overwriteFlag = Boolean.TRUE.equals(overwrite); - int targetCount = emsPointConfigMapper.countBySiteId(siteId); - if (targetCount > 0 && !overwriteFlag) { - throw new ServiceException("目标站点已存在点位配置,请勾选“覆盖已存在点位数据”后重试"); - } - List pointConfigList = parseCsv(file, siteId); if (pointConfigList.isEmpty()) { throw new ServiceException("CSV没有可导入的数据"); } - if (targetCount > 0) { - emsPointConfigMapper.deleteBySiteId(siteId); + List pointIds = pointConfigList.stream() + .map(EmsPointConfig::getPointId) + .filter(StringUtils::isNotBlank) + .distinct() + .collect(Collectors.toList()); + Map existingPointMap = new HashMap<>(); + if (!pointIds.isEmpty()) { + List existingList = emsPointConfigMapper.selectBySiteIdAndPointIds(siteId, pointIds); + if (CollectionUtils.isNotEmpty(existingList)) { + for (EmsPointConfig existing : existingList) { + if (existing != null && StringUtils.isNotBlank(existing.getPointId())) { + existingPointMap.put(existing.getPointId(), existing); + } + } + } } - for (EmsPointConfig pointConfig : pointConfigList) { - pointConfig.setCreateBy(operName); - pointConfig.setUpdateBy(operName); - } - int importCount = 0; + List insertList = new ArrayList<>(); + int updateCount = 0; + int insertCount = 0; long importStart = System.currentTimeMillis(); - for (int fromIndex = 0; fromIndex < pointConfigList.size(); fromIndex += CSV_IMPORT_BATCH_SIZE) { - int toIndex = Math.min(fromIndex + CSV_IMPORT_BATCH_SIZE, pointConfigList.size()); - List batchList = pointConfigList.subList(fromIndex, toIndex); + for (EmsPointConfig pointConfig : pointConfigList) { + EmsPointConfig existing = existingPointMap.get(pointConfig.getPointId()); + if (existing == null) { + pointConfig.setCreateBy(operName); + pointConfig.setUpdateBy(operName); + insertList.add(pointConfig); + continue; + } + pointConfig.setId(existing.getId()); + pointConfig.setUpdateBy(operName); + try { + updateCount += emsPointConfigMapper.updateEmsPointConfigForImport(pointConfig); + } catch (Exception ex) { + throw translatePointConfigPersistenceException(ex, pointConfig.getPointId()); + } + } + + for (int fromIndex = 0; fromIndex < insertList.size(); fromIndex += CSV_IMPORT_BATCH_SIZE) { + int toIndex = Math.min(fromIndex + CSV_IMPORT_BATCH_SIZE, insertList.size()); + List batchList = insertList.subList(fromIndex, toIndex); long batchStart = System.currentTimeMillis(); try { - importCount += emsPointConfigMapper.insertBatchEmsPointConfig(batchList); + insertCount += emsPointConfigMapper.insertBatchEmsPointConfig(batchList); } catch (Exception ex) { throw translatePointConfigPersistenceException(ex, null); } - log.info("点位CSV导入批量入库完成,siteId={}, batch={}~{}, batchSize={}, costMs={}", + log.info("点位CSV导入批量新增完成,siteId={}, batch={}~{}, batchSize={}, costMs={}", siteId, fromIndex + 1, toIndex, batchList.size(), System.currentTimeMillis() - batchStart); } - log.info("点位CSV导入完成,siteId={}, total={}, costMs={}", siteId, importCount, System.currentTimeMillis() - importStart); + int importCount = updateCount + insertCount; + log.info("点位CSV导入完成,siteId={}, total={}, insert={}, update={}, costMs={}", + siteId, importCount, insertCount, updateCount, System.currentTimeMillis() - importStart); invalidatePointConfigCacheBySite(siteId); - return String.format("导入成功:站点 %s,点位 %d 条", siteId, importCount); + return String.format("导入成功:站点 %s,新增 %d 条,更新 %d 条", siteId, insertCount, updateCount); } @Override @@ -228,17 +267,21 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { public List getLatestValues(PointConfigLatestValueRequest request) { List result = new ArrayList<>(); if (request == null || request.getPoints() == null || request.getPoints().isEmpty()) { + log.warn("latestValues 查询参数为空,request: {}", request); return result; } - - Map> configMapCache = new HashMap<>(); + log.info("latestValues 开始查询,pointCount: {}", request.getPoints().size()); for (PointConfigLatestValueItemVo item : request.getPoints()) { - if (item == null || StringUtils.isAnyBlank(item.getSiteId(), item.getDeviceId(), item.getDataKey())) { + if (item == null || StringUtils.isBlank(item.getPointId())) { + log.warn("latestValues 跳过空点位项,item: {}", item); continue; } - PointConfigLatestValueVo latestValue = queryLatestValueFromRedis(item, configMapCache); + PointConfigLatestValueVo latestValue = queryLatestValueByPointIdFromRedis(item); result.add(latestValue); } + long hitCount = result.stream().filter(vo -> vo.getPointValue() != null).count(); + log.info("latestValues 查询结束,total: {}, hit: {}, miss: {}", + result.size(), hitCount, result.size() - hitCount); return result; } @@ -252,50 +295,495 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { if (StringUtils.isAnyBlank(siteId, pointId)) { return new ArrayList<>(); } - EmsPointConfig pointConfig = resolvePointConfigForCurve(siteId, pointId); Date[] range = resolveTimeRange(request); - return queryCurveDataFromInflux(siteId, pointId, pointConfig, range[0], range[1]); + return queryCurveDataFromInflux(siteId, pointId, range[0], range[1]); } - private PointConfigLatestValueVo queryLatestValueFromRedis(PointConfigLatestValueItemVo item, - Map> configMapCache) { + @Override + public String generateRecent7DaysData(PointConfigGenerateRecentRequest request) { + if (request == null || StringUtils.isAnyBlank(request.getSiteId(), request.getPointId())) { + throw new ServiceException("站点ID和点位ID不能为空"); + } + String siteId = StringUtils.trim(request.getSiteId()); + String pointId = StringUtils.trim(request.getPointId()); + String deviceId = StringUtils.trimToEmpty(request.getDeviceId()); + + EmsPointConfig targetConfig = selectPointConfigByExactPointId(siteId, pointId); + if (targetConfig == null || !"calc".equalsIgnoreCase(StringUtils.defaultString(targetConfig.getPointType()))) { + throw new ServiceException("仅计算点支持按表达式生成最近7天数据"); + } + String expression = StringUtils.trimToNull(targetConfig.getCalcExpression()); + if (StringUtils.isBlank(expression)) { + throw new ServiceException("计算点缺少表达式,无法生成数据"); + } + + List payloads = buildCalcRecentPayloads(siteId, deviceId, pointId, expression); + if (payloads.isEmpty()) { + throw new ServiceException("未生成有效数据,请检查表达式依赖点是否有历史数据"); + } + influxPointDataWriter.writeBatch(payloads); + return String.format("已生成点位 %s 最近7天数据,共 %d 条", pointId, payloads.size()); + } + + private List buildCalcRecentPayloads(String siteId, String deviceId, String pointId, String expression) { + CompiledExpression compiledExpression = new CompiledExpression(expression); + List evaluateTimes = buildEvaluateTimes(expression); + if (evaluateTimes.isEmpty()) { + return Collections.emptyList(); + } + EvaluateGranularity granularity = resolveEvaluateGranularity(expression); + LocalDateTime start = evaluateTimes.get(0); + LocalDateTime end = evaluateTimes.get(evaluateTimes.size() - 1); + + Set variables = extractExpressionVariables(expression); + Map> variableSeriesMap = loadVariableSeries(siteId, variables, start.minusDays(35), end); + Map periodDiffStateMap = new HashMap<>(); + initializeRecentPeriodDiffState(expression, evaluateTimes, variableSeriesMap, periodDiffStateMap); + List payloads = new ArrayList<>(evaluateTimes.size()); + for (LocalDateTime evaluateTime : evaluateTimes) { + Map contextValues = buildContextValuesByTime(evaluateTime, variableSeriesMap, granularity); + if (contextValues.isEmpty()) { + continue; + } + BigDecimal value; + try { + prepareAutoPeriodDiffContextValues(expression, contextValues, evaluateTime, periodDiffStateMap); + value = compiledExpression.evaluate(contextValues).asNumber().setScale(4, RoundingMode.HALF_UP); + } catch (IllegalArgumentException ex) { + continue; + } + payloads.add(new InfluxPointDataWriter.PointWritePayload( + siteId, + deviceId, + pointId, + value, + Timestamp.valueOf(evaluateTime) + )); + } + return payloads; + } + + private void initializeRecentPeriodDiffState(String expression, + List evaluateTimes, + Map> variableSeriesMap, + Map periodDiffStateMap) { + if (StringUtils.isBlank(expression) + || CollectionUtils.isEmpty(evaluateTimes) + || variableSeriesMap == null + || periodDiffStateMap == null) { + return; + } + LocalDateTime firstEvaluateTime = evaluateTimes.get(0); + initializeSingleRecentDiffState("DAY_DIFF", expression, AUTO_DAY_DIFF_FUNCTION_PATTERN, + "yyyy-MM-dd", firstEvaluateTime, variableSeriesMap, periodDiffStateMap); + initializeSingleRecentDiffState("MONTH_DIFF", expression, AUTO_MONTH_DIFF_FUNCTION_PATTERN, + "yyyy-MM", firstEvaluateTime, variableSeriesMap, periodDiffStateMap); + initializeSingleRecentDiffState("HOUR_DIFF", expression, AUTO_HOUR_DIFF_FUNCTION_PATTERN, + "yyyy-MM-dd HH", firstEvaluateTime, variableSeriesMap, periodDiffStateMap); + } + + private void initializeSingleRecentDiffState(String functionName, + String expression, + Pattern functionPattern, + String periodFormat, + LocalDateTime firstEvaluateTime, + Map> variableSeriesMap, + Map periodDiffStateMap) { + Set variables = extractAutoDiffVariables(expression, functionPattern); + if (variables.isEmpty()) { + return; + } + long previousMillis = Timestamp.valueOf(firstEvaluateTime).getTime() - 1; + if (previousMillis < Long.MIN_VALUE + 1) { + return; + } + String previousPeriod = DateUtils.parseDateToStr(periodFormat, new Date(previousMillis)); + for (String variable : variables) { + List values = variableSeriesMap.get(variable); + InfluxPointDataWriter.PointValue previous = findLatestInWindow(values, Long.MIN_VALUE, previousMillis); + if (previous == null || previous.getPointValue() == null) { + continue; + } + String stateKey = functionName.toUpperCase(Locale.ROOT) + "_" + variable.toUpperCase(Locale.ROOT); + PeriodDiffState state = periodDiffStateMap.computeIfAbsent(stateKey, k -> new PeriodDiffState()); + state.period = previousPeriod; + state.lastValue = previous.getPointValue(); + } + } + + private List buildEvaluateTimes(String expression) { + LocalDateTime now = LocalDateTime.now(); + List times = new ArrayList<>(); + EvaluateGranularity granularity = resolveEvaluateGranularity(expression); + boolean containsHourDiff = granularity == EvaluateGranularity.HOUR; + boolean containsMonthDiff = granularity == EvaluateGranularity.MONTH; + if (containsHourDiff) { + int totalHours = 24 * 7; + LocalDateTime latestCompletedHourEnd = now.withMinute(0).withSecond(0).withNano(0).minusSeconds(1); + for (int i = totalHours - 1; i >= 0; i--) { + times.add(latestCompletedHourEnd.minusHours(i)); + } + return times; + } + if (containsMonthDiff) { + times.add(now.withSecond(0).withNano(0)); + return times; + } + for (int i = 6; i >= 0; i--) { + LocalDateTime dayTime = now.minusDays(i).withHour(23).withMinute(59).withSecond(59).withNano(0); + if (dayTime.isAfter(now)) { + dayTime = now.withNano(0); + } + times.add(dayTime); + } + return times; + } + + private EvaluateGranularity resolveEvaluateGranularity(String expression) { + if (AUTO_HOUR_DIFF_FUNCTION_PATTERN.matcher(expression).find()) { + return EvaluateGranularity.HOUR; + } + if (AUTO_MONTH_DIFF_FUNCTION_PATTERN.matcher(expression).find()) { + return EvaluateGranularity.MONTH; + } + return EvaluateGranularity.DAY; + } + + private Set extractExpressionVariables(String expression) { + Set reserved = new HashSet<>(); + reserved.add("IF"); + reserved.add("DAY_DIFF"); + reserved.add("MONTH_DIFF"); + reserved.add("HOUR_DIFF"); + Set variables = new HashSet<>(); + for (ExprToken token : tokenizeExpression(expression)) { + if (token.type == ExprTokenType.IDENTIFIER) { + String identifier = StringUtils.defaultString(token.text).trim().toUpperCase(Locale.ROOT); + if (StringUtils.isNotBlank(identifier) && !reserved.contains(identifier)) { + variables.add(identifier); + } + } + } + return variables; + } + + private Map> loadVariableSeries(String siteId, + Set variables, + LocalDateTime start, + LocalDateTime end) { + Map> variableSeriesMap = new HashMap<>(); + if (variables == null || variables.isEmpty()) { + return variableSeriesMap; + } + Date startDate = Timestamp.valueOf(start); + Date endDate = Timestamp.valueOf(end); + for (String variable : variables) { + List values = influxPointDataWriter.queryCurveDataByPointKey(siteId, variable, startDate, endDate); + if (values == null) { + values = new ArrayList<>(); + } + values.sort(Comparator.comparing(InfluxPointDataWriter.PointValue::getDataTime, Comparator.nullsLast(Date::compareTo))); + variableSeriesMap.put(variable, values); + } + return variableSeriesMap; + } + + private Map buildContextValuesByTime(LocalDateTime evaluateTime, + Map> variableSeriesMap, + EvaluateGranularity granularity) { + Map context = new HashMap<>(); + if (variableSeriesMap == null || variableSeriesMap.isEmpty()) { + return context; + } + long evaluateMillis = Timestamp.valueOf(evaluateTime).getTime(); + long windowStartMillis = resolveWindowStartMillis(evaluateTime, granularity); + for (Map.Entry> entry : variableSeriesMap.entrySet()) { + InfluxPointDataWriter.PointValue latest = findLatestInWindow(entry.getValue(), windowStartMillis, evaluateMillis); + if (latest == null && granularity == EvaluateGranularity.MONTH) { + latest = findLatestInWindow(entry.getValue(), Long.MIN_VALUE, evaluateMillis); + } + if (latest == null || latest.getPointValue() == null) { + continue; + } + context.put(entry.getKey(), latest.getPointValue()); + } + return context; + } + + private long resolveWindowStartMillis(LocalDateTime evaluateTime, EvaluateGranularity granularity) { + if (granularity == EvaluateGranularity.HOUR) { + return Timestamp.valueOf(evaluateTime.withMinute(0).withSecond(0).withNano(0)).getTime(); + } + if (granularity == EvaluateGranularity.DAY) { + return Timestamp.valueOf(evaluateTime.withHour(0).withMinute(0).withSecond(0).withNano(0)).getTime(); + } + return Long.MIN_VALUE; + } + + private InfluxPointDataWriter.PointValue findLatestInWindow(List values, + long startMillisInclusive, + long endMillisInclusive) { + if (values == null || values.isEmpty()) { + return null; + } + InfluxPointDataWriter.PointValue latest = null; + for (InfluxPointDataWriter.PointValue value : values) { + if (value == null || value.getDataTime() == null) { + continue; + } + long currentMillis = value.getDataTime().getTime(); + if (currentMillis > endMillisInclusive) { + break; + } + if (currentMillis < startMillisInclusive) { + continue; + } + latest = value; + } + return latest; + } + + private enum EvaluateGranularity { + DAY, + HOUR, + MONTH + } + + private void prepareAutoPeriodDiffContextValues(String expression, + Map contextValues, + LocalDateTime evaluateTime, + Map periodDiffStateMap) { + if (StringUtils.isBlank(expression) || contextValues == null || contextValues.isEmpty()) { + return; + } + prepareSingleAutoDiffContext(expression, contextValues, evaluateTime, "DAY_DIFF", + AUTO_DAY_DIFF_FUNCTION_PATTERN, "yyyy-MM-dd", periodDiffStateMap); + prepareSingleAutoDiffContext(expression, contextValues, evaluateTime, "MONTH_DIFF", + AUTO_MONTH_DIFF_FUNCTION_PATTERN, "yyyy-MM", periodDiffStateMap); + prepareSingleAutoDiffContext(expression, contextValues, evaluateTime, "HOUR_DIFF", + AUTO_HOUR_DIFF_FUNCTION_PATTERN, "yyyy-MM-dd HH", periodDiffStateMap); + } + + private void prepareSingleAutoDiffContext(String expression, + Map contextValues, + LocalDateTime evaluateTime, + String functionName, + Pattern functionPattern, + String periodFormat, + Map periodDiffStateMap) { + Set variables = extractAutoDiffVariables(expression, functionPattern); + if (variables.isEmpty()) { + return; + } + String currentPeriod = DateUtils.parseDateToStr(periodFormat, Timestamp.valueOf(evaluateTime)); + for (String variable : variables) { + BigDecimal currentValue = contextValues.get(variable); + if (currentValue == null) { + continue; + } + BigDecimal diffValue = calculateAutoPeriodDiffValue(functionName, variable, currentValue, currentPeriod, periodDiffStateMap); + contextValues.put(resolveAutoDiffContextKey(functionName, variable), diffValue); + } + } + + private Set extractAutoDiffVariables(String expression, Pattern functionPattern) { + Set variables = new HashSet<>(); + java.util.regex.Matcher matcher = functionPattern.matcher(expression); + while (matcher.find()) { + String variable = StringUtils.defaultString(matcher.group(1)).trim().toUpperCase(Locale.ROOT); + if (StringUtils.isNotBlank(variable)) { + variables.add(variable); + } + } + return variables; + } + + private BigDecimal calculateAutoPeriodDiffValue(String functionName, + String variable, + BigDecimal currentValue, + String currentPeriod, + Map periodDiffStateMap) { + if (currentValue == null || StringUtils.isAnyBlank(functionName, variable, currentPeriod)) { + return BigDecimal.ZERO; + } + String stateKey = functionName.toUpperCase(Locale.ROOT) + "_" + variable.toUpperCase(Locale.ROOT); + PeriodDiffState state = periodDiffStateMap.computeIfAbsent(stateKey, k -> new PeriodDiffState()); + if (!StringUtils.equals(state.period, currentPeriod)) { + state.baseline = state.lastValue == null ? currentValue : state.lastValue; + state.period = currentPeriod; + state.periodDiffValue = currentValue.subtract(state.baseline); + } else if (state.baseline == null) { + state.baseline = currentValue; + state.periodDiffValue = currentValue.subtract(state.baseline); + } else { + state.periodDiffValue = currentValue.subtract(state.baseline); + } + state.lastValue = currentValue; + return state.periodDiffValue == null ? BigDecimal.ZERO : state.periodDiffValue; + } + + private static String resolveAutoDayDiffContextKey(String variable) { + return AUTO_DAY_DIFF_CONTEXT_PREFIX + StringUtils.defaultString(variable).trim().toUpperCase(Locale.ROOT); + } + + private static String resolveAutoMonthDiffContextKey(String variable) { + return AUTO_MONTH_DIFF_CONTEXT_PREFIX + StringUtils.defaultString(variable).trim().toUpperCase(Locale.ROOT); + } + + private static String resolveAutoHourDiffContextKey(String variable) { + return AUTO_HOUR_DIFF_CONTEXT_PREFIX + StringUtils.defaultString(variable).trim().toUpperCase(Locale.ROOT); + } + + private static String resolveAutoDiffContextKey(String functionName, String variable) { + if ("DAY_DIFF".equalsIgnoreCase(functionName)) { + return resolveAutoDayDiffContextKey(variable); + } + if ("MONTH_DIFF".equalsIgnoreCase(functionName)) { + return resolveAutoMonthDiffContextKey(variable); + } + if ("HOUR_DIFF".equalsIgnoreCase(functionName)) { + return resolveAutoHourDiffContextKey(variable); + } + return null; + } + + private PointConfigLatestValueVo queryLatestValueByPointIdFromRedis(PointConfigLatestValueItemVo item) { PointConfigLatestValueVo vo = new PointConfigLatestValueVo(); vo.setSiteId(item.getSiteId()); vo.setDeviceId(item.getDeviceId()); vo.setDataKey(item.getDataKey()); - String redisKey = RedisKeyConstants.ORIGINAL_MQTT_DATA + item.getSiteId() + "_" + item.getDeviceId(); - Object raw = redisCache.getCacheObject(redisKey); - if (raw == null) { + String pointId = StringUtils.trimToNull(item.getPointId()); + vo.setPointId(pointId); + if (StringUtils.isBlank(pointId)) { return vo; } + String hitRedisKey = pointId; + Object raw = redisCache.getCacheObject(pointId); + if (raw == null) { + log.warn("latestValues Redis未命中,siteId: {}, deviceId: {}, pointId: {}, dataKey: {}", + item.getSiteId(), item.getDeviceId(), pointId, item.getDataKey()); + return vo; + } + log.info("latestValues Redis命中,siteId: {}, deviceId: {}, pointId: {}, redisKey: {}, rawType: {}", + item.getSiteId(), item.getDeviceId(), pointId, hitRedisKey, raw.getClass().getName()); + JSONObject root = toJsonObject(raw); if (root == null) { - return vo; - } - JSONObject dataObject = extractDataObject(root); - if (dataObject == null) { + log.warn("latestValues Redis值无法转JSON,siteId: {}, deviceId: {}, pointId: {}, redisKey: {}, raw: {}", + item.getSiteId(), item.getDeviceId(), pointId, hitRedisKey, raw); return vo; } - Object rawValue = getValueIgnoreCase(dataObject, item.getDataKey()); - BigDecimal pointValue = StringUtils.getBigDecimal(rawValue); - if (pointValue != null) { - EmsPointConfig pointConfig = getPointConfig(item.getSiteId(), item.getDeviceId(), item.getDataKey(), configMapCache); - vo.setPointValue(convertPointValue(pointValue, pointConfig)); + Object rawValue = root.get("pointValue"); + BigDecimal pointValue = toBigDecimalOrNull(rawValue); + if (pointValue == null) { + pointValue = readBigDecimalField(raw, "pointValue"); } - vo.setDataTime(extractDataTime(root)); + if (pointValue != null) { + vo.setPointValue(pointValue); + } + + if (StringUtils.isBlank(vo.getSiteId())) { + vo.setSiteId(root.getString("siteId")); + if (StringUtils.isBlank(vo.getSiteId())) { + vo.setSiteId(readStringField(raw, "siteId")); + } + } + if (StringUtils.isBlank(vo.getDeviceId())) { + vo.setDeviceId(root.getString("deviceId")); + if (StringUtils.isBlank(vo.getDeviceId())) { + vo.setDeviceId(readStringField(raw, "deviceId")); + } + } + if (StringUtils.isBlank(vo.getPointId())) { + vo.setPointId(root.getString("pointId")); + if (StringUtils.isBlank(vo.getPointId())) { + vo.setPointId(readStringField(raw, "pointId")); + } + } + Date dataTime = extractCacheDataTime(root); + if (dataTime == null) { + dataTime = readDateField(raw, "dataTime"); + } + vo.setDataTime(dataTime); + log.info("latestValues 解析完成,siteId: {}, deviceId: {}, pointId: {}, dataKey: {}, pointValue: {}, dataTime: {}", + vo.getSiteId(), vo.getDeviceId(), vo.getPointId(), vo.getDataKey(), vo.getPointValue(), vo.getDataTime()); return vo; } - private List queryCurveDataFromInflux(String siteId, String pointId, - EmsPointConfig pointConfig, Date startTime, Date endTime) { - String influxPointKey = resolveInfluxPointKey(pointConfig, pointId); - if (StringUtils.isBlank(influxPointKey)) { + private BigDecimal toBigDecimalOrNull(Object value) { + if (value == null) { + return null; + } + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + try { + return new BigDecimal(value.toString()); + } catch (Exception ex) { + return null; + } + } + + private String readStringField(Object source, String fieldName) { + Object value = readFieldValue(source, fieldName); + return value == null ? null : String.valueOf(value); + } + + private BigDecimal readBigDecimalField(Object source, String fieldName) { + Object value = readFieldValue(source, fieldName); + return toBigDecimalOrNull(value); + } + + private Date readDateField(Object source, String fieldName) { + Object value = readFieldValue(source, fieldName); + if (value instanceof Date) { + return (Date) value; + } + return null; + } + + private Object readFieldValue(Object source, String fieldName) { + if (source == null || StringUtils.isBlank(fieldName)) { + return null; + } + Class current = source.getClass(); + while (current != null) { + try { + Field field = current.getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(source); + } catch (NoSuchFieldException ex) { + current = current.getSuperclass(); + } catch (Exception ex) { + return null; + } + } + return null; + } + + private Date extractCacheDataTime(JSONObject root) { + if (root == null) { + return null; + } + Date dataTime = root.getDate("dataTime"); + if (dataTime != null) { + return dataTime; + } + Long timestamp = root.getLong("timestamp"); + if (timestamp != null) { + return DateUtils.convertUpdateTime(timestamp); + } + return null; + } + + private List queryCurveDataFromInflux(String siteId, String pointId, Date startTime, Date endTime) { + if (StringUtils.isBlank(pointId)) { return new ArrayList<>(); } - List values = influxPointDataWriter.queryCurveDataByPointKey(siteId, influxPointKey, startTime, endTime); + List values = influxPointDataWriter.queryCurveDataByPointKey(siteId, pointId.trim(), startTime, endTime); if (values == null || values.isEmpty()) { return new ArrayList<>(); } @@ -491,35 +979,30 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { return configByKey.get(dataKey.toUpperCase(Locale.ROOT)); } - private String resolveInfluxPointKey(EmsPointConfig pointConfig, String pointId) { - if (pointConfig != null && StringUtils.isNotBlank(pointConfig.getPointId())) { - return pointConfig.getPointId().trim(); - } - if (StringUtils.isNotBlank(pointId)) { - return pointId.trim(); - } - return null; - } - - private EmsPointConfig resolvePointConfigForCurve(String siteId, String pointId) { + private EmsPointConfig selectPointConfigByExactPointId(String siteId, String pointId) { if (StringUtils.isAnyBlank(siteId, pointId)) { return null; } - String cacheKey = RedisKeyConstants.POINT_CONFIG_POINT + siteId + "_" + pointId; + String normalizedPointId = pointId.trim(); + String cacheKey = RedisKeyConstants.POINT_CONFIG_POINT + siteId + "_" + normalizedPointId; EmsPointConfig cached = redisCache.getCacheObject(cacheKey); if (cached != null) { return cached; } EmsPointConfig query = new EmsPointConfig(); query.setSiteId(siteId); - query.setPointId(pointId); + query.setPointId(normalizedPointId); List pointConfigs = emsPointConfigMapper.selectEmsPointConfigList(query); if (CollectionUtils.isEmpty(pointConfigs)) { return null; } - EmsPointConfig latest = pointConfigs.get(0); - redisCache.setCacheObject(cacheKey, latest); - return latest; + for (EmsPointConfig pointConfig : pointConfigs) { + if (pointConfig != null && StringUtils.equals(pointConfig.getPointId(), normalizedPointId)) { + redisCache.setCacheObject(cacheKey, pointConfig); + return pointConfig; + } + } + return null; } private BigDecimal convertPointValue(BigDecimal sourceValue, EmsPointConfig pointConfig) { @@ -534,6 +1017,702 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { .add(b); } + private static class PeriodDiffState { + private String period; + private BigDecimal baseline; + private BigDecimal lastValue; + private BigDecimal periodDiffValue; + } + + private static class MissingVariableException extends IllegalArgumentException { + private MissingVariableException(String variable) { + super("缺少变量: " + variable); + } + } + + private enum ExprTokenType { + NUMBER, + STRING, + IDENTIFIER, + OPERATOR, + LEFT_PAREN, + RIGHT_PAREN, + COMMA, + QUESTION, + COLON, + EOF + } + + private static class ExprToken { + private final ExprTokenType type; + private final String text; + private final BigDecimal number; + private final String stringValue; + + private ExprToken(ExprTokenType type, String text, BigDecimal number, String stringValue) { + this.type = type; + this.text = text; + this.number = number; + this.stringValue = stringValue; + } + + private static ExprToken number(BigDecimal value) { + return new ExprToken(ExprTokenType.NUMBER, null, value, null); + } + + private static ExprToken string(String value) { + return new ExprToken(ExprTokenType.STRING, null, null, value); + } + + private static ExprToken identifier(String value) { + return new ExprToken(ExprTokenType.IDENTIFIER, value, null, null); + } + + private static ExprToken operator(String value) { + return new ExprToken(ExprTokenType.OPERATOR, value, null, null); + } + + private static ExprToken symbol(ExprTokenType type, String text) { + return new ExprToken(type, text, null, null); + } + } + + private static class ExpressionValue { + private final BigDecimal numberValue; + private final String textValue; + + private ExpressionValue(BigDecimal numberValue, String textValue) { + this.numberValue = numberValue; + this.textValue = textValue; + } + + private static ExpressionValue ofNumber(BigDecimal numberValue) { + return new ExpressionValue(numberValue, null); + } + + private static ExpressionValue ofText(String textValue) { + return new ExpressionValue(null, textValue == null ? "" : textValue); + } + + private boolean isNumber() { + return numberValue != null; + } + + private BigDecimal asNumber() { + if (numberValue == null) { + throw new IllegalArgumentException("表达式值不是数值类型: " + textValue); + } + return numberValue; + } + + private String asText() { + return numberValue != null ? numberValue.stripTrailingZeros().toPlainString() : textValue; + } + + private boolean asBoolean() { + return isNumber() ? BigDecimal.ZERO.compareTo(numberValue) != 0 : StringUtils.isNotBlank(textValue); + } + } + + private interface ExpressionNode { + ExpressionValue evaluate(Map contextValues); + } + + private static class NumberNode implements ExpressionNode { + private final BigDecimal value; + + private NumberNode(BigDecimal value) { + this.value = value; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + return ExpressionValue.ofNumber(value); + } + } + + private static class StringNode implements ExpressionNode { + private final String value; + + private StringNode(String value) { + this.value = value; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + return ExpressionValue.ofText(value); + } + } + + private static class VariableNode implements ExpressionNode { + private final String name; + + private VariableNode(String name) { + this.name = name; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + BigDecimal value = contextValues.get(name); + if (value == null) { + throw new MissingVariableException(name); + } + return ExpressionValue.ofNumber(value); + } + + private String getName() { + return name; + } + } + + private static class UnaryNode implements ExpressionNode { + private final String operator; + private final ExpressionNode node; + + private UnaryNode(String operator, ExpressionNode node) { + this.operator = operator; + this.node = node; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + ExpressionValue value = node.evaluate(contextValues); + switch (operator) { + case "+": + return value; + case "-": + return ExpressionValue.ofNumber(value.asNumber().negate()); + case "!": + return value.asBoolean() ? ExpressionValue.ofNumber(BigDecimal.ZERO) : ExpressionValue.ofNumber(BigDecimal.ONE); + default: + throw new IllegalArgumentException("不支持的一元操作符: " + operator); + } + } + } + + private static class BinaryNode implements ExpressionNode { + private final String operator; + private final ExpressionNode left; + private final ExpressionNode right; + + private BinaryNode(String operator, ExpressionNode left, ExpressionNode right) { + this.operator = operator; + this.left = left; + this.right = right; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + if ("&&".equals(operator)) { + ExpressionValue leftValue = left.evaluate(contextValues); + if (!leftValue.asBoolean()) { + return ExpressionValue.ofNumber(BigDecimal.ZERO); + } + return right.evaluate(contextValues).asBoolean() + ? ExpressionValue.ofNumber(BigDecimal.ONE) + : ExpressionValue.ofNumber(BigDecimal.ZERO); + } + if ("||".equals(operator)) { + ExpressionValue leftValue = left.evaluate(contextValues); + if (leftValue.asBoolean()) { + return ExpressionValue.ofNumber(BigDecimal.ONE); + } + return right.evaluate(contextValues).asBoolean() + ? ExpressionValue.ofNumber(BigDecimal.ONE) + : ExpressionValue.ofNumber(BigDecimal.ZERO); + } + + ExpressionValue leftValue = left.evaluate(contextValues); + ExpressionValue rightValue = right.evaluate(contextValues); + switch (operator) { + case "+": + if (!leftValue.isNumber() || !rightValue.isNumber()) { + return ExpressionValue.ofText(leftValue.asText() + rightValue.asText()); + } + return ExpressionValue.ofNumber(leftValue.asNumber().add(rightValue.asNumber())); + case "-": + return ExpressionValue.ofNumber(leftValue.asNumber().subtract(rightValue.asNumber())); + case "*": + return ExpressionValue.ofNumber(leftValue.asNumber().multiply(rightValue.asNumber())); + case "/": + if (BigDecimal.ZERO.compareTo(rightValue.asNumber()) == 0) { + throw new IllegalArgumentException("除数不能为0"); + } + return ExpressionValue.ofNumber(leftValue.asNumber().divide(rightValue.asNumber(), 10, RoundingMode.HALF_UP)); + case ">": + return leftValue.asNumber().compareTo(rightValue.asNumber()) > 0 + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + case ">=": + return leftValue.asNumber().compareTo(rightValue.asNumber()) >= 0 + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + case "<": + return leftValue.asNumber().compareTo(rightValue.asNumber()) < 0 + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + case "<=": + return leftValue.asNumber().compareTo(rightValue.asNumber()) <= 0 + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + case "==": + if (leftValue.isNumber() && rightValue.isNumber()) { + return leftValue.asNumber().compareTo(rightValue.asNumber()) == 0 + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + } + return Objects.equals(leftValue.asText(), rightValue.asText()) + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + case "!=": + if (leftValue.isNumber() && rightValue.isNumber()) { + return leftValue.asNumber().compareTo(rightValue.asNumber()) != 0 + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + } + return !Objects.equals(leftValue.asText(), rightValue.asText()) + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + default: + throw new IllegalArgumentException("不支持的操作符: " + operator); + } + } + } + + private static class TernaryNode implements ExpressionNode { + private final ExpressionNode condition; + private final ExpressionNode trueNode; + private final ExpressionNode falseNode; + + private TernaryNode(ExpressionNode condition, ExpressionNode trueNode, ExpressionNode falseNode) { + this.condition = condition; + this.trueNode = trueNode; + this.falseNode = falseNode; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + return condition.evaluate(contextValues).asBoolean() + ? trueNode.evaluate(contextValues) + : falseNode.evaluate(contextValues); + } + } + + private static class FunctionNode implements ExpressionNode { + private final String functionName; + private final List arguments; + + private FunctionNode(String functionName, List arguments) { + this.functionName = StringUtils.defaultString(functionName).trim().toUpperCase(Locale.ROOT); + this.arguments = arguments == null ? Collections.emptyList() : arguments; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + if (("DAY_DIFF".equals(functionName) + || "MONTH_DIFF".equals(functionName) + || "HOUR_DIFF".equals(functionName)) && arguments.size() == 1) { + ExpressionNode argument = arguments.get(0); + if (!(argument instanceof VariableNode)) { + throw new IllegalArgumentException(functionName + "单参数模式仅支持变量参数"); + } + String variable = ((VariableNode) argument).getName(); + BigDecimal autoDiffValue = contextValues.get(resolveAutoDiffContextKey(functionName, variable)); + if (autoDiffValue == null) { + throw new MissingVariableException(variable); + } + return ExpressionValue.ofNumber(autoDiffValue); + } + if ("DAY_DIFF".equals(functionName) + || "MONTH_DIFF".equals(functionName) + || "HOUR_DIFF".equals(functionName)) { + throw new IllegalArgumentException(functionName + "函数参数数量错误,需1个参数"); + } + throw new IllegalArgumentException("不支持的函数: " + functionName); + } + } + + private static class CompiledExpression { + private final ExpressionNode root; + + private CompiledExpression(String expression) { + List tokens = tokenizeExpression(expression); + ExpressionParser parser = new ExpressionParser(tokens); + this.root = parser.parseExpression(); + if (!parser.isEnd()) { + ExprToken token = parser.peek(); + throw new IllegalArgumentException("表达式尾部有多余内容: " + token.text); + } + } + + private ExpressionValue evaluate(Map contextValues) { + return root.evaluate(contextValues); + } + } + + private static class ExpressionParser { + private final List tokens; + private int index; + + private ExpressionParser(List tokens) { + this.tokens = tokens; + this.index = 0; + } + + private ExpressionNode parseExpression() { + return parseTernary(); + } + + private ExpressionNode parseTernary() { + ExpressionNode condition = parseOr(); + if (match(ExprTokenType.QUESTION)) { + ExpressionNode trueNode = parseTernary(); + expect(ExprTokenType.COLON, "三元表达式缺少 ':'"); + ExpressionNode falseNode = parseTernary(); + return new TernaryNode(condition, trueNode, falseNode); + } + return condition; + } + + private ExpressionNode parseOr() { + ExpressionNode left = parseAnd(); + while (matchOperator("||")) { + left = new BinaryNode("||", left, parseAnd()); + } + return left; + } + + private ExpressionNode parseAnd() { + ExpressionNode left = parseEquality(); + while (matchOperator("&&")) { + left = new BinaryNode("&&", left, parseEquality()); + } + return left; + } + + private ExpressionNode parseEquality() { + ExpressionNode left = parseComparison(); + while (true) { + if (matchOperator("==")) { + left = new BinaryNode("==", left, parseComparison()); + continue; + } + if (matchOperator("!=")) { + left = new BinaryNode("!=", left, parseComparison()); + continue; + } + break; + } + return left; + } + + private ExpressionNode parseComparison() { + ExpressionNode left = parseAddSub(); + while (true) { + if (matchOperator(">=")) { + left = new BinaryNode(">=", left, parseAddSub()); + continue; + } + if (matchOperator("<=")) { + left = new BinaryNode("<=", left, parseAddSub()); + continue; + } + if (matchOperator(">")) { + left = new BinaryNode(">", left, parseAddSub()); + continue; + } + if (matchOperator("<")) { + left = new BinaryNode("<", left, parseAddSub()); + continue; + } + break; + } + return left; + } + + private ExpressionNode parseAddSub() { + ExpressionNode left = parseMulDiv(); + while (true) { + if (matchOperator("+")) { + left = new BinaryNode("+", left, parseMulDiv()); + continue; + } + if (matchOperator("-")) { + left = new BinaryNode("-", left, parseMulDiv()); + continue; + } + break; + } + return left; + } + + private ExpressionNode parseMulDiv() { + ExpressionNode left = parseUnary(); + while (true) { + if (matchOperator("*")) { + left = new BinaryNode("*", left, parseUnary()); + continue; + } + if (matchOperator("/")) { + left = new BinaryNode("/", left, parseUnary()); + continue; + } + break; + } + return left; + } + + private ExpressionNode parseUnary() { + if (matchOperator("+")) { + return new UnaryNode("+", parseUnary()); + } + if (matchOperator("-")) { + return new UnaryNode("-", parseUnary()); + } + if (matchOperator("!")) { + return new UnaryNode("!", parseUnary()); + } + return parsePrimary(); + } + + private ExpressionNode parsePrimary() { + ExprToken token = peek(); + if (match(ExprTokenType.NUMBER)) { + return new NumberNode(token.number); + } + if (match(ExprTokenType.STRING)) { + return new StringNode(token.stringValue); + } + if (match(ExprTokenType.IDENTIFIER)) { + String identifier = token.text; + if (match(ExprTokenType.LEFT_PAREN)) { + return parseFunction(identifier); + } + return new VariableNode(identifier.toUpperCase(Locale.ROOT)); + } + if (match(ExprTokenType.LEFT_PAREN)) { + ExpressionNode nested = parseExpression(); + expect(ExprTokenType.RIGHT_PAREN, "括号不匹配,缺少右括号"); + return nested; + } + throw new IllegalArgumentException("表达式语法错误,当前位置: " + token.text); + } + + private ExpressionNode parseFunction(String identifier) { + if ("IF".equalsIgnoreCase(identifier)) { + ExpressionNode condition = parseExpression(); + expect(ExprTokenType.COMMA, "IF函数参数格式错误,缺少第1个逗号"); + ExpressionNode trueNode = parseExpression(); + expect(ExprTokenType.COMMA, "IF函数参数格式错误,缺少第2个逗号"); + ExpressionNode falseNode = parseExpression(); + expect(ExprTokenType.RIGHT_PAREN, "IF函数缺少右括号"); + return new TernaryNode(condition, trueNode, falseNode); + } + if ("DAY_DIFF".equalsIgnoreCase(identifier) + || "MONTH_DIFF".equalsIgnoreCase(identifier) + || "HOUR_DIFF".equalsIgnoreCase(identifier)) { + List arguments = parseFunctionArguments(identifier); + if (arguments.size() != 1) { + throw new IllegalArgumentException(identifier.toUpperCase(Locale.ROOT) + "函数参数数量错误,需1个参数"); + } + return new FunctionNode(identifier, arguments); + } + throw new IllegalArgumentException("不支持的函数: " + identifier); + } + + private List parseFunctionArguments(String functionName) { + List arguments = new ArrayList<>(); + if (match(ExprTokenType.RIGHT_PAREN)) { + return arguments; + } + while (true) { + arguments.add(parseExpression()); + if (match(ExprTokenType.COMMA)) { + continue; + } + expect(ExprTokenType.RIGHT_PAREN, functionName + "函数缺少右括号"); + return arguments; + } + } + + private ExprToken peek() { + if (index >= tokens.size()) { + return ExprToken.symbol(ExprTokenType.EOF, ""); + } + return tokens.get(index); + } + + private boolean isEnd() { + return peek().type == ExprTokenType.EOF; + } + + private boolean match(ExprTokenType type) { + if (peek().type != type) { + return false; + } + index++; + return true; + } + + private boolean matchOperator(String operator) { + ExprToken token = peek(); + if (token.type != ExprTokenType.OPERATOR || !operator.equals(token.text)) { + return false; + } + index++; + return true; + } + + private void expect(ExprTokenType type, String message) { + if (!match(type)) { + throw new IllegalArgumentException(message); + } + } + } + + private static List tokenizeExpression(String expression) { + if (StringUtils.isBlank(expression)) { + throw new IllegalArgumentException("计算表达式为空"); + } + List tokens = new ArrayList<>(); + int index = 0; + while (index < expression.length()) { + char ch = expression.charAt(index); + if (Character.isWhitespace(ch)) { + index++; + continue; + } + if (Character.isDigit(ch) || ch == '.') { + int start = index; + boolean hasDot = ch == '.'; + index++; + while (index < expression.length()) { + char next = expression.charAt(index); + if (Character.isDigit(next)) { + index++; + continue; + } + if (next == '.' && !hasDot) { + hasDot = true; + index++; + continue; + } + break; + } + String text = expression.substring(start, index); + try { + tokens.add(ExprToken.number(new BigDecimal(text))); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("数值格式错误: " + text); + } + continue; + } + if (Character.isLetter(ch) || ch == '_') { + int start = index; + index++; + while (index < expression.length()) { + char next = expression.charAt(index); + if (Character.isLetterOrDigit(next) || next == '_') { + index++; + continue; + } + break; + } + String text = expression.substring(start, index).trim().toUpperCase(Locale.ROOT); + tokens.add(ExprToken.identifier(text)); + continue; + } + if (ch == '\'' || ch == '"') { + char quote = ch; + index++; + StringBuilder sb = new StringBuilder(); + boolean escaped = false; + while (index < expression.length()) { + char current = expression.charAt(index++); + if (escaped) { + switch (current) { + case 'n': + sb.append('\n'); + break; + case 't': + sb.append('\t'); + break; + case 'r': + sb.append('\r'); + break; + case '\\': + sb.append('\\'); + break; + case '\'': + sb.append('\''); + break; + case '"': + sb.append('"'); + break; + default: + sb.append(current); + break; + } + escaped = false; + continue; + } + if (current == '\\') { + escaped = true; + continue; + } + if (current == quote) { + break; + } + sb.append(current); + } + if (escaped || index > expression.length() || expression.charAt(index - 1) != quote) { + throw new IllegalArgumentException("字符串字面量未闭合"); + } + tokens.add(ExprToken.string(sb.toString())); + continue; + } + if (ch == '(') { + tokens.add(ExprToken.symbol(ExprTokenType.LEFT_PAREN, "(")); + index++; + continue; + } + if (ch == ')') { + tokens.add(ExprToken.symbol(ExprTokenType.RIGHT_PAREN, ")")); + index++; + continue; + } + if (ch == ',') { + tokens.add(ExprToken.symbol(ExprTokenType.COMMA, ",")); + index++; + continue; + } + if (ch == '?') { + tokens.add(ExprToken.symbol(ExprTokenType.QUESTION, "?")); + index++; + continue; + } + if (ch == ':') { + tokens.add(ExprToken.symbol(ExprTokenType.COLON, ":")); + index++; + continue; + } + if (index + 1 < expression.length()) { + String twoChars = expression.substring(index, index + 2); + if ("&&".equals(twoChars) + || "||".equals(twoChars) + || ">=".equals(twoChars) + || "<=".equals(twoChars) + || "==".equals(twoChars) + || "!=".equals(twoChars)) { + tokens.add(ExprToken.operator(twoChars)); + index += 2; + continue; + } + } + if (ch == '+' || ch == '-' || ch == '*' || ch == '/' + || ch == '>' || ch == '<' || ch == '!') { + tokens.add(ExprToken.operator(String.valueOf(ch))); + index++; + continue; + } + throw new IllegalArgumentException("表达式包含非法字符: " + ch); + } + tokens.add(ExprToken.symbol(ExprTokenType.EOF, "")); + return tokens; + } + private List parseCsv(MultipartFile file, String siteId) { List result = new ArrayList<>(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) { @@ -573,8 +1752,15 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { pointConfig.setIsAlarm(getInteger(valueList, headerIndex, "is_alarm", lineNo)); pointConfig.setPointType(getString(valueList, headerIndex, "point_type")); pointConfig.setCalcExpression(getString(valueList, headerIndex, "calc_expression")); + pointConfig.setCollectEnabled(getInteger(valueList, headerIndex, "collect_enabled", lineNo)); + pointConfig.setCollectSource(getString(valueList, headerIndex, "collect_source")); + pointConfig.setModbusRegisterType(getString(valueList, headerIndex, "modbus_register_type")); + pointConfig.setModbusDataType(getString(valueList, headerIndex, "modbus_data_type")); + pointConfig.setModbusReadOrder(getInteger(valueList, headerIndex, "modbus_read_order", lineNo)); + pointConfig.setModbusGroup(getString(valueList, headerIndex, "modbus_group")); pointConfig.setRemark(getString(valueList, headerIndex, "remark")); normalizeAndValidatePointConfig(pointConfig); + applyCollectDefaultsForInsert(pointConfig); result.add(pointConfig); } } catch (IOException e) { @@ -717,6 +1903,24 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { case "calcexpression": case "计算表达式": return "calc_expression"; + case "collectenabled": + case "是否启用采集": + return "collect_enabled"; + case "collectsource": + case "采集来源": + return "collect_source"; + case "modbusregistertype": + case "modbus寄存器类型": + return "modbus_register_type"; + case "modbusdatatype": + case "modbus数据类型": + return "modbus_data_type"; + case "modbusreadorder": + case "modbus读取顺序": + return "modbus_read_order"; + case "modbusgroup": + case "modbus分组": + return "modbus_group"; case "remark": case "备注": return "remark"; @@ -764,6 +1968,9 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { } pointConfig.setDeviceCategory(StringUtils.trimToNull(pointConfig.getDeviceCategory())); pointConfig.setDeviceId(StringUtils.trimToNull(pointConfig.getDeviceId())); + pointConfig.setPointName(StringUtils.trimToNull(pointConfig.getPointName())); + pointConfig.setDataKey(StringUtils.trimToNull(pointConfig.getDataKey())); + pointConfig.setPointDesc(StringUtils.trimToNull(pointConfig.getPointDesc())); pointConfig.setPointType(normalizePointType(pointConfig.getPointType())); pointConfig.setCalcExpression(StringUtils.trimToNull(pointConfig.getCalcExpression())); if ("calc".equals(pointConfig.getPointType())) { @@ -771,13 +1978,28 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { throw new ServiceException("计算点必须填写计算表达式"); } if (!CALC_EXPRESSION_PATTERN.matcher(pointConfig.getCalcExpression()).matches()) { - throw new ServiceException("计算表达式仅支持数字、字母、下划线、空格和四则运算符"); + throw new ServiceException("计算表达式仅支持数字、字母、下划线、空格、运算符和函数语法"); } } else { pointConfig.setCalcExpression(null); } } + private void applyCollectDefaultsForInsert(EmsPointConfig pointConfig) { + if (pointConfig == null) { + return; + } + if (pointConfig.getCollectEnabled() == null) { + pointConfig.setCollectEnabled(0); + } + if (StringUtils.isBlank(pointConfig.getCollectSource())) { + pointConfig.setCollectSource("MQTT"); + } + if (pointConfig.getModbusReadOrder() == null) { + pointConfig.setModbusReadOrder(0); + } + } + private String normalizePointType(String pointType) { String normalized = StringUtils.trimToEmpty(pointType).toLowerCase(Locale.ROOT); if ("calc".equals(normalized) || "calculate".equals(normalized) || "计算点".equals(normalized)) { diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStatsReportServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStatsReportServiceImpl.java index f9d36d6..d730df3 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStatsReportServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStatsReportServiceImpl.java @@ -401,13 +401,8 @@ public class EmsStatsReportServiceImpl implements IEmsStatsReportService ammeterRevenue.setActiveTotalPrice(activePeakPrice.add(activeHighPrice).add(activeFlatPrice).add(activeValleyPrice)); ammeterRevenue.setReActiveTotalPrice(reActivePeakPrice.add(reActiveHighPrice).add(reActiveFlatPrice).add(reActiveValleyPrice)); - // 实际收益=放电价格(尖峰平谷)-充电价格(尖峰平谷) - ammeterRevenue.setActualRevenue( - reActivePeakPrice.subtract(activePeakPrice) - .add(reActiveHighPrice.subtract(activeHighPrice)) - .add(reActiveFlatPrice.subtract(activeFlatPrice)) - .add(reActiveValleyPrice.subtract(activeValleyPrice)) - ); + // 实际收益按“放电总价-充电总价”口径计算 + ammeterRevenue.setActualRevenue(ammeterRevenue.getReActiveTotalPrice().subtract(ammeterRevenue.getActiveTotalPrice())); }); int weatherMissingCount = 0; diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/SingleSiteServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/SingleSiteServiceImpl.java index 2d8a8f1..a7e9b63 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/SingleSiteServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/SingleSiteServiceImpl.java @@ -12,6 +12,7 @@ import com.xzzn.ems.domain.EmsBatteryData; import com.xzzn.ems.domain.EmsBatteryStack; import com.xzzn.ems.domain.EmsCoolingData; import com.xzzn.ems.domain.EmsDhData; +import com.xzzn.ems.domain.EmsDailyChargeData; import com.xzzn.ems.domain.EmsDevicesSetting; import com.xzzn.ems.domain.EmsEmsData; import com.xzzn.ems.domain.EmsPcsBranchData; @@ -215,6 +216,60 @@ public class SingleSiteServiceImpl implements ISingleSiteService { return siteMonitorHomeVo; } + @Override + public SiteMonitorHomeVo getSiteMonitorTotalDataVo(String siteId) { + SiteMonitorHomeVo siteMonitorHomeVo = new SiteMonitorHomeVo(); + if (StringUtils.isEmpty(siteId)) { + return siteMonitorHomeVo; + } + + String nowDate = DateUtils.getDate(); + Map chargeDataMap = emsDailyChargeDataMapper.getAllSiteChargeData(nowDate, siteId); + BigDecimal totalChargedCap = BigDecimal.ZERO; + BigDecimal totalDischargedCap = BigDecimal.ZERO; + if (chargeDataMap != null) { + totalChargedCap = chargeDataMap.get("totalChargedCap") == null ? BigDecimal.ZERO : chargeDataMap.get("totalChargedCap"); + totalDischargedCap = chargeDataMap.get("totalDischargedCap") == null ? BigDecimal.ZERO : chargeDataMap.get("totalDischargedCap"); + } + siteMonitorHomeVo.setTotalChargedCap(totalChargedCap); + siteMonitorHomeVo.setTotalDischargedCap(totalDischargedCap); + + Map revenueMap = emsDailyEnergyDataMapper.getRealTimeRevenue(siteId); + siteMonitorHomeVo.setTotalRevenue(revenueMap == null || revenueMap.get("totalRevenue") == null ? BigDecimal.ZERO : revenueMap.get("totalRevenue")); + siteMonitorHomeVo.setDayRevenue(revenueMap == null || revenueMap.get("dayRevenue") == null ? BigDecimal.ZERO : revenueMap.get("dayRevenue")); + + Date yesterdayDate = DateUtils.toDate(LocalDate.now().minusDays(1)); + EmsDailyChargeData yesterdayChargeData = emsDailyChargeDataMapper.selectBySiteIdAndDateTime(siteId, yesterdayDate); + siteMonitorHomeVo.setYesterdayRevenue(yesterdayChargeData == null || yesterdayChargeData.getDayRevenue() == null + ? BigDecimal.ZERO + : yesterdayChargeData.getDayRevenue()); + + List alarmList = emsAlarmRecordsMapper.getAlarmRecordsBySiteId(siteId); + siteMonitorHomeVo.setSiteMonitorHomeAlarmVo(alarmList); + + LocalDate sevenDaysAgo = LocalDate.now().minusDays(6); + Date startDate = DateUtils.toDate(sevenDaysAgo); + Date endDate = new Date(); + List siteMonitorDataVoList = emsDailyChargeDataMapper.getSingleSiteChargeData(siteId, startDate, endDate); + if (!CollectionUtils.isEmpty(siteMonitorDataVoList)) { + for (SiteMonitorDataVo sitePcsData : siteMonitorDataVoList) { + String pcsDate = sitePcsData.getAmmeterDate(); + if (DateUtils.checkIsToday(pcsDate)) { + siteMonitorHomeVo.setDayChargedCap(sitePcsData.getChargedCap()); + siteMonitorHomeVo.setDayDisChargedCap(sitePcsData.getDisChargedCap()); + } + if (DateUtils.getYesterdayDayString().equals(pcsDate)) { + siteMonitorHomeVo.setYesterdayChargedCap(sitePcsData.getChargedCap()); + siteMonitorHomeVo.setYesterdayDisChargedCap(sitePcsData.getDisChargedCap()); + } + } + } + + siteMonitorHomeVo.setEnergyStorageAvailElec(totalDischargedCap.subtract(totalChargedCap)); + siteMonitorHomeVo.setSiteMonitorDataVo(siteMonitorDataVoList); + return siteMonitorHomeVo; + } + // 获取单站监控实时运行头部数据 @Override public SiteMonitorRunningHeadInfoVo getSiteRunningHeadInfo(String siteId) { diff --git a/ems-system/src/main/resources/mapper/ems/EmsDailyChageDataMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsDailyChageDataMapper.xml index 4b1fb36..da078cd 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsDailyChageDataMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsDailyChageDataMapper.xml @@ -7,12 +7,13 @@ - + + @@ -21,20 +22,22 @@ - select id, site_id, device_id, date_time, total_charge_Data, total_discharge_Data, charge_data, discharge_data, create_by, create_time, update_by, update_time, remark from ems_daily_charge_data + select id, site_id, date_time, total_charge_Data, total_discharge_Data, charge_data, discharge_data, total_revenue, day_revenue, create_by, create_time, update_by, update_time, remark from ems_daily_charge_data + + insert into ems_daily_charge_data site_id, - device_id, date_time, total_charge_Data, total_discharge_Data, charge_data, discharge_data, + total_revenue, + day_revenue, create_by, create_time, update_by, @@ -60,12 +72,13 @@ #{siteId}, - #{deviceId}, #{dateTime}, #{totalChargeData}, #{totalDischargeData}, #{chargeData}, #{dischargeData}, + #{totalRevenue}, + #{dayRevenue}, #{createBy}, #{createTime}, #{updateBy}, @@ -78,12 +91,13 @@ update ems_daily_charge_data site_id = #{siteId}, - device_id = #{deviceId}, date_time = #{dateTime}, total_charge_Data = #{totalChargeData}, total_discharge_Data = #{totalDischargeData}, charge_data = #{chargeData}, discharge_data = #{dischargeData}, + total_revenue = #{totalRevenue}, + day_revenue = #{dayRevenue}, create_by = #{createBy}, create_time = #{createTime}, update_by = #{updateBy}, @@ -108,26 +122,28 @@ INSERT into ems_daily_charge_data ( id, site_id, - device_id, date_time, total_charge_Data, total_discharge_Data, charge_data, discharge_data, + total_revenue, + day_revenue, create_by, create_time, update_by, update_time, remark - ) values ( + ) values ( #{id}, #{siteId}, - #{deviceId}, #{dateTime}, #{totalChargeData}, #{totalDischargeData}, #{chargeData}, #{dischargeData}, + #{totalRevenue}, + #{dayRevenue}, #{createBy}, #{createTime}, #{updateBy}, @@ -139,9 +155,21 @@ total_discharge_Data = #{totalDischargeData}, charge_data = #{chargeData}, discharge_data = #{dischargeData}, + total_revenue = IFNULL(#{totalRevenue}, total_revenue), + day_revenue = IFNULL(#{dayRevenue}, day_revenue), update_time = NOW() + + update ems_daily_charge_data + set total_revenue = #{totalRevenue}, + day_revenue = #{dayRevenue}, + update_by = #{updateBy}, + update_time = NOW() + where site_id = #{siteId} + and date(date_time) = date(#{dateTime}) + + - \ No newline at end of file + diff --git a/ems-system/src/main/resources/mapper/ems/EmsDailyEnergyDataMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsDailyEnergyDataMapper.xml index fe40a83..dbcadb5 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsDailyEnergyDataMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsDailyEnergyDataMapper.xml @@ -8,8 +8,7 @@ - - + @@ -27,7 +26,11 @@ - select id, site_id, data_date, total_revenue, day_revenue, peak_charge_diff, peak_discharge_diff, high_charge_diff, high_discharge_diff, flat_charge_diff, flat_discharge_diff, valley_charge_diff, valley_discharge_diff, calc_time, create_by, create_time, update_by, update_time, remark from ems_daily_energy_data + select id, site_id, data_date, data_hour, + peak_charge_diff, peak_discharge_diff, high_charge_diff, high_discharge_diff, + flat_charge_diff, flat_discharge_diff, valley_charge_diff, valley_discharge_diff, + calc_time, create_by, create_time, update_by, update_time, remark + from ems_daily_energy_data + + INSERT INTO ems_daily_energy_data ( id, site_id, data_date, - total_revenue, - day_revenue, + data_hour, peak_charge_diff, peak_discharge_diff, high_charge_diff, @@ -164,8 +172,7 @@ #{id}, #{siteId}, #{dataDate}, - #{totalRevenue}, - #{dayRevenue}, + #{dataHour}, #{peakChargeDiff}, #{peakDischargeDiff}, #{highChargeDiff}, @@ -181,8 +188,7 @@ NOW(), #{remark} ) ON DUPLICATE KEY UPDATE - total_revenue = #{totalRevenue}, - day_revenue = #{dayRevenue}, + data_hour = #{dataHour}, peak_charge_diff = #{peakChargeDiff}, peak_discharge_diff = #{peakDischargeDiff}, high_charge_diff = #{highChargeDiff}, @@ -196,60 +202,116 @@ diff --git a/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml index b4e3bd6..f3d1d68 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml @@ -52,7 +52,7 @@ and site_id = #{siteId} - and point_id = #{pointId} + and point_id like concat('%', #{pointId}, '%') and device_category = #{deviceCategory} and device_id = #{deviceId} and data_key like concat('%', #{dataKey}, '%') @@ -202,6 +202,36 @@ where id = #{id} + + update ems_point_config + set point_id = #{pointId}, + site_id = #{siteId}, + device_category = #{deviceCategory}, + device_id = #{deviceId}, + point_name = #{pointName}, + data_key = #{dataKey}, + point_desc = #{pointDesc}, + register_address = #{registerAddress}, + data_unit = #{dataUnit}, + data_a = #{dataA}, + data_k = #{dataK}, + data_b = #{dataB}, + data_bit = #{dataBit}, + is_alarm = #{isAlarm}, + point_type = #{pointType}, + calc_expression = #{calcExpression}, + collect_enabled = #{collectEnabled}, + collect_source = #{collectSource}, + modbus_register_type = #{modbusRegisterType}, + modbus_data_type = #{modbusDataType}, + modbus_read_order = #{modbusReadOrder}, + modbus_group = #{modbusGroup}, + update_by = #{updateBy}, + update_time = now(), + remark = #{remark} + where id = #{id} + + delete from ems_point_config where id = #{id} @@ -329,11 +359,11 @@ where collect_enabled = 1 and collect_source = 'MODBUS' - and (point_type is null or point_type <> 'calc') + and (point_type is null or point_type <> 'calc') and (is_alarm is null or is_alarm = 0) - and register_address is not null and register_address <> '' - and modbus_register_type is not null and modbus_register_type <> '' - and modbus_data_type is not null and modbus_data_type <> '' + and register_address is not null and register_address <> '' + and modbus_register_type is not null and modbus_register_type <> '' + and modbus_data_type is not null and modbus_data_type <> '' and site_id = #{siteId} diff --git a/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml index daac50a..e01124b 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml @@ -4,6 +4,14 @@ "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> + + -- 2.49.0 From 515f4402982819544c9b12f9cad61fdcf4962d58 Mon Sep 17 00:00:00 2001 From: dashixiong Date: Mon, 23 Mar 2026 13:47:20 +0800 Subject: [PATCH 10/10] =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ems/EmsDailyChargeDataController.java | 61 +++ .../ems/EmsDailyEnergyDataController.java | 61 +++ .../com/xzzn/quartz/task/CalcPointTask.java | 35 ++ .../xzzn/quartz/task/DailyChargeDataTask.java | 35 ++ .../vo/PointConfigGenerateRecentRequest.java | 38 ++ .../service/IEmsDailyChargeDataService.java | 21 + .../service/IEmsDailyEnergyDataService.java | 21 + .../impl/DeviceDataProcessServiceImpl.java | 387 ++++++++++++------ .../impl/EmsDailyChargeDataServiceImpl.java | 49 +++ .../impl/EmsDailyEnergyDataServiceImpl.java | 49 +++ 10 files changed, 639 insertions(+), 118 deletions(-) create mode 100644 ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsDailyChargeDataController.java create mode 100644 ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsDailyEnergyDataController.java create mode 100644 ems-quartz/src/main/java/com/xzzn/quartz/task/CalcPointTask.java create mode 100644 ems-quartz/src/main/java/com/xzzn/quartz/task/DailyChargeDataTask.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigGenerateRecentRequest.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/service/IEmsDailyChargeDataService.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/service/IEmsDailyEnergyDataService.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDailyChargeDataServiceImpl.java create mode 100644 ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDailyEnergyDataServiceImpl.java diff --git a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsDailyChargeDataController.java b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsDailyChargeDataController.java new file mode 100644 index 0000000..ee53904 --- /dev/null +++ b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsDailyChargeDataController.java @@ -0,0 +1,61 @@ +package com.xzzn.web.controller.ems; + +import com.xzzn.common.annotation.Log; +import com.xzzn.common.core.controller.BaseController; +import com.xzzn.common.core.domain.AjaxResult; +import com.xzzn.common.core.page.TableDataInfo; +import com.xzzn.common.enums.BusinessType; +import com.xzzn.ems.domain.EmsDailyChargeData; +import com.xzzn.ems.service.IEmsDailyChargeDataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 站点管理-充放电修正 Controller + */ +@RestController +@RequestMapping("/ems/dailyChargeData") +public class EmsDailyChargeDataController extends BaseController { + + @Autowired + private IEmsDailyChargeDataService emsDailyChargeDataService; + + @GetMapping("/list") + public TableDataInfo list(EmsDailyChargeData emsDailyChargeData) { + startPage(); + List list = emsDailyChargeDataService.selectEmsDailyChargeDataList(emsDailyChargeData); + return getDataTable(list); + } + + @GetMapping("/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) { + return success(emsDailyChargeDataService.selectEmsDailyChargeDataById(id)); + } + + @Log(title = "站点管理-充放电修正", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody EmsDailyChargeData emsDailyChargeData) { + return toAjax(emsDailyChargeDataService.insertEmsDailyChargeData(emsDailyChargeData, getUsername())); + } + + @Log(title = "站点管理-充放电修正", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody EmsDailyChargeData emsDailyChargeData) { + return toAjax(emsDailyChargeDataService.updateEmsDailyChargeData(emsDailyChargeData, getUsername())); + } + + @Log(title = "站点管理-充放电修正", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) { + return toAjax(emsDailyChargeDataService.deleteEmsDailyChargeDataByIds(ids)); + } +} diff --git a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsDailyEnergyDataController.java b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsDailyEnergyDataController.java new file mode 100644 index 0000000..2294304 --- /dev/null +++ b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsDailyEnergyDataController.java @@ -0,0 +1,61 @@ +package com.xzzn.web.controller.ems; + +import com.xzzn.common.annotation.Log; +import com.xzzn.common.core.controller.BaseController; +import com.xzzn.common.core.domain.AjaxResult; +import com.xzzn.common.core.page.TableDataInfo; +import com.xzzn.common.enums.BusinessType; +import com.xzzn.ems.domain.EmsDailyEnergyData; +import com.xzzn.ems.service.IEmsDailyEnergyDataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 站点管理-数据修正 Controller + */ +@RestController +@RequestMapping("/ems/dailyEnergyData") +public class EmsDailyEnergyDataController extends BaseController { + + @Autowired + private IEmsDailyEnergyDataService emsDailyEnergyDataService; + + @GetMapping("/list") + public TableDataInfo list(EmsDailyEnergyData emsDailyEnergyData) { + startPage(); + List list = emsDailyEnergyDataService.selectEmsDailyEnergyDataList(emsDailyEnergyData); + return getDataTable(list); + } + + @GetMapping("/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) { + return success(emsDailyEnergyDataService.selectEmsDailyEnergyDataById(id)); + } + + @Log(title = "站点管理-数据修正", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody EmsDailyEnergyData emsDailyEnergyData) { + return toAjax(emsDailyEnergyDataService.insertEmsDailyEnergyData(emsDailyEnergyData, getUsername())); + } + + @Log(title = "站点管理-数据修正", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody EmsDailyEnergyData emsDailyEnergyData) { + return toAjax(emsDailyEnergyDataService.updateEmsDailyEnergyData(emsDailyEnergyData, getUsername())); + } + + @Log(title = "站点管理-数据修正", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) { + return toAjax(emsDailyEnergyDataService.deleteEmsDailyEnergyDataByIds(ids)); + } +} diff --git a/ems-quartz/src/main/java/com/xzzn/quartz/task/CalcPointTask.java b/ems-quartz/src/main/java/com/xzzn/quartz/task/CalcPointTask.java new file mode 100644 index 0000000..fb2f177 --- /dev/null +++ b/ems-quartz/src/main/java/com/xzzn/quartz/task/CalcPointTask.java @@ -0,0 +1,35 @@ +package com.xzzn.quartz.task; + +import com.xzzn.common.utils.StringUtils; +import com.xzzn.ems.service.impl.DeviceDataProcessServiceImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component("calcPointTask") +public class CalcPointTask { + + private static final Logger log = LoggerFactory.getLogger(CalcPointTask.class); + + @Autowired + private DeviceDataProcessServiceImpl deviceDataProcessService; + + public void syncAllSites() { + log.info("开始执行计算点轮询任务-全站点"); + deviceDataProcessService.syncCalcPointDataFromInfluxByQuartz(); + log.info("结束执行计算点轮询任务-全站点"); + } + + public void syncBySite(String siteId) { + if (StringUtils.isBlank(siteId)) { + log.warn("计算点轮询任务参数为空,转为全站点执行"); + syncAllSites(); + return; + } + String normalizedSiteId = siteId.trim(); + log.info("开始执行计算点轮询任务-单站点, siteId: {}", normalizedSiteId); + deviceDataProcessService.syncCalcPointDataFromInfluxByQuartz(normalizedSiteId); + log.info("结束执行计算点轮询任务-单站点, siteId: {}", normalizedSiteId); + } +} diff --git a/ems-quartz/src/main/java/com/xzzn/quartz/task/DailyChargeDataTask.java b/ems-quartz/src/main/java/com/xzzn/quartz/task/DailyChargeDataTask.java new file mode 100644 index 0000000..4837538 --- /dev/null +++ b/ems-quartz/src/main/java/com/xzzn/quartz/task/DailyChargeDataTask.java @@ -0,0 +1,35 @@ +package com.xzzn.quartz.task; + +import com.xzzn.common.utils.StringUtils; +import com.xzzn.ems.service.impl.DeviceDataProcessServiceImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component("dailyChargeDataTask") +public class DailyChargeDataTask { + + private static final Logger log = LoggerFactory.getLogger(DailyChargeDataTask.class); + + @Autowired + private DeviceDataProcessServiceImpl deviceDataProcessService; + + public void syncAllSites() { + log.info("开始执行每日充放电数据轮询任务-全站点"); + deviceDataProcessService.syncDailyChargeDataFromInfluxByQuartz(); + log.info("结束执行每日充放电数据轮询任务-全站点"); + } + + public void syncBySite(String siteId) { + if (StringUtils.isBlank(siteId)) { + log.warn("每日充放电数据轮询任务参数为空,转为全站点执行"); + syncAllSites(); + return; + } + String normalizedSiteId = siteId.trim(); + log.info("开始执行每日充放电数据轮询任务-单站点, siteId: {}", normalizedSiteId); + deviceDataProcessService.syncDailyChargeDataFromInfluxByQuartz(normalizedSiteId); + log.info("结束执行每日充放电数据轮询任务-单站点, siteId: {}", normalizedSiteId); + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigGenerateRecentRequest.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigGenerateRecentRequest.java new file mode 100644 index 0000000..1bf20d1 --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigGenerateRecentRequest.java @@ -0,0 +1,38 @@ +package com.xzzn.ems.domain.vo; + +import javax.validation.constraints.NotBlank; + +public class PointConfigGenerateRecentRequest { + + @NotBlank(message = "站点ID不能为空") + private String siteId; + + @NotBlank(message = "点位ID不能为空") + private String pointId; + + private String deviceId; + + public String getSiteId() { + return siteId; + } + + public void setSiteId(String siteId) { + this.siteId = siteId; + } + + public String getPointId() { + return pointId; + } + + public void setPointId(String pointId) { + this.pointId = pointId; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/service/IEmsDailyChargeDataService.java b/ems-system/src/main/java/com/xzzn/ems/service/IEmsDailyChargeDataService.java new file mode 100644 index 0000000..4c7199f --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/service/IEmsDailyChargeDataService.java @@ -0,0 +1,21 @@ +package com.xzzn.ems.service; + +import com.xzzn.ems.domain.EmsDailyChargeData; + +import java.util.List; + +/** + * 站点每日充放电数据Service接口 + */ +public interface IEmsDailyChargeDataService { + + List selectEmsDailyChargeDataList(EmsDailyChargeData emsDailyChargeData); + + EmsDailyChargeData selectEmsDailyChargeDataById(Long id); + + int insertEmsDailyChargeData(EmsDailyChargeData emsDailyChargeData, String operName); + + int updateEmsDailyChargeData(EmsDailyChargeData emsDailyChargeData, String operName); + + int deleteEmsDailyChargeDataByIds(Long[] ids); +} diff --git a/ems-system/src/main/java/com/xzzn/ems/service/IEmsDailyEnergyDataService.java b/ems-system/src/main/java/com/xzzn/ems/service/IEmsDailyEnergyDataService.java new file mode 100644 index 0000000..7b78ded --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/service/IEmsDailyEnergyDataService.java @@ -0,0 +1,21 @@ +package com.xzzn.ems.service; + +import com.xzzn.ems.domain.EmsDailyEnergyData; + +import java.util.List; + +/** + * 站点每日收益数据Service接口 + */ +public interface IEmsDailyEnergyDataService { + + List selectEmsDailyEnergyDataList(EmsDailyEnergyData emsDailyEnergyData); + + EmsDailyEnergyData selectEmsDailyEnergyDataById(Long id); + + int insertEmsDailyEnergyData(EmsDailyEnergyData emsDailyEnergyData, String operName); + + int updateEmsDailyEnergyData(EmsDailyEnergyData emsDailyEnergyData, String operName); + + int deleteEmsDailyEnergyDataByIds(Long[] ids); +} diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java index d41a2fa..00e346d 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java @@ -67,6 +67,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.YearMonth; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.ArrayDeque; @@ -131,6 +132,18 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i private static final String AUTO_HOUR_DIFF_CONTEXT_PREFIX = "__AUTO_HOUR_DIFF__"; private static final String MONITOR_FIELD_PREFIX_HOME = "home__"; private static final String MONITOR_FIELD_PREFIX_TJBB_DBBB = "tjbb_dbbb__"; + private static final String FIELD_CODE_TOTAL_CHARGE = "home__totalChargedCap"; + private static final String FIELD_CODE_TOTAL_DISCHARGE = "home__totalDischargedCap"; + private static final String FIELD_CODE_DAY_CHARGE = "home__dayChargedCap"; + private static final String FIELD_CODE_DAY_DISCHARGE = "home__dayDisChargedCap"; + private static final String FIELD_CODE_PEAK_CHARGE = "tjbb_dbbb__activePeakKwh"; + private static final String FIELD_CODE_PEAK_DISCHARGE = "tjbb_dbbb__reActivePeakKwh"; + private static final String FIELD_CODE_HIGH_CHARGE = "tjbb_dbbb__activeHighKwh"; + private static final String FIELD_CODE_HIGH_DISCHARGE = "tjbb_dbbb__reActiveHighKwh"; + private static final String FIELD_CODE_FLAT_CHARGE = "tjbb_dbbb__activeFlatKwh"; + private static final String FIELD_CODE_FLAT_DISCHARGE = "tjbb_dbbb__reActiveFlatKwh"; + private static final String FIELD_CODE_VALLEY_CHARGE = "tjbb_dbbb__activeValleyKwh"; + private static final String FIELD_CODE_VALLEY_DISCHARGE = "tjbb_dbbb__reActiveValleyKwh"; private static final String[] FIELD_SUFFIX_TOTAL_CHARGE = { "activeTotalKwh", "totalChargeData", "totalCharge", "totalChargedCap", "chargedCap" }; @@ -401,8 +414,8 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i dayChargePointId, dayDischargeMatch == null ? "null" : StringUtils.defaultString(dayDischargeMatch.getFieldCode()), dayDischargePointId); - BigDecimal nowTotalCharge = getPointValueById(normalizedPointValueMap, totalChargePointId); - BigDecimal nowTotalDischarge = getPointValueById(normalizedPointValueMap, totalDischargePointId); + BigDecimal nowTotalCharge = getPointValueByIdRaw(normalizedPointValueMap, totalChargePointId); + BigDecimal nowTotalDischarge = getPointValueByIdRaw(normalizedPointValueMap, totalDischargePointId); BigDecimal nowDayCharge = getLatestPointValueFromRedis(dayChargePointId); BigDecimal nowDayDischarge = getLatestPointValueFromRedis(dayDischargePointId); if (nowTotalCharge == null || nowTotalDischarge == null) { @@ -476,75 +489,109 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i Date queryEndTime = DateUtils.getNowDate(); Date queryStartTime = new Date(queryEndTime.getTime() - DAILY_CHARGE_INFLUX_QUERY_WINDOW_MS); - EmsSiteMonitorPointMatch totalChargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( - mappingList, FIELD_SUFFIX_TOTAL_CHARGE - ); - EmsSiteMonitorPointMatch totalDischargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( - mappingList, FIELD_SUFFIX_TOTAL_DISCHARGE - ); - String totalChargePointId = totalChargeMatch == null ? null : StringUtils.trimToNull(totalChargeMatch.getDataPoint()); - String totalDischargePointId = totalDischargeMatch == null ? null : StringUtils.trimToNull(totalDischargeMatch.getDataPoint()); - log.info("Quartz同步日充放电自检, siteId: {}, totalCharge: {}->{} , totalDischarge: {}->{}", + String totalChargePointId = resolvePointIdByFieldCodeIgnoreDevice(mappingList, FIELD_CODE_TOTAL_CHARGE); + String totalDischargePointId = resolvePointIdByFieldCodeIgnoreDevice(mappingList, FIELD_CODE_TOTAL_DISCHARGE); + String dayChargePointId = resolvePointIdByFieldCodeIgnoreDevice(mappingList, FIELD_CODE_DAY_CHARGE); + String dayDischargePointId = resolvePointIdByFieldCodeIgnoreDevice(mappingList, FIELD_CODE_DAY_DISCHARGE); + if (StringUtils.isBlank(dayChargePointId)) { + dayChargePointId = firstNonBlankPointByFieldSuffixIgnoreDevice(mappingList, FIELD_SUFFIX_DAY_CHARGE); + } + if (StringUtils.isBlank(dayDischargePointId)) { + dayDischargePointId = firstNonBlankPointByFieldSuffixIgnoreDevice(mappingList, FIELD_SUFFIX_DAY_DISCHARGE); + } + log.info("Quartz同步日充放电日累自检, siteId: {}, dayChargePoint: {}, dayDischargePoint: {}", + siteId, + dayChargePointId, + dayDischargePointId); + String peakChargePointId = resolvePointIdByFieldCodeIgnoreDevice(mappingList, FIELD_CODE_PEAK_CHARGE); + String peakDischargePointId = resolvePointIdByFieldCodeIgnoreDevice(mappingList, FIELD_CODE_PEAK_DISCHARGE); + String highChargePointId = resolvePointIdByFieldCodeIgnoreDevice(mappingList, FIELD_CODE_HIGH_CHARGE); + String highDischargePointId = resolvePointIdByFieldCodeIgnoreDevice(mappingList, FIELD_CODE_HIGH_DISCHARGE); + String flatChargePointId = resolvePointIdByFieldCodeIgnoreDevice(mappingList, FIELD_CODE_FLAT_CHARGE); + String flatDischargePointId = resolvePointIdByFieldCodeIgnoreDevice(mappingList, FIELD_CODE_FLAT_DISCHARGE); + String valleyChargePointId = resolvePointIdByFieldCodeIgnoreDevice(mappingList, FIELD_CODE_VALLEY_CHARGE); + String valleyDischargePointId = resolvePointIdByFieldCodeIgnoreDevice(mappingList, FIELD_CODE_VALLEY_DISCHARGE); + + log.info("Quartz同步日充放电自检, siteId: {}, totalCharge: {} , totalDischarge: {}, dayCharge: {}, dayDischarge: {}, peak: {} / {}, high: {} / {}, flat: {} / {}, valley: {} / {}", siteId, - totalChargeMatch == null ? "null" : StringUtils.defaultString(totalChargeMatch.getFieldCode()), totalChargePointId, - totalDischargeMatch == null ? "null" : StringUtils.defaultString(totalDischargeMatch.getFieldCode()), - totalDischargePointId); - if (StringUtils.isBlank(totalChargePointId) && StringUtils.isBlank(totalDischargePointId)) { - log.info("Quartz同步日充放电跳过,未找到总充/总放点位, siteId: {}", siteId); - return; - } + totalDischargePointId, + dayChargePointId, + dayDischargePointId, + peakChargePointId, + peakDischargePointId, + highChargePointId, + highDischargePointId, + flatChargePointId, + flatDischargePointId, + valleyChargePointId, + valleyDischargePointId); - Map pointIdValueMap = new HashMap<>(); - Date dataUpdateTime = null; - if (StringUtils.isNotBlank(totalChargePointId)) { - InfluxPointDataWriter.PointValue totalChargePoint = influxPointDataWriter.queryLatestPointValueByPointKey( - siteId, totalChargePointId, queryStartTime, queryEndTime - ); - if (totalChargePoint != null && totalChargePoint.getPointValue() != null) { - pointIdValueMap.put(totalChargePointId.trim().toUpperCase(), totalChargePoint.getPointValue()); - dataUpdateTime = totalChargePoint.getDataTime(); - } else { - log.info("Quartz同步日充放电未获取到总充点最新值, siteId: {}, pointId: {}, range: {} ~ {}", - siteId, totalChargePointId, - DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, queryStartTime), - DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, queryEndTime)); + Map latestPointValues = new HashMap<>(); + Date latestDataTime = null; + for (String pointId : Arrays.asList( + totalChargePointId, totalDischargePointId, + dayChargePointId, dayDischargePointId, + peakChargePointId, peakDischargePointId, + highChargePointId, highDischargePointId, + flatChargePointId, flatDischargePointId, + valleyChargePointId, valleyDischargePointId)) { + if (StringUtils.isBlank(pointId)) { + continue; } - } - if (StringUtils.isNotBlank(totalDischargePointId)) { - InfluxPointDataWriter.PointValue totalDischargePoint = influxPointDataWriter.queryLatestPointValueByPointKey( - siteId, totalDischargePointId, queryStartTime, queryEndTime - ); - if (totalDischargePoint != null && totalDischargePoint.getPointValue() != null) { - pointIdValueMap.put(totalDischargePointId.trim().toUpperCase(), totalDischargePoint.getPointValue()); - if (dataUpdateTime == null - || (totalDischargePoint.getDataTime() != null - && totalDischargePoint.getDataTime().after(dataUpdateTime))) { - dataUpdateTime = totalDischargePoint.getDataTime(); - } - } else { - log.info("Quartz同步日充放电未获取到总放点最新值, siteId: {}, pointId: {}, range: {} ~ {}", - siteId, totalDischargePointId, - DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, queryStartTime), - DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, queryEndTime)); + LatestPointValue latestPointValue = getLatestPointValuePreferRedis(siteId, pointId, queryStartTime, queryEndTime); + if (latestPointValue == null || latestPointValue.getValue() == null) { + continue; + } + latestPointValues.put(pointId.trim().toUpperCase(), latestPointValue); + if (latestPointValue.getDataTime() != null + && (latestDataTime == null || latestPointValue.getDataTime().after(latestDataTime))) { + latestDataTime = latestPointValue.getDataTime(); } } - if (org.apache.commons.collections4.MapUtils.isEmpty(pointIdValueMap)) { + if (latestPointValues.isEmpty()) { log.info("Quartz同步日充放电跳过,点位最新值为空, siteId: {}", siteId); return; } - if (dataUpdateTime == null) { - log.info("Quartz同步日充放电点位值存在但时间为空, siteId: {}, pointIds: {}", - siteId, pointIdValueMap.keySet()); + + Date recordDateTime = latestDataTime == null ? DateUtils.getNowDate() : latestDataTime; + BigDecimal totalChargeValue = getPointValueById(latestPointValues, totalChargePointId); + BigDecimal totalDischargeValue = getPointValueById(latestPointValues, totalDischargePointId); + BigDecimal dayChargeValue = getPointValueById(latestPointValues, dayChargePointId); + BigDecimal dayDischargeValue = getPointValueById(latestPointValues, dayDischargePointId); + log.info("单站点位值汇总, siteId: {}, totalCharge: {}({}), totalDischarge: {}({}), dayCharge: {}({}), dayDischarge: {}({}), dataTime: {}", + siteId, + totalChargeValue, + totalDischargeValue, + dayChargeValue, + dayDischargeValue, + pointValueSource(latestPointValues, totalChargePointId), + pointValueSource(latestPointValues, totalDischargePointId), + pointValueSource(latestPointValues, dayChargePointId), + pointValueSource(latestPointValues, dayDischargePointId), + DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, recordDateTime)); + if (totalChargeValue == null && totalDischargeValue == null) { + log.info("Quartz同步日充放电跳过,总充/总放值为空, siteId: {}", siteId); } else { - log.info("Quartz同步日充放电准备落库, siteId: {}, dataUpdateTime: {}, pointIds: {}", - siteId, DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, dataUpdateTime), - pointIdValueMap.keySet()); + upsertDailyChargeData(siteId, recordDateTime, totalChargeValue, totalDischargeValue, dayChargeValue, dayDischargeValue); } - updateDailyChargeDataByQuartz(siteId, pointIdValueMap, dataUpdateTime); - updateDailyEnergyHourlyDataByQuartz(siteId, dataUpdateTime); - syncDailyChargeRevenueByQuartz(siteId, dataUpdateTime); + + updateDailyEnergyHourlyDataByQuartz( + siteId, + recordDateTime, + peakChargePointId, + peakDischargePointId, + highChargePointId, + highDischargePointId, + flatChargePointId, + flatDischargePointId, + valleyChargePointId, + valleyDischargePointId, + queryStartTime, + queryEndTime + ); + syncDailyChargeRevenueByQuartz(siteId, recordDateTime); } private void syncDailyChargeRevenueByQuartz(String siteId, Date dataUpdateTime) { @@ -576,7 +623,18 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i siteId, day, dayRevenue, totalRevenue); } - private void updateDailyEnergyHourlyDataByQuartz(String siteId, Date dataUpdateTime) { + private void updateDailyEnergyHourlyDataByQuartz(String siteId, + Date dataUpdateTime, + String peakChargePointId, + String peakDischargePointId, + String highChargePointId, + String highDischargePointId, + String flatChargePointId, + String flatDischargePointId, + String valleyChargePointId, + String valleyDischargePointId, + Date queryStartTime, + Date queryEndTime) { log.info("Quartz日电量小时落库入口, siteId: {}, dataTime: {}", siteId, dataUpdateTime == null ? "null" : DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, dataUpdateTime)); @@ -584,15 +642,12 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return; } - List mappingList = getPointMatchesBySiteId(siteId); - if (CollectionUtils.isEmpty(mappingList)) { - return; - } - Date recordDateTime = dataUpdateTime == null ? DateUtils.getNowDate() : dataUpdateTime; Integer dataHour = resolveHourOfDay(recordDateTime); - String priceKey = RedisKeyConstants.ENERGY_PRICE_TIME + siteId + "_" + LocalDate.now().getYear() + LocalDate.now().getMonthValue(); + LocalDateTime recordLocalTime = LocalDateTime.ofInstant(recordDateTime.toInstant(), ZoneId.systemDefault()); + String priceKey = RedisKeyConstants.ENERGY_PRICE_TIME + siteId + "_" + recordLocalTime.getYear() + + recordLocalTime.getMonthValue(); EnergyPriceVo priceVo = redisCache.getCacheObject(priceKey); if (priceVo == null) { priceVo = emsEnergyPriceConfigService.getCurrentMonthPrice(siteId); @@ -619,55 +674,15 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return; } - EmsSiteMonitorPointMatch peakChargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( - mappingList, FIELD_SUFFIX_PEAK_CHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB - ); - EmsSiteMonitorPointMatch peakDischargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( - mappingList, FIELD_SUFFIX_PEAK_DISCHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB - ); - EmsSiteMonitorPointMatch highChargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( - mappingList, FIELD_SUFFIX_HIGH_CHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB - ); - EmsSiteMonitorPointMatch highDischargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( - mappingList, FIELD_SUFFIX_HIGH_DISCHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB - ); - EmsSiteMonitorPointMatch flatChargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( - mappingList, FIELD_SUFFIX_FLAT_CHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB - ); - EmsSiteMonitorPointMatch flatDischargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( - mappingList, FIELD_SUFFIX_FLAT_DISCHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB - ); - EmsSiteMonitorPointMatch valleyChargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( - mappingList, FIELD_SUFFIX_VALLEY_CHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB - ); - EmsSiteMonitorPointMatch valleyDischargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( - mappingList, FIELD_SUFFIX_VALLEY_DISCHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB - ); - String peakChargePointId = peakChargeMatch == null ? null : StringUtils.trimToNull(peakChargeMatch.getDataPoint()); - String peakDischargePointId = peakDischargeMatch == null ? null : StringUtils.trimToNull(peakDischargeMatch.getDataPoint()); - String highChargePointId = highChargeMatch == null ? null : StringUtils.trimToNull(highChargeMatch.getDataPoint()); - String highDischargePointId = highDischargeMatch == null ? null : StringUtils.trimToNull(highDischargeMatch.getDataPoint()); - String flatChargePointId = flatChargeMatch == null ? null : StringUtils.trimToNull(flatChargeMatch.getDataPoint()); - String flatDischargePointId = flatDischargeMatch == null ? null : StringUtils.trimToNull(flatDischargeMatch.getDataPoint()); - String valleyChargePointId = valleyChargeMatch == null ? null : StringUtils.trimToNull(valleyChargeMatch.getDataPoint()); - String valleyDischargePointId = valleyDischargeMatch == null ? null : StringUtils.trimToNull(valleyDischargeMatch.getDataPoint()); - log.info("Quartz日电量小时自检, siteId: {}, peak: {}->{} / {}->{} , high: {}->{} / {}->{} , flat: {}->{} / {}->{} , valley: {}->{} / {}->{}", + log.info("Quartz日电量小时自检, siteId: {}, peak: {} / {} , high: {} / {} , flat: {} / {} , valley: {} / {}", siteId, - peakChargeMatch == null ? "null" : StringUtils.defaultString(peakChargeMatch.getFieldCode()), peakChargePointId, - peakDischargeMatch == null ? "null" : StringUtils.defaultString(peakDischargeMatch.getFieldCode()), peakDischargePointId, - highChargeMatch == null ? "null" : StringUtils.defaultString(highChargeMatch.getFieldCode()), highChargePointId, - highDischargeMatch == null ? "null" : StringUtils.defaultString(highDischargeMatch.getFieldCode()), highDischargePointId, - flatChargeMatch == null ? "null" : StringUtils.defaultString(flatChargeMatch.getFieldCode()), flatChargePointId, - flatDischargeMatch == null ? "null" : StringUtils.defaultString(flatDischargeMatch.getFieldCode()), flatDischargePointId, - valleyChargeMatch == null ? "null" : StringUtils.defaultString(valleyChargeMatch.getFieldCode()), valleyChargePointId, - valleyDischargeMatch == null ? "null" : StringUtils.defaultString(valleyDischargeMatch.getFieldCode()), valleyDischargePointId); String currentChargePointId; String currentDischargePointId; @@ -677,26 +692,26 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i case "peak": currentChargePointId = peakChargePointId; currentDischargePointId = peakDischargePointId; - currentCharge = getLatestPointValueFromRedis(peakChargePointId); - currentDischarge = getLatestPointValueFromRedis(peakDischargePointId); + currentCharge = getLatestPointValuePreferRedisValue(siteId, peakChargePointId, queryStartTime, queryEndTime); + currentDischarge = getLatestPointValuePreferRedisValue(siteId, peakDischargePointId, queryStartTime, queryEndTime); break; case "high": currentChargePointId = highChargePointId; currentDischargePointId = highDischargePointId; - currentCharge = getLatestPointValueFromRedis(highChargePointId); - currentDischarge = getLatestPointValueFromRedis(highDischargePointId); + currentCharge = getLatestPointValuePreferRedisValue(siteId, highChargePointId, queryStartTime, queryEndTime); + currentDischarge = getLatestPointValuePreferRedisValue(siteId, highDischargePointId, queryStartTime, queryEndTime); break; case "flat": currentChargePointId = flatChargePointId; currentDischargePointId = flatDischargePointId; - currentCharge = getLatestPointValueFromRedis(flatChargePointId); - currentDischarge = getLatestPointValueFromRedis(flatDischargePointId); + currentCharge = getLatestPointValuePreferRedisValue(siteId, flatChargePointId, queryStartTime, queryEndTime); + currentDischarge = getLatestPointValuePreferRedisValue(siteId, flatDischargePointId, queryStartTime, queryEndTime); break; case "valley": currentChargePointId = valleyChargePointId; currentDischargePointId = valleyDischargePointId; - currentCharge = getLatestPointValueFromRedis(valleyChargePointId); - currentDischarge = getLatestPointValueFromRedis(valleyDischargePointId); + currentCharge = getLatestPointValuePreferRedisValue(siteId, valleyChargePointId, queryStartTime, queryEndTime); + currentDischarge = getLatestPointValuePreferRedisValue(siteId, valleyDischargePointId, queryStartTime, queryEndTime); break; default: log.info("Quartz日电量小时落库跳过,未知电价时段类型, siteId: {}, costType: {}", siteId, costType); @@ -751,9 +766,17 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return false; } try { + String normalizedStartTime = startTime.trim(); + String normalizedEndTime = endTime.trim(); LocalTime current = LocalTime.parse(DateUtils.parseDateToStr("HH:mm", dataUpdateTime)); - LocalTime start = LocalTime.parse(startTime); - LocalTime end = LocalTime.parse(endTime); + LocalTime start = LocalTime.parse(normalizedStartTime); + + // 兼容配置中的 24:00,将其视为次日 00:00。 + if ("24:00".equals(normalizedEndTime) || "24:00:00".equals(normalizedEndTime)) { + return !current.isBefore(start); + } + + LocalTime end = LocalTime.parse(normalizedEndTime); if (start.equals(end)) { return true; } @@ -997,18 +1020,38 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return parsed == null ? new LinkedHashMap<>() : new LinkedHashMap<>(parsed); } - private BigDecimal getPointValueById(Map pointIdValueMap, String pointId) { + private BigDecimal getPointValueById(Map pointIdValueMap, String pointId) { + if (pointIdValueMap == null || pointIdValueMap.isEmpty() || StringUtils.isBlank(pointId)) { + return null; + } + LatestPointValue latestPointValue = pointIdValueMap.get(pointId.trim().toUpperCase()); + return latestPointValue == null ? null : latestPointValue.getValue(); + } + + private BigDecimal getPointValueByIdRaw(Map pointIdValueMap, String pointId) { if (pointIdValueMap == null || pointIdValueMap.isEmpty() || StringUtils.isBlank(pointId)) { return null; } return pointIdValueMap.get(pointId.trim().toUpperCase()); } + private String pointValueSource(Map pointIdValueMap, String pointId) { + if (pointIdValueMap == null || pointIdValueMap.isEmpty() || StringUtils.isBlank(pointId)) { + return "-"; + } + LatestPointValue latestPointValue = pointIdValueMap.get(pointId.trim().toUpperCase()); + if (latestPointValue == null) { + return "-"; + } + return latestPointValue.isFromRedis() ? "Redis" : "Influx"; + } + private BigDecimal getLatestPointValueFromRedis(String pointId) { String normalizedPointId = StringUtils.trimToNull(pointId); if (normalizedPointId == null) { return null; } + log.info("点位值自检-Redis读取, pointId: {}", normalizedPointId); List candidates = new ArrayList<>(); candidates.add(normalizedPointId); String upperPointId = normalizedPointId.toUpperCase(); @@ -1037,6 +1080,7 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i pointValue = readBigDecimalField(raw, "pointValue"); } if (pointValue != null) { + log.info("点位值自检-Redis命中, pointId: {}, redisKey: {}", normalizedPointId, candidate); return pointValue; } } catch (Exception ex) { @@ -1046,6 +1090,110 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return null; } + private LatestPointValue getLatestPointValuePreferRedis(String siteId, String pointId, Date queryStartTime, Date queryEndTime) { + if (StringUtils.isBlank(siteId) || StringUtils.isBlank(pointId)) { + return null; + } + BigDecimal redisValue = getLatestPointValueFromRedis(pointId); + if (redisValue != null) { + return new LatestPointValue(redisValue, null, true); + } + log.info("点位值自检-Influx读取, siteId: {}, pointId: {}, range: {} ~ {}", + siteId, pointId, + DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, queryStartTime), + DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, queryEndTime)); + InfluxPointDataWriter.PointValue influxPointValue = influxPointDataWriter.queryLatestPointValueByPointKey( + siteId, pointId, queryStartTime, queryEndTime + ); + if (influxPointValue == null || influxPointValue.getPointValue() == null) { + return null; + } + return new LatestPointValue(influxPointValue.getPointValue(), influxPointValue.getDataTime(), false); + } + + private BigDecimal getLatestPointValuePreferRedisValue(String siteId, String pointId, Date queryStartTime, Date queryEndTime) { + LatestPointValue value = getLatestPointValuePreferRedis(siteId, pointId, queryStartTime, queryEndTime); + return value == null ? null : value.getValue(); + } + + private String resolvePointIdByFieldCodeIgnoreDevice(List mappingList, String fieldCode) { + if (CollectionUtils.isEmpty(mappingList) || StringUtils.isBlank(fieldCode)) { + return null; + } + String normalized = fieldCode.trim().toLowerCase(); + for (EmsSiteMonitorPointMatch mapping : mappingList) { + if (mapping == null || StringUtils.isBlank(mapping.getFieldCode())) { + continue; + } + String dataPoint = StringUtils.defaultString(mapping.getDataPoint()).trim(); + if (StringUtils.isBlank(dataPoint) || DELETED_FIELD_MARK.equalsIgnoreCase(dataPoint)) { + continue; + } + if (mapping.getFieldCode().trim().toLowerCase().equals(normalized)) { + return dataPoint; + } + } + return null; + } + + private void upsertDailyChargeData(String siteId, + Date recordDateTime, + BigDecimal totalChargeValue, + BigDecimal totalDischargeValue, + BigDecimal dayChargeValue, + BigDecimal dayDischargeValue) { + if (StringUtils.isBlank(siteId)) { + return; + } + Date persistTime = recordDateTime == null ? DateUtils.getNowDate() : recordDateTime; + EmsDailyChargeData dailyChargeData = new EmsDailyChargeData(); + dailyChargeData.setSiteId(siteId); + dailyChargeData.setDateTime(persistTime); + dailyChargeData.setTotalChargeData(totalChargeValue); + dailyChargeData.setTotalDischargeData(totalDischargeValue); + dailyChargeData.setChargeData(dayChargeValue); + dailyChargeData.setDischargeData(dayDischargeValue); + + EmsDailyChargeData existedDailyData = emsDailyChargeDataMapper.selectBySiteIdAndDateTime(siteId, persistTime); + if (existedDailyData != null && existedDailyData.getId() != null) { + dailyChargeData.setId(existedDailyData.getId()); + dailyChargeData.setDateTime(existedDailyData.getDateTime()); + } + + Date now = DateUtils.getNowDate(); + dailyChargeData.setCreateBy("system"); + dailyChargeData.setCreateTime(now); + dailyChargeData.setUpdateBy("system"); + dailyChargeData.setUpdateTime(now); + emsDailyChargeDataMapper.insertOrUpdateData(dailyChargeData); + log.info("日充放电累计落库完成, siteId: {}, totalCharge: {}, totalDischarge: {}, dayCharge: {}, dayDischarge: {}", + siteId, totalChargeValue, totalDischargeValue, dayChargeValue, dayDischargeValue); + } + + private static class LatestPointValue { + private final BigDecimal value; + private final Date dataTime; + private final boolean fromRedis; + + LatestPointValue(BigDecimal value, Date dataTime, boolean fromRedis) { + this.value = value; + this.dataTime = dataTime; + this.fromRedis = fromRedis; + } + + public BigDecimal getValue() { + return value; + } + + public Date getDataTime() { + return dataTime; + } + + public boolean isFromRedis() { + return fromRedis; + } + } + private BigDecimal readBigDecimalField(Object source, String fieldName) { Object fieldValue = readFieldValue(source, fieldName); return StringUtils.getBigDecimal(fieldValue); @@ -1165,9 +1313,11 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i } String normalizedSiteId = siteId.trim(); String redisKey = RedisKeyConstants.SITE_MONITOR_POINT_MATCH + normalizedSiteId; + log.info("单站映射自检-读取, siteId: {}, redisKey: {}", normalizedSiteId, redisKey); Object cacheObj = redisCache.getCacheObject(redisKey); List cached = parsePointMatchCache(cacheObj); if (cached != null) { + log.info("单站映射自检-命中缓存, siteId: {}, size: {}", normalizedSiteId, cached.size()); return cached; } List latest = emsSiteMonitorPointMatchMapper.selectBySiteId(normalizedSiteId); @@ -1175,6 +1325,7 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i latest = Collections.emptyList(); } redisCache.setCacheObject(redisKey, latest, MONITOR_POINT_MATCH_REDIS_TTL_SECONDS, TimeUnit.SECONDS); + log.info("单站映射自检-落库后写缓存, siteId: {}, size: {}", normalizedSiteId, latest.size()); return latest; } diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDailyChargeDataServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDailyChargeDataServiceImpl.java new file mode 100644 index 0000000..18c683c --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDailyChargeDataServiceImpl.java @@ -0,0 +1,49 @@ +package com.xzzn.ems.service.impl; + +import com.xzzn.common.utils.DateUtils; +import com.xzzn.ems.domain.EmsDailyChargeData; +import com.xzzn.ems.mapper.EmsDailyChargeDataMapper; +import com.xzzn.ems.service.IEmsDailyChargeDataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 站点每日充放电数据Service实现 + */ +@Service +public class EmsDailyChargeDataServiceImpl implements IEmsDailyChargeDataService { + + @Autowired + private EmsDailyChargeDataMapper emsDailyChargeDataMapper; + + @Override + public List selectEmsDailyChargeDataList(EmsDailyChargeData emsDailyChargeData) { + return emsDailyChargeDataMapper.selectEmsDailyChargeDataList(emsDailyChargeData); + } + + @Override + public EmsDailyChargeData selectEmsDailyChargeDataById(Long id) { + return emsDailyChargeDataMapper.selectEmsDailyChargeDataById(id); + } + + @Override + public int insertEmsDailyChargeData(EmsDailyChargeData emsDailyChargeData, String operName) { + emsDailyChargeData.setCreateBy(operName); + emsDailyChargeData.setCreateTime(DateUtils.getNowDate()); + return emsDailyChargeDataMapper.insertEmsDailyChargeData(emsDailyChargeData); + } + + @Override + public int updateEmsDailyChargeData(EmsDailyChargeData emsDailyChargeData, String operName) { + emsDailyChargeData.setUpdateBy(operName); + emsDailyChargeData.setUpdateTime(DateUtils.getNowDate()); + return emsDailyChargeDataMapper.updateEmsDailyChargeData(emsDailyChargeData); + } + + @Override + public int deleteEmsDailyChargeDataByIds(Long[] ids) { + return emsDailyChargeDataMapper.deleteEmsDailyChargeDataByIds(ids); + } +} diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDailyEnergyDataServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDailyEnergyDataServiceImpl.java new file mode 100644 index 0000000..23a954a --- /dev/null +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDailyEnergyDataServiceImpl.java @@ -0,0 +1,49 @@ +package com.xzzn.ems.service.impl; + +import com.xzzn.common.utils.DateUtils; +import com.xzzn.ems.domain.EmsDailyEnergyData; +import com.xzzn.ems.mapper.EmsDailyEnergyDataMapper; +import com.xzzn.ems.service.IEmsDailyEnergyDataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 站点每日收益数据Service实现 + */ +@Service +public class EmsDailyEnergyDataServiceImpl implements IEmsDailyEnergyDataService { + + @Autowired + private EmsDailyEnergyDataMapper emsDailyEnergyDataMapper; + + @Override + public List selectEmsDailyEnergyDataList(EmsDailyEnergyData emsDailyEnergyData) { + return emsDailyEnergyDataMapper.selectEmsDailyEnergyDataList(emsDailyEnergyData); + } + + @Override + public EmsDailyEnergyData selectEmsDailyEnergyDataById(Long id) { + return emsDailyEnergyDataMapper.selectEmsDailyEnergyDataById(id); + } + + @Override + public int insertEmsDailyEnergyData(EmsDailyEnergyData emsDailyEnergyData, String operName) { + emsDailyEnergyData.setCreateBy(operName); + emsDailyEnergyData.setCreateTime(DateUtils.getNowDate()); + return emsDailyEnergyDataMapper.insertEmsDailyEnergyData(emsDailyEnergyData); + } + + @Override + public int updateEmsDailyEnergyData(EmsDailyEnergyData emsDailyEnergyData, String operName) { + emsDailyEnergyData.setUpdateBy(operName); + emsDailyEnergyData.setUpdateTime(DateUtils.getNowDate()); + return emsDailyEnergyDataMapper.updateEmsDailyEnergyData(emsDailyEnergyData); + } + + @Override + public int deleteEmsDailyEnergyDataByIds(Long[] ids) { + return emsDailyEnergyDataMapper.deleteEmsDailyEnergyDataByIds(ids); + } +} -- 2.49.0