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},