From e4cfd15cb453e4f65476dbf70ea18b4edaa29088 Mon Sep 17 00:00:00 2001 From: dashixiong Date: Wed, 18 Mar 2026 10:06:42 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ems/EmsPointConfigController.java | 7 + .../ems/EmsSiteMonitorController.java | 9 + .../ems/EmsStatisticalReportController.java | 22 + .../xzzn/ems/domain/EmsDailyChargeData.java | 45 +- .../xzzn/ems/domain/EmsDailyEnergyData.java | 32 +- .../vo/PointConfigLatestValueItemVo.java | 9 + .../domain/vo/PointConfigLatestValueVo.java | 9 + .../ems/mapper/EmsDailyChargeDataMapper.java | 11 + .../ems/mapper/EmsDailyEnergyDataMapper.java | 6 + .../xzzn/ems/mapper/EmsPointConfigMapper.java | 2 + .../EmsSiteMonitorPointMatchMapper.java | 2 + .../ems/service/IEmsPointConfigService.java | 3 + .../xzzn/ems/service/ISingleSiteService.java | 2 + .../ems/service/InfluxPointDataWriter.java | 19 +- .../impl/DeviceDataProcessServiceImpl.java | 1800 +++++++++++------ .../impl/EmsDeviceSettingServiceImpl.java | 153 +- .../impl/EmsEnergyPriceConfigServiceImpl.java | 22 +- .../impl/EmsPointCalcConfigServiceImpl.java | 4 +- .../impl/EmsPointConfigServiceImpl.java | 1350 ++++++++++++- .../impl/EmsStatsReportServiceImpl.java | 9 +- .../service/impl/SingleSiteServiceImpl.java | 55 + .../mapper/ems/EmsDailyChageDataMapper.xml | 48 +- .../mapper/ems/EmsDailyEnergyDataMapper.xml | 172 +- .../mapper/ems/EmsPointConfigMapper.xml | 40 +- .../ems/EmsSiteMonitorPointMatchMapper.xml | 8 + 25 files changed, 2948 insertions(+), 891 deletions(-) diff --git a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointConfigController.java b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointConfigController.java index f9d530e..71a2347 100644 --- a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointConfigController.java +++ b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsPointConfigController.java @@ -8,6 +8,7 @@ import com.xzzn.common.enums.BusinessType; import com.xzzn.ems.domain.EmsPointConfig; import com.xzzn.ems.domain.vo.ImportPointTemplateRequest; import com.xzzn.ems.domain.vo.PointConfigCurveRequest; +import com.xzzn.ems.domain.vo.PointConfigGenerateRecentRequest; import com.xzzn.ems.domain.vo.PointConfigLatestValueRequest; import com.xzzn.ems.service.IEmsPointConfigService; import org.springframework.beans.factory.annotation.Autowired; @@ -93,4 +94,10 @@ public class EmsPointConfigController extends BaseController { public AjaxResult curve(@RequestBody PointConfigCurveRequest request) { return success(pointConfigService.getCurveData(request)); } + + @Log(title = "点位配置", businessType = BusinessType.INSERT) + @PostMapping("/generateRecent7Days") + public AjaxResult generateRecent7Days(@Valid @RequestBody PointConfigGenerateRecentRequest request) { + return success(pointConfigService.generateRecent7DaysData(request)); + } } diff --git a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java index 1bd3fbe..079ad52 100644 --- a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java +++ b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsSiteMonitorController.java @@ -52,6 +52,15 @@ public class EmsSiteMonitorController extends BaseController{ return success(iSingleSiteService.getSiteMonitorDataVo(siteId)); } + /** + * 获取单站首页总累计运行数据(基于日表) + */ + @GetMapping("/homeTotalView") + public AjaxResult getSingleSiteHomeTotalView(@RequestParam String siteId) + { + return success(iSingleSiteService.getSiteMonitorTotalDataVo(siteId)); + } + /** * 单站监控-设备监控-实时运行头部数据 */ diff --git a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsStatisticalReportController.java b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsStatisticalReportController.java index d18840b..c82a3d3 100644 --- a/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsStatisticalReportController.java +++ b/ems-admin/src/main/java/com/xzzn/web/controller/ems/EmsStatisticalReportController.java @@ -134,6 +134,17 @@ public class EmsStatisticalReportController extends BaseController return getDataTable(dataList); } + /** + * 统计报表-电表报表(直接基于 ems_daily_energy_data) + */ + @GetMapping("/getAmmeterDataFromDaily") + public TableDataInfo getAmmeterDataFromDaily(StatisAmmeterDateRequest requestVo) + { + startPage(); + List dataList = ieEmsStatsReportService.getAmmeterDataResult(requestVo); + return getDataTable(dataList); + } + /** * 导出电表报表 */ @@ -145,6 +156,17 @@ public class EmsStatisticalReportController extends BaseController ieEmsStatsReportService.exportAmmeterData(response, requestVo); } + /** + * 导出电表报表(直接基于 ems_daily_energy_data) + */ + @PreAuthorize("@ss.hasPermi('system:ammeterData:export')") + @Log(title = "电表报表", businessType = BusinessType.EXPORT) + @PostMapping("/exportAmmeterDataFromDaily") + public void exportAmmeterDataFromDaily(HttpServletResponse response, StatisAmmeterDateRequest requestVo) + { + ieEmsStatsReportService.exportAmmeterData(response, requestVo); + } + /** * 概率统计-电表收益报表 */ diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/EmsDailyChargeData.java b/ems-system/src/main/java/com/xzzn/ems/domain/EmsDailyChargeData.java index 05e5898..bea8886 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/EmsDailyChargeData.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/EmsDailyChargeData.java @@ -25,10 +25,6 @@ public class EmsDailyChargeData extends BaseEntity @Excel(name = "站点id") private String siteId; - /** 设备唯一标识符 */ - @Excel(name = "设备唯一标识符") - private String deviceId; - /** 数据日期:yyyy-MM-dd */ @JsonFormat(pattern = "yyyy-MM-dd") @Excel(name = "数据日期:yyyy-MM-dd", width = 30, dateFormat = "yyyy-MM-dd") @@ -50,6 +46,14 @@ public class EmsDailyChargeData extends BaseEntity @Excel(name = "当日放电量") private BigDecimal dischargeData; + /** 总收入 */ + @Excel(name = "总收入") + private BigDecimal totalRevenue; + + /** 当日实时收入 */ + @Excel(name = "当日实时收入") + private BigDecimal dayRevenue; + public void setId(Long id) { this.id = id; @@ -70,16 +74,6 @@ public class EmsDailyChargeData extends BaseEntity return siteId; } - public void setDeviceId(String deviceId) - { - this.deviceId = deviceId; - } - - public String getDeviceId() - { - return deviceId; - } - public void setDateTime(Date dateTime) { this.dateTime = dateTime; @@ -130,17 +124,38 @@ public class EmsDailyChargeData extends BaseEntity return dischargeData; } + public void setTotalRevenue(BigDecimal totalRevenue) + { + this.totalRevenue = totalRevenue; + } + + public BigDecimal getTotalRevenue() + { + return totalRevenue; + } + + public void setDayRevenue(BigDecimal dayRevenue) + { + this.dayRevenue = dayRevenue; + } + + public BigDecimal getDayRevenue() + { + return dayRevenue; + } + @Override public String toString() { return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) .append("id", getId()) .append("siteId", getSiteId()) - .append("deviceId", getDeviceId()) .append("dateTime", getDateTime()) .append("totalChargeData", getTotalChargeData()) .append("totalDischargeData", getTotalDischargeData()) .append("chargeData", getChargeData()) .append("dischargeData", getDischargeData()) + .append("totalRevenue", getTotalRevenue()) + .append("dayRevenue", getDayRevenue()) .append("createBy", getCreateBy()) .append("createTime", getCreateTime()) .append("updateBy", getUpdateBy()) diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/EmsDailyEnergyData.java b/ems-system/src/main/java/com/xzzn/ems/domain/EmsDailyEnergyData.java index 91f1630..cf3d897 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/EmsDailyEnergyData.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/EmsDailyEnergyData.java @@ -30,13 +30,9 @@ public class EmsDailyEnergyData extends BaseEntity @Excel(name = "数据日期:yyyy-MM-dd", width = 30, dateFormat = "yyyy-MM-dd") private Date dataDate; - /** 总收入 */ - @Excel(name = "总收入") - private BigDecimal totalRevenue; - - /** 当日实时收入 */ - @Excel(name = "当日实时收入") - private BigDecimal dayRevenue; + /** 数据小时(0-23) */ + @Excel(name = "数据小时(0-23)") + private Integer dataHour; /** 尖峰时段充电差值 */ @Excel(name = "尖峰时段充电差值") @@ -71,6 +67,7 @@ public class EmsDailyEnergyData extends BaseEntity private BigDecimal valleyDischargeDiff; /** 差值计算时间(如2025-10-10 23:59:00) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @Excel(name = "差值计算时间", readConverterExp = "如=2025-10-10,2=3:59:00") private Date calcTime; @@ -104,24 +101,14 @@ public class EmsDailyEnergyData extends BaseEntity return dataDate; } - public void setTotalRevenue(BigDecimal totalRevenue) + public void setDataHour(Integer dataHour) { - this.totalRevenue = totalRevenue; + this.dataHour = dataHour; } - public BigDecimal getTotalRevenue() + public Integer getDataHour() { - return totalRevenue; - } - - public void setDayRevenue(BigDecimal dayRevenue) - { - this.dayRevenue = dayRevenue; - } - - public BigDecimal getDayRevenue() - { - return dayRevenue; + return dataHour; } public void setPeakChargeDiff(BigDecimal peakChargeDiff) @@ -220,8 +207,7 @@ public class EmsDailyEnergyData extends BaseEntity .append("id", getId()) .append("siteId", getSiteId()) .append("dataDate", getDataDate()) - .append("totalRevenue", getTotalRevenue()) - .append("dayRevenue", getDayRevenue()) + .append("dataHour", getDataHour()) .append("peakChargeDiff", getPeakChargeDiff()) .append("peakDischargeDiff", getPeakDischargeDiff()) .append("highChargeDiff", getHighChargeDiff()) diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueItemVo.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueItemVo.java index 4f8016d..af5fded 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueItemVo.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueItemVo.java @@ -2,6 +2,7 @@ package com.xzzn.ems.domain.vo; public class PointConfigLatestValueItemVo { private String siteId; + private String pointId; private String deviceId; private String dataKey; @@ -13,6 +14,14 @@ public class PointConfigLatestValueItemVo { this.siteId = siteId; } + public String getPointId() { + return pointId; + } + + public void setPointId(String pointId) { + this.pointId = pointId; + } + public String getDeviceId() { return deviceId; } diff --git a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueVo.java b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueVo.java index 76831c2..af322c3 100644 --- a/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueVo.java +++ b/ems-system/src/main/java/com/xzzn/ems/domain/vo/PointConfigLatestValueVo.java @@ -4,6 +4,7 @@ import java.util.Date; public class PointConfigLatestValueVo { private String siteId; + private String pointId; private String deviceId; private String dataKey; private Object pointValue; @@ -17,6 +18,14 @@ public class PointConfigLatestValueVo { this.siteId = siteId; } + public String getPointId() { + return pointId; + } + + public void setPointId(String pointId) { + this.pointId = pointId; + } + public String getDeviceId() { return deviceId; } diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsDailyChargeDataMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsDailyChargeDataMapper.java index 27c3864..44372b2 100644 --- a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsDailyChargeDataMapper.java +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsDailyChargeDataMapper.java @@ -69,6 +69,17 @@ public interface EmsDailyChargeDataMapper // 插入或更新站点每日充放电数据 public void insertOrUpdateData(EmsDailyChargeData emsDailyChargeData); + // 按站点+日期(天)查询当日已存在记录 + public EmsDailyChargeData selectBySiteIdAndDateTime(@Param("siteId") String siteId, + @Param("dateTime") Date dateTime); + + // 按站点+日期(天)更新收入字段 + public int updateRevenueBySiteAndDate(@Param("siteId") String siteId, + @Param("dateTime") Date dateTime, + @Param("totalRevenue") BigDecimal totalRevenue, + @Param("dayRevenue") BigDecimal dayRevenue, + @Param("updateBy") String updateBy); + // 获取所有站点总充总放 public Map getAllSiteChargeData(@Param("nowData")String nowData, @Param("siteId")String siteId); diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsDailyEnergyDataMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsDailyEnergyDataMapper.java index 536f73d..ff7c2eb 100644 --- a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsDailyEnergyDataMapper.java +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsDailyEnergyDataMapper.java @@ -68,6 +68,12 @@ public interface EmsDailyEnergyDataMapper // 获取站点某日电表数据 public EmsDailyEnergyData getDataByDate(@Param("siteId")String siteId,@Param("today") String today); + + // 按站点+日期+小时查询当小时已存在记录 + public EmsDailyEnergyData selectBySiteIdAndDateHour(@Param("siteId") String siteId, + @Param("dataDate") java.util.Date dataDate, + @Param("dataHour") Integer dataHour); + // 插入或更新每日尖峰平谷差值 public void insertOrUpdateData(EmsDailyEnergyData energyData); // 电表报表 diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java index 324e5ed..2eb97d5 100644 --- a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsPointConfigMapper.java @@ -17,6 +17,8 @@ public interface EmsPointConfigMapper { int updateEmsPointConfig(EmsPointConfig emsPointConfig); + int updateEmsPointConfigForImport(EmsPointConfig emsPointConfig); + int deleteEmsPointConfigById(Long id); int deleteEmsPointConfigByIds(Long[] ids); diff --git a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorPointMatchMapper.java b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorPointMatchMapper.java index 47c5a94..b9fa099 100644 --- a/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorPointMatchMapper.java +++ b/ems-system/src/main/java/com/xzzn/ems/mapper/EmsSiteMonitorPointMatchMapper.java @@ -9,6 +9,8 @@ import java.util.List; * 单站监控字段点位映射 Mapper */ public interface EmsSiteMonitorPointMatchMapper { + List selectDistinctSiteIds(); + List selectBySiteId(@Param("siteId") String siteId); int deleteBySiteId(@Param("siteId") String siteId); diff --git a/ems-system/src/main/java/com/xzzn/ems/service/IEmsPointConfigService.java b/ems-system/src/main/java/com/xzzn/ems/service/IEmsPointConfigService.java index 04abb2a..4cad426 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/IEmsPointConfigService.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/IEmsPointConfigService.java @@ -4,6 +4,7 @@ import com.xzzn.ems.domain.EmsPointConfig; import com.xzzn.ems.domain.vo.ImportPointTemplateRequest; import com.xzzn.ems.domain.vo.PointConfigCurveRequest; import com.xzzn.ems.domain.vo.PointConfigCurveValueVo; +import com.xzzn.ems.domain.vo.PointConfigGenerateRecentRequest; import com.xzzn.ems.domain.vo.PointConfigLatestValueRequest; import com.xzzn.ems.domain.vo.PointConfigLatestValueVo; import org.springframework.web.multipart.MultipartFile; @@ -30,4 +31,6 @@ public interface IEmsPointConfigService { List getLatestValues(PointConfigLatestValueRequest request); List getCurveData(PointConfigCurveRequest request); + + String generateRecent7DaysData(PointConfigGenerateRecentRequest request); } diff --git a/ems-system/src/main/java/com/xzzn/ems/service/ISingleSiteService.java b/ems-system/src/main/java/com/xzzn/ems/service/ISingleSiteService.java index 926d1b7..1ea2971 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/ISingleSiteService.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/ISingleSiteService.java @@ -13,6 +13,8 @@ public interface ISingleSiteService public SiteMonitorHomeVo getSiteMonitorDataVo(String siteId); + public SiteMonitorHomeVo getSiteMonitorTotalDataVo(String siteId); + public SiteMonitorRunningHeadInfoVo getSiteRunningHeadInfo(String siteId); diff --git a/ems-system/src/main/java/com/xzzn/ems/service/InfluxPointDataWriter.java b/ems-system/src/main/java/com/xzzn/ems/service/InfluxPointDataWriter.java index 747c37a..4aed2c3 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/InfluxPointDataWriter.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/InfluxPointDataWriter.java @@ -204,24 +204,7 @@ public class InfluxPointDataWriter { try { String queryUrl = buildQueryUrl(influxQl); - List values = parseInfluxQlResponse(executeRequestWithResponse(methodOrDefault(readMethod, "GET"), queryUrl)); - if (!values.isEmpty()) { - return values; - } - - // 兼容 pointId 大小写差异 - String regexQuery = String.format( - "SELECT \"value\" FROM \"%s\" WHERE \"site_id\" = '%s' AND \"point_key\" =~ /(?i)^%s$/ " + - "AND time >= %dms AND time <= %dms ORDER BY time ASC", - measurement, - escapeTagValue(normalizedSiteId), - escapeRegex(normalizedPointKey), - startTime.getTime(), - endTime.getTime() - ); - return parseInfluxQlResponse( - executeRequestWithResponse(methodOrDefault(readMethod, "GET"), buildQueryUrl(regexQuery)) - ); + return parseInfluxQlResponse(executeRequestWithResponse(methodOrDefault(readMethod, "GET"), queryUrl)); } catch (Exception e) { log.warn("按 pointKey 查询 InfluxDB 曲线失败: {}", e.getMessage()); return Collections.emptyList(); diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java index a5c2b19..d41a2fa 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/DeviceDataProcessServiceImpl.java @@ -5,7 +5,6 @@ import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONException; import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.TypeReference; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.xzzn.common.constant.Constants; @@ -28,6 +27,7 @@ import com.xzzn.common.utils.DateUtils; import com.xzzn.common.utils.MapUtils; import com.xzzn.common.utils.StringUtils; import com.xzzn.ems.domain.*; +import com.xzzn.ems.domain.vo.AmmeterRevenueStatisListVo; import com.xzzn.ems.domain.vo.EnergyPriceTimeRange; import com.xzzn.ems.domain.vo.EnergyPriceVo; import com.xzzn.ems.enums.DeviceMatchTable; @@ -65,6 +65,8 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.YearMonth; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.ArrayDeque; @@ -111,7 +113,10 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i private static final Pattern DTDC_PATTERN = Pattern.compile("DTDC(\\d+)([A-Za-z]*)"); private static final Pattern DB_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$"); private static final Pattern VARIABLE_SAFE_PATTERN = Pattern.compile("[^A-Za-z0-9_]"); - private static final Pattern SIMPLE_CALC_EXPRESSION_PATTERN = Pattern.compile("^[0-9A-Za-z_+\\-*/().\\s]+$"); + private static final Pattern SIMPLE_CALC_EXPRESSION_PATTERN = Pattern.compile("^[0-9A-Za-z_+\\-*/().,?:<>=!&|\\s]+$"); + private static final Pattern AUTO_DAY_DIFF_FUNCTION_PATTERN = Pattern.compile("(?i)DAY_DIFF\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\)"); + private static final Pattern AUTO_MONTH_DIFF_FUNCTION_PATTERN = Pattern.compile("(?i)MONTH_DIFF\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\)"); + private static final Pattern AUTO_HOUR_DIFF_FUNCTION_PATTERN = Pattern.compile("(?i)HOUR_DIFF\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\)"); private static final int POINT_QUEUE_CAPACITY = 100000; private static final int POINT_FLUSH_BATCH_SIZE = 2000; private static final int POINT_FLUSH_MAX_DRAIN_PER_RUN = 20000; @@ -121,15 +126,23 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i private static final String SITE_LEVEL_CALC_DEVICE_ID = "SITE_CALC"; private static final int MONITOR_POINT_MATCH_REDIS_TTL_SECONDS = 300; private static final String DELETED_FIELD_MARK = "__DELETED__"; - private static final String DAILY_ENERGY_RAW_CACHE_PREFIX = "DAILY_ENERGY_RAW_"; - private static final String ENERGY_METRIC_PEAK_CHARGE = "peakCharge"; - private static final String ENERGY_METRIC_PEAK_DISCHARGE = "peakDischarge"; - private static final String ENERGY_METRIC_HIGH_CHARGE = "highCharge"; - private static final String ENERGY_METRIC_HIGH_DISCHARGE = "highDischarge"; - private static final String ENERGY_METRIC_FLAT_CHARGE = "flatCharge"; - private static final String ENERGY_METRIC_FLAT_DISCHARGE = "flatDischarge"; - private static final String ENERGY_METRIC_VALLEY_CHARGE = "valleyCharge"; - private static final String ENERGY_METRIC_VALLEY_DISCHARGE = "valleyDischarge"; + private static final String AUTO_DAY_DIFF_CONTEXT_PREFIX = "__AUTO_DAY_DIFF__"; + private static final String AUTO_MONTH_DIFF_CONTEXT_PREFIX = "__AUTO_MONTH_DIFF__"; + private static final String AUTO_HOUR_DIFF_CONTEXT_PREFIX = "__AUTO_HOUR_DIFF__"; + private static final String MONITOR_FIELD_PREFIX_HOME = "home__"; + private static final String MONITOR_FIELD_PREFIX_TJBB_DBBB = "tjbb_dbbb__"; + private static final String[] FIELD_SUFFIX_TOTAL_CHARGE = { + "activeTotalKwh", "totalChargeData", "totalCharge", "totalChargedCap", "chargedCap" + }; + private static final String[] FIELD_SUFFIX_TOTAL_DISCHARGE = { + "reActiveTotalKwh", "totalDischargeData", "totalDischarge", "totalDischargedCap", "disChargedCap" + }; + private static final String[] FIELD_SUFFIX_DAY_CHARGE = { + "dayChargedCap", "dayChargeData", "dailyChargeData", "dailyChargedCap" + }; + private static final String[] FIELD_SUFFIX_DAY_DISCHARGE = { + "dayDisChargedCap", "dayDischargeData", "dailyDischargeData", "dailyDisChargedCap" + }; private static final String[] FIELD_SUFFIX_PEAK_CHARGE = {"activePeakKwh", "peakChargeDiff", "peakCharge"}; private static final String[] FIELD_SUFFIX_PEAK_DISCHARGE = {"reActivePeakKwh", "peakDischargeDiff", "peakDischarge"}; private static final String[] FIELD_SUFFIX_HIGH_CHARGE = {"activeHighKwh", "highChargeDiff", "highCharge"}; @@ -138,6 +151,8 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i private static final String[] FIELD_SUFFIX_FLAT_DISCHARGE = {"reActiveFlatKwh", "flatDischargeDiff", "flatDischarge"}; private static final String[] FIELD_SUFFIX_VALLEY_CHARGE = {"activeValleyKwh", "valleyChargeDiff", "valleyCharge"}; private static final String[] FIELD_SUFFIX_VALLEY_DISCHARGE = {"reActiveValleyKwh", "valleyDischargeDiff", "valleyDischarge"}; + private static final long DAILY_CHARGE_INFLUX_QUERY_WINDOW_MS = TimeUnit.DAYS.toMillis(2); + private static final long CALC_POINT_INFLUX_QUERY_WINDOW_MS = TimeUnit.DAYS.toMillis(2); @Autowired private EmsBatteryClusterMapper emsBatteryClusterMapper; @@ -233,19 +248,13 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i log.info("开始处理设备数据, siteId: {}, messageSize: {}", siteId, arraylist.size()); Set deviceIds = new LinkedHashSet<>(); Map deviceCategoryMap = buildDeviceCategoryMap(siteId, arraylist); - Map batchContextBySite = buildBatchContextBySite(siteId, arraylist, deviceCategoryMap); - Date latestDataUpdateTime = null; - for (int i = 0; i < arraylist.size(); i++) { JSONObject obj = JSONObject.parseObject(arraylist.get(i).toString()); String deviceId = obj.getString("Device"); - String jsonData = obj.getString("Data"); + String jsonData = resolveJsonDataWithRedisFallback(siteId, deviceId, obj.getString("Data")); Long timestamp = obj.getLong("timestamp"); Date dataUpdateTime = DateUtils.convertUpdateTime(timestamp); - if (dataUpdateTime != null && (latestDataUpdateTime == null || dataUpdateTime.after(latestDataUpdateTime))) { - latestDataUpdateTime = dataUpdateTime; - } if (StringUtils.isNotBlank(deviceId)) { deviceIds.add(deviceId); @@ -264,20 +273,16 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i String deviceCategory = resolveDeviceCategory(siteId, deviceId, deviceCategoryMap); // 旧设备表落库链路已下线,MQTT 数据仅走点位映射与站点监控数据同步。 Map pointIdValueMap = processPointConfigData(siteId, deviceId, deviceCategory, mergedJsonData, dataUpdateTime); + List pointIdSamples = pointIdValueMap.keySet().stream().limit(10).collect(Collectors.toList()); + log.info("MQTT点位映射完成,siteId: {}, deviceId: {}, deviceCategory: {}, pointCount: {}, pointIdSamples: {}", + siteId, deviceId, deviceCategory, pointIdValueMap.size(), pointIdSamples); + cacheLatestPointValuesByPointId(siteId, deviceId, pointIdValueMap, dataUpdateTime); iEmsDeviceSettingService.syncSiteMonitorDataByMqtt(siteId, deviceId, JSON.toJSONString(pointIdValueMap), dataUpdateTime); } catch (Exception e) { log.warn("设备数据处理失败,siteId: {}, deviceId: {}, err: {}", siteId, deviceId, e.getMessage(), e); } } - Map calcPointIdValueMap = executeCalcPointConfigs( - siteId, SITE_LEVEL_CALC_DEVICE_ID, getSiteCalcPointConfigs(siteId), batchContextBySite, latestDataUpdateTime - ); - if (!calcPointIdValueMap.isEmpty()) { - iEmsDeviceSettingService.syncSiteMonitorDataByMqtt( - siteId, SITE_LEVEL_CALC_DEVICE_ID, JSON.toJSONString(calcPointIdValueMap), latestDataUpdateTime - ); - } log.info("结束处理设备数据, siteId: {}, deviceCount: {}, deviceIds: {}, costMs: {}", siteId, deviceIds.size(), String.join(",", deviceIds), System.currentTimeMillis() - startMs); } @@ -343,10 +348,576 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i enqueuePointData(siteId, deviceId, pointId, convertedValue, dataUpdateTime); pointIdValueMap.put(pointId, convertedValue); } - updateDailyEnergyDataByMqtt(siteId, deviceId, deviceCategory, pointIdValueMap, dataUpdateTime); return pointIdValueMap; } + private void updateDailyChargeDataByQuartz(String siteId, + Map pointIdValueMap, + Date dataUpdateTime) { + if (StringUtils.isBlank(siteId) + || org.apache.commons.collections4.MapUtils.isEmpty(pointIdValueMap)) { + return; + } + + List mappingList = getPointMatchesBySiteId(siteId); + if (CollectionUtils.isEmpty(mappingList)) { + return; + } + + Map normalizedPointValueMap = new HashMap<>(); + for (Map.Entry entry : pointIdValueMap.entrySet()) { + if (StringUtils.isBlank(entry.getKey()) || entry.getValue() == null) { + continue; + } + normalizedPointValueMap.put(entry.getKey().trim().toUpperCase(), entry.getValue()); + } + if (normalizedPointValueMap.isEmpty()) { + return; + } + + EmsSiteMonitorPointMatch totalChargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_TOTAL_CHARGE + ); + EmsSiteMonitorPointMatch totalDischargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_TOTAL_DISCHARGE + ); + EmsSiteMonitorPointMatch dayChargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_DAY_CHARGE + ); + EmsSiteMonitorPointMatch dayDischargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_DAY_DISCHARGE + ); + String totalChargePointId = totalChargeMatch == null ? null : StringUtils.trimToNull(totalChargeMatch.getDataPoint()); + String totalDischargePointId = totalDischargeMatch == null ? null : StringUtils.trimToNull(totalDischargeMatch.getDataPoint()); + String dayChargePointId = dayChargeMatch == null ? null : StringUtils.trimToNull(dayChargeMatch.getDataPoint()); + String dayDischargePointId = dayDischargeMatch == null ? null : StringUtils.trimToNull(dayDischargeMatch.getDataPoint()); + log.info("日充放电自检, siteId: {}, totalCharge: {}->{} , totalDischarge: {}->{} , dayCharge: {}->{} , dayDischarge: {}->{}", + siteId, + totalChargeMatch == null ? "null" : StringUtils.defaultString(totalChargeMatch.getFieldCode()), + totalChargePointId, + totalDischargeMatch == null ? "null" : StringUtils.defaultString(totalDischargeMatch.getFieldCode()), + totalDischargePointId, + dayChargeMatch == null ? "null" : StringUtils.defaultString(dayChargeMatch.getFieldCode()), + dayChargePointId, + dayDischargeMatch == null ? "null" : StringUtils.defaultString(dayDischargeMatch.getFieldCode()), + dayDischargePointId); + BigDecimal nowTotalCharge = getPointValueById(normalizedPointValueMap, totalChargePointId); + BigDecimal nowTotalDischarge = getPointValueById(normalizedPointValueMap, totalDischargePointId); + BigDecimal nowDayCharge = getLatestPointValueFromRedis(dayChargePointId); + BigDecimal nowDayDischarge = getLatestPointValueFromRedis(dayDischargePointId); + if (nowTotalCharge == null || nowTotalDischarge == null) { + log.info("日充放电累计未匹配到有效点位值, siteId: {}, totalChargePointId: {}, totalDischargePointId: {}, pointKeys: {}", + siteId, totalChargePointId, totalDischargePointId, normalizedPointValueMap.keySet()); + return; + } + + EmsDailyChargeData dailyChargeData = new EmsDailyChargeData(); + Date recordDateTime = dataUpdateTime == null ? DateUtils.getNowDate() : dataUpdateTime; + dailyChargeData.setSiteId(siteId); + dailyChargeData.setDateTime(recordDateTime); + dailyChargeData.setTotalChargeData(nowTotalCharge); + dailyChargeData.setTotalDischargeData(nowTotalDischarge); + dailyChargeData.setChargeData(nowDayCharge); + dailyChargeData.setDischargeData(nowDayDischarge); + + EmsDailyChargeData existedDailyData = emsDailyChargeDataMapper.selectBySiteIdAndDateTime(siteId, recordDateTime); + if (existedDailyData != null && existedDailyData.getId() != null) { + dailyChargeData.setId(existedDailyData.getId()); + dailyChargeData.setDateTime(existedDailyData.getDateTime()); + } + + Date now = DateUtils.getNowDate(); + dailyChargeData.setCreateBy("system"); + dailyChargeData.setCreateTime(now); + dailyChargeData.setUpdateBy("system"); + dailyChargeData.setUpdateTime(now); + emsDailyChargeDataMapper.insertOrUpdateData(dailyChargeData); + + log.info("日充放电累计落库完成, siteId: {}, total[充:{} 放:{}], daily[充:{} 放:{}], points[总充:{} 总放:{} 日充:{} 日放:{}]", + siteId, + dailyChargeData.getTotalChargeData(), dailyChargeData.getTotalDischargeData(), + dailyChargeData.getChargeData(), dailyChargeData.getDischargeData(), + totalChargePointId, totalDischargePointId, dayChargePointId, dayDischargePointId); + } + + public void syncDailyChargeDataFromInfluxByQuartz() { + syncDailyChargeDataFromInfluxByQuartz(null); + } + + public void syncDailyChargeDataFromInfluxByQuartz(String siteId) { + List siteIds; + if (StringUtils.isNotBlank(siteId)) { + siteIds = Collections.singletonList(siteId.trim()); + } else { + siteIds = emsSiteMonitorPointMatchMapper.selectDistinctSiteIds(); + } + if (CollectionUtils.isEmpty(siteIds)) { + log.info("Quartz同步日充放电跳过,未找到站点列表, siteIdParam: {}", siteId); + return; + } + for (String currentSiteId : siteIds) { + if (StringUtils.isBlank(currentSiteId)) { + continue; + } + try { + syncSingleSiteDailyChargeFromInflux(currentSiteId.trim()); + } catch (Exception ex) { + log.error("Quartz同步日充放电失败, siteId: {}, err: {}", currentSiteId, ex.getMessage(), ex); + } + } + } + + private void syncSingleSiteDailyChargeFromInflux(String siteId) { + List mappingList = getPointMatchesBySiteId(siteId); + if (CollectionUtils.isEmpty(mappingList)) { + log.info("Quartz同步日充放电跳过,未找到点位映射, siteId: {}", siteId); + return; + } + + Date queryEndTime = DateUtils.getNowDate(); + Date queryStartTime = new Date(queryEndTime.getTime() - DAILY_CHARGE_INFLUX_QUERY_WINDOW_MS); + EmsSiteMonitorPointMatch totalChargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_TOTAL_CHARGE + ); + EmsSiteMonitorPointMatch totalDischargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_TOTAL_DISCHARGE + ); + String totalChargePointId = totalChargeMatch == null ? null : StringUtils.trimToNull(totalChargeMatch.getDataPoint()); + String totalDischargePointId = totalDischargeMatch == null ? null : StringUtils.trimToNull(totalDischargeMatch.getDataPoint()); + log.info("Quartz同步日充放电自检, siteId: {}, totalCharge: {}->{} , totalDischarge: {}->{}", + siteId, + totalChargeMatch == null ? "null" : StringUtils.defaultString(totalChargeMatch.getFieldCode()), + totalChargePointId, + totalDischargeMatch == null ? "null" : StringUtils.defaultString(totalDischargeMatch.getFieldCode()), + totalDischargePointId); + if (StringUtils.isBlank(totalChargePointId) && StringUtils.isBlank(totalDischargePointId)) { + log.info("Quartz同步日充放电跳过,未找到总充/总放点位, siteId: {}", siteId); + return; + } + + Map pointIdValueMap = new HashMap<>(); + Date dataUpdateTime = null; + if (StringUtils.isNotBlank(totalChargePointId)) { + InfluxPointDataWriter.PointValue totalChargePoint = influxPointDataWriter.queryLatestPointValueByPointKey( + siteId, totalChargePointId, queryStartTime, queryEndTime + ); + if (totalChargePoint != null && totalChargePoint.getPointValue() != null) { + pointIdValueMap.put(totalChargePointId.trim().toUpperCase(), totalChargePoint.getPointValue()); + dataUpdateTime = totalChargePoint.getDataTime(); + } else { + log.info("Quartz同步日充放电未获取到总充点最新值, siteId: {}, pointId: {}, range: {} ~ {}", + siteId, totalChargePointId, + DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, queryStartTime), + DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, queryEndTime)); + } + } + if (StringUtils.isNotBlank(totalDischargePointId)) { + InfluxPointDataWriter.PointValue totalDischargePoint = influxPointDataWriter.queryLatestPointValueByPointKey( + siteId, totalDischargePointId, queryStartTime, queryEndTime + ); + if (totalDischargePoint != null && totalDischargePoint.getPointValue() != null) { + pointIdValueMap.put(totalDischargePointId.trim().toUpperCase(), totalDischargePoint.getPointValue()); + if (dataUpdateTime == null + || (totalDischargePoint.getDataTime() != null + && totalDischargePoint.getDataTime().after(dataUpdateTime))) { + dataUpdateTime = totalDischargePoint.getDataTime(); + } + } else { + log.info("Quartz同步日充放电未获取到总放点最新值, siteId: {}, pointId: {}, range: {} ~ {}", + siteId, totalDischargePointId, + DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, queryStartTime), + DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, queryEndTime)); + } + } + + if (org.apache.commons.collections4.MapUtils.isEmpty(pointIdValueMap)) { + log.info("Quartz同步日充放电跳过,点位最新值为空, siteId: {}", siteId); + return; + } + if (dataUpdateTime == null) { + log.info("Quartz同步日充放电点位值存在但时间为空, siteId: {}, pointIds: {}", + siteId, pointIdValueMap.keySet()); + } else { + log.info("Quartz同步日充放电准备落库, siteId: {}, dataUpdateTime: {}, pointIds: {}", + siteId, DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, dataUpdateTime), + pointIdValueMap.keySet()); + } + updateDailyChargeDataByQuartz(siteId, pointIdValueMap, dataUpdateTime); + updateDailyEnergyHourlyDataByQuartz(siteId, dataUpdateTime); + syncDailyChargeRevenueByQuartz(siteId, dataUpdateTime); + } + + private void syncDailyChargeRevenueByQuartz(String siteId, Date dataUpdateTime) { + if (StringUtils.isBlank(siteId)) { + return; + } + Date revenueDate = dataUpdateTime == null ? DateUtils.getNowDate() : dataUpdateTime; + String day = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, revenueDate); + List revenueRows = emsDailyEnergyDataMapper.getRevenueDataBySiteId(siteId, day, day); + + BigDecimal dayRevenue = BigDecimal.ZERO; + if (CollectionUtils.isNotEmpty(revenueRows)) { + AmmeterRevenueStatisListVo revenueRow = revenueRows.get(0); + BigDecimal activeTotalPrice = safeBigDecimal(revenueRow.getActivePeakPrice()) + .add(safeBigDecimal(revenueRow.getActiveHighPrice())) + .add(safeBigDecimal(revenueRow.getActiveFlatPrice())) + .add(safeBigDecimal(revenueRow.getActiveValleyPrice())); + BigDecimal reActiveTotalPrice = safeBigDecimal(revenueRow.getReActivePeakPrice()) + .add(safeBigDecimal(revenueRow.getReActiveHighPrice())) + .add(safeBigDecimal(revenueRow.getReActiveFlatPrice())) + .add(safeBigDecimal(revenueRow.getReActiveValleyPrice())); + dayRevenue = reActiveTotalPrice.subtract(activeTotalPrice); + } + BigDecimal totalRevenue = getYestLastData(siteId).add(dayRevenue); + + syncDailyRevenueToChargeData(siteId, revenueDate, totalRevenue, dayRevenue); + + log.info("Quartz同步日收益到充放电表完成, siteId: {}, date: {}, dayRevenue: {}, totalRevenue: {}", + siteId, day, dayRevenue, totalRevenue); + } + + private void updateDailyEnergyHourlyDataByQuartz(String siteId, Date dataUpdateTime) { + log.info("Quartz日电量小时落库入口, siteId: {}, dataTime: {}", + siteId, + dataUpdateTime == null ? "null" : DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, dataUpdateTime)); + if (StringUtils.isBlank(siteId)) { + return; + } + + List mappingList = getPointMatchesBySiteId(siteId); + if (CollectionUtils.isEmpty(mappingList)) { + return; + } + + Date recordDateTime = dataUpdateTime == null ? DateUtils.getNowDate() : dataUpdateTime; + Integer dataHour = resolveHourOfDay(recordDateTime); + + String priceKey = RedisKeyConstants.ENERGY_PRICE_TIME + siteId + "_" + LocalDate.now().getYear() + LocalDate.now().getMonthValue(); + EnergyPriceVo priceVo = redisCache.getCacheObject(priceKey); + if (priceVo == null) { + priceVo = emsEnergyPriceConfigService.getCurrentMonthPrice(siteId); + redisCache.setCacheObject(priceKey, priceVo, 31, TimeUnit.DAYS); + } + if (priceVo == null || CollectionUtils.isEmpty(priceVo.getRange())) { + log.info("Quartz日电量小时落库跳过,未找到有效电价时段配置, siteId: {}", siteId); + return; + } + + String costType = null; + for (EnergyPriceTimeRange timeRange : priceVo.getRange()) { + if (timeRange == null || StringUtils.isBlank(timeRange.getCostType())) { + continue; + } + if (isInPriceTimeRange(timeRange.getStartTime(), timeRange.getEndTime(), recordDateTime)) { + costType = timeRange.getCostType(); + break; + } + } + if (StringUtils.isBlank(costType)) { + log.info("Quartz日电量小时落库跳过,未命中电价时段, siteId: {}, dataTime: {}", + siteId, DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, recordDateTime)); + return; + } + + EmsSiteMonitorPointMatch peakChargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_PEAK_CHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB + ); + EmsSiteMonitorPointMatch peakDischargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_PEAK_DISCHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB + ); + EmsSiteMonitorPointMatch highChargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_HIGH_CHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB + ); + EmsSiteMonitorPointMatch highDischargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_HIGH_DISCHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB + ); + EmsSiteMonitorPointMatch flatChargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_FLAT_CHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB + ); + EmsSiteMonitorPointMatch flatDischargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_FLAT_DISCHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB + ); + EmsSiteMonitorPointMatch valleyChargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_VALLEY_CHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB + ); + EmsSiteMonitorPointMatch valleyDischargeMatch = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, FIELD_SUFFIX_VALLEY_DISCHARGE, MONITOR_FIELD_PREFIX_TJBB_DBBB + ); + String peakChargePointId = peakChargeMatch == null ? null : StringUtils.trimToNull(peakChargeMatch.getDataPoint()); + String peakDischargePointId = peakDischargeMatch == null ? null : StringUtils.trimToNull(peakDischargeMatch.getDataPoint()); + String highChargePointId = highChargeMatch == null ? null : StringUtils.trimToNull(highChargeMatch.getDataPoint()); + String highDischargePointId = highDischargeMatch == null ? null : StringUtils.trimToNull(highDischargeMatch.getDataPoint()); + String flatChargePointId = flatChargeMatch == null ? null : StringUtils.trimToNull(flatChargeMatch.getDataPoint()); + String flatDischargePointId = flatDischargeMatch == null ? null : StringUtils.trimToNull(flatDischargeMatch.getDataPoint()); + String valleyChargePointId = valleyChargeMatch == null ? null : StringUtils.trimToNull(valleyChargeMatch.getDataPoint()); + String valleyDischargePointId = valleyDischargeMatch == null ? null : StringUtils.trimToNull(valleyDischargeMatch.getDataPoint()); + log.info("Quartz日电量小时自检, siteId: {}, peak: {}->{} / {}->{} , high: {}->{} / {}->{} , flat: {}->{} / {}->{} , valley: {}->{} / {}->{}", + siteId, + peakChargeMatch == null ? "null" : StringUtils.defaultString(peakChargeMatch.getFieldCode()), + peakChargePointId, + peakDischargeMatch == null ? "null" : StringUtils.defaultString(peakDischargeMatch.getFieldCode()), + peakDischargePointId, + highChargeMatch == null ? "null" : StringUtils.defaultString(highChargeMatch.getFieldCode()), + highChargePointId, + highDischargeMatch == null ? "null" : StringUtils.defaultString(highDischargeMatch.getFieldCode()), + highDischargePointId, + flatChargeMatch == null ? "null" : StringUtils.defaultString(flatChargeMatch.getFieldCode()), + flatChargePointId, + flatDischargeMatch == null ? "null" : StringUtils.defaultString(flatDischargeMatch.getFieldCode()), + flatDischargePointId, + valleyChargeMatch == null ? "null" : StringUtils.defaultString(valleyChargeMatch.getFieldCode()), + valleyChargePointId, + valleyDischargeMatch == null ? "null" : StringUtils.defaultString(valleyDischargeMatch.getFieldCode()), + valleyDischargePointId); + String currentChargePointId; + String currentDischargePointId; + BigDecimal currentCharge; + BigDecimal currentDischarge; + switch (costType) { + case "peak": + currentChargePointId = peakChargePointId; + currentDischargePointId = peakDischargePointId; + currentCharge = getLatestPointValueFromRedis(peakChargePointId); + currentDischarge = getLatestPointValueFromRedis(peakDischargePointId); + break; + case "high": + currentChargePointId = highChargePointId; + currentDischargePointId = highDischargePointId; + currentCharge = getLatestPointValueFromRedis(highChargePointId); + currentDischarge = getLatestPointValueFromRedis(highDischargePointId); + break; + case "flat": + currentChargePointId = flatChargePointId; + currentDischargePointId = flatDischargePointId; + currentCharge = getLatestPointValueFromRedis(flatChargePointId); + currentDischarge = getLatestPointValueFromRedis(flatDischargePointId); + break; + case "valley": + currentChargePointId = valleyChargePointId; + currentDischargePointId = valleyDischargePointId; + currentCharge = getLatestPointValueFromRedis(valleyChargePointId); + currentDischarge = getLatestPointValueFromRedis(valleyDischargePointId); + break; + default: + log.info("Quartz日电量小时落库跳过,未知电价时段类型, siteId: {}, costType: {}", siteId, costType); + return; + } + + if (currentCharge == null && currentDischarge == null) { + log.info("Quartz日电量小时值未匹配到有效Redis点位值, siteId: {}, costType: {}, chargePoint: {}, dischargePoint: {}", + siteId, costType, currentChargePointId, currentDischargePointId); + return; + } + + EmsDailyEnergyData energyData = new EmsDailyEnergyData(); + energyData.setSiteId(siteId); + energyData.setDataDate(recordDateTime); + energyData.setDataHour(dataHour); + if ("peak".equals(costType)) { + energyData.setPeakChargeDiff(currentCharge); + energyData.setPeakDischargeDiff(currentDischarge); + } else if ("high".equals(costType)) { + energyData.setHighChargeDiff(currentCharge); + energyData.setHighDischargeDiff(currentDischarge); + } else if ("flat".equals(costType)) { + energyData.setFlatChargeDiff(currentCharge); + energyData.setFlatDischargeDiff(currentDischarge); + } else if ("valley".equals(costType)) { + energyData.setValleyChargeDiff(currentCharge); + energyData.setValleyDischargeDiff(currentDischarge); + } + energyData.setCalcTime(recordDateTime); + + EmsDailyEnergyData existedEnergyData = emsDailyEnergyDataMapper.selectBySiteIdAndDateHour(siteId, recordDateTime, dataHour); + if (existedEnergyData != null && existedEnergyData.getId() != null) { + energyData.setId(existedEnergyData.getId()); + energyData.setDataDate(existedEnergyData.getDataDate()); + energyData.setDataHour(existedEnergyData.getDataHour()); + } + + Date now = DateUtils.getNowDate(); + energyData.setCreateBy("system"); + energyData.setCreateTime(now); + energyData.setUpdateBy("system"); + energyData.setUpdateTime(now); + emsDailyEnergyDataMapper.insertOrUpdateData(energyData); + + log.info("Quartz日电量小时落库完成, siteId: {}, hour: {}, costType: {}, charge: {}, discharge: {}, chargePoint: {}, dischargePoint: {}", + siteId, dataHour, costType, currentCharge, currentDischarge, currentChargePointId, currentDischargePointId); + } + + private boolean isInPriceTimeRange(String startTime, String endTime, Date dataUpdateTime) { + if (StringUtils.isAnyBlank(startTime, endTime) || dataUpdateTime == null) { + return false; + } + try { + LocalTime current = LocalTime.parse(DateUtils.parseDateToStr("HH:mm", dataUpdateTime)); + LocalTime start = LocalTime.parse(startTime); + LocalTime end = LocalTime.parse(endTime); + if (start.equals(end)) { + return true; + } + if (start.isBefore(end)) { + return !current.isBefore(start) && current.isBefore(end); + } + return !current.isBefore(start) || current.isBefore(end); + } catch (Exception ex) { + log.warn("解析电价时段失败, start: {}, end: {}, err: {}", startTime, endTime, ex.getMessage()); + return false; + } + } + + private Integer resolveHourOfDay(Date dateTime) { + Date targetTime = dateTime == null ? DateUtils.getNowDate() : dateTime; + String hourText = DateUtils.parseDateToStr("HH", targetTime); + try { + return Integer.parseInt(hourText); + } catch (NumberFormatException ex) { + return LocalDateTime.now().getHour(); + } + } + + public void syncCalcPointDataFromInfluxByQuartz() { + syncCalcPointDataFromInfluxByQuartz(null); + } + + public void syncCalcPointDataFromInfluxByQuartz(String siteId) { + List siteIds; + if (StringUtils.isNotBlank(siteId)) { + siteIds = Collections.singletonList(siteId.trim()); + } else { + siteIds = emsSiteMonitorPointMatchMapper.selectDistinctSiteIds(); + } + if (CollectionUtils.isEmpty(siteIds)) { + return; + } + for (String currentSiteId : siteIds) { + if (StringUtils.isBlank(currentSiteId)) { + continue; + } + try { + syncSingleSiteCalcPointFromInflux(currentSiteId.trim()); + } catch (Exception ex) { + log.error("Quartz同步计算点失败, siteId: {}, err: {}", currentSiteId, ex.getMessage(), ex); + } + } + } + + private void syncSingleSiteCalcPointFromInflux(String siteId) { + List calcPointConfigs = getSiteCalcPointConfigs(siteId); + if (CollectionUtils.isEmpty(calcPointConfigs)) { + return; + } + List sourcePointConfigs = getSiteSourcePointConfigs(siteId); + if (CollectionUtils.isEmpty(sourcePointConfigs)) { + log.info("Quartz同步计算点跳过,未找到源点位配置, siteId: {}", siteId); + return; + } + + Date queryEndTime = DateUtils.getNowDate(); + Date queryStartTime = new Date(queryEndTime.getTime() - CALC_POINT_INFLUX_QUERY_WINDOW_MS); + Map contextValues = new HashMap<>(); + Date latestDataTime = null; + for (EmsPointConfig sourcePointConfig : sourcePointConfigs) { + if (sourcePointConfig == null) { + continue; + } + String pointId = resolveInfluxPointKey(sourcePointConfig); + if (StringUtils.isBlank(pointId)) { + continue; + } + InfluxPointDataWriter.PointValue latestPointValue = influxPointDataWriter.queryLatestPointValueByPointKey( + siteId, pointId, queryStartTime, queryEndTime + ); + if (latestPointValue == null || latestPointValue.getPointValue() == null) { + continue; + } + putSourcePointValueToContext(sourcePointConfig, latestPointValue.getPointValue(), contextValues); + if (latestDataTime == null + || (latestPointValue.getDataTime() != null && latestPointValue.getDataTime().after(latestDataTime))) { + latestDataTime = latestPointValue.getDataTime(); + } + } + + if (org.apache.commons.collections4.MapUtils.isEmpty(contextValues)) { + log.info("Quartz同步计算点跳过,InfluxDB未查询到源点位最新值, siteId: {}", siteId); + return; + } + Date calcTime = latestDataTime == null ? queryEndTime : latestDataTime; + Map calcPointIdValueMap = executeCalcPointConfigs( + siteId, SITE_LEVEL_CALC_DEVICE_ID, calcPointConfigs, contextValues, calcTime + ); + if (!calcPointIdValueMap.isEmpty()) { + cacheLatestPointValuesByPointId(siteId, SITE_LEVEL_CALC_DEVICE_ID, calcPointIdValueMap, calcTime); + iEmsDeviceSettingService.syncSiteMonitorDataByMqtt( + siteId, SITE_LEVEL_CALC_DEVICE_ID, JSON.toJSONString(calcPointIdValueMap), calcTime + ); + } + } + + private String firstNonBlankPointByFieldSuffixIgnoreDevice(List mappingList, + String[] fieldSuffixes, + String... fieldPrefixes) { + EmsSiteMonitorPointMatch match = firstNonBlankMatchByFieldSuffixIgnoreDevice( + mappingList, fieldSuffixes, fieldPrefixes + ); + return match == null ? null : StringUtils.trimToNull(match.getDataPoint()); + } + + private EmsSiteMonitorPointMatch firstNonBlankMatchByFieldSuffixIgnoreDevice(List mappingList, + String[] fieldSuffixes, + String... fieldPrefixes) { + if (CollectionUtils.isEmpty(mappingList) || fieldSuffixes == null || fieldSuffixes.length == 0) { + return null; + } + List normalizedPrefixes = new ArrayList<>(); + if (fieldPrefixes != null && fieldPrefixes.length > 0) { + for (String prefix : fieldPrefixes) { + String normalizedPrefix = StringUtils.trimToNull(prefix); + if (normalizedPrefix != null) { + normalizedPrefixes.add(normalizedPrefix.toLowerCase()); + } + } + } + for (String suffix : fieldSuffixes) { + if (StringUtils.isBlank(suffix)) { + continue; + } + String normalizedSuffix = suffix.trim().toLowerCase(); + for (EmsSiteMonitorPointMatch mapping : mappingList) { + if (mapping == null || StringUtils.isBlank(mapping.getFieldCode())) { + continue; + } + String dataPoint = StringUtils.defaultString(mapping.getDataPoint()).trim(); + if (StringUtils.isBlank(dataPoint) || DELETED_FIELD_MARK.equalsIgnoreCase(dataPoint)) { + continue; + } + String fieldCode = mapping.getFieldCode().trim().toLowerCase(); + if (!normalizedPrefixes.isEmpty()) { + if (fieldCode.equals(normalizedSuffix)) { + return mapping; + } + boolean matchedPrefix = false; + for (String prefix : normalizedPrefixes) { + if (fieldCode.startsWith(prefix)) { + matchedPrefix = true; + String stripped = fieldCode.substring(prefix.length()); + if (stripped.equals(normalizedSuffix)) { + return mapping; + } + } + } + if (matchedPrefix) { + continue; + } + } else if (fieldCode.equals(normalizedSuffix)) { + return mapping; + } + } + } + return null; + } + private String mergeWithLatestRedisData(String siteId, String deviceId, String currentJsonData) { Map currentDataMap = parseDataJsonToMap(currentJsonData); if (StringUtils.isAnyBlank(siteId, deviceId)) { @@ -368,6 +939,22 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return JSON.toJSONString(merged); } + private String resolveJsonDataWithRedisFallback(String siteId, String deviceId, String jsonData) { + if (!checkJsonDataEmpty(jsonData)) { + return jsonData; + } + if (StringUtils.isAnyBlank(siteId, deviceId)) { + return jsonData; + } + String redisKey = RedisKeyConstants.ORIGINAL_MQTT_DATA + siteId + "_" + deviceId; + Object latestObj = redisCache.getCacheObject(redisKey); + Map latestDataMap = extractDataMapFromLatest(latestObj); + if (latestDataMap.isEmpty()) { + return jsonData; + } + return JSON.toJSONString(latestDataMap); + } + private Map extractDataMapFromLatest(Object latestObj) { if (latestObj == null) { return new LinkedHashMap<>(); @@ -410,238 +997,6 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return parsed == null ? new LinkedHashMap<>() : new LinkedHashMap<>(parsed); } - private void updateDailyEnergyDataByMqtt(String siteId, String deviceId, String deviceCategory, - Map pointIdValueMap, - Date dataUpdateTime) { - if (StringUtils.isBlank(siteId) || StringUtils.isBlank(deviceId) - || org.apache.commons.collections4.MapUtils.isEmpty(pointIdValueMap)) { - return; - } - // 仅处理储能电表,避免其它设备误触发日报计算 - if (!SiteDevice.METE.name().equalsIgnoreCase(deviceId) - && !DeviceCategory.AMMETER.getCode().equalsIgnoreCase(StringUtils.defaultString(deviceCategory))) { - return; - } - - Map mappingByFieldAndDevice = getMonitorPointMappingByFieldAndDevice(siteId); - if (mappingByFieldAndDevice.isEmpty()) { - return; - } - - Map normalizedPointValueMap = new HashMap<>(); - for (Map.Entry entry : pointIdValueMap.entrySet()) { - if (StringUtils.isBlank(entry.getKey()) || entry.getValue() == null) { - continue; - } - normalizedPointValueMap.put(entry.getKey().trim().toUpperCase(), entry.getValue()); - } - if (normalizedPointValueMap.isEmpty()) { - return; - } - - String peakChargePointId = firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, FIELD_SUFFIX_PEAK_CHARGE); - String peakDischargePointId = firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, FIELD_SUFFIX_PEAK_DISCHARGE); - String highChargePointId = firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, FIELD_SUFFIX_HIGH_CHARGE); - String highDischargePointId = firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, FIELD_SUFFIX_HIGH_DISCHARGE); - String flatChargePointId = firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, FIELD_SUFFIX_FLAT_CHARGE); - String flatDischargePointId = firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, FIELD_SUFFIX_FLAT_DISCHARGE); - String valleyChargePointId = firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, FIELD_SUFFIX_VALLEY_CHARGE); - String valleyDischargePointId = firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, FIELD_SUFFIX_VALLEY_DISCHARGE); - - Map currentTotals = new HashMap<>(); - currentTotals.put(ENERGY_METRIC_PEAK_CHARGE, getPointValueById(normalizedPointValueMap, peakChargePointId)); - currentTotals.put(ENERGY_METRIC_PEAK_DISCHARGE, getPointValueById(normalizedPointValueMap, peakDischargePointId)); - currentTotals.put(ENERGY_METRIC_HIGH_CHARGE, getPointValueById(normalizedPointValueMap, highChargePointId)); - currentTotals.put(ENERGY_METRIC_HIGH_DISCHARGE, getPointValueById(normalizedPointValueMap, highDischargePointId)); - currentTotals.put(ENERGY_METRIC_FLAT_CHARGE, getPointValueById(normalizedPointValueMap, flatChargePointId)); - currentTotals.put(ENERGY_METRIC_FLAT_DISCHARGE, getPointValueById(normalizedPointValueMap, flatDischargePointId)); - currentTotals.put(ENERGY_METRIC_VALLEY_CHARGE, getPointValueById(normalizedPointValueMap, valleyChargePointId)); - currentTotals.put(ENERGY_METRIC_VALLEY_DISCHARGE, getPointValueById(normalizedPointValueMap, valleyDischargePointId)); - log.info("日电量分时映射命中, siteId: {}, deviceId: {}, points[尖充:{} 尖放:{} 峰充:{} 峰放:{} 平充:{} 平放:{} 谷充:{} 谷放:{}]", - siteId, deviceId, - peakChargePointId, peakDischargePointId, - highChargePointId, highDischargePointId, - flatChargePointId, flatDischargePointId, - valleyChargePointId, valleyDischargePointId); - boolean hasAnyCurrentTotal = currentTotals.values().stream().anyMatch(Objects::nonNull); - if (!hasAnyCurrentTotal) { - log.info("日电量累计未匹配到有效点位值, siteId: {}, deviceId: {}, pointKeys: {}", - siteId, deviceId, normalizedPointValueMap.keySet()); - return; - } - log.info("日电量累计当前值, siteId: {}, deviceId: {}, currentTotals: {}", - siteId, deviceId, currentTotals); - - Map lastTotals = getCachedDailyEnergyRawTotals(siteId, deviceId); - if (org.apache.commons.collections4.MapUtils.isEmpty(lastTotals)) { - lastTotals = loadYesterdayAmmeterTotals(siteId, deviceId); - if (!org.apache.commons.collections4.MapUtils.isEmpty(lastTotals)) { - log.info("日电量累计使用昨日末值作为基线, siteId: {}, deviceId: {}, lastTotals: {}", - siteId, deviceId, lastTotals); - } - } - if (org.apache.commons.collections4.MapUtils.isEmpty(lastTotals)) { - log.info("日电量累计缺少上一条基线, 仅缓存当前值等待下一次计算, siteId: {}, deviceId: {}, currentTotals: {}", - siteId, deviceId, currentTotals); - String redisKey = buildDailyEnergyRawCacheKey(siteId, deviceId); - redisCache.setCacheObject(redisKey, currentTotals, 2, TimeUnit.DAYS); - return; - } - EmsDailyEnergyData energyData = initEnergyData(siteId); - accumulateDailyEnergyDiff(energyData, currentTotals, lastTotals, siteId, deviceId); - recalculateDailyEnergyRevenue(siteId, energyData); - energyData.setCalcTime(dataUpdateTime == null ? DateUtils.getNowDate() : dataUpdateTime); - emsDailyEnergyDataMapper.insertOrUpdateData(energyData); - log.info("日电量累计落库完成, siteId: {}, deviceId: {}, diff[尖充:{} 尖放:{} 峰充:{} 峰放:{} 平充:{} 平放:{} 谷充:{} 谷放:{}], dayRevenue: {}, totalRevenue: {}", - siteId, deviceId, - energyData.getPeakChargeDiff(), energyData.getPeakDischargeDiff(), - energyData.getHighChargeDiff(), energyData.getHighDischargeDiff(), - energyData.getFlatChargeDiff(), energyData.getFlatDischargeDiff(), - energyData.getValleyChargeDiff(), energyData.getValleyDischargeDiff(), - energyData.getDayRevenue(), energyData.getTotalRevenue()); - - String redisKey = buildDailyEnergyRawCacheKey(siteId, deviceId); - redisCache.setCacheObject(redisKey, currentTotals, 2, TimeUnit.DAYS); - } - - private Map loadYesterdayAmmeterTotals(String siteId, String deviceId) { - if (StringUtils.isAnyBlank(siteId, deviceId)) { - return Collections.emptyMap(); - } - String yestDate = DateUtils.getYesterdayDate(); - EmsAmmeterData yestData = emsAmmeterDataMapper.getYestLatestDate(siteId, deviceId, yestDate); - if (yestData == null) { - return Collections.emptyMap(); - } - Map lastTotals = new HashMap<>(); - lastTotals.put(ENERGY_METRIC_PEAK_CHARGE, safeBigDecimal(yestData.getCurrentForwardActivePeak())); - lastTotals.put(ENERGY_METRIC_PEAK_DISCHARGE, safeBigDecimal(yestData.getCurrentReverseActivePeak())); - lastTotals.put(ENERGY_METRIC_HIGH_CHARGE, safeBigDecimal(yestData.getCurrentForwardActiveHigh())); - lastTotals.put(ENERGY_METRIC_HIGH_DISCHARGE, safeBigDecimal(yestData.getCurrentReverseActiveHigh())); - lastTotals.put(ENERGY_METRIC_FLAT_CHARGE, safeBigDecimal(yestData.getCurrentForwardActiveFlat())); - lastTotals.put(ENERGY_METRIC_FLAT_DISCHARGE, safeBigDecimal(yestData.getCurrentReverseActiveFlat())); - lastTotals.put(ENERGY_METRIC_VALLEY_CHARGE, safeBigDecimal(yestData.getCurrentForwardActiveValley())); - lastTotals.put(ENERGY_METRIC_VALLEY_DISCHARGE, safeBigDecimal(yestData.getCurrentReverseActiveValley())); - return lastTotals; - } - - private void accumulateDailyEnergyDiff(EmsDailyEnergyData energyData, - Map currentTotals, - Map lastTotals, - String siteId, - String deviceId) { - if (energyData == null || org.apache.commons.collections4.MapUtils.isEmpty(currentTotals)) { - return; - } - addMetricIncrement(energyData, ENERGY_METRIC_PEAK_CHARGE, currentTotals, lastTotals, siteId, deviceId); - addMetricIncrement(energyData, ENERGY_METRIC_PEAK_DISCHARGE, currentTotals, lastTotals, siteId, deviceId); - addMetricIncrement(energyData, ENERGY_METRIC_HIGH_CHARGE, currentTotals, lastTotals, siteId, deviceId); - addMetricIncrement(energyData, ENERGY_METRIC_HIGH_DISCHARGE, currentTotals, lastTotals, siteId, deviceId); - addMetricIncrement(energyData, ENERGY_METRIC_FLAT_CHARGE, currentTotals, lastTotals, siteId, deviceId); - addMetricIncrement(energyData, ENERGY_METRIC_FLAT_DISCHARGE, currentTotals, lastTotals, siteId, deviceId); - addMetricIncrement(energyData, ENERGY_METRIC_VALLEY_CHARGE, currentTotals, lastTotals, siteId, deviceId); - addMetricIncrement(energyData, ENERGY_METRIC_VALLEY_DISCHARGE, currentTotals, lastTotals, siteId, deviceId); - } - - private void addMetricIncrement(EmsDailyEnergyData energyData, - String metric, - Map currentTotals, - Map lastTotals, - String siteId, - String deviceId) { - BigDecimal current = currentTotals.get(metric); - if (current == null) { - return; - } - BigDecimal last = lastTotals == null ? null : lastTotals.get(metric); - if (last == null) { - return; - } - BigDecimal delta = current.subtract(last); - if (delta.compareTo(BigDecimal.ZERO) < 0) { - log.warn("日电量累计值回退,跳过本次增量,siteId: {}, deviceId: {}, metric: {}, current: {}, last: {}", - siteId, deviceId, metric, current, last); - return; - } - - if (ENERGY_METRIC_PEAK_CHARGE.equals(metric)) { - energyData.setPeakChargeDiff(safeBigDecimal(energyData.getPeakChargeDiff()).add(delta)); - return; - } - if (ENERGY_METRIC_PEAK_DISCHARGE.equals(metric)) { - energyData.setPeakDischargeDiff(safeBigDecimal(energyData.getPeakDischargeDiff()).add(delta)); - return; - } - if (ENERGY_METRIC_HIGH_CHARGE.equals(metric)) { - energyData.setHighChargeDiff(safeBigDecimal(energyData.getHighChargeDiff()).add(delta)); - return; - } - if (ENERGY_METRIC_HIGH_DISCHARGE.equals(metric)) { - energyData.setHighDischargeDiff(safeBigDecimal(energyData.getHighDischargeDiff()).add(delta)); - return; - } - if (ENERGY_METRIC_FLAT_CHARGE.equals(metric)) { - energyData.setFlatChargeDiff(safeBigDecimal(energyData.getFlatChargeDiff()).add(delta)); - return; - } - if (ENERGY_METRIC_FLAT_DISCHARGE.equals(metric)) { - energyData.setFlatDischargeDiff(safeBigDecimal(energyData.getFlatDischargeDiff()).add(delta)); - return; - } - if (ENERGY_METRIC_VALLEY_CHARGE.equals(metric)) { - energyData.setValleyChargeDiff(safeBigDecimal(energyData.getValleyChargeDiff()).add(delta)); - return; - } - if (ENERGY_METRIC_VALLEY_DISCHARGE.equals(metric)) { - energyData.setValleyDischargeDiff(safeBigDecimal(energyData.getValleyDischargeDiff()).add(delta)); - } - } - - private void recalculateDailyEnergyRevenue(String siteId, EmsDailyEnergyData energyData) { - if (StringUtils.isBlank(siteId) || energyData == null) { - return; - } - String priceKey = RedisKeyConstants.ENERGY_PRICE_TIME + siteId + "_" + LocalDate.now().getYear() + LocalDate.now().getMonthValue(); - EnergyPriceVo priceVo = redisCache.getCacheObject(priceKey); - if (priceVo == null) { - priceVo = emsEnergyPriceConfigService.getCurrentMonthPrice(siteId); - redisCache.setCacheObject(priceKey, priceVo, 31, TimeUnit.DAYS); - } - if (priceVo == null) { - energyData.setDayRevenue(BigDecimal.ZERO); - energyData.setTotalRevenue(getYestLastData(siteId)); - return; - } - - BigDecimal peakRevenue = safeBigDecimal(energyData.getPeakDischargeDiff()) - .subtract(safeBigDecimal(energyData.getPeakChargeDiff())) - .multiply(safeBigDecimal(priceVo.getPeak())); - BigDecimal highRevenue = safeBigDecimal(energyData.getHighDischargeDiff()) - .subtract(safeBigDecimal(energyData.getHighChargeDiff())) - .multiply(safeBigDecimal(priceVo.getHigh())); - BigDecimal flatRevenue = safeBigDecimal(energyData.getFlatDischargeDiff()) - .subtract(safeBigDecimal(energyData.getFlatChargeDiff())) - .multiply(safeBigDecimal(priceVo.getFlat())); - BigDecimal valleyRevenue = safeBigDecimal(energyData.getValleyDischargeDiff()) - .subtract(safeBigDecimal(energyData.getValleyChargeDiff())) - .multiply(safeBigDecimal(priceVo.getValley())); - - BigDecimal dayRevenue = peakRevenue.add(highRevenue).add(flatRevenue).add(valleyRevenue); - energyData.setDayRevenue(dayRevenue); - energyData.setTotalRevenue(getYestLastData(siteId).add(dayRevenue)); - } - - private BigDecimal resolveEnergyMetricValue(Map mappingByFieldAndDevice, - String deviceId, - Map pointIdValueMap, - String[] fieldSuffixes) { - String pointId = firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, fieldSuffixes); - if (StringUtils.isBlank(pointId)) { - return null; - } - return pointIdValueMap.get(pointId.trim().toUpperCase()); - } - private BigDecimal getPointValueById(Map pointIdValueMap, String pointId) { if (pointIdValueMap == null || pointIdValueMap.isEmpty() || StringUtils.isBlank(pointId)) { return null; @@ -649,9 +1004,82 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return pointIdValueMap.get(pointId.trim().toUpperCase()); } + private BigDecimal getLatestPointValueFromRedis(String pointId) { + String normalizedPointId = StringUtils.trimToNull(pointId); + if (normalizedPointId == null) { + return null; + } + List candidates = new ArrayList<>(); + candidates.add(normalizedPointId); + String upperPointId = normalizedPointId.toUpperCase(); + if (!StringUtils.equals(upperPointId, normalizedPointId)) { + candidates.add(upperPointId); + } + String lowerPointId = normalizedPointId.toLowerCase(); + if (!StringUtils.equals(lowerPointId, normalizedPointId) + && !StringUtils.equals(lowerPointId, upperPointId)) { + candidates.add(lowerPointId); + } + for (String candidate : candidates) { + Object raw = redisCache.getCacheObject(candidate); + if (raw == null) { + continue; + } + try { + JSONObject valueObj = raw instanceof JSONObject + ? (JSONObject) raw + : JSON.parseObject(JSON.toJSONString(raw)); + if (valueObj == null) { + continue; + } + BigDecimal pointValue = StringUtils.getBigDecimal(valueObj.get("pointValue")); + if (pointValue == null) { + pointValue = readBigDecimalField(raw, "pointValue"); + } + if (pointValue != null) { + return pointValue; + } + } catch (Exception ex) { + log.warn("读取点位Redis值失败, pointId: {}, redisKey: {}, err: {}", normalizedPointId, candidate, ex.getMessage()); + } + } + return null; + } + + private BigDecimal readBigDecimalField(Object source, String fieldName) { + Object fieldValue = readFieldValue(source, fieldName); + return StringUtils.getBigDecimal(fieldValue); + } + + private Object readFieldValue(Object source, String fieldName) { + if (source == null || StringUtils.isBlank(fieldName)) { + return null; + } + Class current = source.getClass(); + while (current != null) { + try { + Field field = current.getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(source); + } catch (NoSuchFieldException ex) { + current = current.getSuperclass(); + } catch (Exception ex) { + return null; + } + } + return null; + } + private String firstNonBlankPointByFieldSuffix(Map mappingByFieldAndDevice, String deviceId, String[] fieldSuffixes) { + return firstNonBlankPointByFieldSuffix(mappingByFieldAndDevice, deviceId, fieldSuffixes, MONITOR_FIELD_PREFIX_HOME); + } + + private String firstNonBlankPointByFieldSuffix(Map mappingByFieldAndDevice, + String deviceId, + String[] fieldSuffixes, + String... fieldPrefixes) { if (mappingByFieldAndDevice == null || fieldSuffixes == null || fieldSuffixes.length == 0) { return null; } @@ -661,11 +1089,15 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i continue; } String normalizedSuffix = suffix.trim().toLowerCase(); - EmsSiteMonitorPointMatch exactMatch = resolvePointMatchByFieldSuffix(mappingByFieldAndDevice, normalizedDeviceId, normalizedSuffix); + EmsSiteMonitorPointMatch exactMatch = resolvePointMatchByFieldSuffix( + mappingByFieldAndDevice, normalizedDeviceId, normalizedSuffix, fieldPrefixes + ); if (exactMatch != null && StringUtils.isNotBlank(exactMatch.getDataPoint())) { return exactMatch.getDataPoint().trim(); } - EmsSiteMonitorPointMatch fallbackMatch = resolvePointMatchByFieldSuffix(mappingByFieldAndDevice, "", normalizedSuffix); + EmsSiteMonitorPointMatch fallbackMatch = resolvePointMatchByFieldSuffix( + mappingByFieldAndDevice, "", normalizedSuffix, fieldPrefixes + ); if (fallbackMatch != null && StringUtils.isNotBlank(fallbackMatch.getDataPoint())) { return fallbackMatch.getDataPoint().trim(); } @@ -675,10 +1107,20 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i private EmsSiteMonitorPointMatch resolvePointMatchByFieldSuffix(Map mappingByFieldAndDevice, String deviceId, - String suffixLowerCase) { + String suffixLowerCase, + String... fieldPrefixes) { if (mappingByFieldAndDevice == null || StringUtils.isBlank(suffixLowerCase)) { return null; } + List normalizedPrefixes = new ArrayList<>(); + if (fieldPrefixes != null && fieldPrefixes.length > 0) { + for (String prefix : fieldPrefixes) { + String normalizedPrefix = StringUtils.trimToNull(prefix); + if (normalizedPrefix != null) { + normalizedPrefixes.add(normalizedPrefix.toLowerCase()); + } + } + } for (Map.Entry entry : mappingByFieldAndDevice.entrySet()) { EmsSiteMonitorPointMatch mapping = entry.getValue(); if (mapping == null || StringUtils.isBlank(mapping.getFieldCode())) { @@ -693,7 +1135,10 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i continue; } String fieldCode = mapping.getFieldCode().trim().toLowerCase(); - if (fieldCode.endsWith(suffixLowerCase)) { + if (!normalizedPrefixes.isEmpty() && normalizedPrefixes.stream().noneMatch(fieldCode::startsWith)) { + continue; + } + if (fieldCode.equals(suffixLowerCase)) { return mapping; } } @@ -763,44 +1208,6 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return StringUtils.defaultString(fieldCode).trim() + "|" + StringUtils.defaultString(deviceId).trim(); } - private Map getCachedDailyEnergyRawTotals(String siteId, String deviceId) { - String redisKey = buildDailyEnergyRawCacheKey(siteId, deviceId); - Object cacheObj = redisCache.getCacheObject(redisKey); - if (cacheObj == null) { - return Collections.emptyMap(); - } - try { - if (cacheObj instanceof Map) { - Map mapObj = (Map) cacheObj; - Map parsed = new HashMap<>(); - for (Map.Entry entry : mapObj.entrySet()) { - if (entry.getKey() == null || entry.getValue() == null) { - continue; - } - String key = String.valueOf(entry.getKey()).trim(); - if (StringUtils.isBlank(key)) { - continue; - } - BigDecimal value = StringUtils.getBigDecimal(entry.getValue()); - if (value != null) { - parsed.put(key, value); - } - } - return parsed; - } - return JSON.parseObject(JSON.toJSONString(cacheObj), new TypeReference>() { - }); - } catch (Exception e) { - log.warn("解析日电量原始累计缓存失败,siteId: {}, deviceId: {}, err: {}", - siteId, deviceId, e.getMessage()); - return Collections.emptyMap(); - } - } - - private String buildDailyEnergyRawCacheKey(String siteId, String deviceId) { - return DAILY_ENERGY_RAW_CACHE_PREFIX + StringUtils.defaultString(siteId).trim() + "_" + StringUtils.defaultString(deviceId).trim(); - } - private BigDecimal safeBigDecimal(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } @@ -855,7 +1262,7 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i String deviceId = item.getString("Device"); String devicePrefix = normalizeVariablePart(deviceId); String deviceCategory = resolveDeviceCategory(siteId, deviceId, deviceCategoryMap); - String jsonData = item.getString("Data"); + String jsonData = resolveJsonDataWithRedisFallback(siteId, deviceId, item.getString("Data")); if (checkJsonDataEmpty(jsonData)) { continue; } @@ -1010,7 +1417,8 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i String calcDataKey = entry.getKey(); EmsPointConfig calcPointConfig = entry.getValue(); try { - ExpressionValue calcValue = evaluateCalcExpression(calcPointConfig.getCalcExpression(), contextValues); + ExpressionValue calcValue = evaluateCalcExpression( + calcPointConfig.getCalcExpression(), contextValues, siteId, dataUpdateTime); if (calcValue.isNumber()) { contextValues.put(calcDataKey, calcValue.asNumber()); putPointValueToContext(calcPointConfig, calcValue.asNumber(), contextValues); @@ -1076,15 +1484,207 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return StringUtils.isNotBlank(pointId) ? pointId : null; } - private ExpressionValue evaluateCalcExpression(String expression, Map contextValues) { + private ExpressionValue evaluateCalcExpression(String expression, Map contextValues, + String siteId, Date dataUpdateTime) { if (StringUtils.isBlank(expression)) { throw new IllegalArgumentException("计算表达式为空"); } if (!SIMPLE_CALC_EXPRESSION_PATTERN.matcher(expression).matches()) { - throw new IllegalArgumentException("计算表达式仅支持四则运算"); + throw new IllegalArgumentException("计算表达式仅支持数字、字母、下划线、空格、运算符和函数语法"); } + Map evaluateContext = contextValues == null + ? new HashMap<>() + : new HashMap<>(contextValues); + prepareAutoPeriodDiffContextValues(expression, evaluateContext, siteId, dataUpdateTime); CompiledExpression compiledExpression = calcExpressionCache.computeIfAbsent(expression, this::compileExpression); - return compiledExpression.evaluate(contextValues == null ? Collections.emptyMap() : contextValues); + return compiledExpression.evaluate(evaluateContext); + } + + private void prepareAutoPeriodDiffContextValues(String expression, Map contextValues, + String siteId, Date dataUpdateTime) { + if (StringUtils.isBlank(expression) || contextValues == null || contextValues.isEmpty()) { + return; + } + Date effectiveTime = dataUpdateTime == null ? DateUtils.getNowDate() : dataUpdateTime; + prepareSingleAutoDiffContext(expression, contextValues, siteId, effectiveTime, "DAY_DIFF", + AUTO_DAY_DIFF_FUNCTION_PATTERN); + prepareSingleAutoDiffContext(expression, contextValues, siteId, effectiveTime, "MONTH_DIFF", + AUTO_MONTH_DIFF_FUNCTION_PATTERN); + prepareSingleAutoDiffContext(expression, contextValues, siteId, effectiveTime, "HOUR_DIFF", + AUTO_HOUR_DIFF_FUNCTION_PATTERN); + } + + private void prepareSingleAutoDiffContext(String expression, Map contextValues, String siteId, + Date effectiveTime, String functionName, Pattern functionPattern) { + Set variables = extractAutoDiffVariables(expression, functionPattern); + if (variables.isEmpty()) { + return; + } + for (String variable : variables) { + BigDecimal currentValue = contextValues.get(variable); + if (currentValue == null) { + continue; + } + BigDecimal diffValue = calculateAutoPeriodDiffValue(siteId, functionName, variable, currentValue, effectiveTime); + contextValues.put(resolveAutoDiffContextKey(functionName, variable), diffValue); + } + } + + private Set extractAutoDiffVariables(String expression, Pattern functionPattern) { + Set variables = new HashSet<>(); + Matcher matcher = functionPattern.matcher(expression); + while (matcher.find()) { + String variable = StringUtils.defaultString(matcher.group(1)).trim().toUpperCase(); + if (StringUtils.isNotBlank(variable)) { + variables.add(variable); + } + } + return variables; + } + + private BigDecimal calculateAutoPeriodDiffValue(String siteId, String functionName, String variable, + BigDecimal currentValue, Date effectiveTime) { + if (currentValue == null || StringUtils.isAnyBlank(siteId, functionName, variable) || effectiveTime == null) { + return BigDecimal.ZERO; + } + if ("DAY_DIFF".equalsIgnoreCase(functionName)) { + return calculateDayDiffValueFromInflux(siteId, variable, currentValue, effectiveTime); + } + if ("MONTH_DIFF".equalsIgnoreCase(functionName)) { + return calculateMonthDiffValueFromInflux(siteId, variable, currentValue, effectiveTime); + } + if ("HOUR_DIFF".equalsIgnoreCase(functionName)) { + return calculateHourDiffValueFromInflux(siteId, variable, currentValue, effectiveTime); + } + return BigDecimal.ZERO; + } + + private BigDecimal calculateDayDiffValueFromInflux(String siteId, String variable, + BigDecimal currentValue, Date effectiveTime) { + LocalDateTime currentDateTime = DateUtils.toLocalDateTime(effectiveTime); + if (currentDateTime == null) { + return BigDecimal.ZERO; + } + LocalDate currentDate = currentDateTime.toLocalDate(); + Date currentDayStart = DateUtils.toDate(currentDate); + Date previousDayStart = DateUtils.toDate(currentDate.minusDays(1)); + Date previousDayEnd = new Date(currentDayStart.getTime() - 1); + return calculateDiffByPreviousPeriodLastValue(siteId, variable, currentValue, previousDayStart, previousDayEnd); + } + + private BigDecimal calculateMonthDiffValueFromInflux(String siteId, String variable, + BigDecimal currentValue, Date effectiveTime) { + LocalDateTime currentDateTime = DateUtils.toLocalDateTime(effectiveTime); + if (currentDateTime == null) { + return BigDecimal.ZERO; + } + YearMonth currentMonth = YearMonth.from(currentDateTime); + LocalDate currentMonthStartDate = currentMonth.atDay(1); + Date currentMonthStart = DateUtils.toDate(currentMonthStartDate); + YearMonth previousMonth = currentMonth.minusMonths(1); + Date previousMonthStart = DateUtils.toDate(previousMonth.atDay(1)); + Date previousMonthEnd = new Date(currentMonthStart.getTime() - 1); + return calculateDiffByPreviousPeriodLastValue(siteId, variable, currentValue, previousMonthStart, previousMonthEnd); + } + + private BigDecimal calculateHourDiffValueFromInflux(String siteId, String variable, + BigDecimal currentValue, Date effectiveTime) { + LocalDateTime currentDateTime = DateUtils.toLocalDateTime(effectiveTime); + if (currentDateTime == null) { + return BigDecimal.ZERO; + } + LocalDateTime currentHourStartTime = currentDateTime.withMinute(0).withSecond(0).withNano(0); + Date currentHourStart = DateUtils.toDate(currentHourStartTime); + Date previousHourStart = DateUtils.toDate(currentHourStartTime.minusHours(1)); + Date previousHourEnd = new Date(currentHourStart.getTime() - 1); + return calculateDiffByPreviousPeriodLastValue(siteId, variable, currentValue, previousHourStart, previousHourEnd); + } + + private BigDecimal calculateDiffByPreviousPeriodLastValue(String siteId, String variable, + BigDecimal currentValue, Date previousPeriodStart, + Date previousPeriodEnd) { + if (StringUtils.isAnyBlank(siteId, variable) + || currentValue == null + || previousPeriodStart == null + || previousPeriodEnd == null + || previousPeriodEnd.before(previousPeriodStart)) { + return BigDecimal.ZERO; + } + String pointKey = resolveInfluxPointKeyByVariable(siteId, variable); + if (StringUtils.isBlank(pointKey)) { + return BigDecimal.ZERO; + } + InfluxPointDataWriter.PointValue previousPeriodLastPoint = influxPointDataWriter.queryLatestPointValueByPointKey( + siteId, pointKey, previousPeriodStart, previousPeriodEnd + ); + if (previousPeriodLastPoint == null || previousPeriodLastPoint.getPointValue() == null) { + return BigDecimal.ZERO; + } + return currentValue.subtract(previousPeriodLastPoint.getPointValue()); + } + + private String resolveInfluxPointKeyByVariable(String siteId, String variable) { + if (StringUtils.isAnyBlank(siteId, variable)) { + return null; + } + String normalizedVariable = normalizeVariablePart(variable); + String upperVariable = variable.trim().toUpperCase(); + List sourcePointConfigs = getSiteSourcePointConfigs(siteId); + for (EmsPointConfig sourcePointConfig : sourcePointConfigs) { + if (sourcePointConfig == null) { + continue; + } + String pointKey = resolveInfluxPointKey(sourcePointConfig); + if (StringUtils.isBlank(pointKey)) { + continue; + } + + String pointIdKey = resolvePointContextKey(sourcePointConfig); + if (StringUtils.isNotBlank(pointIdKey) && upperVariable.equals(pointIdKey)) { + return pointKey; + } + + String normalizedDataKey = normalizeVariablePart(sourcePointConfig.getDataKey()); + if (StringUtils.isNotBlank(normalizedVariable) + && StringUtils.isNotBlank(normalizedDataKey) + && normalizedVariable.equals(normalizedDataKey)) { + return pointKey; + } + + String devicePrefix = normalizeVariablePart(sourcePointConfig.getDeviceId()); + if (StringUtils.isNotBlank(devicePrefix) + && StringUtils.isNotBlank(normalizedDataKey) + && StringUtils.isNotBlank(normalizedVariable) + && normalizedVariable.equals(devicePrefix + "_" + normalizedDataKey)) { + return pointKey; + } + } + return null; + } + + private static String resolveAutoDayDiffContextKey(String variable) { + return AUTO_DAY_DIFF_CONTEXT_PREFIX + StringUtils.defaultString(variable).trim().toUpperCase(); + } + + private static String resolveAutoMonthDiffContextKey(String variable) { + return AUTO_MONTH_DIFF_CONTEXT_PREFIX + StringUtils.defaultString(variable).trim().toUpperCase(); + } + + private static String resolveAutoHourDiffContextKey(String variable) { + return AUTO_HOUR_DIFF_CONTEXT_PREFIX + StringUtils.defaultString(variable).trim().toUpperCase(); + } + + private static String resolveAutoDiffContextKey(String functionName, String variable) { + if ("DAY_DIFF".equalsIgnoreCase(functionName)) { + return resolveAutoDayDiffContextKey(variable); + } + if ("MONTH_DIFF".equalsIgnoreCase(functionName)) { + return resolveAutoMonthDiffContextKey(variable); + } + if ("HOUR_DIFF".equalsIgnoreCase(functionName)) { + return resolveAutoHourDiffContextKey(variable); + } + return null; } private CompiledExpression compileExpression(String expression) { @@ -1174,6 +1774,84 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return new ArrayList<>(uniqueByPointId.values()); } + private List getSiteSourcePointConfigs(String siteId) { + if (StringUtils.isBlank(siteId)) { + return Collections.emptyList(); + } + EmsPointConfig query = new EmsPointConfig(); + query.setSiteId(siteId); + List pointConfigs = emsPointConfigMapper.selectEmsPointConfigList(query); + if (CollectionUtils.isEmpty(pointConfigs)) { + return Collections.emptyList(); + } + Map uniqueByPointId = new LinkedHashMap<>(); + for (EmsPointConfig pointConfig : pointConfigs) { + if (pointConfig == null || isComputedPoint(pointConfig)) { + continue; + } + String pointId = resolveInfluxPointKey(pointConfig); + if (StringUtils.isBlank(pointId)) { + continue; + } + uniqueByPointId.putIfAbsent(pointId.trim().toUpperCase(), pointConfig); + } + return new ArrayList<>(uniqueByPointId.values()); + } + + private void putSourcePointValueToContext(EmsPointConfig pointConfig, BigDecimal pointValue, Map contextValues) { + if (pointConfig == null || pointValue == null || contextValues == null) { + return; + } + String pointContextKey = resolvePointContextKey(pointConfig); + if (StringUtils.isNotBlank(pointContextKey)) { + contextValues.put(pointContextKey, pointValue); + } + String normalizedDataKey = normalizeVariablePart(pointConfig.getDataKey()); + if (StringUtils.isNotBlank(normalizedDataKey)) { + contextValues.put(normalizedDataKey, pointValue); + String devicePrefix = normalizeVariablePart(pointConfig.getDeviceId()); + if (StringUtils.isNotBlank(devicePrefix)) { + contextValues.put(devicePrefix + "_" + normalizedDataKey, pointValue); + } + } + } + + private void cacheLatestPointValuesByPointId(String siteId, String deviceId, + Map pointIdValueMap, + Date dataTime) { + if (StringUtils.isBlank(siteId) || org.apache.commons.collections4.MapUtils.isEmpty(pointIdValueMap)) { + log.warn("MQTT点位写Redis跳过,siteId: {}, deviceId: {}, pointMapEmpty: {}", + siteId, deviceId, org.apache.commons.collections4.MapUtils.isEmpty(pointIdValueMap)); + return; + } + Date latestTime = dataTime == null ? DateUtils.getNowDate() : dataTime; + int writeCount = 0; + List writeSamples = new ArrayList<>(); + for (Map.Entry entry : pointIdValueMap.entrySet()) { + String pointId = StringUtils.trimToNull(entry.getKey()); + BigDecimal pointValue = entry.getValue(); + if (StringUtils.isBlank(pointId) || pointValue == null) { + log.warn("MQTT点位写Redis忽略无效值,siteId: {}, deviceId: {}, rawPointId: {}, pointValue: {}", + siteId, deviceId, entry.getKey(), pointValue); + continue; + } + JSONObject cacheValue = new JSONObject(); + cacheValue.put("siteId", siteId); + cacheValue.put("deviceId", deviceId); + cacheValue.put("pointId", pointId); + cacheValue.put("pointValue", pointValue); + cacheValue.put("dataTime", latestTime); + cacheValue.put("timestamp", latestTime == null ? null : latestTime.getTime()); + redisCache.setCacheObject(pointId, cacheValue); + writeCount++; + if (writeSamples.size() < 10) { + writeSamples.add(pointId); + } + } + log.info("MQTT点位写Redis完成,siteId: {}, deviceId: {}, writeCount: {}, writeSamples: {}, dataTime: {}", + siteId, deviceId, writeCount, writeSamples, latestTime); + } + private static class PointDataRecord { private final String siteId; private final String deviceId; @@ -1210,6 +1888,54 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i } } + private static class PointLatestCacheValue { + private String siteId; + private String deviceId; + private String pointId; + private BigDecimal pointValue; + private Date dataTime; + + private String getSiteId() { + return siteId; + } + + private void setSiteId(String siteId) { + this.siteId = siteId; + } + + private String getDeviceId() { + return deviceId; + } + + private void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + private String getPointId() { + return pointId; + } + + private void setPointId(String pointId) { + this.pointId = pointId; + } + + private BigDecimal getPointValue() { + return pointValue; + } + + private void setPointValue(BigDecimal pointValue) { + this.pointValue = pointValue; + } + + private Date getDataTime() { + return dataTime; + } + + private void setDataTime(Date dataTime) { + this.dataTime = dataTime; + } + } + private static class MissingVariableException extends IllegalArgumentException { private final String variable; @@ -1353,6 +2079,10 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i } return ExpressionValue.ofNumber(value); } + + private String getName() { + return name; + } } private static class UnaryNode implements ExpressionNode { @@ -1480,6 +2210,40 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i } } + private static class FunctionNode implements ExpressionNode { + private final String functionName; + private final List arguments; + + private FunctionNode(String functionName, List arguments) { + this.functionName = StringUtils.defaultString(functionName).trim().toUpperCase(); + this.arguments = arguments == null ? Collections.emptyList() : arguments; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + if (("DAY_DIFF".equals(functionName) + || "MONTH_DIFF".equals(functionName) + || "HOUR_DIFF".equals(functionName)) && arguments.size() == 1) { + ExpressionNode argument = arguments.get(0); + if (!(argument instanceof VariableNode)) { + throw new IllegalArgumentException(functionName + "单参数模式仅支持变量参数"); + } + String variable = ((VariableNode) argument).getName(); + BigDecimal autoDiffValue = contextValues.get(resolveAutoDiffContextKey(functionName, variable)); + if (autoDiffValue == null) { + throw new MissingVariableException(variable); + } + return ExpressionValue.ofNumber(autoDiffValue); + } + if ("DAY_DIFF".equals(functionName) + || "MONTH_DIFF".equals(functionName) + || "HOUR_DIFF".equals(functionName)) { + throw new IllegalArgumentException(functionName + "函数参数数量错误,需1个参数"); + } + throw new IllegalArgumentException("不支持的函数: " + functionName); + } + } + private static class CompiledExpression { private final ExpressionNode root; @@ -1634,16 +2398,7 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i if (match(ExprTokenType.IDENTIFIER)) { String identifier = token.text; if (match(ExprTokenType.LEFT_PAREN)) { - if (!"IF".equalsIgnoreCase(identifier)) { - throw new IllegalArgumentException("不支持的函数: " + identifier); - } - ExpressionNode condition = parseExpression(); - expect(ExprTokenType.COMMA, "IF函数参数格式错误,缺少第1个逗号"); - ExpressionNode trueNode = parseExpression(); - expect(ExprTokenType.COMMA, "IF函数参数格式错误,缺少第2个逗号"); - ExpressionNode falseNode = parseExpression(); - expect(ExprTokenType.RIGHT_PAREN, "IF函数缺少右括号"); - return new TernaryNode(condition, trueNode, falseNode); + return parseFunction(identifier); } return new VariableNode(identifier.toUpperCase()); } @@ -1655,6 +2410,55 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i throw new IllegalArgumentException("表达式语法错误,当前位置: " + token.text); } + private ExpressionNode parseFunction(String identifier) { + if ("IF".equalsIgnoreCase(identifier)) { + ExpressionNode condition = parseExpression(); + expect(ExprTokenType.COMMA, "IF函数参数格式错误,缺少第1个逗号"); + ExpressionNode trueNode = parseExpression(); + expect(ExprTokenType.COMMA, "IF函数参数格式错误,缺少第2个逗号"); + ExpressionNode falseNode = parseExpression(); + expect(ExprTokenType.RIGHT_PAREN, "IF函数缺少右括号"); + return new TernaryNode(condition, trueNode, falseNode); + } + if ("DAY_DIFF".equalsIgnoreCase(identifier)) { + List arguments = parseFunctionArguments(identifier); + if (arguments.size() != 1) { + throw new IllegalArgumentException("DAY_DIFF函数参数数量错误,需1个参数"); + } + return new FunctionNode(identifier, arguments); + } + if ("MONTH_DIFF".equalsIgnoreCase(identifier)) { + List arguments = parseFunctionArguments(identifier); + if (arguments.size() != 1) { + throw new IllegalArgumentException("MONTH_DIFF函数参数数量错误,需1个参数"); + } + return new FunctionNode(identifier, arguments); + } + if ("HOUR_DIFF".equalsIgnoreCase(identifier)) { + List arguments = parseFunctionArguments(identifier); + if (arguments.size() != 1) { + throw new IllegalArgumentException("HOUR_DIFF函数参数数量错误,需1个参数"); + } + return new FunctionNode(identifier, arguments); + } + throw new IllegalArgumentException("不支持的函数: " + identifier); + } + + private List parseFunctionArguments(String functionName) { + List arguments = new ArrayList<>(); + if (match(ExprTokenType.RIGHT_PAREN)) { + return arguments; + } + while (true) { + arguments.add(parseExpression()); + if (match(ExprTokenType.COMMA)) { + continue; + } + expect(ExprTokenType.RIGHT_PAREN, functionName + "函数缺少右括号"); + return arguments; + } + } + private ExprToken peek() { if (index >= tokens.size()) { return ExprToken.symbol(ExprTokenType.EOF, ""); @@ -2750,7 +3554,6 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i // 初始化当日数据 EmsDailyChargeData emsDailyChargeData = new EmsDailyChargeData(); emsDailyChargeData.setSiteId(siteId); - emsDailyChargeData.setDeviceId(deviceId); emsDailyChargeData.setDateTime(DateUtils.getNowDate()); emsDailyChargeData.setTotalChargeData(pcsData.getTotalAcChargeEnergy()); emsDailyChargeData.setTotalDischargeData(pcsData.getTotalAcDischargeEnergy()); @@ -2780,9 +3583,6 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return; } - // 获取上次数据,便于后面计算差值均无则默认0 - EmsAmmeterData lastAmmeterData = getLastAmmeterData(siteId, deviceId); - EmsAmmeterData dataMete = new EmsAmmeterData(); // 更新时间 dataMete.setDataUpdateTime(dataUpdateTime); @@ -2804,80 +3604,14 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i // 处理储能电表-METE每日充放电数据 dealDailyChargeDate(siteId, deviceId, dataMete); - // 处理储能电表-METE每日数据(尖、峰、平、谷差值) -// if (SiteEnum.FX.getCode().equals(siteId)) { - dealAmmeterDailyDate(siteId, dataMete, dataUpdateTime, lastAmmeterData); -// } else if (SiteEnum.DDS.getCode().equals(siteId)) { -// dealDailyEnergyData(siteId, dataMete, yestData); -// } + // ems_daily_energy_data 仅保留页面修正与Quartz写入 } } - private EmsAmmeterData getLastAmmeterData(String siteId, String deviceId) { - // 先从redis取,取不到查数据 - EmsAmmeterData lastData = redisCache.getCacheObject(RedisKeyConstants.AMMETER + siteId + "_" + deviceId); - if (lastData == null) { - lastData = emsAmmeterDataMapper.getLastData(siteId, deviceId); - if (lastData == null) { - lastData = new EmsAmmeterData(); - lastData.setSiteId(siteId); - lastData.setDeviceId(deviceId); - lastData.setCurrentForwardActiveTotal(BigDecimal.ZERO); - lastData.setCurrentReverseActiveTotal(BigDecimal.ZERO); - } - } - return lastData; - } - - private void dealDDSDailyChargeDate(String siteId, EmsAmmeterData currentData, String deviceId) { - // 初始化今日充放电 - BigDecimal dailyDisChargeDate = new BigDecimal(0); - BigDecimal dailyChargeDate = new BigDecimal(0); - -// BigDecimal nowTotalDisChargeDate = StringUtils.getBigDecimal(obj.get("DQFXZYGDN")); -// BigDecimal nowTotalChargeDate = StringUtils.getBigDecimal(obj.get("DQZXZYGDN")); - BigDecimal nowTotalDisChargeDate = currentData.getCurrentReverseActiveTotal(); - BigDecimal nowTotalChargeDate = currentData.getCurrentForwardActiveTotal(); - // 初始化当日数据-总的 - EmsDailyChargeData emsDailyChargeData = initDailyChargeData(siteId, deviceId, nowTotalChargeDate, nowTotalDisChargeDate); - // 获取redis存放昨日最晚数据 - String yestDate = DateUtils.getYesterdayDate(); - String yestDateRedisKey = RedisKeyConstants.AMMETER + siteId + "_" + deviceId + "_" + yestDate; - EmsAmmeterData yestData = redisCache.getCacheObject(yestDateRedisKey); - if (yestData == null) { - // redis没有这查电表总数据表取截止到昨日最新第一条数据 - yestData = emsAmmeterDataMapper.getYestLatestDate(siteId, deviceId, yestDate); - // 数据存redis-有效期1天 - redisCache.setCacheObject(yestDateRedisKey, yestData, Constants.DATE_VALID_TIME, TimeUnit.DAYS); - } - if (yestData != null) { - // 今日总数据-昨日总数据=今日充放电 - BigDecimal yestTotalDisChargeDate = yestData.getCurrentReverseActiveTotal(); - BigDecimal yestTotalChargeDate = yestData.getCurrentForwardActiveTotal(); - - dailyChargeDate = nowTotalChargeDate.subtract(yestTotalChargeDate); - dailyDisChargeDate = nowTotalDisChargeDate.subtract(yestTotalDisChargeDate); - emsDailyChargeData.setChargeData(dailyChargeDate); - emsDailyChargeData.setDischargeData(dailyDisChargeDate); - } - - // 插入或更新每日充放电数据表 - emsDailyChargeDataMapper.insertOrUpdateData(emsDailyChargeData); - - // 初始化数据-尖峰平谷 - EmsDailyEnergyData energyData = initEnergyData(siteId); - // 计算尖峰平谷差值,更新表 - calcEnergyDiffAndRevenue(siteId, energyData, currentData, yestData); - energyData.setCalcTime(DateUtils.getNowDate()); - // 插入或更新电表每日差值数据表 - emsDailyEnergyDataMapper.insertOrUpdateData(energyData); - } - private EmsDailyChargeData initDailyChargeData(String siteId, String deviceId, BigDecimal nowTotalChargeDate, BigDecimal nowTotalDisChargeDate) { EmsDailyChargeData emsDailyChargeData = new EmsDailyChargeData(); emsDailyChargeData.setSiteId(siteId); - emsDailyChargeData.setDeviceId(deviceId); emsDailyChargeData.setDateTime(DateUtils.getNowDate()); emsDailyChargeData.setTotalChargeData(nowTotalChargeDate); emsDailyChargeData.setTotalDischargeData(nowTotalDisChargeDate); @@ -2888,15 +3622,7 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return emsDailyChargeData; } - private void dealDailyEnergyData(String siteId, EmsAmmeterData currentData, EmsAmmeterData yestData) { - // 初始化数据-尖峰平谷 - EmsDailyEnergyData energyData = initEnergyData(siteId); - // 计算尖峰平谷差值,更新表 - calcEnergyDiffAndRevenue(siteId, energyData, currentData, yestData); - energyData.setCalcTime(DateUtils.getNowDate()); - // 插入或更新电表每日差值数据表 - emsDailyEnergyDataMapper.insertOrUpdateData(energyData); - } + private EmsAmmeterData dealDailyChargeDate(String siteId, String deviceId, EmsAmmeterData currentData) { // 初始化今日充放电 BigDecimal dailyDisChargeDate = new BigDecimal(0); @@ -2933,83 +3659,6 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return yestData; } - private void calcEnergyDiffAndRevenue(String siteId, EmsDailyEnergyData energyData, EmsAmmeterData currentData, EmsAmmeterData yestData) { - - // 获取当月电价 - String key = RedisKeyConstants.ENERGY_PRICE_TIME + siteId + "_" + LocalDate.now().getYear() + LocalDate.now().getMonthValue(); - EnergyPriceVo priceVo = redisCache.getCacheObject(key); - - // 计算尖峰平谷差值 - // 正反向-尖 -// BigDecimal peakChargeDiff = StringUtils.getBigDecimal(obj.get("DQZXYGJDN")) -// .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentForwardActivePeak()); - BigDecimal peakChargeDiff = currentData.getCurrentForwardActivePeak() - .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentForwardActivePeak()); - energyData.setPeakChargeDiff(peakChargeDiff); -// BigDecimal peakDischargeDiff = StringUtils.getBigDecimal(obj.get("DQFXYGJDN")) -// .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentReverseActivePeak()); - BigDecimal peakDischargeDiff = currentData.getCurrentReverseActivePeak() - .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentReverseActivePeak()); - energyData.setPeakDischargeDiff(peakDischargeDiff); - // 正反向-峰 -// BigDecimal highChargeDiff = StringUtils.getBigDecimal(obj.get("DQZXYGFDN")) -// .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentForwardActiveHigh()); - BigDecimal highChargeDiff = currentData.getCurrentForwardActiveHigh() - .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentForwardActiveHigh()); - energyData.setHighChargeDiff(highChargeDiff); -// BigDecimal highDischargeDiff = StringUtils.getBigDecimal(obj.get("DQFXYGFDN")) -// .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentReverseActiveHigh()); - BigDecimal highDischargeDiff = currentData.getCurrentReverseActiveHigh() - .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentReverseActiveHigh()); - energyData.setHighDischargeDiff(highDischargeDiff); - // 正反向-平 -// BigDecimal flatChargeDiff = StringUtils.getBigDecimal(obj.get("DQZXYGPDN")) -// .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentForwardActiveFlat()); - BigDecimal flatChargeDiff = currentData.getCurrentForwardActiveFlat() - .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentForwardActiveFlat()); - energyData.setFlatChargeDiff(flatChargeDiff); -// BigDecimal flatDisChargeDiff = StringUtils.getBigDecimal(obj.get("DQFXYGPDN")) -// .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentReverseActiveFlat()); - BigDecimal flatDisChargeDiff = currentData.getCurrentReverseActiveFlat() - .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentReverseActiveFlat()); - energyData.setFlatDischargeDiff(flatDisChargeDiff); - // 正反向-谷 -// BigDecimal valleyChargeDiff = StringUtils.getBigDecimal(obj.get("DQZXYGGDN")) -// .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentForwardActiveValley()); - BigDecimal valleyChargeDiff = currentData.getCurrentForwardActiveValley() - .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentForwardActiveValley()); - energyData.setValleyChargeDiff(valleyChargeDiff); -// BigDecimal valleyDisChargeDiff = StringUtils.getBigDecimal(obj.get("DQFXYGGDN")) -// .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentReverseActiveValley()); - BigDecimal valleyDisChargeDiff = currentData.getCurrentReverseActiveValley() - .subtract(yestData == null ? new BigDecimal(0) : yestData.getCurrentReverseActiveValley()); - energyData.setValleyDischargeDiff(valleyDisChargeDiff); - - - BigDecimal totalRevenue = getYestLastData(siteId); - BigDecimal dayRevenue = BigDecimal.ZERO; - BigDecimal price = BigDecimal.ZERO; - // 计算当日收益,尖峰平谷收益累加,(放电量-充电量)*电价 - if (priceVo != null) { - price = priceVo.getPeak() == null ? BigDecimal.ZERO : priceVo.getPeak(); - BigDecimal peakRevenue = peakDischargeDiff.subtract(peakChargeDiff).multiply(price); - dayRevenue = dayRevenue.add(peakRevenue); - price = priceVo.getHigh() == null ? BigDecimal.ZERO : priceVo.getHigh(); - BigDecimal highRevenue = highDischargeDiff.subtract(highChargeDiff).multiply(price); - dayRevenue = dayRevenue.add(highRevenue); - price = priceVo.getFlat() == null ? BigDecimal.ZERO : priceVo.getFlat(); - BigDecimal flatRevenue = flatDisChargeDiff.subtract(flatChargeDiff).multiply(price); - dayRevenue = dayRevenue.add(flatRevenue); - price = priceVo.getValley() == null ? BigDecimal.ZERO : priceVo.getValley(); - BigDecimal valleyRevenue = valleyDisChargeDiff.subtract(valleyChargeDiff).multiply(price); - dayRevenue = dayRevenue.add(valleyRevenue); - energyData.setDayRevenue(dayRevenue); - } - // 总收益 = 昨日总收益+今日实时收益 - totalRevenue = totalRevenue.add(dayRevenue); - energyData.setTotalRevenue(totalRevenue); - } - private BigDecimal getYestLastData(String siteId) { // dds存的是累计到昨日总收益 String yestDate = DateUtils.getYesterdayDayString(); @@ -3029,175 +3678,39 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i return yestLastTotalRevenue; } - private void dealAmmeterDailyDate(String siteId, EmsAmmeterData currentData, Date dataUpdateTime, EmsAmmeterData lastData) { - EmsDailyEnergyData energyData = initEnergyData(siteId); - energyData.setCalcTime(DateUtils.getNowDate()); - - // 先获取当月电价配置,redis没有这查数据库,都没有则返回 - String priceKey = RedisKeyConstants.ENERGY_PRICE_TIME + siteId + "_" + LocalDate.now().getYear() + LocalDate.now().getMonthValue(); - EnergyPriceVo priceVo = redisCache.getCacheObject(priceKey); - if (priceVo == null) { - priceVo = emsEnergyPriceConfigService.getCurrentMonthPrice(siteId); - redisCache.setCacheObject(priceKey, priceVo, 31, TimeUnit.DAYS); - if (priceVo == null) { - emsDailyEnergyDataMapper.insertOrUpdateData(energyData); - return; - } + private void syncDailyRevenueToChargeData(String siteId, Date revenueDate, BigDecimal totalRevenue, BigDecimal dayRevenue) { + if (StringUtils.isBlank(siteId)) { + return; } - List timeRanges = priceVo.getRange(); - if (timeRanges == null) { - emsDailyEnergyDataMapper.insertOrUpdateData(energyData); + Date persistDate = revenueDate == null ? DateUtils.getNowDate() : revenueDate; + BigDecimal persistTotalRevenue = safeBigDecimal(totalRevenue); + BigDecimal persistDayRevenue = safeBigDecimal(dayRevenue); + + int updated = emsDailyChargeDataMapper.updateRevenueBySiteAndDate(siteId, persistDate, persistTotalRevenue, persistDayRevenue, "system"); + if (updated > 0) { return; } - // 根据时间范围判断数据类型(尖峰平谷),无法确定数据类型则不处理 - String costType = ""; - String startTime = ""; - for (EnergyPriceTimeRange timeRange : timeRanges) { - startTime = timeRange.getStartTime(); - if (isInPriceTimeRange(startTime, timeRange.getEndTime(), dataUpdateTime)) { - costType = timeRange.getCostType(); - break; - } - } - if (StringUtils.isEmpty(costType)) { - emsDailyEnergyDataMapper.insertOrUpdateData(energyData); + EmsDailyChargeData existedData = emsDailyChargeDataMapper.selectBySiteIdAndDateTime(siteId, persistDate); + if (existedData != null) { + existedData.setTotalRevenue(persistTotalRevenue); + existedData.setDayRevenue(persistDayRevenue); + existedData.setUpdateBy("system"); + existedData.setUpdateTime(DateUtils.getNowDate()); + emsDailyChargeDataMapper.updateEmsDailyChargeData(existedData); return; } - // 根据 costType,计算本次与上次数据差值,累加到对应的数据类型里面 - setDiffByCostType(siteId, costType, energyData, lastData, currentData, priceVo); - - // 插入或更新电表每日差值数据表 - emsDailyEnergyDataMapper.insertOrUpdateData(energyData); - } - - private void setDiffByCostType(String siteId, String costType, EmsDailyEnergyData energyData, EmsAmmeterData lastData, - EmsAmmeterData currentData, EnergyPriceVo priceVo) { -// BigDecimal currentChargeData = StringUtils.getBigDecimal(obj.get("ZXYGDN")); -// BigDecimal currentDischargeData = StringUtils.getBigDecimal(obj.get("FXYGDN")); - BigDecimal currentChargeData = currentData.getCurrentForwardActiveTotal(); - BigDecimal currentDischargeData = currentData.getCurrentReverseActiveTotal(); - currentChargeData = currentChargeData != null ? currentChargeData : BigDecimal.ZERO; - currentDischargeData = currentDischargeData != null ? currentDischargeData : BigDecimal.ZERO; - - // 获取上次实时总收益+当日实时总收益,初始化电价 - Map revenueMap = getRealTimeData(siteId); - BigDecimal totalRevenue = revenueMap.get("totalRevenue") == null ? BigDecimal.ZERO : revenueMap.get("totalRevenue"); - BigDecimal dayRevenue = revenueMap.get("dayRevenue") == null ? BigDecimal.ZERO : revenueMap.get("dayRevenue"); - BigDecimal price = BigDecimal.ZERO; - // 计算时段差值,按照数据类型累加 - // 计算当日累加收益,尖峰平谷收益在当日原基础累加,(放电量-充电量)*电价 - BigDecimal chargeDiffData = currentChargeData.subtract( - lastData.getCurrentForwardActiveTotal() == null ? BigDecimal.ZERO : lastData.getCurrentForwardActiveTotal()); - BigDecimal disChargeDiffData = currentDischargeData.subtract( - lastData.getCurrentReverseActiveTotal() == null ? BigDecimal.ZERO : lastData.getCurrentReverseActiveTotal() - ); - switch (costType) { - case "peak": - // 增加电量 - BigDecimal peakCharge = energyData.getPeakChargeDiff() == null ? BigDecimal.ZERO : energyData.getPeakChargeDiff(); - energyData.setPeakChargeDiff(peakCharge.add(chargeDiffData)); - BigDecimal peakDischarge = energyData.getPeakDischargeDiff() == null ? BigDecimal.ZERO : energyData.getPeakDischargeDiff(); - energyData.setPeakDischargeDiff(peakDischarge.add(disChargeDiffData)); - - // 电价 - price = priceVo.getPeak() == null ? BigDecimal.ZERO : priceVo.getPeak(); - break; - case "high": - // 增加电量 - BigDecimal highCharge = energyData.getHighChargeDiff() == null ? BigDecimal.ZERO : energyData.getHighChargeDiff(); - energyData.setHighChargeDiff(highCharge.add(chargeDiffData)); - BigDecimal highDischarge = energyData.getHighDischargeDiff() == null ? BigDecimal.ZERO : energyData.getHighDischargeDiff(); - energyData.setHighDischargeDiff(highDischarge.add(disChargeDiffData)); - - // 电价 - price = priceVo.getHigh() == null ? BigDecimal.ZERO : priceVo.getHigh(); - break; - case "flat": - // 增加电量 - BigDecimal flatCharge = energyData.getFlatChargeDiff() == null ? BigDecimal.ZERO : energyData.getFlatChargeDiff(); - energyData.setFlatChargeDiff(flatCharge.add(chargeDiffData)); - BigDecimal flatDischarge = energyData.getFlatDischargeDiff() == null ? BigDecimal.ZERO : energyData.getFlatDischargeDiff(); - energyData.setFlatDischargeDiff(flatDischarge.add(disChargeDiffData)); - - // 电价 - price = priceVo.getFlat() == null ? BigDecimal.ZERO : priceVo.getFlat(); - break; - case "valley": - // 增加电量 - BigDecimal valleyCharge = energyData.getValleyChargeDiff() == null ? BigDecimal.ZERO : energyData.getValleyChargeDiff(); - energyData.setValleyChargeDiff(valleyCharge.add(chargeDiffData)); - BigDecimal valleyDischarge = energyData.getValleyDischargeDiff() == null ? BigDecimal.ZERO : energyData.getValleyDischargeDiff(); - energyData.setValleyDischargeDiff(valleyDischarge.add(disChargeDiffData)); - - // 电价 - price = priceVo.getValley() == null ? BigDecimal.ZERO : priceVo.getValley(); - break; - default: - return; - } - - // 计算本次累加收益 - BigDecimal addRevenue = disChargeDiffData.subtract(chargeDiffData).multiply(price); - dayRevenue = dayRevenue.add(addRevenue); - energyData.setDayRevenue(dayRevenue); - // 总收益 = 上次实时总收益+今日实时增加的收益 - totalRevenue = totalRevenue.add(addRevenue); - energyData.setTotalRevenue(totalRevenue); - - // 存redis便于下次取用 - String today = DateUtils.getDate(); - String redisKey = RedisKeyConstants.FXX_REALTIME_REVENUE + siteId + "_" + today; - Map realTimeRevenue = new HashMap<>(); - realTimeRevenue.put("totalRevenue", totalRevenue); - realTimeRevenue.put("dayRevenue", dayRevenue); - redisCache.setCacheObject(redisKey, realTimeRevenue, 1, TimeUnit.DAYS); - } - - private Map getRealTimeData(String siteId) { - // fx取实时总收益和当天实时收益 - String today = DateUtils.getDate(); - String redisKey = RedisKeyConstants.FXX_REALTIME_REVENUE + siteId + "_" + today; - Map realTimeRevenue = redisCache.getCacheObject(redisKey); - if (realTimeRevenue == null) { - // 查数据库 - realTimeRevenue = emsEnergyPriceConfigService.getDayRevenueMap(siteId); - if (realTimeRevenue == null) { - realTimeRevenue = new HashMap<>(); - realTimeRevenue.put("totalRevenue", BigDecimal.ZERO); - realTimeRevenue.put("dayRevenue", BigDecimal.ZERO); - } - redisCache.setCacheObject(redisKey, realTimeRevenue, 1, TimeUnit.DAYS); - } - return realTimeRevenue; - } - - private boolean isInPriceTimeRange(String startTime, String endTime, Date dataUpdateTime) { - if (startTime == null || endTime == null || dataUpdateTime == null) { - return false; - } - LocalDateTime time = DateUtils.toLocalDateTime(dataUpdateTime); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm"); - String dataTimeStr = time.format(formatter); - // 比较时间范围 - return dataTimeStr.compareTo(startTime) >= 0 - && dataTimeStr.compareTo(endTime) < 0; - } - - private EmsDailyEnergyData initEnergyData(String siteId) { - // 先获取数据库当天数据,存在则更新时间,不存在则初始化 - EmsDailyEnergyData energyData = emsDailyEnergyDataMapper.getDataByDate(siteId, DateUtils.getDate()); - if (energyData == null) { - energyData = new EmsDailyEnergyData(); - energyData.setSiteId(siteId); - energyData.setDataDate(DateUtils.getNowDate()); - energyData.setCreateBy("system"); - energyData.setCreateTime(DateUtils.getNowDate()); - } - energyData.setUpdateBy("system"); - energyData.setCalcTime(DateUtils.getNowDate()); - return energyData; + EmsDailyChargeData chargeData = new EmsDailyChargeData(); + chargeData.setSiteId(siteId); + chargeData.setDateTime(persistDate); + chargeData.setTotalRevenue(persistTotalRevenue); + chargeData.setDayRevenue(persistDayRevenue); + chargeData.setCreateBy("system"); + chargeData.setCreateTime(DateUtils.getNowDate()); + chargeData.setUpdateBy("system"); + chargeData.setUpdateTime(DateUtils.getNowDate()); + emsDailyChargeDataMapper.insertEmsDailyChargeData(chargeData); } // 数据分组处理 @@ -3564,19 +4077,16 @@ public class DeviceDataProcessServiceImpl extends AbstractBatteryDataProcessor i // 空数据不处理 private boolean checkJsonDataEmpty(String jsonData) { - boolean flag = false; + if (StringUtils.isEmpty(jsonData)) { + return true; + } try { - if (StringUtils.isEmpty(jsonData)) { - flag = true; - } JsonNode jsonNode = objectMapper.readTree(jsonData); // 判断是否为空对象({}) - if (jsonNode.isObject() && jsonNode.isEmpty()) { - flag = true; - } - } catch (JsonProcessingException e) { - throw new RuntimeException(e); + return jsonNode.isObject() && jsonNode.isEmpty(); + } catch (Exception e) { + log.warn("设备数据Data字段解析失败,按空数据处理,data: {}, err: {}", jsonData, e.getMessage()); + return true; } - return flag; } } diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java index aa9bd62..4336a71 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsDeviceSettingServiceImpl.java @@ -19,6 +19,7 @@ import com.xzzn.common.enums.WorkStatus; import com.xzzn.common.exception.ServiceException; import com.xzzn.common.utils.DateUtils; import com.xzzn.common.utils.StringUtils; +import com.xzzn.ems.domain.EmsDailyChargeData; import com.xzzn.ems.domain.EmsDevicesSetting; import com.xzzn.ems.domain.EmsPointEnumMatch; import com.xzzn.ems.domain.EmsPointConfig; @@ -32,11 +33,14 @@ import com.xzzn.ems.domain.vo.PointDataRequest; import com.xzzn.ems.domain.vo.PointQueryResponse; import com.xzzn.ems.domain.vo.SiteMonitorDataSaveItemVo; import com.xzzn.ems.domain.vo.SiteMonitorDataSaveRequest; +import com.xzzn.ems.domain.vo.SiteMonitorDataVo; import com.xzzn.ems.domain.vo.SiteMonitorProjectDisplayVo; import com.xzzn.ems.domain.vo.SiteMonitorProjectPointMappingSaveRequest; import com.xzzn.ems.domain.vo.SiteMonitorProjectPointMappingVo; import com.xzzn.ems.domain.vo.WorkStatusEnumMappingVo; import com.xzzn.ems.mapper.EmsBatteryDataMinutesMapper; +import com.xzzn.ems.mapper.EmsDailyChargeDataMapper; +import com.xzzn.ems.mapper.EmsDailyEnergyDataMapper; import com.xzzn.ems.mapper.EmsDevicesSettingMapper; import com.xzzn.ems.mapper.EmsPcsSettingMapper; import com.xzzn.ems.mapper.EmsPointConfigMapper; @@ -52,6 +56,7 @@ import com.xzzn.system.domain.SysOperLog; import com.xzzn.system.service.ISysOperLogService; import java.math.BigDecimal; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -123,6 +128,12 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService private static final long PROJECT_DISPLAY_CACHE_TTL_MS = 15_000L; private static final int MONITOR_POINT_MATCH_REDIS_TTL_SECONDS = 300; private static final int DISPLAY_DEBUG_SAMPLE_SIZE = 20; + private static final Set HOME_CHARGE_FIELD_SET = new HashSet<>(Arrays.asList( + "dayChargedCap", "dayDisChargedCap", "yesterdayChargedCap", "yesterdayDisChargedCap", "totalChargedCap", "totalDischargedCap" + )); + private static final Set HOME_REVENUE_FIELD_SET = new HashSet<>(Arrays.asList( + "totalRevenue", "dayRevenue", "yesterdayRevenue" + )); @Autowired private EmsDevicesSettingMapper emsDevicesMapper; @Autowired @@ -138,6 +149,10 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService @Autowired private EmsBatteryDataMinutesMapper emsBatteryDataMinutesMapper; @Autowired + private EmsDailyChargeDataMapper emsDailyChargeDataMapper; + @Autowired + private EmsDailyEnergyDataMapper emsDailyEnergyDataMapper; + @Autowired private EmsBatteryClusterServiceImpl emsBatteryClusterServiceImpl; @Autowired private ModbusProcessor modbusProcessor; @@ -695,7 +710,6 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService .filter(fieldCodeSet::contains) .forEach(deletedFieldCodeSet::add); } - validatePcsCurvePointMappings(siteId, request.getMappings(), deletedFieldCodeSet); int deletedRows = emsSiteMonitorPointMatchMapper.deleteBySiteId(siteId); List saveList = new ArrayList<>(); @@ -936,45 +950,6 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService return StringUtils.defaultString(deviceCategory) + "-" + StringUtils.defaultString(matchField); } - private void validatePcsCurvePointMappings(String siteId, List mappings, Set deletedFieldCodeSet) { - List> pcsDevices = emsDevicesMapper.getDeviceInfosBySiteIdAndCategory(siteId, DeviceCategory.PCS.getCode()); - Set pcsDeviceIdSet = new HashSet<>(); - if (pcsDevices != null) { - pcsDevices.stream() - .filter(Objects::nonNull) - .map(item -> item.get("id")) - .filter(Objects::nonNull) - .map(String::valueOf) - .map(String::trim) - .filter(StringUtils::isNotBlank) - .forEach(pcsDeviceIdSet::add); - } - validateSingleCurveFieldMapping(FIELD_CURVE_PCS_ACTIVE_POWER, "PCS有功功率曲线点位", pcsDeviceIdSet, mappings, deletedFieldCodeSet); - validateSingleCurveFieldMapping(FIELD_CURVE_PCS_REACTIVE_POWER, "PCS无功功率曲线点位", pcsDeviceIdSet, mappings, deletedFieldCodeSet); - } - - private void validateSingleCurveFieldMapping(String fieldCode, String fieldName, Set pcsDeviceIdSet, - List mappings, Set deletedFieldCodeSet) { - if (deletedFieldCodeSet != null && deletedFieldCodeSet.contains(fieldCode) && !pcsDeviceIdSet.isEmpty()) { - throw new ServiceException(fieldName + "不能删除,且配置数量必须与PCS设备数量一致"); - } - Set configuredDeviceIdSet = new HashSet<>(); - if (mappings != null) { - mappings.stream() - .filter(Objects::nonNull) - .filter(item -> fieldCode.equals(StringUtils.trim(item.getFieldCode()))) - .filter(item -> StringUtils.isNotBlank(item.getDataPoint())) - .map(SiteMonitorProjectPointMappingVo::getDeviceId) - .map(StringUtils::trim) - .filter(StringUtils::isNotBlank) - .forEach(configuredDeviceIdSet::add); - } - - if (configuredDeviceIdSet.size() != pcsDeviceIdSet.size() || !configuredDeviceIdSet.equals(pcsDeviceIdSet)) { - throw new ServiceException(String.format("%s数量需与PCS设备数量一致:PCS设备%d个,已配置%d个", fieldName, pcsDeviceIdSet.size(), configuredDeviceIdSet.size())); - } - } - private SiteMonitorProjectPointMappingVo buildMappingVo(EmsSiteMonitorItem item, EmsSiteMonitorPointMatch pointMatch, String deviceId, String deviceName) { SiteMonitorProjectPointMappingVo vo = new SiteMonitorProjectPointMappingVo(); @@ -1117,6 +1092,7 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService Map tjbbLatestMap = safeRedisMap(redisCache.getCacheMap(buildSiteMonitorLatestRedisKey(normalizedSiteId, MODULE_TJBB))); Map> enumDataCodeMapByScope = buildEnumDataCodeMapByScope(normalizedSiteId); Map pointSnapshotCache = new HashMap<>(); + HomeRunningSnapshot homeRunningSnapshot = buildHomeRunningSnapshot(normalizedSiteId); List result = new ArrayList<>(); int totalCount = 0; @@ -1138,6 +1114,18 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService result.add(vo); continue; } + if (MODULE_HOME.equals(mapping.getModuleCode()) && homeRunningSnapshot != null) { + String homeFieldKey = resolveHomeFieldKey(mapping.getFieldCode()); + if (StringUtils.isNotBlank(homeFieldKey)) { + String homeFieldValue = homeRunningSnapshot.getValue(homeFieldKey); + if (homeFieldValue != null) { + vo.setFieldValue(homeFieldValue); + vo.setValueTime(homeRunningSnapshot.getValueTime()); + result.add(vo); + continue; + } + } + } // 与“点位配置列表最新值”一致:按 pointId -> 点位配置(dataKey/deviceId) -> MQTT 最新报文读取 PointLatestSnapshot latestSnapshot = null; String dataPoint = StringUtils.trim(mapping.getDataPoint()); @@ -1207,6 +1195,91 @@ public class EmsDeviceSettingServiceImpl implements IEmsDeviceSettingService return result; } + private HomeRunningSnapshot buildHomeRunningSnapshot(String siteId) { + if (StringUtils.isBlank(siteId)) { + return null; + } + HomeRunningSnapshot snapshot = new HomeRunningSnapshot(); + String today = DateUtils.getDate(); + Map totalChargeData = emsDailyChargeDataMapper.getAllSiteChargeData(today, siteId); + if (totalChargeData != null) { + snapshot.put("totalChargedCap", totalChargeData.get("totalChargedCap")); + snapshot.put("totalDischargedCap", totalChargeData.get("totalDischargedCap")); + } + + LocalDate now = LocalDate.now(); + LocalDate yesterday = now.minusDays(1); + List chargeList = emsDailyChargeDataMapper.getSingleSiteChargeData( + siteId, + DateUtils.toDate(yesterday), + DateUtils.toDate(now) + ); + if (!CollectionUtils.isEmpty(chargeList)) { + for (SiteMonitorDataVo item : chargeList) { + if (item == null || StringUtils.isBlank(item.getAmmeterDate())) { + continue; + } + if (today.equals(item.getAmmeterDate())) { + snapshot.put("dayChargedCap", item.getChargedCap()); + snapshot.put("dayDisChargedCap", item.getDisChargedCap()); + } else if (DateUtils.getYesterdayDayString().equals(item.getAmmeterDate())) { + snapshot.put("yesterdayChargedCap", item.getChargedCap()); + snapshot.put("yesterdayDisChargedCap", item.getDisChargedCap()); + } + } + } + + EmsDailyChargeData todayChargeData = emsDailyChargeDataMapper.selectBySiteIdAndDateTime(siteId, DateUtils.toDate(now)); + EmsDailyChargeData yesterdayChargeData = emsDailyChargeDataMapper.selectBySiteIdAndDateTime(siteId, DateUtils.toDate(yesterday)); + if (todayChargeData != null) { + snapshot.put("totalRevenue", todayChargeData.getTotalRevenue()); + snapshot.put("dayRevenue", todayChargeData.getDayRevenue()); + } else { + Map latestRevenueData = emsDailyEnergyDataMapper.getRealTimeRevenue(siteId); + if (latestRevenueData != null) { + snapshot.put("totalRevenue", latestRevenueData.get("totalRevenue")); + snapshot.put("dayRevenue", latestRevenueData.get("dayRevenue")); + } + } + if (yesterdayChargeData != null) { + snapshot.put("yesterdayRevenue", yesterdayChargeData.getDayRevenue()); + } + return snapshot; + } + + private String resolveHomeFieldKey(String fieldCode) { + String normalizedFieldCode = StringUtils.trim(fieldCode); + if (StringUtils.isBlank(normalizedFieldCode)) { + return null; + } + int splitIndex = normalizedFieldCode.lastIndexOf("__"); + String fieldKey = splitIndex >= 0 ? normalizedFieldCode.substring(splitIndex + 2) : normalizedFieldCode; + if (HOME_CHARGE_FIELD_SET.contains(fieldKey) || HOME_REVENUE_FIELD_SET.contains(fieldKey)) { + return fieldKey; + } + return null; + } + + private static class HomeRunningSnapshot { + private final Map valueMap = new HashMap<>(); + private final Date valueTime = new Date(); + + public void put(String key, BigDecimal value) { + if (StringUtils.isBlank(key) || value == null) { + return; + } + valueMap.put(key, value.stripTrailingZeros().toPlainString()); + } + + public String getValue(String key) { + return valueMap.get(key); + } + + public Date getValueTime() { + return valueTime; + } + } + private Map> buildEnumDataCodeMapByScope(String siteId) { Map> result = new HashMap<>(); if (StringUtils.isBlank(siteId)) { diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsEnergyPriceConfigServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsEnergyPriceConfigServiceImpl.java index 682282a..8276e26 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsEnergyPriceConfigServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsEnergyPriceConfigServiceImpl.java @@ -4,11 +4,13 @@ import com.xzzn.common.constant.RedisKeyConstants; import com.xzzn.common.core.redis.RedisCache; import com.xzzn.common.utils.DateUtils; import com.xzzn.common.utils.StringUtils; +import com.xzzn.ems.domain.EmsDailyChargeData; import com.xzzn.ems.domain.EmsDailyEnergyData; import com.xzzn.ems.domain.EmsEnergyPriceConfig; import com.xzzn.ems.domain.EmsPriceTimeConfig; import com.xzzn.ems.domain.vo.EnergyPriceTimeRange; import com.xzzn.ems.domain.vo.EnergyPriceVo; +import com.xzzn.ems.mapper.EmsDailyChargeDataMapper; import com.xzzn.ems.mapper.EmsDailyEnergyDataMapper; import com.xzzn.ems.mapper.EmsEnergyPriceConfigMapper; import com.xzzn.ems.mapper.EmsPriceTimeConfigMapper; @@ -17,6 +19,7 @@ import com.xzzn.ems.service.IEmsEnergyPriceConfigService; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; +import java.util.Date; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -45,6 +48,8 @@ public class EmsEnergyPriceConfigServiceImpl implements IEmsEnergyPriceConfigSer @Autowired private RedisCache redisCache; @Autowired + private EmsDailyChargeDataMapper emsDailyChargeDataMapper; + @Autowired private EmsDailyEnergyDataMapper emsDailyEnergyDataMapper; /** @@ -280,22 +285,21 @@ public class EmsEnergyPriceConfigServiceImpl implements IEmsEnergyPriceConfigSer BigDecimal dayRevenue = BigDecimal.ZERO; BigDecimal yesterdayRevenue = BigDecimal.ZERO; // 获取昨日数据 - String yesterday = DateUtils.getYesterdayDayString(); - EmsDailyEnergyData yesterdayData = emsDailyEnergyDataMapper.getDataByDate(siteId, yesterday); + Date yesterday = DateUtils.toDate(LocalDate.now().minusDays(1)); + EmsDailyChargeData yesterdayData = emsDailyChargeDataMapper.selectBySiteIdAndDateTime(siteId, yesterday); if (yesterdayData != null) { yesterdayRevenue = yesterdayData.getDayRevenue() == null ? BigDecimal.ZERO : yesterdayData.getDayRevenue(); } // 当日实时数据 String today = DateUtils.getDate(); + Map lastData = emsDailyEnergyDataMapper.getRealTimeRevenue(siteId); + if (lastData != null) { + totalRevenue = lastData.get("totalRevenue") == null ? BigDecimal.ZERO : lastData.get("totalRevenue"); + } + EmsDailyEnergyData todayData = emsDailyEnergyDataMapper.getDataByDate(siteId,today); - if (todayData == null) { - Map lastData = emsDailyEnergyDataMapper.getRealTimeRevenue(siteId); - if (lastData != null) { - totalRevenue = lastData.get("totalRevenue") == null ? BigDecimal.ZERO : lastData.get("totalRevenue"); - } - } else { - totalRevenue = todayData.getTotalRevenue() == null ? BigDecimal.ZERO : todayData.getTotalRevenue(); + if (todayData != null) { // 获取当月电价 int currentMonth = LocalDate.now().getMonthValue(); int currentYear = LocalDate.now().getYear(); diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointCalcConfigServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointCalcConfigServiceImpl.java index 8d1b753..5fe7d81 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointCalcConfigServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointCalcConfigServiceImpl.java @@ -16,7 +16,7 @@ import java.util.stream.Collectors; @Service public class EmsPointCalcConfigServiceImpl implements IEmsPointCalcConfigService { - private static final Pattern CALC_EXPRESSION_PATTERN = Pattern.compile("^[0-9A-Za-z_+\\-*/().\\s]+$"); + private static final Pattern CALC_EXPRESSION_PATTERN = Pattern.compile("^[0-9A-Za-z_+\\-*/().,?:<>=!&|\\s]+$"); private static final String CALC_POINT_TYPE = "calc"; @Autowired @@ -91,7 +91,7 @@ public class EmsPointCalcConfigServiceImpl implements IEmsPointCalcConfigService throw new ServiceException("计算表达式不能为空"); } if (!CALC_EXPRESSION_PATTERN.matcher(expression).matches()) { - throw new ServiceException("计算表达式仅支持数字、字母、下划线、空格和四则运算符"); + throw new ServiceException("计算表达式仅支持数字、字母、下划线、空格、运算符和函数语法"); } } diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java index 21178d7..09e9515 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsPointConfigServiceImpl.java @@ -11,6 +11,7 @@ import com.xzzn.ems.domain.EmsPointConfig; import com.xzzn.ems.domain.vo.ImportPointTemplateRequest; import com.xzzn.ems.domain.vo.PointConfigCurveRequest; import com.xzzn.ems.domain.vo.PointConfigCurveValueVo; +import com.xzzn.ems.domain.vo.PointConfigGenerateRecentRequest; import com.xzzn.ems.domain.vo.PointConfigLatestValueItemVo; import com.xzzn.ems.domain.vo.PointConfigLatestValueRequest; import com.xzzn.ems.domain.vo.PointConfigLatestValueVo; @@ -30,7 +31,9 @@ import org.springframework.web.multipart.MultipartFile; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; +import java.lang.reflect.Field; import java.math.BigDecimal; +import java.math.RoundingMode; import java.nio.charset.StandardCharsets; import java.sql.Timestamp; import java.time.LocalDateTime; @@ -38,11 +41,16 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Locale; +import java.util.Objects; +import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -54,7 +62,13 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { private static final Pattern DB_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$"); private static final Pattern INTEGER_PATTERN = Pattern.compile("^-?\\d+$"); private static final Pattern DECIMAL_PATTERN = Pattern.compile("^-?\\d+(\\.\\d+)?$"); - private static final Pattern CALC_EXPRESSION_PATTERN = Pattern.compile("^[0-9A-Za-z_+\\-*/().\\s]+$"); + private static final Pattern CALC_EXPRESSION_PATTERN = Pattern.compile("^[0-9A-Za-z_+\\-*/().,?:<>=!&|\\s]+$"); + private static final Pattern AUTO_DAY_DIFF_FUNCTION_PATTERN = Pattern.compile("(?i)DAY_DIFF\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\)"); + private static final Pattern AUTO_MONTH_DIFF_FUNCTION_PATTERN = Pattern.compile("(?i)MONTH_DIFF\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\)"); + private static final Pattern AUTO_HOUR_DIFF_FUNCTION_PATTERN = Pattern.compile("(?i)HOUR_DIFF\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\)"); + private static final String AUTO_DAY_DIFF_CONTEXT_PREFIX = "__AUTO_DAY_DIFF__"; + private static final String AUTO_MONTH_DIFF_CONTEXT_PREFIX = "__AUTO_MONTH_DIFF__"; + private static final String AUTO_HOUR_DIFF_CONTEXT_PREFIX = "__AUTO_HOUR_DIFF__"; private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @Autowired @@ -80,6 +94,7 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { throw new ServiceException("站点ID不能为空"); } normalizeAndValidatePointConfig(pointConfig); + applyCollectDefaultsForInsert(pointConfig); pointConfig.setCreateBy(operName); pointConfig.setUpdateBy(operName); int rows; @@ -177,43 +192,67 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { throw new ServiceException("仅支持上传CSV文件"); } - boolean overwriteFlag = Boolean.TRUE.equals(overwrite); - int targetCount = emsPointConfigMapper.countBySiteId(siteId); - if (targetCount > 0 && !overwriteFlag) { - throw new ServiceException("目标站点已存在点位配置,请勾选“覆盖已存在点位数据”后重试"); - } - List pointConfigList = parseCsv(file, siteId); if (pointConfigList.isEmpty()) { throw new ServiceException("CSV没有可导入的数据"); } - if (targetCount > 0) { - emsPointConfigMapper.deleteBySiteId(siteId); + List pointIds = pointConfigList.stream() + .map(EmsPointConfig::getPointId) + .filter(StringUtils::isNotBlank) + .distinct() + .collect(Collectors.toList()); + Map existingPointMap = new HashMap<>(); + if (!pointIds.isEmpty()) { + List existingList = emsPointConfigMapper.selectBySiteIdAndPointIds(siteId, pointIds); + if (CollectionUtils.isNotEmpty(existingList)) { + for (EmsPointConfig existing : existingList) { + if (existing != null && StringUtils.isNotBlank(existing.getPointId())) { + existingPointMap.put(existing.getPointId(), existing); + } + } + } } - for (EmsPointConfig pointConfig : pointConfigList) { - pointConfig.setCreateBy(operName); - pointConfig.setUpdateBy(operName); - } - int importCount = 0; + List insertList = new ArrayList<>(); + int updateCount = 0; + int insertCount = 0; long importStart = System.currentTimeMillis(); - for (int fromIndex = 0; fromIndex < pointConfigList.size(); fromIndex += CSV_IMPORT_BATCH_SIZE) { - int toIndex = Math.min(fromIndex + CSV_IMPORT_BATCH_SIZE, pointConfigList.size()); - List batchList = pointConfigList.subList(fromIndex, toIndex); + for (EmsPointConfig pointConfig : pointConfigList) { + EmsPointConfig existing = existingPointMap.get(pointConfig.getPointId()); + if (existing == null) { + pointConfig.setCreateBy(operName); + pointConfig.setUpdateBy(operName); + insertList.add(pointConfig); + continue; + } + pointConfig.setId(existing.getId()); + pointConfig.setUpdateBy(operName); + try { + updateCount += emsPointConfigMapper.updateEmsPointConfigForImport(pointConfig); + } catch (Exception ex) { + throw translatePointConfigPersistenceException(ex, pointConfig.getPointId()); + } + } + + for (int fromIndex = 0; fromIndex < insertList.size(); fromIndex += CSV_IMPORT_BATCH_SIZE) { + int toIndex = Math.min(fromIndex + CSV_IMPORT_BATCH_SIZE, insertList.size()); + List batchList = insertList.subList(fromIndex, toIndex); long batchStart = System.currentTimeMillis(); try { - importCount += emsPointConfigMapper.insertBatchEmsPointConfig(batchList); + insertCount += emsPointConfigMapper.insertBatchEmsPointConfig(batchList); } catch (Exception ex) { throw translatePointConfigPersistenceException(ex, null); } - log.info("点位CSV导入批量入库完成,siteId={}, batch={}~{}, batchSize={}, costMs={}", + log.info("点位CSV导入批量新增完成,siteId={}, batch={}~{}, batchSize={}, costMs={}", siteId, fromIndex + 1, toIndex, batchList.size(), System.currentTimeMillis() - batchStart); } - log.info("点位CSV导入完成,siteId={}, total={}, costMs={}", siteId, importCount, System.currentTimeMillis() - importStart); + int importCount = updateCount + insertCount; + log.info("点位CSV导入完成,siteId={}, total={}, insert={}, update={}, costMs={}", + siteId, importCount, insertCount, updateCount, System.currentTimeMillis() - importStart); invalidatePointConfigCacheBySite(siteId); - return String.format("导入成功:站点 %s,点位 %d 条", siteId, importCount); + return String.format("导入成功:站点 %s,新增 %d 条,更新 %d 条", siteId, insertCount, updateCount); } @Override @@ -228,17 +267,21 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { public List getLatestValues(PointConfigLatestValueRequest request) { List result = new ArrayList<>(); if (request == null || request.getPoints() == null || request.getPoints().isEmpty()) { + log.warn("latestValues 查询参数为空,request: {}", request); return result; } - - Map> configMapCache = new HashMap<>(); + log.info("latestValues 开始查询,pointCount: {}", request.getPoints().size()); for (PointConfigLatestValueItemVo item : request.getPoints()) { - if (item == null || StringUtils.isAnyBlank(item.getSiteId(), item.getDeviceId(), item.getDataKey())) { + if (item == null || StringUtils.isBlank(item.getPointId())) { + log.warn("latestValues 跳过空点位项,item: {}", item); continue; } - PointConfigLatestValueVo latestValue = queryLatestValueFromRedis(item, configMapCache); + PointConfigLatestValueVo latestValue = queryLatestValueByPointIdFromRedis(item); result.add(latestValue); } + long hitCount = result.stream().filter(vo -> vo.getPointValue() != null).count(); + log.info("latestValues 查询结束,total: {}, hit: {}, miss: {}", + result.size(), hitCount, result.size() - hitCount); return result; } @@ -252,50 +295,495 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { if (StringUtils.isAnyBlank(siteId, pointId)) { return new ArrayList<>(); } - EmsPointConfig pointConfig = resolvePointConfigForCurve(siteId, pointId); Date[] range = resolveTimeRange(request); - return queryCurveDataFromInflux(siteId, pointId, pointConfig, range[0], range[1]); + return queryCurveDataFromInflux(siteId, pointId, range[0], range[1]); } - private PointConfigLatestValueVo queryLatestValueFromRedis(PointConfigLatestValueItemVo item, - Map> configMapCache) { + @Override + public String generateRecent7DaysData(PointConfigGenerateRecentRequest request) { + if (request == null || StringUtils.isAnyBlank(request.getSiteId(), request.getPointId())) { + throw new ServiceException("站点ID和点位ID不能为空"); + } + String siteId = StringUtils.trim(request.getSiteId()); + String pointId = StringUtils.trim(request.getPointId()); + String deviceId = StringUtils.trimToEmpty(request.getDeviceId()); + + EmsPointConfig targetConfig = selectPointConfigByExactPointId(siteId, pointId); + if (targetConfig == null || !"calc".equalsIgnoreCase(StringUtils.defaultString(targetConfig.getPointType()))) { + throw new ServiceException("仅计算点支持按表达式生成最近7天数据"); + } + String expression = StringUtils.trimToNull(targetConfig.getCalcExpression()); + if (StringUtils.isBlank(expression)) { + throw new ServiceException("计算点缺少表达式,无法生成数据"); + } + + List payloads = buildCalcRecentPayloads(siteId, deviceId, pointId, expression); + if (payloads.isEmpty()) { + throw new ServiceException("未生成有效数据,请检查表达式依赖点是否有历史数据"); + } + influxPointDataWriter.writeBatch(payloads); + return String.format("已生成点位 %s 最近7天数据,共 %d 条", pointId, payloads.size()); + } + + private List buildCalcRecentPayloads(String siteId, String deviceId, String pointId, String expression) { + CompiledExpression compiledExpression = new CompiledExpression(expression); + List evaluateTimes = buildEvaluateTimes(expression); + if (evaluateTimes.isEmpty()) { + return Collections.emptyList(); + } + EvaluateGranularity granularity = resolveEvaluateGranularity(expression); + LocalDateTime start = evaluateTimes.get(0); + LocalDateTime end = evaluateTimes.get(evaluateTimes.size() - 1); + + Set variables = extractExpressionVariables(expression); + Map> variableSeriesMap = loadVariableSeries(siteId, variables, start.minusDays(35), end); + Map periodDiffStateMap = new HashMap<>(); + initializeRecentPeriodDiffState(expression, evaluateTimes, variableSeriesMap, periodDiffStateMap); + List payloads = new ArrayList<>(evaluateTimes.size()); + for (LocalDateTime evaluateTime : evaluateTimes) { + Map contextValues = buildContextValuesByTime(evaluateTime, variableSeriesMap, granularity); + if (contextValues.isEmpty()) { + continue; + } + BigDecimal value; + try { + prepareAutoPeriodDiffContextValues(expression, contextValues, evaluateTime, periodDiffStateMap); + value = compiledExpression.evaluate(contextValues).asNumber().setScale(4, RoundingMode.HALF_UP); + } catch (IllegalArgumentException ex) { + continue; + } + payloads.add(new InfluxPointDataWriter.PointWritePayload( + siteId, + deviceId, + pointId, + value, + Timestamp.valueOf(evaluateTime) + )); + } + return payloads; + } + + private void initializeRecentPeriodDiffState(String expression, + List evaluateTimes, + Map> variableSeriesMap, + Map periodDiffStateMap) { + if (StringUtils.isBlank(expression) + || CollectionUtils.isEmpty(evaluateTimes) + || variableSeriesMap == null + || periodDiffStateMap == null) { + return; + } + LocalDateTime firstEvaluateTime = evaluateTimes.get(0); + initializeSingleRecentDiffState("DAY_DIFF", expression, AUTO_DAY_DIFF_FUNCTION_PATTERN, + "yyyy-MM-dd", firstEvaluateTime, variableSeriesMap, periodDiffStateMap); + initializeSingleRecentDiffState("MONTH_DIFF", expression, AUTO_MONTH_DIFF_FUNCTION_PATTERN, + "yyyy-MM", firstEvaluateTime, variableSeriesMap, periodDiffStateMap); + initializeSingleRecentDiffState("HOUR_DIFF", expression, AUTO_HOUR_DIFF_FUNCTION_PATTERN, + "yyyy-MM-dd HH", firstEvaluateTime, variableSeriesMap, periodDiffStateMap); + } + + private void initializeSingleRecentDiffState(String functionName, + String expression, + Pattern functionPattern, + String periodFormat, + LocalDateTime firstEvaluateTime, + Map> variableSeriesMap, + Map periodDiffStateMap) { + Set variables = extractAutoDiffVariables(expression, functionPattern); + if (variables.isEmpty()) { + return; + } + long previousMillis = Timestamp.valueOf(firstEvaluateTime).getTime() - 1; + if (previousMillis < Long.MIN_VALUE + 1) { + return; + } + String previousPeriod = DateUtils.parseDateToStr(periodFormat, new Date(previousMillis)); + for (String variable : variables) { + List values = variableSeriesMap.get(variable); + InfluxPointDataWriter.PointValue previous = findLatestInWindow(values, Long.MIN_VALUE, previousMillis); + if (previous == null || previous.getPointValue() == null) { + continue; + } + String stateKey = functionName.toUpperCase(Locale.ROOT) + "_" + variable.toUpperCase(Locale.ROOT); + PeriodDiffState state = periodDiffStateMap.computeIfAbsent(stateKey, k -> new PeriodDiffState()); + state.period = previousPeriod; + state.lastValue = previous.getPointValue(); + } + } + + private List buildEvaluateTimes(String expression) { + LocalDateTime now = LocalDateTime.now(); + List times = new ArrayList<>(); + EvaluateGranularity granularity = resolveEvaluateGranularity(expression); + boolean containsHourDiff = granularity == EvaluateGranularity.HOUR; + boolean containsMonthDiff = granularity == EvaluateGranularity.MONTH; + if (containsHourDiff) { + int totalHours = 24 * 7; + LocalDateTime latestCompletedHourEnd = now.withMinute(0).withSecond(0).withNano(0).minusSeconds(1); + for (int i = totalHours - 1; i >= 0; i--) { + times.add(latestCompletedHourEnd.minusHours(i)); + } + return times; + } + if (containsMonthDiff) { + times.add(now.withSecond(0).withNano(0)); + return times; + } + for (int i = 6; i >= 0; i--) { + LocalDateTime dayTime = now.minusDays(i).withHour(23).withMinute(59).withSecond(59).withNano(0); + if (dayTime.isAfter(now)) { + dayTime = now.withNano(0); + } + times.add(dayTime); + } + return times; + } + + private EvaluateGranularity resolveEvaluateGranularity(String expression) { + if (AUTO_HOUR_DIFF_FUNCTION_PATTERN.matcher(expression).find()) { + return EvaluateGranularity.HOUR; + } + if (AUTO_MONTH_DIFF_FUNCTION_PATTERN.matcher(expression).find()) { + return EvaluateGranularity.MONTH; + } + return EvaluateGranularity.DAY; + } + + private Set extractExpressionVariables(String expression) { + Set reserved = new HashSet<>(); + reserved.add("IF"); + reserved.add("DAY_DIFF"); + reserved.add("MONTH_DIFF"); + reserved.add("HOUR_DIFF"); + Set variables = new HashSet<>(); + for (ExprToken token : tokenizeExpression(expression)) { + if (token.type == ExprTokenType.IDENTIFIER) { + String identifier = StringUtils.defaultString(token.text).trim().toUpperCase(Locale.ROOT); + if (StringUtils.isNotBlank(identifier) && !reserved.contains(identifier)) { + variables.add(identifier); + } + } + } + return variables; + } + + private Map> loadVariableSeries(String siteId, + Set variables, + LocalDateTime start, + LocalDateTime end) { + Map> variableSeriesMap = new HashMap<>(); + if (variables == null || variables.isEmpty()) { + return variableSeriesMap; + } + Date startDate = Timestamp.valueOf(start); + Date endDate = Timestamp.valueOf(end); + for (String variable : variables) { + List values = influxPointDataWriter.queryCurveDataByPointKey(siteId, variable, startDate, endDate); + if (values == null) { + values = new ArrayList<>(); + } + values.sort(Comparator.comparing(InfluxPointDataWriter.PointValue::getDataTime, Comparator.nullsLast(Date::compareTo))); + variableSeriesMap.put(variable, values); + } + return variableSeriesMap; + } + + private Map buildContextValuesByTime(LocalDateTime evaluateTime, + Map> variableSeriesMap, + EvaluateGranularity granularity) { + Map context = new HashMap<>(); + if (variableSeriesMap == null || variableSeriesMap.isEmpty()) { + return context; + } + long evaluateMillis = Timestamp.valueOf(evaluateTime).getTime(); + long windowStartMillis = resolveWindowStartMillis(evaluateTime, granularity); + for (Map.Entry> entry : variableSeriesMap.entrySet()) { + InfluxPointDataWriter.PointValue latest = findLatestInWindow(entry.getValue(), windowStartMillis, evaluateMillis); + if (latest == null && granularity == EvaluateGranularity.MONTH) { + latest = findLatestInWindow(entry.getValue(), Long.MIN_VALUE, evaluateMillis); + } + if (latest == null || latest.getPointValue() == null) { + continue; + } + context.put(entry.getKey(), latest.getPointValue()); + } + return context; + } + + private long resolveWindowStartMillis(LocalDateTime evaluateTime, EvaluateGranularity granularity) { + if (granularity == EvaluateGranularity.HOUR) { + return Timestamp.valueOf(evaluateTime.withMinute(0).withSecond(0).withNano(0)).getTime(); + } + if (granularity == EvaluateGranularity.DAY) { + return Timestamp.valueOf(evaluateTime.withHour(0).withMinute(0).withSecond(0).withNano(0)).getTime(); + } + return Long.MIN_VALUE; + } + + private InfluxPointDataWriter.PointValue findLatestInWindow(List values, + long startMillisInclusive, + long endMillisInclusive) { + if (values == null || values.isEmpty()) { + return null; + } + InfluxPointDataWriter.PointValue latest = null; + for (InfluxPointDataWriter.PointValue value : values) { + if (value == null || value.getDataTime() == null) { + continue; + } + long currentMillis = value.getDataTime().getTime(); + if (currentMillis > endMillisInclusive) { + break; + } + if (currentMillis < startMillisInclusive) { + continue; + } + latest = value; + } + return latest; + } + + private enum EvaluateGranularity { + DAY, + HOUR, + MONTH + } + + private void prepareAutoPeriodDiffContextValues(String expression, + Map contextValues, + LocalDateTime evaluateTime, + Map periodDiffStateMap) { + if (StringUtils.isBlank(expression) || contextValues == null || contextValues.isEmpty()) { + return; + } + prepareSingleAutoDiffContext(expression, contextValues, evaluateTime, "DAY_DIFF", + AUTO_DAY_DIFF_FUNCTION_PATTERN, "yyyy-MM-dd", periodDiffStateMap); + prepareSingleAutoDiffContext(expression, contextValues, evaluateTime, "MONTH_DIFF", + AUTO_MONTH_DIFF_FUNCTION_PATTERN, "yyyy-MM", periodDiffStateMap); + prepareSingleAutoDiffContext(expression, contextValues, evaluateTime, "HOUR_DIFF", + AUTO_HOUR_DIFF_FUNCTION_PATTERN, "yyyy-MM-dd HH", periodDiffStateMap); + } + + private void prepareSingleAutoDiffContext(String expression, + Map contextValues, + LocalDateTime evaluateTime, + String functionName, + Pattern functionPattern, + String periodFormat, + Map periodDiffStateMap) { + Set variables = extractAutoDiffVariables(expression, functionPattern); + if (variables.isEmpty()) { + return; + } + String currentPeriod = DateUtils.parseDateToStr(periodFormat, Timestamp.valueOf(evaluateTime)); + for (String variable : variables) { + BigDecimal currentValue = contextValues.get(variable); + if (currentValue == null) { + continue; + } + BigDecimal diffValue = calculateAutoPeriodDiffValue(functionName, variable, currentValue, currentPeriod, periodDiffStateMap); + contextValues.put(resolveAutoDiffContextKey(functionName, variable), diffValue); + } + } + + private Set extractAutoDiffVariables(String expression, Pattern functionPattern) { + Set variables = new HashSet<>(); + java.util.regex.Matcher matcher = functionPattern.matcher(expression); + while (matcher.find()) { + String variable = StringUtils.defaultString(matcher.group(1)).trim().toUpperCase(Locale.ROOT); + if (StringUtils.isNotBlank(variable)) { + variables.add(variable); + } + } + return variables; + } + + private BigDecimal calculateAutoPeriodDiffValue(String functionName, + String variable, + BigDecimal currentValue, + String currentPeriod, + Map periodDiffStateMap) { + if (currentValue == null || StringUtils.isAnyBlank(functionName, variable, currentPeriod)) { + return BigDecimal.ZERO; + } + String stateKey = functionName.toUpperCase(Locale.ROOT) + "_" + variable.toUpperCase(Locale.ROOT); + PeriodDiffState state = periodDiffStateMap.computeIfAbsent(stateKey, k -> new PeriodDiffState()); + if (!StringUtils.equals(state.period, currentPeriod)) { + state.baseline = state.lastValue == null ? currentValue : state.lastValue; + state.period = currentPeriod; + state.periodDiffValue = currentValue.subtract(state.baseline); + } else if (state.baseline == null) { + state.baseline = currentValue; + state.periodDiffValue = currentValue.subtract(state.baseline); + } else { + state.periodDiffValue = currentValue.subtract(state.baseline); + } + state.lastValue = currentValue; + return state.periodDiffValue == null ? BigDecimal.ZERO : state.periodDiffValue; + } + + private static String resolveAutoDayDiffContextKey(String variable) { + return AUTO_DAY_DIFF_CONTEXT_PREFIX + StringUtils.defaultString(variable).trim().toUpperCase(Locale.ROOT); + } + + private static String resolveAutoMonthDiffContextKey(String variable) { + return AUTO_MONTH_DIFF_CONTEXT_PREFIX + StringUtils.defaultString(variable).trim().toUpperCase(Locale.ROOT); + } + + private static String resolveAutoHourDiffContextKey(String variable) { + return AUTO_HOUR_DIFF_CONTEXT_PREFIX + StringUtils.defaultString(variable).trim().toUpperCase(Locale.ROOT); + } + + private static String resolveAutoDiffContextKey(String functionName, String variable) { + if ("DAY_DIFF".equalsIgnoreCase(functionName)) { + return resolveAutoDayDiffContextKey(variable); + } + if ("MONTH_DIFF".equalsIgnoreCase(functionName)) { + return resolveAutoMonthDiffContextKey(variable); + } + if ("HOUR_DIFF".equalsIgnoreCase(functionName)) { + return resolveAutoHourDiffContextKey(variable); + } + return null; + } + + private PointConfigLatestValueVo queryLatestValueByPointIdFromRedis(PointConfigLatestValueItemVo item) { PointConfigLatestValueVo vo = new PointConfigLatestValueVo(); vo.setSiteId(item.getSiteId()); vo.setDeviceId(item.getDeviceId()); vo.setDataKey(item.getDataKey()); - String redisKey = RedisKeyConstants.ORIGINAL_MQTT_DATA + item.getSiteId() + "_" + item.getDeviceId(); - Object raw = redisCache.getCacheObject(redisKey); - if (raw == null) { + String pointId = StringUtils.trimToNull(item.getPointId()); + vo.setPointId(pointId); + if (StringUtils.isBlank(pointId)) { return vo; } + String hitRedisKey = pointId; + Object raw = redisCache.getCacheObject(pointId); + if (raw == null) { + log.warn("latestValues Redis未命中,siteId: {}, deviceId: {}, pointId: {}, dataKey: {}", + item.getSiteId(), item.getDeviceId(), pointId, item.getDataKey()); + return vo; + } + log.info("latestValues Redis命中,siteId: {}, deviceId: {}, pointId: {}, redisKey: {}, rawType: {}", + item.getSiteId(), item.getDeviceId(), pointId, hitRedisKey, raw.getClass().getName()); + JSONObject root = toJsonObject(raw); if (root == null) { - return vo; - } - JSONObject dataObject = extractDataObject(root); - if (dataObject == null) { + log.warn("latestValues Redis值无法转JSON,siteId: {}, deviceId: {}, pointId: {}, redisKey: {}, raw: {}", + item.getSiteId(), item.getDeviceId(), pointId, hitRedisKey, raw); return vo; } - Object rawValue = getValueIgnoreCase(dataObject, item.getDataKey()); - BigDecimal pointValue = StringUtils.getBigDecimal(rawValue); - if (pointValue != null) { - EmsPointConfig pointConfig = getPointConfig(item.getSiteId(), item.getDeviceId(), item.getDataKey(), configMapCache); - vo.setPointValue(convertPointValue(pointValue, pointConfig)); + Object rawValue = root.get("pointValue"); + BigDecimal pointValue = toBigDecimalOrNull(rawValue); + if (pointValue == null) { + pointValue = readBigDecimalField(raw, "pointValue"); } - vo.setDataTime(extractDataTime(root)); + if (pointValue != null) { + vo.setPointValue(pointValue); + } + + if (StringUtils.isBlank(vo.getSiteId())) { + vo.setSiteId(root.getString("siteId")); + if (StringUtils.isBlank(vo.getSiteId())) { + vo.setSiteId(readStringField(raw, "siteId")); + } + } + if (StringUtils.isBlank(vo.getDeviceId())) { + vo.setDeviceId(root.getString("deviceId")); + if (StringUtils.isBlank(vo.getDeviceId())) { + vo.setDeviceId(readStringField(raw, "deviceId")); + } + } + if (StringUtils.isBlank(vo.getPointId())) { + vo.setPointId(root.getString("pointId")); + if (StringUtils.isBlank(vo.getPointId())) { + vo.setPointId(readStringField(raw, "pointId")); + } + } + Date dataTime = extractCacheDataTime(root); + if (dataTime == null) { + dataTime = readDateField(raw, "dataTime"); + } + vo.setDataTime(dataTime); + log.info("latestValues 解析完成,siteId: {}, deviceId: {}, pointId: {}, dataKey: {}, pointValue: {}, dataTime: {}", + vo.getSiteId(), vo.getDeviceId(), vo.getPointId(), vo.getDataKey(), vo.getPointValue(), vo.getDataTime()); return vo; } - private List queryCurveDataFromInflux(String siteId, String pointId, - EmsPointConfig pointConfig, Date startTime, Date endTime) { - String influxPointKey = resolveInfluxPointKey(pointConfig, pointId); - if (StringUtils.isBlank(influxPointKey)) { + private BigDecimal toBigDecimalOrNull(Object value) { + if (value == null) { + return null; + } + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + try { + return new BigDecimal(value.toString()); + } catch (Exception ex) { + return null; + } + } + + private String readStringField(Object source, String fieldName) { + Object value = readFieldValue(source, fieldName); + return value == null ? null : String.valueOf(value); + } + + private BigDecimal readBigDecimalField(Object source, String fieldName) { + Object value = readFieldValue(source, fieldName); + return toBigDecimalOrNull(value); + } + + private Date readDateField(Object source, String fieldName) { + Object value = readFieldValue(source, fieldName); + if (value instanceof Date) { + return (Date) value; + } + return null; + } + + private Object readFieldValue(Object source, String fieldName) { + if (source == null || StringUtils.isBlank(fieldName)) { + return null; + } + Class current = source.getClass(); + while (current != null) { + try { + Field field = current.getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(source); + } catch (NoSuchFieldException ex) { + current = current.getSuperclass(); + } catch (Exception ex) { + return null; + } + } + return null; + } + + private Date extractCacheDataTime(JSONObject root) { + if (root == null) { + return null; + } + Date dataTime = root.getDate("dataTime"); + if (dataTime != null) { + return dataTime; + } + Long timestamp = root.getLong("timestamp"); + if (timestamp != null) { + return DateUtils.convertUpdateTime(timestamp); + } + return null; + } + + private List queryCurveDataFromInflux(String siteId, String pointId, Date startTime, Date endTime) { + if (StringUtils.isBlank(pointId)) { return new ArrayList<>(); } - List values = influxPointDataWriter.queryCurveDataByPointKey(siteId, influxPointKey, startTime, endTime); + List values = influxPointDataWriter.queryCurveDataByPointKey(siteId, pointId.trim(), startTime, endTime); if (values == null || values.isEmpty()) { return new ArrayList<>(); } @@ -491,35 +979,30 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { return configByKey.get(dataKey.toUpperCase(Locale.ROOT)); } - private String resolveInfluxPointKey(EmsPointConfig pointConfig, String pointId) { - if (pointConfig != null && StringUtils.isNotBlank(pointConfig.getPointId())) { - return pointConfig.getPointId().trim(); - } - if (StringUtils.isNotBlank(pointId)) { - return pointId.trim(); - } - return null; - } - - private EmsPointConfig resolvePointConfigForCurve(String siteId, String pointId) { + private EmsPointConfig selectPointConfigByExactPointId(String siteId, String pointId) { if (StringUtils.isAnyBlank(siteId, pointId)) { return null; } - String cacheKey = RedisKeyConstants.POINT_CONFIG_POINT + siteId + "_" + pointId; + String normalizedPointId = pointId.trim(); + String cacheKey = RedisKeyConstants.POINT_CONFIG_POINT + siteId + "_" + normalizedPointId; EmsPointConfig cached = redisCache.getCacheObject(cacheKey); if (cached != null) { return cached; } EmsPointConfig query = new EmsPointConfig(); query.setSiteId(siteId); - query.setPointId(pointId); + query.setPointId(normalizedPointId); List pointConfigs = emsPointConfigMapper.selectEmsPointConfigList(query); if (CollectionUtils.isEmpty(pointConfigs)) { return null; } - EmsPointConfig latest = pointConfigs.get(0); - redisCache.setCacheObject(cacheKey, latest); - return latest; + for (EmsPointConfig pointConfig : pointConfigs) { + if (pointConfig != null && StringUtils.equals(pointConfig.getPointId(), normalizedPointId)) { + redisCache.setCacheObject(cacheKey, pointConfig); + return pointConfig; + } + } + return null; } private BigDecimal convertPointValue(BigDecimal sourceValue, EmsPointConfig pointConfig) { @@ -534,6 +1017,702 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { .add(b); } + private static class PeriodDiffState { + private String period; + private BigDecimal baseline; + private BigDecimal lastValue; + private BigDecimal periodDiffValue; + } + + private static class MissingVariableException extends IllegalArgumentException { + private MissingVariableException(String variable) { + super("缺少变量: " + variable); + } + } + + private enum ExprTokenType { + NUMBER, + STRING, + IDENTIFIER, + OPERATOR, + LEFT_PAREN, + RIGHT_PAREN, + COMMA, + QUESTION, + COLON, + EOF + } + + private static class ExprToken { + private final ExprTokenType type; + private final String text; + private final BigDecimal number; + private final String stringValue; + + private ExprToken(ExprTokenType type, String text, BigDecimal number, String stringValue) { + this.type = type; + this.text = text; + this.number = number; + this.stringValue = stringValue; + } + + private static ExprToken number(BigDecimal value) { + return new ExprToken(ExprTokenType.NUMBER, null, value, null); + } + + private static ExprToken string(String value) { + return new ExprToken(ExprTokenType.STRING, null, null, value); + } + + private static ExprToken identifier(String value) { + return new ExprToken(ExprTokenType.IDENTIFIER, value, null, null); + } + + private static ExprToken operator(String value) { + return new ExprToken(ExprTokenType.OPERATOR, value, null, null); + } + + private static ExprToken symbol(ExprTokenType type, String text) { + return new ExprToken(type, text, null, null); + } + } + + private static class ExpressionValue { + private final BigDecimal numberValue; + private final String textValue; + + private ExpressionValue(BigDecimal numberValue, String textValue) { + this.numberValue = numberValue; + this.textValue = textValue; + } + + private static ExpressionValue ofNumber(BigDecimal numberValue) { + return new ExpressionValue(numberValue, null); + } + + private static ExpressionValue ofText(String textValue) { + return new ExpressionValue(null, textValue == null ? "" : textValue); + } + + private boolean isNumber() { + return numberValue != null; + } + + private BigDecimal asNumber() { + if (numberValue == null) { + throw new IllegalArgumentException("表达式值不是数值类型: " + textValue); + } + return numberValue; + } + + private String asText() { + return numberValue != null ? numberValue.stripTrailingZeros().toPlainString() : textValue; + } + + private boolean asBoolean() { + return isNumber() ? BigDecimal.ZERO.compareTo(numberValue) != 0 : StringUtils.isNotBlank(textValue); + } + } + + private interface ExpressionNode { + ExpressionValue evaluate(Map contextValues); + } + + private static class NumberNode implements ExpressionNode { + private final BigDecimal value; + + private NumberNode(BigDecimal value) { + this.value = value; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + return ExpressionValue.ofNumber(value); + } + } + + private static class StringNode implements ExpressionNode { + private final String value; + + private StringNode(String value) { + this.value = value; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + return ExpressionValue.ofText(value); + } + } + + private static class VariableNode implements ExpressionNode { + private final String name; + + private VariableNode(String name) { + this.name = name; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + BigDecimal value = contextValues.get(name); + if (value == null) { + throw new MissingVariableException(name); + } + return ExpressionValue.ofNumber(value); + } + + private String getName() { + return name; + } + } + + private static class UnaryNode implements ExpressionNode { + private final String operator; + private final ExpressionNode node; + + private UnaryNode(String operator, ExpressionNode node) { + this.operator = operator; + this.node = node; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + ExpressionValue value = node.evaluate(contextValues); + switch (operator) { + case "+": + return value; + case "-": + return ExpressionValue.ofNumber(value.asNumber().negate()); + case "!": + return value.asBoolean() ? ExpressionValue.ofNumber(BigDecimal.ZERO) : ExpressionValue.ofNumber(BigDecimal.ONE); + default: + throw new IllegalArgumentException("不支持的一元操作符: " + operator); + } + } + } + + private static class BinaryNode implements ExpressionNode { + private final String operator; + private final ExpressionNode left; + private final ExpressionNode right; + + private BinaryNode(String operator, ExpressionNode left, ExpressionNode right) { + this.operator = operator; + this.left = left; + this.right = right; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + if ("&&".equals(operator)) { + ExpressionValue leftValue = left.evaluate(contextValues); + if (!leftValue.asBoolean()) { + return ExpressionValue.ofNumber(BigDecimal.ZERO); + } + return right.evaluate(contextValues).asBoolean() + ? ExpressionValue.ofNumber(BigDecimal.ONE) + : ExpressionValue.ofNumber(BigDecimal.ZERO); + } + if ("||".equals(operator)) { + ExpressionValue leftValue = left.evaluate(contextValues); + if (leftValue.asBoolean()) { + return ExpressionValue.ofNumber(BigDecimal.ONE); + } + return right.evaluate(contextValues).asBoolean() + ? ExpressionValue.ofNumber(BigDecimal.ONE) + : ExpressionValue.ofNumber(BigDecimal.ZERO); + } + + ExpressionValue leftValue = left.evaluate(contextValues); + ExpressionValue rightValue = right.evaluate(contextValues); + switch (operator) { + case "+": + if (!leftValue.isNumber() || !rightValue.isNumber()) { + return ExpressionValue.ofText(leftValue.asText() + rightValue.asText()); + } + return ExpressionValue.ofNumber(leftValue.asNumber().add(rightValue.asNumber())); + case "-": + return ExpressionValue.ofNumber(leftValue.asNumber().subtract(rightValue.asNumber())); + case "*": + return ExpressionValue.ofNumber(leftValue.asNumber().multiply(rightValue.asNumber())); + case "/": + if (BigDecimal.ZERO.compareTo(rightValue.asNumber()) == 0) { + throw new IllegalArgumentException("除数不能为0"); + } + return ExpressionValue.ofNumber(leftValue.asNumber().divide(rightValue.asNumber(), 10, RoundingMode.HALF_UP)); + case ">": + return leftValue.asNumber().compareTo(rightValue.asNumber()) > 0 + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + case ">=": + return leftValue.asNumber().compareTo(rightValue.asNumber()) >= 0 + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + case "<": + return leftValue.asNumber().compareTo(rightValue.asNumber()) < 0 + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + case "<=": + return leftValue.asNumber().compareTo(rightValue.asNumber()) <= 0 + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + case "==": + if (leftValue.isNumber() && rightValue.isNumber()) { + return leftValue.asNumber().compareTo(rightValue.asNumber()) == 0 + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + } + return Objects.equals(leftValue.asText(), rightValue.asText()) + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + case "!=": + if (leftValue.isNumber() && rightValue.isNumber()) { + return leftValue.asNumber().compareTo(rightValue.asNumber()) != 0 + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + } + return !Objects.equals(leftValue.asText(), rightValue.asText()) + ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO); + default: + throw new IllegalArgumentException("不支持的操作符: " + operator); + } + } + } + + private static class TernaryNode implements ExpressionNode { + private final ExpressionNode condition; + private final ExpressionNode trueNode; + private final ExpressionNode falseNode; + + private TernaryNode(ExpressionNode condition, ExpressionNode trueNode, ExpressionNode falseNode) { + this.condition = condition; + this.trueNode = trueNode; + this.falseNode = falseNode; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + return condition.evaluate(contextValues).asBoolean() + ? trueNode.evaluate(contextValues) + : falseNode.evaluate(contextValues); + } + } + + private static class FunctionNode implements ExpressionNode { + private final String functionName; + private final List arguments; + + private FunctionNode(String functionName, List arguments) { + this.functionName = StringUtils.defaultString(functionName).trim().toUpperCase(Locale.ROOT); + this.arguments = arguments == null ? Collections.emptyList() : arguments; + } + + @Override + public ExpressionValue evaluate(Map contextValues) { + if (("DAY_DIFF".equals(functionName) + || "MONTH_DIFF".equals(functionName) + || "HOUR_DIFF".equals(functionName)) && arguments.size() == 1) { + ExpressionNode argument = arguments.get(0); + if (!(argument instanceof VariableNode)) { + throw new IllegalArgumentException(functionName + "单参数模式仅支持变量参数"); + } + String variable = ((VariableNode) argument).getName(); + BigDecimal autoDiffValue = contextValues.get(resolveAutoDiffContextKey(functionName, variable)); + if (autoDiffValue == null) { + throw new MissingVariableException(variable); + } + return ExpressionValue.ofNumber(autoDiffValue); + } + if ("DAY_DIFF".equals(functionName) + || "MONTH_DIFF".equals(functionName) + || "HOUR_DIFF".equals(functionName)) { + throw new IllegalArgumentException(functionName + "函数参数数量错误,需1个参数"); + } + throw new IllegalArgumentException("不支持的函数: " + functionName); + } + } + + private static class CompiledExpression { + private final ExpressionNode root; + + private CompiledExpression(String expression) { + List tokens = tokenizeExpression(expression); + ExpressionParser parser = new ExpressionParser(tokens); + this.root = parser.parseExpression(); + if (!parser.isEnd()) { + ExprToken token = parser.peek(); + throw new IllegalArgumentException("表达式尾部有多余内容: " + token.text); + } + } + + private ExpressionValue evaluate(Map contextValues) { + return root.evaluate(contextValues); + } + } + + private static class ExpressionParser { + private final List tokens; + private int index; + + private ExpressionParser(List tokens) { + this.tokens = tokens; + this.index = 0; + } + + private ExpressionNode parseExpression() { + return parseTernary(); + } + + private ExpressionNode parseTernary() { + ExpressionNode condition = parseOr(); + if (match(ExprTokenType.QUESTION)) { + ExpressionNode trueNode = parseTernary(); + expect(ExprTokenType.COLON, "三元表达式缺少 ':'"); + ExpressionNode falseNode = parseTernary(); + return new TernaryNode(condition, trueNode, falseNode); + } + return condition; + } + + private ExpressionNode parseOr() { + ExpressionNode left = parseAnd(); + while (matchOperator("||")) { + left = new BinaryNode("||", left, parseAnd()); + } + return left; + } + + private ExpressionNode parseAnd() { + ExpressionNode left = parseEquality(); + while (matchOperator("&&")) { + left = new BinaryNode("&&", left, parseEquality()); + } + return left; + } + + private ExpressionNode parseEquality() { + ExpressionNode left = parseComparison(); + while (true) { + if (matchOperator("==")) { + left = new BinaryNode("==", left, parseComparison()); + continue; + } + if (matchOperator("!=")) { + left = new BinaryNode("!=", left, parseComparison()); + continue; + } + break; + } + return left; + } + + private ExpressionNode parseComparison() { + ExpressionNode left = parseAddSub(); + while (true) { + if (matchOperator(">=")) { + left = new BinaryNode(">=", left, parseAddSub()); + continue; + } + if (matchOperator("<=")) { + left = new BinaryNode("<=", left, parseAddSub()); + continue; + } + if (matchOperator(">")) { + left = new BinaryNode(">", left, parseAddSub()); + continue; + } + if (matchOperator("<")) { + left = new BinaryNode("<", left, parseAddSub()); + continue; + } + break; + } + return left; + } + + private ExpressionNode parseAddSub() { + ExpressionNode left = parseMulDiv(); + while (true) { + if (matchOperator("+")) { + left = new BinaryNode("+", left, parseMulDiv()); + continue; + } + if (matchOperator("-")) { + left = new BinaryNode("-", left, parseMulDiv()); + continue; + } + break; + } + return left; + } + + private ExpressionNode parseMulDiv() { + ExpressionNode left = parseUnary(); + while (true) { + if (matchOperator("*")) { + left = new BinaryNode("*", left, parseUnary()); + continue; + } + if (matchOperator("/")) { + left = new BinaryNode("/", left, parseUnary()); + continue; + } + break; + } + return left; + } + + private ExpressionNode parseUnary() { + if (matchOperator("+")) { + return new UnaryNode("+", parseUnary()); + } + if (matchOperator("-")) { + return new UnaryNode("-", parseUnary()); + } + if (matchOperator("!")) { + return new UnaryNode("!", parseUnary()); + } + return parsePrimary(); + } + + private ExpressionNode parsePrimary() { + ExprToken token = peek(); + if (match(ExprTokenType.NUMBER)) { + return new NumberNode(token.number); + } + if (match(ExprTokenType.STRING)) { + return new StringNode(token.stringValue); + } + if (match(ExprTokenType.IDENTIFIER)) { + String identifier = token.text; + if (match(ExprTokenType.LEFT_PAREN)) { + return parseFunction(identifier); + } + return new VariableNode(identifier.toUpperCase(Locale.ROOT)); + } + if (match(ExprTokenType.LEFT_PAREN)) { + ExpressionNode nested = parseExpression(); + expect(ExprTokenType.RIGHT_PAREN, "括号不匹配,缺少右括号"); + return nested; + } + throw new IllegalArgumentException("表达式语法错误,当前位置: " + token.text); + } + + private ExpressionNode parseFunction(String identifier) { + if ("IF".equalsIgnoreCase(identifier)) { + ExpressionNode condition = parseExpression(); + expect(ExprTokenType.COMMA, "IF函数参数格式错误,缺少第1个逗号"); + ExpressionNode trueNode = parseExpression(); + expect(ExprTokenType.COMMA, "IF函数参数格式错误,缺少第2个逗号"); + ExpressionNode falseNode = parseExpression(); + expect(ExprTokenType.RIGHT_PAREN, "IF函数缺少右括号"); + return new TernaryNode(condition, trueNode, falseNode); + } + if ("DAY_DIFF".equalsIgnoreCase(identifier) + || "MONTH_DIFF".equalsIgnoreCase(identifier) + || "HOUR_DIFF".equalsIgnoreCase(identifier)) { + List arguments = parseFunctionArguments(identifier); + if (arguments.size() != 1) { + throw new IllegalArgumentException(identifier.toUpperCase(Locale.ROOT) + "函数参数数量错误,需1个参数"); + } + return new FunctionNode(identifier, arguments); + } + throw new IllegalArgumentException("不支持的函数: " + identifier); + } + + private List parseFunctionArguments(String functionName) { + List arguments = new ArrayList<>(); + if (match(ExprTokenType.RIGHT_PAREN)) { + return arguments; + } + while (true) { + arguments.add(parseExpression()); + if (match(ExprTokenType.COMMA)) { + continue; + } + expect(ExprTokenType.RIGHT_PAREN, functionName + "函数缺少右括号"); + return arguments; + } + } + + private ExprToken peek() { + if (index >= tokens.size()) { + return ExprToken.symbol(ExprTokenType.EOF, ""); + } + return tokens.get(index); + } + + private boolean isEnd() { + return peek().type == ExprTokenType.EOF; + } + + private boolean match(ExprTokenType type) { + if (peek().type != type) { + return false; + } + index++; + return true; + } + + private boolean matchOperator(String operator) { + ExprToken token = peek(); + if (token.type != ExprTokenType.OPERATOR || !operator.equals(token.text)) { + return false; + } + index++; + return true; + } + + private void expect(ExprTokenType type, String message) { + if (!match(type)) { + throw new IllegalArgumentException(message); + } + } + } + + private static List tokenizeExpression(String expression) { + if (StringUtils.isBlank(expression)) { + throw new IllegalArgumentException("计算表达式为空"); + } + List tokens = new ArrayList<>(); + int index = 0; + while (index < expression.length()) { + char ch = expression.charAt(index); + if (Character.isWhitespace(ch)) { + index++; + continue; + } + if (Character.isDigit(ch) || ch == '.') { + int start = index; + boolean hasDot = ch == '.'; + index++; + while (index < expression.length()) { + char next = expression.charAt(index); + if (Character.isDigit(next)) { + index++; + continue; + } + if (next == '.' && !hasDot) { + hasDot = true; + index++; + continue; + } + break; + } + String text = expression.substring(start, index); + try { + tokens.add(ExprToken.number(new BigDecimal(text))); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("数值格式错误: " + text); + } + continue; + } + if (Character.isLetter(ch) || ch == '_') { + int start = index; + index++; + while (index < expression.length()) { + char next = expression.charAt(index); + if (Character.isLetterOrDigit(next) || next == '_') { + index++; + continue; + } + break; + } + String text = expression.substring(start, index).trim().toUpperCase(Locale.ROOT); + tokens.add(ExprToken.identifier(text)); + continue; + } + if (ch == '\'' || ch == '"') { + char quote = ch; + index++; + StringBuilder sb = new StringBuilder(); + boolean escaped = false; + while (index < expression.length()) { + char current = expression.charAt(index++); + if (escaped) { + switch (current) { + case 'n': + sb.append('\n'); + break; + case 't': + sb.append('\t'); + break; + case 'r': + sb.append('\r'); + break; + case '\\': + sb.append('\\'); + break; + case '\'': + sb.append('\''); + break; + case '"': + sb.append('"'); + break; + default: + sb.append(current); + break; + } + escaped = false; + continue; + } + if (current == '\\') { + escaped = true; + continue; + } + if (current == quote) { + break; + } + sb.append(current); + } + if (escaped || index > expression.length() || expression.charAt(index - 1) != quote) { + throw new IllegalArgumentException("字符串字面量未闭合"); + } + tokens.add(ExprToken.string(sb.toString())); + continue; + } + if (ch == '(') { + tokens.add(ExprToken.symbol(ExprTokenType.LEFT_PAREN, "(")); + index++; + continue; + } + if (ch == ')') { + tokens.add(ExprToken.symbol(ExprTokenType.RIGHT_PAREN, ")")); + index++; + continue; + } + if (ch == ',') { + tokens.add(ExprToken.symbol(ExprTokenType.COMMA, ",")); + index++; + continue; + } + if (ch == '?') { + tokens.add(ExprToken.symbol(ExprTokenType.QUESTION, "?")); + index++; + continue; + } + if (ch == ':') { + tokens.add(ExprToken.symbol(ExprTokenType.COLON, ":")); + index++; + continue; + } + if (index + 1 < expression.length()) { + String twoChars = expression.substring(index, index + 2); + if ("&&".equals(twoChars) + || "||".equals(twoChars) + || ">=".equals(twoChars) + || "<=".equals(twoChars) + || "==".equals(twoChars) + || "!=".equals(twoChars)) { + tokens.add(ExprToken.operator(twoChars)); + index += 2; + continue; + } + } + if (ch == '+' || ch == '-' || ch == '*' || ch == '/' + || ch == '>' || ch == '<' || ch == '!') { + tokens.add(ExprToken.operator(String.valueOf(ch))); + index++; + continue; + } + throw new IllegalArgumentException("表达式包含非法字符: " + ch); + } + tokens.add(ExprToken.symbol(ExprTokenType.EOF, "")); + return tokens; + } + private List parseCsv(MultipartFile file, String siteId) { List result = new ArrayList<>(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) { @@ -573,8 +1752,15 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { pointConfig.setIsAlarm(getInteger(valueList, headerIndex, "is_alarm", lineNo)); pointConfig.setPointType(getString(valueList, headerIndex, "point_type")); pointConfig.setCalcExpression(getString(valueList, headerIndex, "calc_expression")); + pointConfig.setCollectEnabled(getInteger(valueList, headerIndex, "collect_enabled", lineNo)); + pointConfig.setCollectSource(getString(valueList, headerIndex, "collect_source")); + pointConfig.setModbusRegisterType(getString(valueList, headerIndex, "modbus_register_type")); + pointConfig.setModbusDataType(getString(valueList, headerIndex, "modbus_data_type")); + pointConfig.setModbusReadOrder(getInteger(valueList, headerIndex, "modbus_read_order", lineNo)); + pointConfig.setModbusGroup(getString(valueList, headerIndex, "modbus_group")); pointConfig.setRemark(getString(valueList, headerIndex, "remark")); normalizeAndValidatePointConfig(pointConfig); + applyCollectDefaultsForInsert(pointConfig); result.add(pointConfig); } } catch (IOException e) { @@ -717,6 +1903,24 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { case "calcexpression": case "计算表达式": return "calc_expression"; + case "collectenabled": + case "是否启用采集": + return "collect_enabled"; + case "collectsource": + case "采集来源": + return "collect_source"; + case "modbusregistertype": + case "modbus寄存器类型": + return "modbus_register_type"; + case "modbusdatatype": + case "modbus数据类型": + return "modbus_data_type"; + case "modbusreadorder": + case "modbus读取顺序": + return "modbus_read_order"; + case "modbusgroup": + case "modbus分组": + return "modbus_group"; case "remark": case "备注": return "remark"; @@ -764,6 +1968,9 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { } pointConfig.setDeviceCategory(StringUtils.trimToNull(pointConfig.getDeviceCategory())); pointConfig.setDeviceId(StringUtils.trimToNull(pointConfig.getDeviceId())); + pointConfig.setPointName(StringUtils.trimToNull(pointConfig.getPointName())); + pointConfig.setDataKey(StringUtils.trimToNull(pointConfig.getDataKey())); + pointConfig.setPointDesc(StringUtils.trimToNull(pointConfig.getPointDesc())); pointConfig.setPointType(normalizePointType(pointConfig.getPointType())); pointConfig.setCalcExpression(StringUtils.trimToNull(pointConfig.getCalcExpression())); if ("calc".equals(pointConfig.getPointType())) { @@ -771,13 +1978,28 @@ public class EmsPointConfigServiceImpl implements IEmsPointConfigService { throw new ServiceException("计算点必须填写计算表达式"); } if (!CALC_EXPRESSION_PATTERN.matcher(pointConfig.getCalcExpression()).matches()) { - throw new ServiceException("计算表达式仅支持数字、字母、下划线、空格和四则运算符"); + throw new ServiceException("计算表达式仅支持数字、字母、下划线、空格、运算符和函数语法"); } } else { pointConfig.setCalcExpression(null); } } + private void applyCollectDefaultsForInsert(EmsPointConfig pointConfig) { + if (pointConfig == null) { + return; + } + if (pointConfig.getCollectEnabled() == null) { + pointConfig.setCollectEnabled(0); + } + if (StringUtils.isBlank(pointConfig.getCollectSource())) { + pointConfig.setCollectSource("MQTT"); + } + if (pointConfig.getModbusReadOrder() == null) { + pointConfig.setModbusReadOrder(0); + } + } + private String normalizePointType(String pointType) { String normalized = StringUtils.trimToEmpty(pointType).toLowerCase(Locale.ROOT); if ("calc".equals(normalized) || "calculate".equals(normalized) || "计算点".equals(normalized)) { diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStatsReportServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStatsReportServiceImpl.java index f9d36d6..d730df3 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStatsReportServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/EmsStatsReportServiceImpl.java @@ -401,13 +401,8 @@ public class EmsStatsReportServiceImpl implements IEmsStatsReportService ammeterRevenue.setActiveTotalPrice(activePeakPrice.add(activeHighPrice).add(activeFlatPrice).add(activeValleyPrice)); ammeterRevenue.setReActiveTotalPrice(reActivePeakPrice.add(reActiveHighPrice).add(reActiveFlatPrice).add(reActiveValleyPrice)); - // 实际收益=放电价格(尖峰平谷)-充电价格(尖峰平谷) - ammeterRevenue.setActualRevenue( - reActivePeakPrice.subtract(activePeakPrice) - .add(reActiveHighPrice.subtract(activeHighPrice)) - .add(reActiveFlatPrice.subtract(activeFlatPrice)) - .add(reActiveValleyPrice.subtract(activeValleyPrice)) - ); + // 实际收益按“放电总价-充电总价”口径计算 + ammeterRevenue.setActualRevenue(ammeterRevenue.getReActiveTotalPrice().subtract(ammeterRevenue.getActiveTotalPrice())); }); int weatherMissingCount = 0; diff --git a/ems-system/src/main/java/com/xzzn/ems/service/impl/SingleSiteServiceImpl.java b/ems-system/src/main/java/com/xzzn/ems/service/impl/SingleSiteServiceImpl.java index 2d8a8f1..a7e9b63 100644 --- a/ems-system/src/main/java/com/xzzn/ems/service/impl/SingleSiteServiceImpl.java +++ b/ems-system/src/main/java/com/xzzn/ems/service/impl/SingleSiteServiceImpl.java @@ -12,6 +12,7 @@ import com.xzzn.ems.domain.EmsBatteryData; import com.xzzn.ems.domain.EmsBatteryStack; import com.xzzn.ems.domain.EmsCoolingData; import com.xzzn.ems.domain.EmsDhData; +import com.xzzn.ems.domain.EmsDailyChargeData; import com.xzzn.ems.domain.EmsDevicesSetting; import com.xzzn.ems.domain.EmsEmsData; import com.xzzn.ems.domain.EmsPcsBranchData; @@ -215,6 +216,60 @@ public class SingleSiteServiceImpl implements ISingleSiteService { return siteMonitorHomeVo; } + @Override + public SiteMonitorHomeVo getSiteMonitorTotalDataVo(String siteId) { + SiteMonitorHomeVo siteMonitorHomeVo = new SiteMonitorHomeVo(); + if (StringUtils.isEmpty(siteId)) { + return siteMonitorHomeVo; + } + + String nowDate = DateUtils.getDate(); + Map chargeDataMap = emsDailyChargeDataMapper.getAllSiteChargeData(nowDate, siteId); + BigDecimal totalChargedCap = BigDecimal.ZERO; + BigDecimal totalDischargedCap = BigDecimal.ZERO; + if (chargeDataMap != null) { + totalChargedCap = chargeDataMap.get("totalChargedCap") == null ? BigDecimal.ZERO : chargeDataMap.get("totalChargedCap"); + totalDischargedCap = chargeDataMap.get("totalDischargedCap") == null ? BigDecimal.ZERO : chargeDataMap.get("totalDischargedCap"); + } + siteMonitorHomeVo.setTotalChargedCap(totalChargedCap); + siteMonitorHomeVo.setTotalDischargedCap(totalDischargedCap); + + Map revenueMap = emsDailyEnergyDataMapper.getRealTimeRevenue(siteId); + siteMonitorHomeVo.setTotalRevenue(revenueMap == null || revenueMap.get("totalRevenue") == null ? BigDecimal.ZERO : revenueMap.get("totalRevenue")); + siteMonitorHomeVo.setDayRevenue(revenueMap == null || revenueMap.get("dayRevenue") == null ? BigDecimal.ZERO : revenueMap.get("dayRevenue")); + + Date yesterdayDate = DateUtils.toDate(LocalDate.now().minusDays(1)); + EmsDailyChargeData yesterdayChargeData = emsDailyChargeDataMapper.selectBySiteIdAndDateTime(siteId, yesterdayDate); + siteMonitorHomeVo.setYesterdayRevenue(yesterdayChargeData == null || yesterdayChargeData.getDayRevenue() == null + ? BigDecimal.ZERO + : yesterdayChargeData.getDayRevenue()); + + List alarmList = emsAlarmRecordsMapper.getAlarmRecordsBySiteId(siteId); + siteMonitorHomeVo.setSiteMonitorHomeAlarmVo(alarmList); + + LocalDate sevenDaysAgo = LocalDate.now().minusDays(6); + Date startDate = DateUtils.toDate(sevenDaysAgo); + Date endDate = new Date(); + List siteMonitorDataVoList = emsDailyChargeDataMapper.getSingleSiteChargeData(siteId, startDate, endDate); + if (!CollectionUtils.isEmpty(siteMonitorDataVoList)) { + for (SiteMonitorDataVo sitePcsData : siteMonitorDataVoList) { + String pcsDate = sitePcsData.getAmmeterDate(); + if (DateUtils.checkIsToday(pcsDate)) { + siteMonitorHomeVo.setDayChargedCap(sitePcsData.getChargedCap()); + siteMonitorHomeVo.setDayDisChargedCap(sitePcsData.getDisChargedCap()); + } + if (DateUtils.getYesterdayDayString().equals(pcsDate)) { + siteMonitorHomeVo.setYesterdayChargedCap(sitePcsData.getChargedCap()); + siteMonitorHomeVo.setYesterdayDisChargedCap(sitePcsData.getDisChargedCap()); + } + } + } + + siteMonitorHomeVo.setEnergyStorageAvailElec(totalDischargedCap.subtract(totalChargedCap)); + siteMonitorHomeVo.setSiteMonitorDataVo(siteMonitorDataVoList); + return siteMonitorHomeVo; + } + // 获取单站监控实时运行头部数据 @Override public SiteMonitorRunningHeadInfoVo getSiteRunningHeadInfo(String siteId) { diff --git a/ems-system/src/main/resources/mapper/ems/EmsDailyChageDataMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsDailyChageDataMapper.xml index 4b1fb36..da078cd 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsDailyChageDataMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsDailyChageDataMapper.xml @@ -7,12 +7,13 @@ - + + @@ -21,20 +22,22 @@ - select id, site_id, device_id, date_time, total_charge_Data, total_discharge_Data, charge_data, discharge_data, create_by, create_time, update_by, update_time, remark from ems_daily_charge_data + select id, site_id, date_time, total_charge_Data, total_discharge_Data, charge_data, discharge_data, total_revenue, day_revenue, create_by, create_time, update_by, update_time, remark from ems_daily_charge_data + + insert into ems_daily_charge_data site_id, - device_id, date_time, total_charge_Data, total_discharge_Data, charge_data, discharge_data, + total_revenue, + day_revenue, create_by, create_time, update_by, @@ -60,12 +72,13 @@ #{siteId}, - #{deviceId}, #{dateTime}, #{totalChargeData}, #{totalDischargeData}, #{chargeData}, #{dischargeData}, + #{totalRevenue}, + #{dayRevenue}, #{createBy}, #{createTime}, #{updateBy}, @@ -78,12 +91,13 @@ update ems_daily_charge_data site_id = #{siteId}, - device_id = #{deviceId}, date_time = #{dateTime}, total_charge_Data = #{totalChargeData}, total_discharge_Data = #{totalDischargeData}, charge_data = #{chargeData}, discharge_data = #{dischargeData}, + total_revenue = #{totalRevenue}, + day_revenue = #{dayRevenue}, create_by = #{createBy}, create_time = #{createTime}, update_by = #{updateBy}, @@ -108,26 +122,28 @@ INSERT into ems_daily_charge_data ( id, site_id, - device_id, date_time, total_charge_Data, total_discharge_Data, charge_data, discharge_data, + total_revenue, + day_revenue, create_by, create_time, update_by, update_time, remark - ) values ( + ) values ( #{id}, #{siteId}, - #{deviceId}, #{dateTime}, #{totalChargeData}, #{totalDischargeData}, #{chargeData}, #{dischargeData}, + #{totalRevenue}, + #{dayRevenue}, #{createBy}, #{createTime}, #{updateBy}, @@ -139,9 +155,21 @@ total_discharge_Data = #{totalDischargeData}, charge_data = #{chargeData}, discharge_data = #{dischargeData}, + total_revenue = IFNULL(#{totalRevenue}, total_revenue), + day_revenue = IFNULL(#{dayRevenue}, day_revenue), update_time = NOW() + + update ems_daily_charge_data + set total_revenue = #{totalRevenue}, + day_revenue = #{dayRevenue}, + update_by = #{updateBy}, + update_time = NOW() + where site_id = #{siteId} + and date(date_time) = date(#{dateTime}) + + - \ No newline at end of file + diff --git a/ems-system/src/main/resources/mapper/ems/EmsDailyEnergyDataMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsDailyEnergyDataMapper.xml index fe40a83..dbcadb5 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsDailyEnergyDataMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsDailyEnergyDataMapper.xml @@ -8,8 +8,7 @@ - - + @@ -27,7 +26,11 @@ - select id, site_id, data_date, total_revenue, day_revenue, peak_charge_diff, peak_discharge_diff, high_charge_diff, high_discharge_diff, flat_charge_diff, flat_discharge_diff, valley_charge_diff, valley_discharge_diff, calc_time, create_by, create_time, update_by, update_time, remark from ems_daily_energy_data + select id, site_id, data_date, data_hour, + peak_charge_diff, peak_discharge_diff, high_charge_diff, high_discharge_diff, + flat_charge_diff, flat_discharge_diff, valley_charge_diff, valley_discharge_diff, + calc_time, create_by, create_time, update_by, update_time, remark + from ems_daily_energy_data + + INSERT INTO ems_daily_energy_data ( id, site_id, data_date, - total_revenue, - day_revenue, + data_hour, peak_charge_diff, peak_discharge_diff, high_charge_diff, @@ -164,8 +172,7 @@ #{id}, #{siteId}, #{dataDate}, - #{totalRevenue}, - #{dayRevenue}, + #{dataHour}, #{peakChargeDiff}, #{peakDischargeDiff}, #{highChargeDiff}, @@ -181,8 +188,7 @@ NOW(), #{remark} ) ON DUPLICATE KEY UPDATE - total_revenue = #{totalRevenue}, - day_revenue = #{dayRevenue}, + data_hour = #{dataHour}, peak_charge_diff = #{peakChargeDiff}, peak_discharge_diff = #{peakDischargeDiff}, high_charge_diff = #{highChargeDiff}, @@ -196,60 +202,116 @@ diff --git a/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml index b4e3bd6..f3d1d68 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsPointConfigMapper.xml @@ -52,7 +52,7 @@ and site_id = #{siteId} - and point_id = #{pointId} + and point_id like concat('%', #{pointId}, '%') and device_category = #{deviceCategory} and device_id = #{deviceId} and data_key like concat('%', #{dataKey}, '%') @@ -202,6 +202,36 @@ where id = #{id} + + update ems_point_config + set point_id = #{pointId}, + site_id = #{siteId}, + device_category = #{deviceCategory}, + device_id = #{deviceId}, + point_name = #{pointName}, + data_key = #{dataKey}, + point_desc = #{pointDesc}, + register_address = #{registerAddress}, + data_unit = #{dataUnit}, + data_a = #{dataA}, + data_k = #{dataK}, + data_b = #{dataB}, + data_bit = #{dataBit}, + is_alarm = #{isAlarm}, + point_type = #{pointType}, + calc_expression = #{calcExpression}, + collect_enabled = #{collectEnabled}, + collect_source = #{collectSource}, + modbus_register_type = #{modbusRegisterType}, + modbus_data_type = #{modbusDataType}, + modbus_read_order = #{modbusReadOrder}, + modbus_group = #{modbusGroup}, + update_by = #{updateBy}, + update_time = now(), + remark = #{remark} + where id = #{id} + + delete from ems_point_config where id = #{id} @@ -329,11 +359,11 @@ where collect_enabled = 1 and collect_source = 'MODBUS' - and (point_type is null or point_type <> 'calc') + and (point_type is null or point_type <> 'calc') and (is_alarm is null or is_alarm = 0) - and register_address is not null and register_address <> '' - and modbus_register_type is not null and modbus_register_type <> '' - and modbus_data_type is not null and modbus_data_type <> '' + and register_address is not null and register_address <> '' + and modbus_register_type is not null and modbus_register_type <> '' + and modbus_data_type is not null and modbus_data_type <> '' and site_id = #{siteId} diff --git a/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml b/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml index daac50a..e01124b 100644 --- a/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml +++ b/ems-system/src/main/resources/mapper/ems/EmsSiteMonitorPointMatchMapper.xml @@ -4,6 +4,14 @@ "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> + +