diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareCalculator.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareCalculator.java new file mode 100644 index 0000000..36e5aed --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareCalculator.java @@ -0,0 +1,75 @@ +package com.njcn.gather.steady.checksquare.component; + +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareSegmentVO; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * 数据校验连续性计算组件。 + */ +@Component +public class SteadyChecksquareCalculator { + + public static final String STATUS_NORMAL = "NORMAL"; + public static final String STATUS_MISSING = "MISSING"; + private static final DateTimeFormatter OUTPUT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + public List buildSegments(List slots, Set actualSlots, + int intervalMinutes) { + List result = new ArrayList(); + if (slots == null || slots.isEmpty()) { + return result; + } + String currentStatus = resolveStatus(slots.get(0), actualSlots); + LocalDateTime segmentStart = slots.get(0); + LocalDateTime previousSlot = slots.get(0); + int pointCount = 1; + for (int i = 1; i < slots.size(); i++) { + LocalDateTime slot = slots.get(i); + String status = resolveStatus(slot, actualSlots); + if (!currentStatus.equals(status)) { + result.add(buildSegment(segmentStart, previousSlot, currentStatus, pointCount, intervalMinutes)); + segmentStart = slot; + pointCount = 0; + currentStatus = status; + } + previousSlot = slot; + pointCount++; + } + result.add(buildSegment(segmentStart, previousSlot, currentStatus, pointCount, intervalMinutes)); + return result; + } + + public int maxContinuousMissingMinutes(List segments) { + int result = 0; + if (segments == null) { + return result; + } + for (SteadyChecksquareSegmentVO segment : segments) { + if (segment != null && STATUS_MISSING.equals(segment.getStatus()) && segment.getDurationMinutes() != null) { + result = Math.max(result, segment.getDurationMinutes()); + } + } + return result; + } + + private SteadyChecksquareSegmentVO buildSegment(LocalDateTime startTime, LocalDateTime endTime, String status, + int pointCount, int intervalMinutes) { + SteadyChecksquareSegmentVO segment = new SteadyChecksquareSegmentVO(); + segment.setStartTime(OUTPUT_TIME_FORMATTER.format(startTime)); + segment.setEndTime(OUTPUT_TIME_FORMATTER.format(endTime)); + segment.setStatus(status); + segment.setMissingPointCount(STATUS_MISSING.equals(status) ? pointCount : 0); + segment.setDurationMinutes(pointCount * intervalMinutes); + return segment; + } + + private String resolveStatus(LocalDateTime slot, Set actualSlots) { + return actualSlots != null && actualSlots.contains(slot) ? STATUS_NORMAL : STATUS_MISSING; + } +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponent.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponent.java new file mode 100644 index 0000000..721875e --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponent.java @@ -0,0 +1,201 @@ +package com.njcn.gather.steady.checksquare.component; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.steady.datavie.config.SteadyInfluxDbProperties; +import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URLEncoder; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.HashSet; +import java.util.Set; + +/** + * 数据校验 InfluxDB 查询组件。 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class SteadyChecksquareInfluxQueryComponent { + + private static final DateTimeFormatter INFLUX_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private final SteadyInfluxDbProperties properties; + + public Set queryExistingSlots(SteadyTrendResolvedFieldBO field, LocalDateTime startTime, + LocalDateTime endTime, int intervalMinutes) { + validateConfig(); + String query = buildChecksquareQuery(field, startTime, endTime); + long startMillis = System.currentTimeMillis(); + log.info("数据校验 InfluxDB 查询开始,measurement={},field={},lineId={},phase={},statType={},query={}", + field.getMeasurement(), field.getField(), field.getLineId(), field.getPhase(), field.getStatType(), query); + try { + String body = executeQuery(query); + Set slots = parseExistingSlots(body, intervalMinutes); + log.info("数据校验 InfluxDB 查询结束,slotCount={},costMs={}", slots.size(), System.currentTimeMillis() - startMillis); + return slots; + } catch (RuntimeException ex) { + log.warn("数据校验 InfluxDB 查询异常,costMs={},error={}", System.currentTimeMillis() - startMillis, ex.getMessage()); + throw ex; + } + } + + public String buildChecksquareQuery(SteadyTrendResolvedFieldBO field, LocalDateTime startTime, LocalDateTime endTime) { + StringBuilder sql = new StringBuilder(); + sql.append("SELECT \"").append(field.getField()).append("\" AS \"value\""); + sql.append(" FROM \"").append(field.getMeasurement()).append("\""); + sql.append(" WHERE time >= '").append(INFLUX_TIME_FORMATTER.format(startTime)).append("'"); + sql.append(" AND time <= '").append(INFLUX_TIME_FORMATTER.format(endTime)).append("'"); + sql.append(" AND \"line_id\" = '").append(escapeTagValue(field.getLineId())).append("'"); + sql.append(" AND \"phasic_type\" = '").append(escapeTagValue(field.getPhase())).append("'"); + if (hasValueTypeTag(field.getMeasurement())) { + sql.append(" AND \"value_type\" = '").append(resolveValueType(field.getStatType())).append("'"); + } + sql.append(" ORDER BY time ASC"); + return sql.toString(); + } + + private Set parseExistingSlots(String body, int intervalMinutes) { + try { + JsonNode root = OBJECT_MAPPER.readTree(body); + JsonNode values = root.path("results").path(0).path("series").path(0).path("values"); + Set result = new HashSet(); + if (!values.isArray()) { + return result; + } + for (JsonNode value : values) { + if (value.size() < 2 || value.get(1).isNull()) { + continue; + } + LocalDateTime time = parseInfluxTime(value.get(0).asText()); + if (time != null) { + result.add(alignToPreviousSlot(time, intervalMinutes)); + } + } + return result; + } catch (IOException ex) { + throw fail("InfluxDB 返回结果解析失败:" + ex.getMessage()); + } + } + + private LocalDateTime alignToPreviousSlot(LocalDateTime time, int intervalMinutes) { + LocalDateTime minuteFloor = time.withSecond(0).withNano(0); + int minuteOfDay = minuteFloor.getHour() * 60 + minuteFloor.getMinute(); + int remainder = minuteOfDay % intervalMinutes; + return minuteFloor.minusMinutes(remainder); + } + + private LocalDateTime parseInfluxTime(String value) { + try { + return OffsetDateTime.parse(value).withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime(); + } catch (RuntimeException ex) { + return null; + } + } + + private String executeQuery(String query) { + HttpURLConnection connection = null; + try { + URL url = new URL(buildQueryUrl(query)); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(properties.getConnectTimeoutMs()); + connection.setReadTimeout(properties.getReadTimeoutMs()); + int status = connection.getResponseCode(); + InputStream stream = status >= 200 && status < 300 ? connection.getInputStream() : connection.getErrorStream(); + String body = readBody(stream); + if (status < 200 || status >= 300) { + throw fail("InfluxDB 查询失败:" + body); + } + return body; + } catch (IOException ex) { + throw fail("InfluxDB 查询异常:" + ex.getMessage()); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + private String buildQueryUrl(String query) throws IOException { + StringBuilder url = new StringBuilder(trimRightSlash(properties.getUrl())).append("/query?"); + url.append("db=").append(encode(properties.getDatabase())); + if (properties.getUsername() != null && !properties.getUsername().trim().isEmpty()) { + url.append("&u=").append(encode(properties.getUsername().trim())); + } + if (properties.getPassword() != null && !properties.getPassword().trim().isEmpty()) { + url.append("&p=").append(encode(properties.getPassword())); + } + url.append("&q=").append(encode(query)); + return url.toString(); + } + + private void validateConfig() { + if (properties.getUrl() == null || properties.getUrl().trim().isEmpty()) { + throw fail("InfluxDB 地址未配置"); + } + if (properties.getDatabase() == null || properties.getDatabase().trim().isEmpty()) { + throw fail("InfluxDB database 未配置"); + } + } + + private String readBody(InputStream stream) throws IOException { + if (stream == null) { + return ""; + } + BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)); + StringBuilder body = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + body.append(line); + } + return body.toString(); + } + + private String escapeTagValue(String value) { + return value == null ? "" : value.replace("\\", "\\\\").replace("'", "\\'"); + } + + private String resolveValueType(String statType) { + if (statType == null || statType.trim().isEmpty()) { + return "AVG"; + } + return statType.trim().toUpperCase(); + } + + private boolean hasValueTypeTag(String measurement) { + return !"data_flicker".equals(measurement) && !"data_fluc".equals(measurement) && !"data_plt".equals(measurement); + } + + private String trimRightSlash(String value) { + String text = value.trim(); + while (text.endsWith("/")) { + text = text.substring(0, text.length() - 1); + } + return text; + } + + private String encode(String value) throws IOException { + return URLEncoder.encode(value, StandardCharsets.UTF_8.name()); + } + + private BusinessException fail(String message) { + return new BusinessException(CommonResponseEnum.FAIL, message); + } +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/controller/SteadyChecksquareController.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/controller/SteadyChecksquareController.java new file mode 100644 index 0000000..f3d35f5 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/controller/SteadyChecksquareController.java @@ -0,0 +1,43 @@ +package com.njcn.gather.steady.checksquare.controller; + +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.enums.common.LogEnum; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.common.utils.LogUtil; +import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareQueryVO; +import com.njcn.gather.steady.checksquare.service.SteadyChecksquareService; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 数据校验接口。 + */ +@Slf4j +@Api(tags = "数据校验") +@RestController +@RequestMapping("/steady/data-view/checksquare") +@RequiredArgsConstructor +public class SteadyChecksquareController extends BaseController { + + private final SteadyChecksquareService checksquareService; + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询数据校验结果") + @PostMapping("/query") + public HttpResult query(@RequestBody SteadyChecksquareQueryParam param) { + String methodDescribe = getMethodDescribe("query"); + LogUtil.njcnDebug(log, "{},开始查询数据校验结果,param={}", methodDescribe, param); + SteadyChecksquareQueryVO result = checksquareService.query(param); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/param/SteadyChecksquareQueryParam.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/param/SteadyChecksquareQueryParam.java new file mode 100644 index 0000000..680bd04 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/param/SteadyChecksquareQueryParam.java @@ -0,0 +1,33 @@ +package com.njcn.gather.steady.checksquare.pojo.param; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 数据校验查询参数。 + */ +@Data +@ApiModel("数据校验查询参数") +public class SteadyChecksquareQueryParam implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("监测点 ID") + private String lineId; + + @ApiModelProperty("指标编码") + private List indicatorCodes; + + @ApiModelProperty("开始时间,格式 yyyy-MM-dd HH:mm:ss") + private String timeStart; + + @ApiModelProperty("结束时间,格式 yyyy-MM-dd HH:mm:ss") + private String timeEnd; + + @ApiModelProperty("谐波次数,谐波指标按请求次数查询") + private List harmonicOrders; +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareItemVO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareItemVO.java new file mode 100644 index 0000000..b31d994 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareItemVO.java @@ -0,0 +1,62 @@ +package com.njcn.gather.steady.checksquare.pojo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * 数据校验总览项。 + */ +@Data +@ApiModel("数据校验总览项") +public class SteadyChecksquareItemVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("校验项唯一键") + private String itemKey; + + @ApiModelProperty("指标编码") + private String indicatorCode; + + @ApiModelProperty("指标名称") + private String indicatorName; + + @ApiModelProperty("谐波次数") + private Integer harmonicOrder; + + @ApiModelProperty("当前校验项统计间隔,单位分钟") + private Integer intervalMinutes; + + @ApiModelProperty("时间范围内是否存在任意数据") + private Boolean hasData; + + @ApiModelProperty("期望点数") + private Integer expectedPointCount; + + @ApiModelProperty("实际点数") + private Integer actualPointCount; + + @ApiModelProperty("缺失点数") + private Integer missingPointCount; + + @ApiModelProperty("缺失率") + private BigDecimal missingRate; + + @ApiModelProperty("缺失率文本") + private String missingRateText; + + @ApiModelProperty("最大连续缺失时长,单位分钟") + private Integer maxContinuousMissingMinutes; + + @ApiModelProperty("统计类型摘要") + private List statSummaries = new ArrayList(); + + @ApiModelProperty("统计类型明细") + private List statDetails = new ArrayList(); +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareQueryVO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareQueryVO.java new file mode 100644 index 0000000..7ba697b --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareQueryVO.java @@ -0,0 +1,37 @@ +package com.njcn.gather.steady.checksquare.pojo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 数据校验查询结果。 + */ +@Data +@ApiModel("数据校验查询结果") +public class SteadyChecksquareQueryVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("监测点 ID") + private String lineId; + + @ApiModelProperty("监测点名称") + private String lineName; + + @ApiModelProperty("开始时间") + private String timeStart; + + @ApiModelProperty("结束时间") + private String timeEnd; + + @ApiModelProperty("统计间隔,单位分钟") + private Integer intervalMinutes; + + @ApiModelProperty("校验项") + private List items = new ArrayList(); +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareSegmentVO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareSegmentVO.java new file mode 100644 index 0000000..b455860 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareSegmentVO.java @@ -0,0 +1,32 @@ +package com.njcn.gather.steady.checksquare.pojo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * 数据校验连续性区间。 + */ +@Data +@ApiModel("数据校验连续性区间") +public class SteadyChecksquareSegmentVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("开始时间") + private String startTime; + + @ApiModelProperty("结束时间") + private String endTime; + + @ApiModelProperty("状态,NORMAL/MISSING") + private String status; + + @ApiModelProperty("缺失点数") + private Integer missingPointCount; + + @ApiModelProperty("持续时长,单位分钟") + private Integer durationMinutes; +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareStatDetailVO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareStatDetailVO.java new file mode 100644 index 0000000..1971097 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareStatDetailVO.java @@ -0,0 +1,28 @@ +package com.njcn.gather.steady.checksquare.pojo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 数据校验统计类型明细。 + */ +@Data +@ApiModel("数据校验统计类型明细") +public class SteadyChecksquareStatDetailVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("统计类型") + private String statType; + + @ApiModelProperty("是否支持") + private Boolean supported; + + @ApiModelProperty("连续性区间") + private List segments = new ArrayList(); +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareStatSummaryVO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareStatSummaryVO.java new file mode 100644 index 0000000..0cb2b36 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareStatSummaryVO.java @@ -0,0 +1,45 @@ +package com.njcn.gather.steady.checksquare.pojo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * 数据校验统计类型摘要。 + */ +@Data +@ApiModel("数据校验统计类型摘要") +public class SteadyChecksquareStatSummaryVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("统计类型") + private String statType; + + @ApiModelProperty("是否支持") + private Boolean supported; + + @ApiModelProperty("是否存在数据") + private Boolean hasData; + + @ApiModelProperty("期望点数") + private Integer expectedPointCount; + + @ApiModelProperty("实际点数") + private Integer actualPointCount; + + @ApiModelProperty("缺失点数") + private Integer missingPointCount; + + @ApiModelProperty("缺失率") + private BigDecimal missingRate; + + @ApiModelProperty("缺失率文本") + private String missingRateText; + + @ApiModelProperty("最大连续缺失时长,单位分钟") + private Integer maxContinuousMissingMinutes; +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/SteadyChecksquareService.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/SteadyChecksquareService.java new file mode 100644 index 0000000..622af24 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/SteadyChecksquareService.java @@ -0,0 +1,12 @@ +package com.njcn.gather.steady.checksquare.service; + +import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareQueryVO; + +/** + * 数据校验服务。 + */ +public interface SteadyChecksquareService { + + SteadyChecksquareQueryVO query(SteadyChecksquareQueryParam param); +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImpl.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImpl.java new file mode 100644 index 0000000..d90a9c6 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImpl.java @@ -0,0 +1,349 @@ +package com.njcn.gather.steady.checksquare.service.impl; + +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.steady.checksquare.component.SteadyChecksquareCalculator; +import com.njcn.gather.steady.checksquare.component.SteadyChecksquareInfluxQueryComponent; +import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareQueryVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareSegmentVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareStatDetailVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareStatSummaryVO; +import com.njcn.gather.steady.checksquare.service.SteadyChecksquareService; +import com.njcn.gather.steady.datavie.component.SteadyTrendIndicatorCatalog; +import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendIndicatorDefinitionBO; +import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO; +import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendSeriesFieldBO; +import com.njcn.gather.tool.adddata.component.AddDataTimeSlotCalculator; +import com.njcn.gather.tool.addledger.pojo.constant.AddLedgerConst; +import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO; +import com.njcn.gather.tool.addledger.service.AddLedgerService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 数据校验服务实现。 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { + + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final String EMPTY_TEXT = "-"; + private static final int FLICKER_SHORT_INTERVAL_MINUTES = 10; + private static final int FLICKER_LONG_INTERVAL_MINUTES = 120; + + private final SteadyTrendIndicatorCatalog indicatorCatalog; + private final SteadyChecksquareInfluxQueryComponent influxQueryComponent; + private final SteadyChecksquareCalculator calculator; + private final AddDataTimeSlotCalculator timeSlotCalculator; + private final AddLedgerService addLedgerService; + + @Override + public SteadyChecksquareQueryVO query(SteadyChecksquareQueryParam param) { + validateParam(param); + String lineId = trimToNull(param.getLineId()); + LocalDateTime startTime = parseRequiredTime(param.getTimeStart(), "开始时间不能为空"); + LocalDateTime endTime = parseRequiredTime(param.getTimeEnd(), "结束时间不能为空"); + if (startTime.isAfter(endTime)) { + throw fail("开始时间不能大于结束时间"); + } + AddLedgerLinePathVO linePath = requireLinePath(lineId); + int intervalMinutes = resolveIntervalMinutes(linePath); + SteadyChecksquareQueryVO result = new SteadyChecksquareQueryVO(); + result.setLineId(lineId); + result.setLineName(trimToNull(linePath.getLineName()) == null ? EMPTY_TEXT : linePath.getLineName()); + result.setTimeStart(param.getTimeStart()); + result.setTimeEnd(param.getTimeEnd()); + result.setIntervalMinutes(intervalMinutes); + + long startMillis = System.currentTimeMillis(); + List indicatorCodes = normalizeTextList(param.getIndicatorCodes()); + List harmonicOrders = normalizeHarmonicOrders(param.getHarmonicOrders()); + log.info("数据校验查询开始,lineId={},indicatorCount={},timeStart={},timeEnd={},intervalMinutes={}", + lineId, indicatorCodes.size(), startTime, endTime, intervalMinutes); + for (String indicatorCode : indicatorCodes) { + SteadyTrendIndicatorDefinitionBO indicator = requireIndicator(indicatorCode); + int itemIntervalMinutes = resolveIndicatorIntervalMinutes(indicator, intervalMinutes); + List itemSlots = timeSlotCalculator.buildTimeSlots(startTime, endTime, itemIntervalMinutes); + result.getItems().addAll(buildIndicatorItems(lineId, indicator, harmonicOrders, startTime, endTime, itemSlots, itemIntervalMinutes)); + } + log.info("数据校验查询结束,lineId={},itemCount={},costMs={}", lineId, result.getItems().size(), System.currentTimeMillis() - startMillis); + return result; + } + + private List buildIndicatorItems(String lineId, SteadyTrendIndicatorDefinitionBO indicator, + List harmonicOrders, + LocalDateTime startTime, LocalDateTime endTime, + List slots, int intervalMinutes) { + List result = new ArrayList(); + if (Boolean.TRUE.equals(indicator.getHarmonic())) { + for (Integer order : requireValidHarmonicOrders(indicator, harmonicOrders)) { + result.add(buildItem(lineId, indicator, order, startTime, endTime, slots, intervalMinutes)); + } + return result; + } + result.add(buildItem(lineId, indicator, null, startTime, endTime, slots, intervalMinutes)); + return result; + } + + private SteadyChecksquareItemVO buildItem(String lineId, SteadyTrendIndicatorDefinitionBO indicator, Integer harmonicOrder, + LocalDateTime startTime, LocalDateTime endTime, + List slots, int intervalMinutes) { + SteadyChecksquareItemVO item = new SteadyChecksquareItemVO(); + item.setItemKey(buildItemKey(lineId, indicator, harmonicOrder)); + item.setIndicatorCode(indicator.getIndicatorCode()); + item.setIndicatorName(indicator.getName()); + item.setHarmonicOrder(harmonicOrder); + item.setIntervalMinutes(intervalMinutes); + + int totalExpected = 0; + int totalActual = 0; + int maxContinuousMissingMinutes = 0; + boolean hasData = false; + for (String statType : indicator.getSupportStats()) { + Set actualSlots = queryMergedActualSlots(lineId, indicator, harmonicOrder, statType, startTime, endTime, intervalMinutes); + Set effectiveActualSlots = retainExpectedSlots(slots, actualSlots); + List segments = calculator.buildSegments(slots, effectiveActualSlots, intervalMinutes); + SteadyChecksquareStatSummaryVO summary = buildSummary(statType, slots.size(), effectiveActualSlots.size(), segments); + SteadyChecksquareStatDetailVO detail = buildDetail(statType, segments); + item.getStatSummaries().add(summary); + item.getStatDetails().add(detail); + totalExpected += summary.getExpectedPointCount(); + totalActual += summary.getActualPointCount(); + maxContinuousMissingMinutes = Math.max(maxContinuousMissingMinutes, summary.getMaxContinuousMissingMinutes()); + hasData = hasData || Boolean.TRUE.equals(summary.getHasData()); + } + + item.setHasData(hasData); + item.setExpectedPointCount(totalExpected); + item.setActualPointCount(totalActual); + item.setMissingPointCount(Math.max(0, totalExpected - totalActual)); + item.setMissingRate(calculateRate(item.getMissingPointCount(), totalExpected)); + item.setMissingRateText(formatRateText(item.getMissingRate())); + item.setMaxContinuousMissingMinutes(maxContinuousMissingMinutes); + return item; + } + + private Set queryMergedActualSlots(String lineId, SteadyTrendIndicatorDefinitionBO indicator, Integer harmonicOrder, + String statType, LocalDateTime startTime, LocalDateTime endTime, + int intervalMinutes) { + Set result = new HashSet(); + for (String phase : indicator.getPhaseCodes()) { + SteadyTrendResolvedFieldBO field = buildResolvedField(lineId, indicator, harmonicOrder, phase, statType); + result.addAll(influxQueryComponent.queryExistingSlots(field, startTime, endTime, intervalMinutes)); + } + return result; + } + + private Set retainExpectedSlots(List slots, Set actualSlots) { + Set result = new HashSet(); + if (slots == null || actualSlots == null || actualSlots.isEmpty()) { + return result; + } + for (LocalDateTime slot : slots) { + if (actualSlots.contains(slot)) { + result.add(slot); + } + } + return result; + } + + private SteadyTrendResolvedFieldBO buildResolvedField(String lineId, SteadyTrendIndicatorDefinitionBO indicator, + Integer harmonicOrder, String phase, String statType) { + SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO(); + field.setMeasurement(indicator.getTableName()); + field.setField(resolveField(indicator, harmonicOrder)); + field.setLineId(lineId); + field.setIndicatorCode(indicator.getIndicatorCode()); + field.setIndicatorName(indicator.getName()); + field.setPhase(phase); + field.setStatType(statType); + field.setUnit(indicator.getUnit()); + return field; + } + + private String resolveField(SteadyTrendIndicatorDefinitionBO indicator, Integer harmonicOrder) { + if (Boolean.TRUE.equals(indicator.getHarmonic())) { + return indicator.getHarmonicFieldPrefix() + "_" + harmonicOrder; + } + List fields = indicator.getSeriesFields(); + if (fields == null || fields.isEmpty()) { + throw fail("稳态指标不支持:" + indicator.getIndicatorCode()); + } + return fields.get(0).getField(); + } + + private SteadyChecksquareStatSummaryVO buildSummary(String statType, int expectedCount, int actualCount, + List segments) { + SteadyChecksquareStatSummaryVO summary = new SteadyChecksquareStatSummaryVO(); + summary.setStatType(statType); + summary.setSupported(true); + summary.setHasData(actualCount > 0); + summary.setExpectedPointCount(expectedCount); + summary.setActualPointCount(actualCount); + summary.setMissingPointCount(Math.max(0, expectedCount - actualCount)); + summary.setMissingRate(calculateRate(summary.getMissingPointCount(), expectedCount)); + summary.setMissingRateText(formatRateText(summary.getMissingRate())); + summary.setMaxContinuousMissingMinutes(calculator.maxContinuousMissingMinutes(segments)); + return summary; + } + + private SteadyChecksquareStatDetailVO buildDetail(String statType, List segments) { + SteadyChecksquareStatDetailVO detail = new SteadyChecksquareStatDetailVO(); + detail.setStatType(statType); + detail.setSupported(true); + detail.setSegments(segments); + return detail; + } + + private String buildItemKey(String lineId, SteadyTrendIndicatorDefinitionBO indicator, Integer harmonicOrder) { + if (harmonicOrder == null) { + return lineId + "|" + indicator.getIndicatorCode(); + } + return lineId + "|" + indicator.getIndicatorCode() + "|" + harmonicOrder; + } + + private void validateParam(SteadyChecksquareQueryParam param) { + if (param == null) { + throw fail("数据校验参数不能为空"); + } + if (trimToNull(param.getLineId()) == null) { + throw fail("监测点 ID 不能为空"); + } + if (normalizeTextList(param.getIndicatorCodes()).isEmpty()) { + throw fail("指标不能为空"); + } + parseRequiredTime(param.getTimeStart(), "开始时间不能为空"); + parseRequiredTime(param.getTimeEnd(), "结束时间不能为空"); + } + + private LocalDateTime parseRequiredTime(String time, String emptyMessage) { + String text = trimToNull(time); + if (text == null) { + throw fail(emptyMessage); + } + try { + return LocalDateTime.parse(text, TIME_FORMATTER); + } catch (DateTimeParseException ex) { + throw fail("时间格式不正确,仅支持 yyyy-MM-dd HH:mm:ss"); + } + } + + private AddLedgerLinePathVO requireLinePath(String lineId) { + Map linePathMap = addLedgerService.listLinePathByLineIds(Collections.singletonList(lineId)); + AddLedgerLinePathVO linePath = linePathMap.get(lineId); + if (linePath == null) { + throw fail("监测点不存在或不可用"); + } + return linePath; + } + + private int resolveIntervalMinutes(AddLedgerLinePathVO linePath) { + Integer interval = linePath.getLineInterval(); + if (interval == null || interval <= 0) { + return AddLedgerConst.LINE_INTERVAL_DEFAULT; + } + return interval; + } + + private int resolveIndicatorIntervalMinutes(SteadyTrendIndicatorDefinitionBO indicator, int lineIntervalMinutes) { + String indicatorCode = indicator == null ? null : indicator.getIndicatorCode(); + if ("FLUC".equals(indicatorCode) || "PST".equals(indicatorCode)) { + return FLICKER_SHORT_INTERVAL_MINUTES; + } + if ("PLT".equals(indicatorCode)) { + return FLICKER_LONG_INTERVAL_MINUTES; + } + return lineIntervalMinutes; + } + + private SteadyTrendIndicatorDefinitionBO requireIndicator(String indicatorCode) { + SteadyTrendIndicatorDefinitionBO indicator = indicatorCatalog.getIndicator(indicatorCode); + if (indicator == null) { + throw fail("稳态指标不支持:" + indicatorCode); + } + return indicator; + } + + private BigDecimal calculateRate(int missingCount, int expectedCount) { + if (expectedCount <= 0) { + return BigDecimal.ZERO.setScale(6, RoundingMode.HALF_UP); + } + return new BigDecimal(missingCount).divide(new BigDecimal(expectedCount), 6, RoundingMode.HALF_UP); + } + + private String formatRateText(BigDecimal rate) { + if (rate == null) { + return null; + } + return rate.multiply(new BigDecimal("100")).setScale(2, RoundingMode.HALF_UP).toPlainString() + "%"; + } + + private List normalizeTextList(List values) { + if (values == null || values.isEmpty()) { + return new ArrayList(); + } + Set result = new LinkedHashSet(); + for (String value : values) { + String text = trimToNull(value); + if (text != null) { + result.add(text); + } + } + return new ArrayList(result); + } + + private List normalizeHarmonicOrders(List values) { + if (values == null || values.isEmpty()) { + return new ArrayList(); + } + List result = new ArrayList(); + for (Integer value : values) { + if (value != null && !result.contains(value)) { + result.add(value); + } + } + return result; + } + + private List requireValidHarmonicOrders(SteadyTrendIndicatorDefinitionBO indicator, List harmonicOrders) { + if (harmonicOrders == null || harmonicOrders.isEmpty()) { + throw fail("谐波次数不能为空"); + } + for (Integer order : harmonicOrders) { + if (order < indicator.getHarmonicOrderStart() || order > indicator.getHarmonicOrderEnd()) { + throw fail("谐波次数只能在 " + indicator.getHarmonicOrderStart() + " 到 " + indicator.getHarmonicOrderEnd() + " 之间"); + } + } + return harmonicOrders; + } + + private String trimToNull(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + + private BusinessException fail(String message) { + return new BusinessException(CommonResponseEnum.FAIL, message); + } +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/component/SteadyInfluxQueryComponent.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/component/SteadyInfluxQueryComponent.java index 94c42ea..526885f 100644 --- a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/component/SteadyInfluxQueryComponent.java +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/component/SteadyInfluxQueryComponent.java @@ -8,6 +8,7 @@ import com.njcn.gather.steady.datavie.config.SteadyInfluxDbProperties; import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO; import com.njcn.gather.steady.datavie.pojo.vo.SteadyTrendPointVO; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.io.BufferedReader; @@ -29,6 +30,7 @@ import java.util.List; /** * 稳态趋势 InfluxDB 查询组件。 */ +@Slf4j @Component @RequiredArgsConstructor public class SteadyInfluxQueryComponent { @@ -43,8 +45,32 @@ public class SteadyInfluxQueryComponent { LocalDateTime endTime, Integer qualityFlag) { validateConfig(); String query = buildTrendQuery(field, startTime, endTime, qualityFlag); - String body = executeQuery(query); - return parseTrendPoints(body); + String diagnostic = buildTrendQueryDiagnostic(field, startTime, endTime, qualityFlag); + long startMillis = System.currentTimeMillis(); + log.info("稳态趋势 InfluxDB 查询开始,{},query={}", diagnostic, query); + try { + String body = executeQuery(query); + List points = parseTrendPoints(body); + log.info("稳态趋势 InfluxDB 查询结束,{},pointCount={},costMs={}", diagnostic, points.size(), System.currentTimeMillis() - startMillis); + return points; + } catch (RuntimeException ex) { + log.warn("稳态趋势 InfluxDB 查询异常,{},costMs={},error={}", diagnostic, System.currentTimeMillis() - startMillis, ex.getMessage()); + throw ex; + } + } + + String buildTrendQueryDiagnostic(SteadyTrendResolvedFieldBO field, LocalDateTime startTime, LocalDateTime endTime, + Integer qualityFlag) { + StringBuilder diagnostic = new StringBuilder(); + diagnostic.append("measurement=").append(field.getMeasurement()); + diagnostic.append(", field=").append(field.getField()); + diagnostic.append(", lineId=").append(field.getLineId()); + diagnostic.append(", phase=").append(field.getPhase()); + diagnostic.append(", statType=").append(resolveValueType(field.getStatType())); + diagnostic.append(", qualityFlag=").append(qualityFlag); + diagnostic.append(", timeStart=").append(startTime); + diagnostic.append(", timeEnd=").append(endTime); + return diagnostic.toString(); } public String buildTrendQuery(SteadyTrendResolvedFieldBO field, LocalDateTime startTime, LocalDateTime endTime, diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/component/SteadyTrendFieldResolver.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/component/SteadyTrendFieldResolver.java index c6d32f6..836817b 100644 --- a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/component/SteadyTrendFieldResolver.java +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/component/SteadyTrendFieldResolver.java @@ -26,7 +26,7 @@ public class SteadyTrendFieldResolver { private static final int MAX_LINE_COUNT = 8; private static final int MAX_INDICATOR_COUNT = 8; private static final int MAX_SERIES_COUNT = 24; - private static final int MAX_HARMONIC_ORDER_COUNT = 6; + private static final int MAX_HARMONIC_ORDER_COUNT = 3; private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private final SteadyTrendIndicatorCatalog indicatorCatalog; @@ -89,7 +89,7 @@ public class SteadyTrendFieldResolver { throw fail("谐波次数不能为空"); } if (orders.size() > MAX_HARMONIC_ORDER_COUNT) { - throw fail("谐波次数最多选择 6 个"); + throw fail("谐波次数不允许一次展示超过3个"); } List result = new ArrayList(); for (Integer order : orders) { diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/pojo/param/SteadyTrendQueryParam.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/pojo/param/SteadyTrendQueryParam.java index 97fc81e..0511a3b 100644 --- a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/pojo/param/SteadyTrendQueryParam.java +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/pojo/param/SteadyTrendQueryParam.java @@ -32,6 +32,6 @@ public class SteadyTrendQueryParam { @ApiModelProperty("质量标识") private Integer qualityFlag; - @ApiModelProperty("谐波次数,谐波指标必填,最多 6 个") + @ApiModelProperty("谐波次数,谐波指标必填,默认最多展示 3 个") private List harmonicOrders = new ArrayList(); } diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/service/impl/SteadyDataViewTrendServiceImpl.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/service/impl/SteadyDataViewTrendServiceImpl.java index be9031b..ebcef22 100644 --- a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/service/impl/SteadyDataViewTrendServiceImpl.java +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/datavie/service/impl/SteadyDataViewTrendServiceImpl.java @@ -13,6 +13,7 @@ import com.njcn.gather.steady.datavie.service.SteadyDataViewTrendService; import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO; import com.njcn.gather.tool.addledger.service.AddLedgerService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.math.BigDecimal; @@ -28,6 +29,7 @@ import java.util.Map; /** * 稳态趋势查询服务实现。 */ +@Slf4j @Service @RequiredArgsConstructor public class SteadyDataViewTrendServiceImpl implements SteadyDataViewTrendService { @@ -70,11 +72,14 @@ public class SteadyDataViewTrendServiceImpl implements SteadyDataViewTrendServic result.setSampled(false); result.setLoadableDays(resolveLoadableDays(startTime, endTime)); int displayPointCount = 0; + long startMillis = System.currentTimeMillis(); + log.info("稳态趋势查询开始,seriesCount={},timeStart={},timeEnd={},qualityFlag={}", fields.size(), startTime, endTime, param.getQualityFlag()); for (SteadyTrendResolvedFieldBO field : fields) { List points = influxQueryComponent.queryTrendPoints(field, startTime, endTime, param.getQualityFlag()); displayPointCount += points.size(); result.getSeries().add(buildSeries(field, points)); } + log.info("稳态趋势查询结束,seriesCount={},displayPointCount={},costMs={}", fields.size(), displayPointCount, System.currentTimeMillis() - startMillis); /* * 当前 Influx 查询按曲线独立执行,未额外发 count 查询;sourcePointCount 保持与实际返回点数一致。 * 后续如需要精确原始点数,可单独增加 count(field) 查询。 diff --git a/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareCalculatorTest.java b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareCalculatorTest.java new file mode 100644 index 0000000..8a8b557 --- /dev/null +++ b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareCalculatorTest.java @@ -0,0 +1,38 @@ +package com.njcn.gather.steady.checksquare.component; + +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareSegmentVO; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +/** + * 数据校验缺失区间计算测试。 + */ +class SteadyChecksquareCalculatorTest { + + @Test + void shouldMergeContinuousMissingSlots() { + SteadyChecksquareCalculator calculator = new SteadyChecksquareCalculator(); + List slots = Arrays.asList( + LocalDateTime.of(2026, 5, 1, 0, 0), + LocalDateTime.of(2026, 5, 1, 0, 1), + LocalDateTime.of(2026, 5, 1, 0, 2), + LocalDateTime.of(2026, 5, 1, 0, 3), + LocalDateTime.of(2026, 5, 1, 0, 4) + ); + + List segments = calculator.buildSegments(slots, + new HashSet(Arrays.asList(slots.get(0), slots.get(3))), 1); + + Assertions.assertEquals(4, segments.size()); + Assertions.assertEquals("MISSING", segments.get(1).getStatus()); + Assertions.assertEquals("2026-05-01 00:01:00", segments.get(1).getStartTime()); + Assertions.assertEquals("2026-05-01 00:02:00", segments.get(1).getEndTime()); + Assertions.assertEquals(Integer.valueOf(2), segments.get(1).getMissingPointCount()); + Assertions.assertEquals(Integer.valueOf(2), segments.get(1).getDurationMinutes()); + } +} diff --git a/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponentTest.java b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponentTest.java new file mode 100644 index 0000000..83de0ee --- /dev/null +++ b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponentTest.java @@ -0,0 +1,36 @@ +package com.njcn.gather.steady.checksquare.component; + +import com.njcn.gather.steady.datavie.config.SteadyInfluxDbProperties; +import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +/** + * 数据校验 InfluxQL 构造契约测试。 + */ +class SteadyChecksquareInfluxQueryComponentTest { + + @Test + void shouldBuildChecksquareQueryWithoutQualityFlag() { + SteadyChecksquareInfluxQueryComponent component = new SteadyChecksquareInfluxQueryComponent(new SteadyInfluxDbProperties()); + SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO(); + field.setMeasurement("data_v"); + field.setField("rms"); + field.setLineId("line-001"); + field.setPhase("A"); + field.setStatType("AVG"); + + String query = component.buildChecksquareQuery(field, + LocalDateTime.of(2026, 5, 1, 0, 0, 0), + LocalDateTime.of(2026, 5, 1, 23, 59, 59)); + + Assertions.assertTrue(query.contains("SELECT \"rms\" AS \"value\"")); + Assertions.assertTrue(query.contains("\"line_id\" = 'line-001'")); + Assertions.assertTrue(query.contains("\"phasic_type\" = 'A'")); + Assertions.assertTrue(query.contains("\"value_type\" = 'AVG'")); + Assertions.assertFalse(query.contains("quality_flag")); + Assertions.assertFalse(query.contains("GROUP BY time")); + } +} diff --git a/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/controller/SteadyChecksquareControllerTest.java b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/controller/SteadyChecksquareControllerTest.java new file mode 100644 index 0000000..0d1c597 --- /dev/null +++ b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/controller/SteadyChecksquareControllerTest.java @@ -0,0 +1,24 @@ +package com.njcn.gather.steady.checksquare.controller; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.lang.reflect.Method; + +/** + * 数据校验接口契约测试。 + */ +class SteadyChecksquareControllerTest { + + @Test + void shouldExposeChecksquareQueryEndpointInSeparateController() throws Exception { + RequestMapping requestMapping = SteadyChecksquareController.class.getAnnotation(RequestMapping.class); + Assertions.assertArrayEquals(new String[]{"/steady/data-view/checksquare"}, requestMapping.value()); + + Method method = SteadyChecksquareController.class.getDeclaredMethod("query", com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam.class); + PostMapping postMapping = method.getAnnotation(PostMapping.class); + Assertions.assertArrayEquals(new String[]{"/query"}, postMapping.value()); + } +} diff --git a/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/pojo/param/SteadyChecksquareQueryParamTest.java b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/pojo/param/SteadyChecksquareQueryParamTest.java new file mode 100644 index 0000000..d97ff41 --- /dev/null +++ b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/pojo/param/SteadyChecksquareQueryParamTest.java @@ -0,0 +1,32 @@ +package com.njcn.gather.steady.checksquare.pojo.param; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; + +/** + * 数据校验查询参数契约测试。 + */ +class SteadyChecksquareQueryParamTest { + + @Test + void shouldOnlyExposeChecksquareQueryFields() { + Assertions.assertNotNull(field("lineId")); + Assertions.assertNotNull(field("indicatorCodes")); + Assertions.assertNotNull(field("timeStart")); + Assertions.assertNotNull(field("timeEnd")); + Assertions.assertNull(field("qualityFlag")); + Assertions.assertNull(field("statTypes")); + Assertions.assertNull(field("phases")); + Assertions.assertNotNull(field("harmonicOrders")); + } + + private Field field(String name) { + try { + return SteadyChecksquareQueryParam.class.getDeclaredField(name); + } catch (NoSuchFieldException ex) { + return null; + } + } +} diff --git a/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImplTest.java b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImplTest.java new file mode 100644 index 0000000..57a49d8 --- /dev/null +++ b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImplTest.java @@ -0,0 +1,130 @@ +package com.njcn.gather.steady.checksquare.service.impl; + +import com.njcn.gather.steady.checksquare.component.SteadyChecksquareCalculator; +import com.njcn.gather.steady.checksquare.component.SteadyChecksquareInfluxQueryComponent; +import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareQueryVO; +import com.njcn.gather.steady.datavie.component.SteadyTrendIndicatorCatalog; +import com.njcn.gather.tool.adddata.component.AddDataTimeSlotCalculator; +import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO; +import com.njcn.gather.tool.addledger.service.AddLedgerService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * 数据校验服务测试。 + */ +class SteadyChecksquareServiceImplTest { + + @Test + void shouldUseFixedFlickerIntervalsPerIndicator() { + SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + AddLedgerService addLedgerService = mock(AddLedgerService.class); + SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(), + influxQueryComponent, new SteadyChecksquareCalculator(), new AddDataTimeSlotCalculator(), addLedgerService); + AddLedgerLinePathVO linePath = new AddLedgerLinePathVO(); + linePath.setLineId("line-001"); + linePath.setLineName("进线一"); + linePath.setLineInterval(1); + when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001")))) + .thenReturn(Collections.singletonMap("line-001", linePath)); + when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(10))) + .thenReturn(new HashSet(Arrays.asList( + LocalDateTime.of(2026, 5, 1, 0, 0), + LocalDateTime.of(2026, 5, 1, 0, 10)))); + when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(120))) + .thenReturn(new HashSet(Collections.singletonList( + LocalDateTime.of(2026, 5, 1, 0, 0)))); + + SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam(); + param.setLineId("line-001"); + param.setIndicatorCodes(Arrays.asList("FLUC", "PST", "PLT")); + param.setTimeStart("2026-05-01 00:00:00"); + param.setTimeEnd("2026-05-01 02:00:00"); + + SteadyChecksquareQueryVO result = service.query(param); + + Assertions.assertEquals(Integer.valueOf(1), result.getIntervalMinutes()); + Assertions.assertEquals(3, result.getItems().size()); + assertItemInterval(result.getItems().get(0), "FLUC", 10, 13); + assertItemInterval(result.getItems().get(1), "PST", 10, 13); + assertItemInterval(result.getItems().get(2), "PLT", 120, 2); + } + + @Test + void shouldOnlyQueryRequestedHarmonicOrders() { + SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + AddLedgerService addLedgerService = mock(AddLedgerService.class); + SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(), + influxQueryComponent, new SteadyChecksquareCalculator(), new AddDataTimeSlotCalculator(), addLedgerService); + AddLedgerLinePathVO linePath = new AddLedgerLinePathVO(); + linePath.setLineId("line-001"); + linePath.setLineName("进线一"); + linePath.setLineInterval(1); + when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001")))) + .thenReturn(Collections.singletonMap("line-001", linePath)); + when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1))) + .thenReturn(new HashSet(Collections.singletonList( + LocalDateTime.of(2026, 5, 1, 0, 0)))); + + SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam(); + param.setLineId("line-001"); + param.setIndicatorCodes(Collections.singletonList("V_HARMONIC")); + param.setHarmonicOrders(Collections.singletonList(5)); + param.setTimeStart("2026-05-01 00:00:00"); + param.setTimeEnd("2026-05-01 00:01:00"); + + SteadyChecksquareQueryVO result = service.query(param); + + Assertions.assertEquals(1, result.getItems().size()); + Assertions.assertEquals(Integer.valueOf(5), result.getItems().get(0).getHarmonicOrder()); + } + + @Test + void shouldKeepRequestedHarmonicOrdersDistinctAndOrdered() { + SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + AddLedgerService addLedgerService = mock(AddLedgerService.class); + SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(), + influxQueryComponent, new SteadyChecksquareCalculator(), new AddDataTimeSlotCalculator(), addLedgerService); + AddLedgerLinePathVO linePath = new AddLedgerLinePathVO(); + linePath.setLineId("line-001"); + linePath.setLineName("进线一"); + linePath.setLineInterval(1); + when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001")))) + .thenReturn(Collections.singletonMap("line-001", linePath)); + when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1))) + .thenReturn(new HashSet()); + + SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam(); + param.setLineId("line-001"); + param.setIndicatorCodes(Collections.singletonList("V_HARMONIC")); + param.setHarmonicOrders(Arrays.asList(7, 5, 7)); + param.setTimeStart("2026-05-01 00:00:00"); + param.setTimeEnd("2026-05-01 00:01:00"); + + SteadyChecksquareQueryVO result = service.query(param); + + List items = result.getItems(); + Assertions.assertEquals(2, items.size()); + Assertions.assertEquals(Integer.valueOf(7), items.get(0).getHarmonicOrder()); + Assertions.assertEquals(Integer.valueOf(5), items.get(1).getHarmonicOrder()); + } + + private void assertItemInterval(SteadyChecksquareItemVO item, String indicatorCode, int intervalMinutes, int expectedPointCount) { + Assertions.assertEquals(indicatorCode, item.getIndicatorCode()); + Assertions.assertEquals(Integer.valueOf(intervalMinutes), item.getIntervalMinutes()); + Assertions.assertEquals(Integer.valueOf(expectedPointCount), item.getExpectedPointCount()); + } +} diff --git a/steady/steady-DataView/src/test/java/com/njcn/gather/steady/datavie/component/SteadyInfluxQueryComponentTest.java b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/datavie/component/SteadyInfluxQueryComponentTest.java index f431d0c..ac14cf8 100644 --- a/steady/steady-DataView/src/test/java/com/njcn/gather/steady/datavie/component/SteadyInfluxQueryComponentTest.java +++ b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/datavie/component/SteadyInfluxQueryComponentTest.java @@ -73,6 +73,31 @@ class SteadyInfluxQueryComponentTest { Assertions.assertTrue(query.contains("\"value_type\" = 'AVG'")); } + @Test + void shouldBuildDiagnosticTextForTrendQuery() { + SteadyInfluxQueryComponent component = new SteadyInfluxQueryComponent(new SteadyInfluxDbProperties()); + SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO(); + field.setMeasurement("data_harmpower_p"); + field.setField("p_3"); + field.setLineId("f828bc42132841c2aeebc6859f5a9b7c"); + field.setPhase("A"); + field.setStatType("AVG"); + + String diagnostic = component.buildTrendQueryDiagnostic(field, + LocalDateTime.of(2026, 5, 1, 0, 0, 0), + LocalDateTime.of(2026, 5, 31, 23, 59, 59), + 0); + + Assertions.assertTrue(diagnostic.contains("measurement=data_harmpower_p")); + Assertions.assertTrue(diagnostic.contains("field=p_3")); + Assertions.assertTrue(diagnostic.contains("lineId=f828bc42132841c2aeebc6859f5a9b7c")); + Assertions.assertTrue(diagnostic.contains("phase=A")); + Assertions.assertTrue(diagnostic.contains("statType=AVG")); + Assertions.assertTrue(diagnostic.contains("qualityFlag=0")); + Assertions.assertTrue(diagnostic.contains("timeStart=2026-05-01T00:00")); + Assertions.assertTrue(diagnostic.contains("timeEnd=2026-05-31T23:59:59")); + } + @Test void shouldSkipValueTypeWhenMeasurementHasNoValueTypeTag() { SteadyInfluxQueryComponent component = new SteadyInfluxQueryComponent(new SteadyInfluxDbProperties()); diff --git a/steady/steady-DataView/src/test/java/com/njcn/gather/steady/datavie/component/SteadyTrendFieldResolverTest.java b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/datavie/component/SteadyTrendFieldResolverTest.java index 642e919..364f811 100644 --- a/steady/steady-DataView/src/test/java/com/njcn/gather/steady/datavie/component/SteadyTrendFieldResolverTest.java +++ b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/datavie/component/SteadyTrendFieldResolverTest.java @@ -109,6 +109,21 @@ class SteadyTrendFieldResolverTest { Assertions.assertTrue(exception.getMessage().contains("谐波次数不能为空")); } + @Test + void shouldRejectHarmonicTrendWithMoreThanThreeOrders() { + SteadyTrendQueryParam param = new SteadyTrendQueryParam(); + param.setLineIds(Arrays.asList("line-001")); + param.setIndicatorCodes(Arrays.asList("V_HARMONIC")); + param.setStatTypes(Arrays.asList("AVG")); + param.setHarmonicOrders(Arrays.asList(3, 5, 7, 11)); + param.setTimeStart("2026-05-01 00:00:00"); + param.setTimeEnd("2026-05-01 01:00:00"); + + BusinessException exception = Assertions.assertThrows(BusinessException.class, () -> resolver.resolveFields(param)); + + Assertions.assertTrue(exception.getMessage().contains("谐波次数不允许一次展示超过3个")); + } + @Test void shouldResolveSelectedHarmonicOrdersForAllCatalogPhases() { SteadyTrendQueryParam param = new SteadyTrendQueryParam(); diff --git a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/mapper/mapping/AddLedgerLineMapper.xml b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/mapper/mapping/AddLedgerLineMapper.xml index d1a5629..c19ec19 100644 --- a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/mapper/mapping/AddLedgerLineMapper.xml +++ b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/mapper/mapping/AddLedgerLineMapper.xml @@ -34,7 +34,8 @@ equipment.name AS equipmentName, equipment.mac AS equipmentMac, line.line_id AS lineId, - line.name AS lineName + line.name AS lineName, + line.line_interval AS lineInterval FROM cs_line line INNER JOIN cs_equipment_delivery equipment ON equipment.id = line.device_id INNER JOIN cs_project project ON project.id = equipment.associated_project @@ -58,7 +59,8 @@ equipment.name AS equipmentName, equipment.mac AS equipmentMac, line.line_id AS lineId, - line.name AS lineName + line.name AS lineName, + line.line_interval AS lineInterval FROM cs_line line INNER JOIN cs_equipment_delivery equipment ON equipment.id = line.device_id INNER JOIN cs_project project ON project.id = equipment.associated_project diff --git a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/vo/AddLedgerLinePathVO.java b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/vo/AddLedgerLinePathVO.java index f37855b..1912639 100644 --- a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/vo/AddLedgerLinePathVO.java +++ b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/vo/AddLedgerLinePathVO.java @@ -29,4 +29,7 @@ public class AddLedgerLinePathVO implements Serializable { private String lineId; private String lineName; + + /** 监测点统计间隔,单位分钟。 */ + private Integer lineInterval; }