diff --git a/.gitignore b/.gitignore index c6cb740..399ac3e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ target/ logs/ docs/ +.codex-tmp/ +.docs/ # Log file *.log diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareHarmonicParityRuleComponent.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareHarmonicParityRuleComponent.java new file mode 100644 index 0000000..d1c55d5 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareHarmonicParityRuleComponent.java @@ -0,0 +1,193 @@ +package com.njcn.gather.steady.checksquare.component; + +import com.njcn.gather.steady.checksquare.pojo.bo.SteadyChecksquareValuePointBO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityDetailVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityRuleVO; +import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendIndicatorDefinitionBO; +import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 谐波偶次与局部奇次基线关系规则。 + */ +@Component +@RequiredArgsConstructor +public class SteadyChecksquareHarmonicParityRuleComponent { + + private static final DateTimeFormatter OUTPUT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final BigDecimal THRESHOLD_MULTIPLIER = new BigDecimal("2"); + private static final BigDecimal EVEN_HARMONIC_DEADBAND_VALUE = new BigDecimal("0.1"); + private static final int MIN_ODD_REFERENCE_COUNT = 2; + + private final SteadyChecksquareInfluxQueryComponent influxQueryComponent; + + public SteadyChecksquareHarmonicParityRuleVO check(String lineId, SteadyTrendIndicatorDefinitionBO indicator, + LocalDateTime startTime, LocalDateTime endTime, + int intervalMinutes) { + SteadyChecksquareHarmonicParityRuleVO result = new SteadyChecksquareHarmonicParityRuleVO(); + if (!supportHarmonicParityRule(indicator)) { + return result; + } + for (String statType : indicator.getSupportStats()) { + for (String phase : indicator.getPhaseCodes()) { + Map> valueMap = queryOrderValueMap(lineId, indicator, phase, + statType, startTime, endTime, intervalMinutes); + appendAbnormalDetails(result, phase, statType, indicator, valueMap); + } + } + result.setAbnormalPointCount(result.getAbnormalDetails().size()); + result.setAbnormal(result.getAbnormalPointCount() > 0); + return result; + } + + private boolean supportHarmonicParityRule(SteadyTrendIndicatorDefinitionBO indicator) { + return indicator != null && Boolean.TRUE.equals(indicator.getHarmonic()) + && indicator.getHarmonicOrderStart() != null && indicator.getHarmonicOrderEnd() != null; + } + + private Map> queryOrderValueMap(String lineId, + SteadyTrendIndicatorDefinitionBO indicator, + String phase, String statType, + LocalDateTime startTime, + LocalDateTime endTime, + int intervalMinutes) { + Map> result = new LinkedHashMap>(); + List fields = new ArrayList(); + for (int order = indicator.getHarmonicOrderStart(); order <= indicator.getHarmonicOrderEnd(); order++) { + fields.add(buildResolvedField(lineId, indicator, order, phase, statType)); + } + Map> fieldValueMap = + influxQueryComponent.queryValuePointMap(fields, startTime, endTime, intervalMinutes); + if (fieldValueMap == null) { + fieldValueMap = Collections.emptyMap(); + } + for (int order = indicator.getHarmonicOrderStart(); order <= indicator.getHarmonicOrderEnd(); order++) { + result.put(order, toValueMap(fieldValueMap.get(indicator.getHarmonicFieldPrefix() + "_" + order))); + } + return result; + } + + private void appendAbnormalDetails(SteadyChecksquareHarmonicParityRuleVO result, String phase, String statType, + SteadyTrendIndicatorDefinitionBO indicator, + Map> valueMap) { + for (int order = firstEvenOrder(indicator.getHarmonicOrderStart()); order <= indicator.getHarmonicOrderEnd(); order += 2) { + Map evenValues = valueMap.get(order); + if (evenValues == null || evenValues.isEmpty()) { + continue; + } + for (Map.Entry entry : evenValues.entrySet()) { + appendAbnormalDetailIfNecessary(result, phase, statType, order, entry.getKey(), entry.getValue(), valueMap); + } + } + } + + private void appendAbnormalDetailIfNecessary(SteadyChecksquareHarmonicParityRuleVO result, String phase, + String statType, int evenOrder, LocalDateTime time, + BigDecimal evenValue, + Map> valueMap) { + if (evenValue == null || evenValue.compareTo(EVEN_HARMONIC_DEADBAND_VALUE) <= 0) { + return; + } + List oddOrders = buildOddReferenceOrders(evenOrder); + List oddValues = new ArrayList(); + List effectiveOddOrders = new ArrayList(); + for (Integer oddOrder : oddOrders) { + Map values = valueMap.get(oddOrder); + BigDecimal oddValue = values == null ? null : values.get(time); + if (oddValue != null) { + effectiveOddOrders.add(oddOrder); + oddValues.add(oddValue); + } + } + if (oddValues.size() < MIN_ODD_REFERENCE_COUNT) { + return; + } + BigDecimal median = calculateMedian(oddValues); + if (median == null || evenValue.compareTo(median.multiply(THRESHOLD_MULTIPLIER)) <= 0) { + return; + } + result.getAbnormalDetails().add(buildDetail(time, phase, statType, evenOrder, evenValue, + effectiveOddOrders, oddValues, median)); + } + + private SteadyChecksquareHarmonicParityDetailVO buildDetail(LocalDateTime time, String phase, String statType, + Integer evenOrder, BigDecimal evenValue, + List oddOrders, List oddValues, + BigDecimal median) { + SteadyChecksquareHarmonicParityDetailVO detail = new SteadyChecksquareHarmonicParityDetailVO(); + detail.setTime(OUTPUT_TIME_FORMATTER.format(time)); + detail.setPhase(phase); + detail.setStatType(statType); + detail.setEvenHarmonicOrder(evenOrder); + detail.setEvenValue(evenValue); + detail.setOddHarmonicOrders(new ArrayList(oddOrders)); + detail.setOddValues(new ArrayList(oddValues)); + detail.setOddMedianValue(median); + detail.setThresholdMultiplier(THRESHOLD_MULTIPLIER); + return detail; + } + + private List buildOddReferenceOrders(int evenOrder) { + List result = new ArrayList(); + result.add(evenOrder - 3); + result.add(evenOrder - 1); + result.add(evenOrder + 1); + result.add(evenOrder + 3); + return result; + } + + private BigDecimal calculateMedian(List values) { + if (values == null || values.isEmpty()) { + return null; + } + List sorted = new ArrayList(values); + Collections.sort(sorted, Comparator.naturalOrder()); + int middleIndex = sorted.size() / 2; + if (sorted.size() % 2 == 1) { + return sorted.get(middleIndex); + } + return sorted.get(middleIndex - 1).add(sorted.get(middleIndex)).divide(new BigDecimal("2")); + } + + private int firstEvenOrder(int startOrder) { + return startOrder % 2 == 0 ? startOrder : startOrder + 1; + } + + private Map toValueMap(List points) { + Map result = new LinkedHashMap(); + if (points == null || points.isEmpty()) { + return result; + } + for (SteadyChecksquareValuePointBO point : points) { + if (point != null && point.getTime() != null && point.getValue() != null) { + result.put(point.getTime(), point.getValue()); + } + } + 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(indicator.getHarmonicFieldPrefix() + "_" + harmonicOrder); + field.setLineId(lineId); + field.setIndicatorCode(indicator.getIndicatorCode()); + field.setIndicatorName(indicator.getName()); + field.setPhase(phase); + field.setStatType(statType); + field.setUnit(indicator.getUnit()); + return field; + } +} 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 index 721875e..c3841d1 100644 --- 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 @@ -4,6 +4,7 @@ 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.checksquare.pojo.bo.SteadyChecksquareValuePointBO; import com.njcn.gather.steady.datavie.config.SteadyInfluxDbProperties; import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO; import lombok.RequiredArgsConstructor; @@ -14,6 +15,7 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.math.BigDecimal; import java.net.HttpURLConnection; import java.net.URLEncoder; import java.net.URL; @@ -22,7 +24,11 @@ import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -35,28 +41,121 @@ 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 static final ThreadLocal>> REQUEST_VALUE_CACHE = + new ThreadLocal>>(); private final SteadyInfluxDbProperties properties; + public void enableRequestCache() { + REQUEST_VALUE_CACHE.set(new LinkedHashMap>()); + } + + public void clearRequestCache() { + REQUEST_VALUE_CACHE.remove(); + } + public Set queryExistingSlots(SteadyTrendResolvedFieldBO field, LocalDateTime startTime, LocalDateTime endTime, int intervalMinutes) { + List points = queryValuePoints(field, startTime, endTime, intervalMinutes); + Set result = new HashSet(); + for (SteadyChecksquareValuePointBO point : points) { + if (point != null && point.getTime() != null) { + result.add(point.getTime()); + } + } + return result; + } + + public List queryValuePoints(SteadyTrendResolvedFieldBO field, LocalDateTime startTime, + LocalDateTime endTime, int intervalMinutes) { validateConfig(); - String query = buildChecksquareQuery(field, startTime, endTime); + String query = buildValuePointQuery(field, startTime, endTime); + String cacheKey = buildCacheKey(query, intervalMinutes); + Map> cache = REQUEST_VALUE_CACHE.get(); + if (cache != null && cache.containsKey(cacheKey)) { + return new ArrayList(cache.get(cacheKey)); + } long startMillis = System.currentTimeMillis(); - log.info("数据校验 InfluxDB 查询开始,measurement={},field={},lineId={},phase={},statType={},query={}", + 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; + List points = parseValuePoints(body, intervalMinutes); + if (cache != null) { + cache.put(cacheKey, new ArrayList(points)); + } + log.info("数据校验指标值 InfluxDB 查询结束,pointCount={},costMs={}", points.size(), System.currentTimeMillis() - startMillis); + return points; } catch (RuntimeException ex) { - log.warn("数据校验 InfluxDB 查询异常,costMs={},error={}", System.currentTimeMillis() - startMillis, ex.getMessage()); + log.warn("数据校验指标值 InfluxDB 查询异常,costMs={},error={}", System.currentTimeMillis() - startMillis, ex.getMessage()); throw ex; } } + public Map> queryValuePointMap(List fields, + LocalDateTime startTime, + LocalDateTime endTime, + int intervalMinutes) { + Map> result = + new LinkedHashMap>(); + if (fields == null || fields.isEmpty()) { + return result; + } + if (fields.size() == 1) { + SteadyTrendResolvedFieldBO field = fields.get(0); + result.put(field.getField(), queryValuePoints(field, startTime, endTime, intervalMinutes)); + return result; + } + validateConfig(); + Map> cache = REQUEST_VALUE_CACHE.get(); + List missingFields = new ArrayList(); + for (SteadyTrendResolvedFieldBO field : fields) { + String cacheKey = buildCacheKey(buildValuePointQuery(field, startTime, endTime), intervalMinutes); + if (cache != null && cache.containsKey(cacheKey)) { + result.put(field.getField(), new ArrayList(cache.get(cacheKey))); + } else { + missingFields.add(field); + } + } + if (!missingFields.isEmpty()) { + String query = buildBatchValuePointQuery(missingFields, startTime, endTime); + long startMillis = System.currentTimeMillis(); + SteadyTrendResolvedFieldBO first = missingFields.get(0); + log.info("数据校验指标值 InfluxDB 批量查询开始,measurement={},fieldCount={},lineId={},phase={},statType={},query={}", + first.getMeasurement(), missingFields.size(), first.getLineId(), first.getPhase(), first.getStatType(), query); + try { + Map> queried = parseBatchValuePoints(executeQuery(query), intervalMinutes); + for (SteadyTrendResolvedFieldBO field : missingFields) { + List points = queried.get(field.getField()); + if (points == null) { + points = new ArrayList(); + } + result.put(field.getField(), points); + if (cache != null) { + String cacheKey = buildCacheKey(buildValuePointQuery(field, startTime, endTime), intervalMinutes); + cache.put(cacheKey, new ArrayList(points)); + } + } + log.info("数据校验指标值 InfluxDB 批量查询结束,fieldCount={},costMs={}", + missingFields.size(), System.currentTimeMillis() - startMillis); + } catch (RuntimeException ex) { + log.warn("数据校验指标值 InfluxDB 批量查询异常,fieldCount={},costMs={},error={}", + missingFields.size(), System.currentTimeMillis() - startMillis, ex.getMessage()); + throw ex; + } + } + return result; + } + public String buildChecksquareQuery(SteadyTrendResolvedFieldBO field, LocalDateTime startTime, LocalDateTime endTime) { + return buildValuePointQuery(field, startTime, endTime); + } + + private String buildCacheKey(String query, int intervalMinutes) { + return query + "|intervalMinutes=" + intervalMinutes; + } + + public String buildValuePointQuery(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("\""); @@ -71,11 +170,33 @@ public class SteadyChecksquareInfluxQueryComponent { return sql.toString(); } - private Set parseExistingSlots(String body, int intervalMinutes) { + public String buildBatchValuePointQuery(List fields, LocalDateTime startTime, LocalDateTime endTime) { + SteadyTrendResolvedFieldBO first = fields.get(0); + StringBuilder sql = new StringBuilder("SELECT "); + for (int i = 0; i < fields.size(); i++) { + SteadyTrendResolvedFieldBO field = fields.get(i); + if (i > 0) { + sql.append(", "); + } + sql.append("\"").append(field.getField()).append("\" AS \"").append(field.getField()).append("\""); + } + sql.append(" FROM \"").append(first.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(first.getLineId())).append("'"); + sql.append(" AND \"phasic_type\" = '").append(escapeTagValue(first.getPhase())).append("'"); + if (hasValueTypeTag(first.getMeasurement())) { + sql.append(" AND \"value_type\" = '").append(resolveValueType(first.getStatType())).append("'"); + } + sql.append(" ORDER BY time ASC"); + return sql.toString(); + } + + private List parseValuePoints(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(); + List result = new ArrayList(); if (!values.isArray()) { return result; } @@ -84,13 +205,64 @@ public class SteadyChecksquareInfluxQueryComponent { continue; } LocalDateTime time = parseInfluxTime(value.get(0).asText()); - if (time != null) { - result.add(alignToPreviousSlot(time, intervalMinutes)); + if (time == null) { + continue; + } + SteadyChecksquareValuePointBO point = new SteadyChecksquareValuePointBO(); + point.setTime(alignToPreviousSlot(time, intervalMinutes)); + point.setValue(new BigDecimal(value.get(1).asText())); + result.add(point); + } + return result; + } catch (IOException ex) { + throw fail("InfluxDB 返回结果解析失败:" + ex.getMessage()); + } catch (NumberFormatException ex) { + throw fail("InfluxDB 返回指标值格式不正确:" + ex.getMessage()); + } + } + + private Map> parseBatchValuePoints(String body, int intervalMinutes) { + try { + JsonNode root = OBJECT_MAPPER.readTree(body); + JsonNode series = root.path("results").path(0).path("series").path(0); + JsonNode columns = series.path("columns"); + JsonNode values = series.path("values"); + Map columnMap = new LinkedHashMap(); + Map> result = + new LinkedHashMap>(); + if (!columns.isArray() || !values.isArray()) { + return result; + } + for (int i = 1; i < columns.size(); i++) { + String fieldName = columns.get(i).asText(); + columnMap.put(i, fieldName); + result.put(fieldName, new ArrayList()); + } + for (JsonNode row : values) { + if (row.size() < 2) { + continue; + } + LocalDateTime time = parseInfluxTime(row.get(0).asText()); + if (time == null) { + continue; + } + LocalDateTime slot = alignToPreviousSlot(time, intervalMinutes); + for (Map.Entry entry : columnMap.entrySet()) { + JsonNode value = row.get(entry.getKey()); + if (value == null || value.isNull()) { + continue; + } + SteadyChecksquareValuePointBO point = new SteadyChecksquareValuePointBO(); + point.setTime(slot); + point.setValue(new BigDecimal(value.asText())); + result.get(entry.getValue()).add(point); } } return result; } catch (IOException ex) { throw fail("InfluxDB 返回结果解析失败:" + ex.getMessage()); + } catch (NumberFormatException ex) { + throw fail("InfluxDB 返回指标值格式不正确:" + ex.getMessage()); } } diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponent.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponent.java new file mode 100644 index 0000000..1555a59 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponent.java @@ -0,0 +1,145 @@ +package com.njcn.gather.steady.checksquare.component; + +import com.njcn.gather.steady.checksquare.pojo.bo.SteadyChecksquareValuePointBO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderDetailVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderRuleVO; +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 lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 数据校验指标值大小关系规则。 + */ +@Component +@RequiredArgsConstructor +public class SteadyChecksquareValueOrderRuleComponent { + + private static final DateTimeFormatter OUTPUT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final List REQUIRED_STATS = Collections.unmodifiableList(Arrays.asList("MAX", "CP95", "AVG", "MIN")); + private static final int ABNORMAL_THRESHOLD = 1; + + private final SteadyChecksquareInfluxQueryComponent influxQueryComponent; + + public SteadyChecksquareValueOrderRuleVO check(String lineId, SteadyTrendIndicatorDefinitionBO indicator, + Integer harmonicOrder, LocalDateTime startTime, + LocalDateTime endTime, int intervalMinutes) { + SteadyChecksquareValueOrderRuleVO result = new SteadyChecksquareValueOrderRuleVO(); + if (!supportValueOrderRule(indicator)) { + return result; + } + for (String phase : indicator.getPhaseCodes()) { + Map> statValueMap = queryStatValueMap(lineId, indicator, + harmonicOrder, phase, startTime, endTime, intervalMinutes); + appendAbnormalDetails(result, phase, harmonicOrder, statValueMap); + } + result.setAbnormalPointCount(result.getAbnormalDetails().size()); + result.setAbnormal(result.getAbnormalPointCount() > ABNORMAL_THRESHOLD); + return result; + } + + private boolean supportValueOrderRule(SteadyTrendIndicatorDefinitionBO indicator) { + return indicator != null && indicator.getSupportStats() != null && indicator.getSupportStats().containsAll(REQUIRED_STATS); + } + + private Map> queryStatValueMap(String lineId, + SteadyTrendIndicatorDefinitionBO indicator, + Integer harmonicOrder, String phase, + LocalDateTime startTime, LocalDateTime endTime, + int intervalMinutes) { + Map> result = new LinkedHashMap>(); + for (String statType : REQUIRED_STATS) { + SteadyTrendResolvedFieldBO field = buildResolvedField(lineId, indicator, harmonicOrder, phase, statType); + result.put(statType, toValueMap(influxQueryComponent.queryValuePoints(field, startTime, endTime, intervalMinutes))); + } + return result; + } + + private void appendAbnormalDetails(SteadyChecksquareValueOrderRuleVO result, String phase, Integer harmonicOrder, + Map> statValueMap) { + Map maxValues = statValueMap.get("MAX"); + Map cp95Values = statValueMap.get("CP95"); + Map avgValues = statValueMap.get("AVG"); + Map minValues = statValueMap.get("MIN"); + if (maxValues == null || cp95Values == null || avgValues == null || minValues == null) { + return; + } + for (Map.Entry entry : maxValues.entrySet()) { + LocalDateTime time = entry.getKey(); + BigDecimal maxValue = entry.getValue(); + BigDecimal cp95Value = cp95Values.get(time); + BigDecimal avgValue = avgValues.get(time); + BigDecimal minValue = minValues.get(time); + // 缺少任一统计值时由缺数校验负责,不重复计入大小关系异常。 + if (maxValue == null || cp95Value == null || avgValue == null || minValue == null) { + continue; + } + if (maxValue.compareTo(cp95Value) >= 0 && cp95Value.compareTo(avgValue) >= 0 && avgValue.compareTo(minValue) >= 0) { + continue; + } + result.getAbnormalDetails().add(buildDetail(time, phase, harmonicOrder, maxValue, minValue, avgValue, cp95Value)); + } + } + + private SteadyChecksquareValueOrderDetailVO buildDetail(LocalDateTime time, String phase, Integer harmonicOrder, + BigDecimal maxValue, BigDecimal minValue, + BigDecimal avgValue, BigDecimal cp95Value) { + SteadyChecksquareValueOrderDetailVO detail = new SteadyChecksquareValueOrderDetailVO(); + detail.setTime(OUTPUT_TIME_FORMATTER.format(time)); + detail.setPhase(phase); + detail.setHarmonicOrder(harmonicOrder); + detail.setMaxValue(maxValue); + detail.setMinValue(minValue); + detail.setAvgValue(avgValue); + detail.setCp95Value(cp95Value); + return detail; + } + + private Map toValueMap(List points) { + Map result = new LinkedHashMap(); + if (points == null || points.isEmpty()) { + return result; + } + for (SteadyChecksquareValuePointBO point : points) { + if (point != null && point.getTime() != null && point.getValue() != null) { + result.put(point.getTime(), point.getValue()); + } + } + 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()) { + return ""; + } + return fields.get(0).getField(); + } +} 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 index f3d35f5..c0bfa9a 100644 --- 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 @@ -1,12 +1,18 @@ package com.njcn.gather.steady.checksquare.controller; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.constant.OperateType; 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.SteadyChecksquareHistoryQueryParam; import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareCreateVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemDetailVO; import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareQueryVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareTaskVO; import com.njcn.gather.steady.checksquare.service.SteadyChecksquareService; import com.njcn.web.controller.BaseController; import com.njcn.web.utils.HttpResultUtil; @@ -14,9 +20,12 @@ import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** @@ -32,12 +41,43 @@ public class SteadyChecksquareController extends BaseController { private final SteadyChecksquareService checksquareService; @OperateInfo(info = LogEnum.BUSINESS_COMMON) - @ApiOperation("查询数据校验结果") + @ApiOperation("查询数据校验历史记录") @PostMapping("/query") - public HttpResult query(@RequestBody SteadyChecksquareQueryParam param) { + public HttpResult> query(@RequestBody @Validated SteadyChecksquareHistoryQueryParam param) { String methodDescribe = getMethodDescribe("query"); - LogUtil.njcnDebug(log, "{},开始查询数据校验结果,param={}", methodDescribe, param); - SteadyChecksquareQueryVO result = checksquareService.query(param); + LogUtil.njcnDebug(log, "{},开始查询数据校验历史记录,param={}", methodDescribe, param); + Page result = checksquareService.query(param); return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD) + @ApiOperation("新增数据校验记录") + @PostMapping("/create") + public HttpResult create(@RequestBody @Validated SteadyChecksquareQueryParam param) { + String methodDescribe = getMethodDescribe("create"); + LogUtil.njcnDebug(log, "{},开始新增数据校验记录,param={}", methodDescribe, param); + SteadyChecksquareCreateVO result = checksquareService.create(param); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询数据校验任务详情") + @GetMapping("/detail") + public HttpResult detail(@RequestParam("taskId") String taskId) { + String methodDescribe = getMethodDescribe("detail"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, checksquareService.detail(taskId), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询数据校验检测项明细") + @GetMapping("/item-detail") + public HttpResult itemDetail(@RequestParam("itemId") String itemId, + @RequestParam("detailType") String detailType, + @RequestParam(value = "statType", required = false) String statType, + @RequestParam(value = "pageNum", required = false) Integer pageNum, + @RequestParam(value = "pageSize", required = false) Integer pageSize) { + String methodDescribe = getMethodDescribe("itemDetail"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, + checksquareService.itemDetail(itemId, detailType, statType, pageNum, pageSize), methodDescribe); + } } diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/mapper/SteadyChecksquareDetailMapper.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/mapper/SteadyChecksquareDetailMapper.java new file mode 100644 index 0000000..cc428ae --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/mapper/SteadyChecksquareDetailMapper.java @@ -0,0 +1,10 @@ +package com.njcn.gather.steady.checksquare.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareDetailPO; + +/** + * 数据校验明细 Mapper。 + */ +public interface SteadyChecksquareDetailMapper extends BaseMapper { +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/mapper/SteadyChecksquareItemMapper.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/mapper/SteadyChecksquareItemMapper.java new file mode 100644 index 0000000..f37fb1c --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/mapper/SteadyChecksquareItemMapper.java @@ -0,0 +1,10 @@ +package com.njcn.gather.steady.checksquare.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareItemPO; + +/** + * 数据校验检测项 Mapper。 + */ +public interface SteadyChecksquareItemMapper extends BaseMapper { +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/mapper/SteadyChecksquareStatSummaryMapper.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/mapper/SteadyChecksquareStatSummaryMapper.java new file mode 100644 index 0000000..5a01345 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/mapper/SteadyChecksquareStatSummaryMapper.java @@ -0,0 +1,10 @@ +package com.njcn.gather.steady.checksquare.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareStatSummaryPO; + +/** + * 数据校验统计摘要 Mapper。 + */ +public interface SteadyChecksquareStatSummaryMapper extends BaseMapper { +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/mapper/SteadyChecksquareTaskMapper.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/mapper/SteadyChecksquareTaskMapper.java new file mode 100644 index 0000000..53c6007 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/mapper/SteadyChecksquareTaskMapper.java @@ -0,0 +1,10 @@ +package com.njcn.gather.steady.checksquare.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareTaskPO; + +/** + * 数据校验任务 Mapper。 + */ +public interface SteadyChecksquareTaskMapper extends BaseMapper { +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/bo/SteadyChecksquareValuePointBO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/bo/SteadyChecksquareValuePointBO.java new file mode 100644 index 0000000..bf5df4a --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/bo/SteadyChecksquareValuePointBO.java @@ -0,0 +1,22 @@ +package com.njcn.gather.steady.checksquare.pojo.bo; + +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 数据校验指标值时间点。 + */ +@Data +public class SteadyChecksquareValuePointBO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 对齐后的统计时间。 */ + private LocalDateTime time; + + /** 指标值。 */ + private BigDecimal value; +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/constant/SteadyChecksquareConst.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/constant/SteadyChecksquareConst.java new file mode 100644 index 0000000..a588aa0 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/constant/SteadyChecksquareConst.java @@ -0,0 +1,18 @@ +package com.njcn.gather.steady.checksquare.pojo.constant; + +/** + * 数据校验常量。 + */ +public final class SteadyChecksquareConst { + + public static final int STATE_DELETED = 0; + public static final int STATE_ENABLED = 1; + + public static final String TASK_STATUS_SUCCESS = "SUCCESS"; + public static final String DETAIL_TYPE_SEGMENT = "SEGMENT"; + public static final String DETAIL_TYPE_VALUE_ORDER = "VALUE_ORDER"; + public static final String DETAIL_TYPE_HARMONIC_PARITY = "HARMONIC_PARITY"; + + private SteadyChecksquareConst() { + } +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/param/SteadyChecksquareHistoryQueryParam.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/param/SteadyChecksquareHistoryQueryParam.java new file mode 100644 index 0000000..4a21acd --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/param/SteadyChecksquareHistoryQueryParam.java @@ -0,0 +1,31 @@ +package com.njcn.gather.steady.checksquare.pojo.param; + +import com.njcn.web.pojo.param.BaseParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 数据校验历史查询参数。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ApiModel("数据校验历史查询参数") +public class SteadyChecksquareHistoryQueryParam extends BaseParam { + + @ApiModelProperty("监测点 ID") + private String lineId; + + @ApiModelProperty("指标编码") + private String indicatorCode; + + @ApiModelProperty("检测开始时间,格式 yyyy-MM-dd HH:mm:ss") + private String timeStart; + + @ApiModelProperty("检测结束时间,格式 yyyy-MM-dd HH:mm:ss") + private String timeEnd; + + @ApiModelProperty("是否存在异常") + private Boolean hasAbnormal; +} 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 index 680bd04..611ac57 100644 --- 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 @@ -8,10 +8,10 @@ import java.io.Serializable; import java.util.List; /** - * 数据校验查询参数。 + * 数据校验新增检测参数。 */ @Data -@ApiModel("数据校验查询参数") +@ApiModel("数据校验新增检测参数") public class SteadyChecksquareQueryParam implements Serializable { private static final long serialVersionUID = 1L; @@ -27,7 +27,4 @@ public class SteadyChecksquareQueryParam implements Serializable { @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/po/SteadyChecksquareDetailPO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/po/SteadyChecksquareDetailPO.java new file mode 100644 index 0000000..1be1729 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/po/SteadyChecksquareDetailPO.java @@ -0,0 +1,67 @@ +package com.njcn.gather.steady.checksquare.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 数据校验明细。 + */ +@Data +@TableName("steady_checksquare_detail") +public class SteadyChecksquareDetailPO implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + @TableField("item_id") + private String itemId; + @TableField("detail_type") + private String detailType; + @TableField("stat_type") + private String statType; + @TableField("start_time") + private LocalDateTime startTime; + @TableField("end_time") + private LocalDateTime endTime; + @TableField("point_time") + private LocalDateTime pointTime; + @TableField("segment_status") + private String segmentStatus; + @TableField("missing_point_count") + private Integer missingPointCount; + @TableField("duration_minutes") + private Integer durationMinutes; + @TableField("phase") + private String phase; + @TableField("harmonic_order") + private Integer harmonicOrder; + @TableField("max_value") + private BigDecimal maxValue; + @TableField("min_value") + private BigDecimal minValue; + @TableField("avg_value") + private BigDecimal avgValue; + @TableField("cp95_value") + private BigDecimal cp95Value; + @TableField("even_harmonic_order") + private Integer evenHarmonicOrder; + @TableField("even_value") + private BigDecimal evenValue; + @TableField("odd_harmonic_orders_json") + private String oddHarmonicOrdersJson; + @TableField("odd_values_json") + private String oddValuesJson; + @TableField("odd_median_value") + private BigDecimal oddMedianValue; + @TableField("threshold_multiplier") + private BigDecimal thresholdMultiplier; + @TableField("create_time") + private LocalDateTime createTime; +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/po/SteadyChecksquareItemPO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/po/SteadyChecksquareItemPO.java new file mode 100644 index 0000000..6a56ea7 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/po/SteadyChecksquareItemPO.java @@ -0,0 +1,63 @@ +package com.njcn.gather.steady.checksquare.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 数据校验检测项。 + */ +@Data +@TableName("steady_checksquare_item") +public class SteadyChecksquareItemPO implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + @TableField("task_id") + private String taskId; + @TableField("item_key") + private String itemKey; + @TableField("indicator_code") + private String indicatorCode; + @TableField("indicator_name") + private String indicatorName; + @TableField("harmonic_order") + private Integer harmonicOrder; + @TableField("interval_minutes") + private Integer intervalMinutes; + @TableField("has_data") + private Integer hasData; + @TableField("expected_point_count") + private Integer expectedPointCount; + @TableField("actual_point_count") + private Integer actualPointCount; + @TableField("missing_point_count") + private Integer missingPointCount; + @TableField("missing_rate") + private BigDecimal missingRate; + @TableField("missing_rate_text") + private String missingRateText; + @TableField("max_continuous_missing_minutes") + private Integer maxContinuousMissingMinutes; + @TableField("abnormal") + private Integer abnormal; + @TableField("abnormal_point_count") + private Integer abnormalPointCount; + @TableField("harmonic_parity_abnormal") + private Integer harmonicParityAbnormal; + @TableField("harmonic_parity_abnormal_point_count") + private Integer harmonicParityAbnormalPointCount; + @TableField("state") + private Integer state; + @TableField("create_time") + private LocalDateTime createTime; + @TableField("update_time") + private LocalDateTime updateTime; +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/po/SteadyChecksquareStatSummaryPO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/po/SteadyChecksquareStatSummaryPO.java new file mode 100644 index 0000000..5f4697c --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/po/SteadyChecksquareStatSummaryPO.java @@ -0,0 +1,45 @@ +package com.njcn.gather.steady.checksquare.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 数据校验统计摘要。 + */ +@Data +@TableName("steady_checksquare_stat_summary") +public class SteadyChecksquareStatSummaryPO implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + @TableField("item_id") + private String itemId; + @TableField("stat_type") + private String statType; + @TableField("supported") + private Integer supported; + @TableField("has_data") + private Integer hasData; + @TableField("expected_point_count") + private Integer expectedPointCount; + @TableField("actual_point_count") + private Integer actualPointCount; + @TableField("missing_point_count") + private Integer missingPointCount; + @TableField("missing_rate") + private BigDecimal missingRate; + @TableField("missing_rate_text") + private String missingRateText; + @TableField("max_continuous_missing_minutes") + private Integer maxContinuousMissingMinutes; + @TableField("create_time") + private LocalDateTime createTime; +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/po/SteadyChecksquareTaskPO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/po/SteadyChecksquareTaskPO.java new file mode 100644 index 0000000..bcc75b2 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/po/SteadyChecksquareTaskPO.java @@ -0,0 +1,59 @@ +package com.njcn.gather.steady.checksquare.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 数据校验任务。 + */ +@Data +@TableName("steady_checksquare_task") +public class SteadyChecksquareTaskPO implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId("id") + private String id; + @TableField("task_no") + private String taskNo; + @TableField("line_id") + private String lineId; + @TableField("line_name") + private String lineName; + @TableField("time_start") + private LocalDateTime timeStart; + @TableField("time_end") + private LocalDateTime timeEnd; + @TableField("interval_minutes") + private Integer intervalMinutes; + @TableField("indicator_codes_json") + private String indicatorCodesJson; + @TableField("indicator_codes_text") + private String indicatorCodesText; + @TableField("task_status") + private String taskStatus; + @TableField("item_count") + private Integer itemCount; + @TableField("abnormal_item_count") + private Integer abnormalItemCount; + @TableField("max_missing_rate") + private BigDecimal maxMissingRate; + @TableField("result_message") + private String resultMessage; + @TableField("state") + private Integer state; + @TableField("create_by") + private String createBy; + @TableField("create_time") + private LocalDateTime createTime; + @TableField("update_by") + private String updateBy; + @TableField("update_time") + private LocalDateTime updateTime; +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareCreateVO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareCreateVO.java new file mode 100644 index 0000000..a974484 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareCreateVO.java @@ -0,0 +1,44 @@ +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 SteadyChecksquareCreateVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("任务 ID") + private String taskId; + + @ApiModelProperty("任务编号") + private String taskNo; + + @ApiModelProperty("监测点 ID") + private String lineId; + + @ApiModelProperty("监测点名称") + private String lineName; + + @ApiModelProperty("开始时间") + private String timeStart; + + @ApiModelProperty("结束时间") + private String timeEnd; + + @ApiModelProperty("统计间隔,单位分钟") + private Integer intervalMinutes; + + @ApiModelProperty("检测项数量") + private Integer itemCount; + + @ApiModelProperty("异常检测项数量") + private Integer abnormalItemCount; +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareHarmonicParityDetailVO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareHarmonicParityDetailVO.java new file mode 100644 index 0000000..0e16a06 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareHarmonicParityDetailVO.java @@ -0,0 +1,47 @@ +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 SteadyChecksquareHarmonicParityDetailVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("时间") + private String time; + + @ApiModelProperty("相别") + private String phase; + + @ApiModelProperty("统计类型") + private String statType; + + @ApiModelProperty("偶次谐波次数") + private Integer evenHarmonicOrder; + + @ApiModelProperty("偶次谐波值") + private BigDecimal evenValue; + + @ApiModelProperty("参与比较的奇次谐波次数") + private List oddHarmonicOrders = new ArrayList(); + + @ApiModelProperty("参与比较的奇次谐波值") + private List oddValues = new ArrayList(); + + @ApiModelProperty("奇次谐波中位数") + private BigDecimal oddMedianValue; + + @ApiModelProperty("异常阈值倍数") + private BigDecimal thresholdMultiplier; +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareHarmonicParityRuleVO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareHarmonicParityRuleVO.java new file mode 100644 index 0000000..cef4a62 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareHarmonicParityRuleVO.java @@ -0,0 +1,23 @@ +package com.njcn.gather.steady.checksquare.pojo.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 谐波奇偶关系规则结果。 + */ +@Data +public class SteadyChecksquareHarmonicParityRuleVO implements Serializable { + + private static final long serialVersionUID = 1L; + + private Boolean abnormal = false; + + private Integer abnormalPointCount = 0; + + private List abnormalDetails = + new ArrayList(); +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareItemDetailVO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareItemDetailVO.java new file mode 100644 index 0000000..ee86da2 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareItemDetailVO.java @@ -0,0 +1,48 @@ +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 SteadyChecksquareItemDetailVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("检测项 ID") + private String itemId; + + @ApiModelProperty("明细类型") + private String detailType; + + @ApiModelProperty("统计类型") + private String statType; + + @ApiModelProperty("当前页码;未分页查询时为空") + private Integer pageNum; + + @ApiModelProperty("每页条数;未分页查询时为空") + private Integer pageSize; + + @ApiModelProperty("总记录数;未分页查询时为空") + private Long total; + + @ApiModelProperty("缺失区间") + private List segments = new ArrayList(); + + @ApiModelProperty("大小关系异常明细") + private List valueOrderDetails = + new ArrayList(); + + @ApiModelProperty("谐波奇偶关系异常明细") + private List harmonicParityDetails = + new ArrayList(); +} 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 index b31d994..400e37a 100644 --- 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 @@ -18,6 +18,9 @@ public class SteadyChecksquareItemVO implements Serializable { private static final long serialVersionUID = 1L; + @ApiModelProperty("检测项 ID") + private String itemId; + @ApiModelProperty("校验项唯一键") private String itemKey; @@ -54,6 +57,25 @@ public class SteadyChecksquareItemVO implements Serializable { @ApiModelProperty("最大连续缺失时长,单位分钟") private Integer maxContinuousMissingMinutes; + @ApiModelProperty("指标值大小关系是否异常") + private Boolean abnormal; + + @ApiModelProperty("指标值大小关系异常累计值") + private Integer abnormalPointCount; + + @ApiModelProperty("指标值大小关系异常明细") + private List abnormalDetails = new ArrayList(); + + @ApiModelProperty("谐波奇偶关系是否异常") + private Boolean harmonicParityAbnormal; + + @ApiModelProperty("谐波奇偶关系异常累计值") + private Integer harmonicParityAbnormalPointCount; + + @ApiModelProperty("谐波奇偶关系异常明细") + private List harmonicParityAbnormalDetails = + new ArrayList(); + @ApiModelProperty("统计类型摘要") private List statSummaries = 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 index 7ba697b..b6cac48 100644 --- 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 @@ -17,6 +17,12 @@ public class SteadyChecksquareQueryVO implements Serializable { private static final long serialVersionUID = 1L; + @ApiModelProperty("任务 ID") + private String taskId; + + @ApiModelProperty("任务编号") + private String taskNo; + @ApiModelProperty("监测点 ID") private String lineId; 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 index b455860..4b80e06 100644 --- 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 @@ -24,6 +24,9 @@ public class SteadyChecksquareSegmentVO implements Serializable { @ApiModelProperty("状态,NORMAL/MISSING") private String status; + @ApiModelProperty("谐波次数") + private Integer harmonicOrder; + @ApiModelProperty("缺失点数") private Integer missingPointCount; diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareTaskVO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareTaskVO.java new file mode 100644 index 0000000..c8904ec --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareTaskVO.java @@ -0,0 +1,54 @@ +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 SteadyChecksquareTaskVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("任务 ID") + private String taskId; + + @ApiModelProperty("任务编号") + private String taskNo; + + @ApiModelProperty("监测点 ID") + private String lineId; + + @ApiModelProperty("监测点名称") + private String lineName; + + @ApiModelProperty("开始时间") + private String timeStart; + + @ApiModelProperty("结束时间") + private String timeEnd; + + @ApiModelProperty("统计间隔,单位分钟") + private Integer intervalMinutes; + + @ApiModelProperty("任务状态") + private String taskStatus; + + @ApiModelProperty("检测项数量") + private Integer itemCount; + + @ApiModelProperty("异常检测项数量") + private Integer abnormalItemCount; + + @ApiModelProperty("最大缺失率") + private BigDecimal maxMissingRate; + + @ApiModelProperty("创建时间") + private String createTime; +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareValueOrderDetailVO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareValueOrderDetailVO.java new file mode 100644 index 0000000..0fdb0e4 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareValueOrderDetailVO.java @@ -0,0 +1,39 @@ +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 SteadyChecksquareValueOrderDetailVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty("时间") + private String time; + + @ApiModelProperty("相别") + private String phase; + + @ApiModelProperty("谐波次数") + private Integer harmonicOrder; + + @ApiModelProperty("最大值") + private BigDecimal maxValue; + + @ApiModelProperty("最小值") + private BigDecimal minValue; + + @ApiModelProperty("平均值") + private BigDecimal avgValue; + + @ApiModelProperty("CP95 值") + private BigDecimal cp95Value; +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareValueOrderRuleVO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareValueOrderRuleVO.java new file mode 100644 index 0000000..b0530b5 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareValueOrderRuleVO.java @@ -0,0 +1,22 @@ +package com.njcn.gather.steady.checksquare.pojo.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 数据校验指标值大小关系规则结果。 + */ +@Data +public class SteadyChecksquareValueOrderRuleVO implements Serializable { + + private static final long serialVersionUID = 1L; + + private Boolean abnormal = false; + + private Integer abnormalPointCount = 0; + + private List abnormalDetails = new ArrayList(); +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/SteadyChecksquareDetailService.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/SteadyChecksquareDetailService.java new file mode 100644 index 0000000..6f17e74 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/SteadyChecksquareDetailService.java @@ -0,0 +1,10 @@ +package com.njcn.gather.steady.checksquare.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareDetailPO; + +/** + * 数据校验明细服务。 + */ +public interface SteadyChecksquareDetailService extends IService { +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/SteadyChecksquareItemService.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/SteadyChecksquareItemService.java new file mode 100644 index 0000000..683a0da --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/SteadyChecksquareItemService.java @@ -0,0 +1,10 @@ +package com.njcn.gather.steady.checksquare.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareItemPO; + +/** + * 数据校验检测项服务。 + */ +public interface SteadyChecksquareItemService extends IService { +} 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 index 622af24..f5a34d6 100644 --- 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 @@ -1,12 +1,26 @@ package com.njcn.gather.steady.checksquare.service; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareHistoryQueryParam; import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareCreateVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemDetailVO; import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareQueryVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareTaskVO; /** * 数据校验服务。 */ public interface SteadyChecksquareService { - SteadyChecksquareQueryVO query(SteadyChecksquareQueryParam param); + Page query(SteadyChecksquareHistoryQueryParam param); + + SteadyChecksquareCreateVO create(SteadyChecksquareQueryParam param); + + SteadyChecksquareQueryVO detail(String taskId); + + SteadyChecksquareItemDetailVO itemDetail(String itemId, String detailType, String statType); + + SteadyChecksquareItemDetailVO itemDetail(String itemId, String detailType, String statType, + Integer pageNum, Integer pageSize); } diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/SteadyChecksquareStatSummaryService.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/SteadyChecksquareStatSummaryService.java new file mode 100644 index 0000000..f96906b --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/SteadyChecksquareStatSummaryService.java @@ -0,0 +1,10 @@ +package com.njcn.gather.steady.checksquare.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareStatSummaryPO; + +/** + * 数据校验统计摘要服务。 + */ +public interface SteadyChecksquareStatSummaryService extends IService { +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/SteadyChecksquareTaskService.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/SteadyChecksquareTaskService.java new file mode 100644 index 0000000..afe5edb --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/SteadyChecksquareTaskService.java @@ -0,0 +1,10 @@ +package com.njcn.gather.steady.checksquare.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareTaskPO; + +/** + * 数据校验任务服务。 + */ +public interface SteadyChecksquareTaskService extends IService { +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareDetailServiceImpl.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareDetailServiceImpl.java new file mode 100644 index 0000000..1f69d89 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareDetailServiceImpl.java @@ -0,0 +1,15 @@ +package com.njcn.gather.steady.checksquare.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njcn.gather.steady.checksquare.mapper.SteadyChecksquareDetailMapper; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareDetailPO; +import com.njcn.gather.steady.checksquare.service.SteadyChecksquareDetailService; +import org.springframework.stereotype.Service; + +/** + * 数据校验明细服务实现。 + */ +@Service +public class SteadyChecksquareDetailServiceImpl extends ServiceImpl + implements SteadyChecksquareDetailService { +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareItemServiceImpl.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareItemServiceImpl.java new file mode 100644 index 0000000..e29cfe5 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareItemServiceImpl.java @@ -0,0 +1,15 @@ +package com.njcn.gather.steady.checksquare.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njcn.gather.steady.checksquare.mapper.SteadyChecksquareItemMapper; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareItemPO; +import com.njcn.gather.steady.checksquare.service.SteadyChecksquareItemService; +import org.springframework.stereotype.Service; + +/** + * 数据校验检测项服务实现。 + */ +@Service +public class SteadyChecksquareItemServiceImpl extends ServiceImpl + implements SteadyChecksquareItemService { +} 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 index d90a9c6..c62f42c 100644 --- 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 @@ -1,16 +1,39 @@ package com.njcn.gather.steady.checksquare.service.impl; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +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.checksquare.component.SteadyChecksquareCalculator; +import com.njcn.gather.steady.checksquare.component.SteadyChecksquareHarmonicParityRuleComponent; import com.njcn.gather.steady.checksquare.component.SteadyChecksquareInfluxQueryComponent; +import com.njcn.gather.steady.checksquare.component.SteadyChecksquareValueOrderRuleComponent; +import com.njcn.gather.steady.checksquare.pojo.constant.SteadyChecksquareConst; +import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareHistoryQueryParam; import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareDetailPO; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareItemPO; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareStatSummaryPO; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareTaskPO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareCreateVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityDetailVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityRuleVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemDetailVO; 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.pojo.vo.SteadyChecksquareTaskVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderDetailVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderRuleVO; +import com.njcn.gather.steady.checksquare.service.SteadyChecksquareDetailService; +import com.njcn.gather.steady.checksquare.service.SteadyChecksquareItemService; import com.njcn.gather.steady.checksquare.service.SteadyChecksquareService; +import com.njcn.gather.steady.checksquare.service.SteadyChecksquareStatSummaryService; +import com.njcn.gather.steady.checksquare.service.SteadyChecksquareTaskService; +import com.njcn.gather.steady.checksquare.util.SteadyChecksquareIdUtil; 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; @@ -19,9 +42,12 @@ 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 com.njcn.web.factory.PageFactory; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.support.TransactionTemplate; import java.math.BigDecimal; import java.math.RoundingMode; @@ -29,12 +55,15 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; /** * 数据校验服务实现。 @@ -48,15 +77,128 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { 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 static final int HARMONIC_AGGREGATE_ORDER_START = 2; + private static final int HARMONIC_AGGREGATE_ORDER_END = 50; private final SteadyTrendIndicatorCatalog indicatorCatalog; private final SteadyChecksquareInfluxQueryComponent influxQueryComponent; private final SteadyChecksquareCalculator calculator; + private final SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent; + private final SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent; private final AddDataTimeSlotCalculator timeSlotCalculator; private final AddLedgerService addLedgerService; + private final SteadyChecksquareTaskService taskService; + private final SteadyChecksquareItemService itemService; + private final SteadyChecksquareStatSummaryService statSummaryService; + private final SteadyChecksquareDetailService detailService; + private final ObjectMapper objectMapper; + + @Autowired(required = false) + private TransactionTemplate transactionTemplate; @Override - public SteadyChecksquareQueryVO query(SteadyChecksquareQueryParam param) { + public Page query(SteadyChecksquareHistoryQueryParam param) { + SteadyChecksquareHistoryQueryParam query = param == null ? new SteadyChecksquareHistoryQueryParam() : param; + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(SteadyChecksquareTaskPO::getState, SteadyChecksquareConst.STATE_ENABLED) + .eq(trimToNull(query.getLineId()) != null, SteadyChecksquareTaskPO::getLineId, trimToNull(query.getLineId())) + .like(trimToNull(query.getIndicatorCode()) != null, SteadyChecksquareTaskPO::getIndicatorCodesText, "|" + trimToNull(query.getIndicatorCode()) + "|") + .ge(trimToNull(query.getTimeStart()) != null, SteadyChecksquareTaskPO::getTimeStart, parseOptionalTime(query.getTimeStart())) + .le(trimToNull(query.getTimeEnd()) != null, SteadyChecksquareTaskPO::getTimeEnd, parseOptionalTime(query.getTimeEnd())) + .gt(Boolean.TRUE.equals(query.getHasAbnormal()), SteadyChecksquareTaskPO::getAbnormalItemCount, 0) + .orderByDesc(SteadyChecksquareTaskPO::getCreateTime); + Page page = taskService.page(new Page( + PageFactory.getPageNum(query), PageFactory.getPageSize(query)), wrapper); + Page result = new Page(page.getCurrent(), page.getSize(), page.getTotal()); + result.setRecords(page.getRecords().stream().map(this::toTaskVO).collect(Collectors.toList())); + return result; + } + + @Override + public SteadyChecksquareCreateVO create(SteadyChecksquareQueryParam param) { + influxQueryComponent.enableRequestCache(); + SteadyChecksquareQueryVO result; + try { + result = calculate(param); + } finally { + influxQueryComponent.clearRequestCache(); + } + SteadyChecksquareTaskPO task = saveResultInTransaction(param, result); + return toCreateVO(task); + } + + @Override + public SteadyChecksquareQueryVO detail(String taskId) { + SteadyChecksquareTaskPO task = requireTask(taskId); + List items = itemService.lambdaQuery() + .eq(SteadyChecksquareItemPO::getTaskId, task.getId()) + .eq(SteadyChecksquareItemPO::getState, SteadyChecksquareConst.STATE_ENABLED) + .list(); + SteadyChecksquareQueryVO result = new SteadyChecksquareQueryVO(); + result.setTaskId(task.getId()); + result.setTaskNo(task.getTaskNo()); + result.setLineId(task.getLineId()); + result.setLineName(task.getLineName()); + result.setTimeStart(formatTime(task.getTimeStart())); + result.setTimeEnd(formatTime(task.getTimeEnd())); + result.setIntervalMinutes(task.getIntervalMinutes()); + Map> summaryMap = loadSummaryMap(items); + for (SteadyChecksquareItemPO item : items) { + SteadyChecksquareItemVO itemVO = toItemVO(item); + List summaries = summaryMap.get(item.getId()); + itemVO.setStatSummaries(summaries == null ? new ArrayList() : summaries); + result.getItems().add(itemVO); + } + return result; + } + + @Override + public SteadyChecksquareItemDetailVO itemDetail(String itemId, String detailType, String statType) { + return itemDetail(itemId, detailType, statType, null, null); + } + + @Override + public SteadyChecksquareItemDetailVO itemDetail(String itemId, String detailType, String statType, + Integer pageNum, Integer pageSize) { + SteadyChecksquareItemPO item = requireItem(itemId); + String type = trimToNull(detailType); + if (type == null) { + throw fail("明细类型不能为空"); + } + if (!isSupportedDetailType(type)) { + throw fail("明细类型不支持:" + type); + } + SteadyChecksquareItemDetailVO result = new SteadyChecksquareItemDetailVO(); + result.setItemId(item.getId()); + result.setDetailType(type); + result.setStatType(statType); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(SteadyChecksquareDetailPO::getItemId, item.getId()) + .eq(SteadyChecksquareDetailPO::getDetailType, type) + .eq(trimToNull(statType) != null, SteadyChecksquareDetailPO::getStatType, trimToNull(statType)) + .orderByAsc(SteadyChecksquareDetailPO::getStartTime) + .orderByAsc(SteadyChecksquareDetailPO::getPointTime); + List details; + if (isPaged(pageNum, pageSize)) { + Page page = detailService.page( + new Page(pageNum, pageSize), wrapper); + result.setPageNum((int) page.getCurrent()); + result.setPageSize((int) page.getSize()); + result.setTotal(page.getTotal()); + details = page.getRecords() == null ? new ArrayList() : page.getRecords(); + } else { + details = detailService.list(wrapper); + if (details == null) { + details = new ArrayList(); + } + } + for (SteadyChecksquareDetailPO detail : details) { + fillItemDetail(result, detail); + } + return result; + } + + private SteadyChecksquareQueryVO calculate(SteadyChecksquareQueryParam param) { validateParam(param); String lineId = trimToNull(param.getLineId()); LocalDateTime startTime = parseRequiredTime(param.getTimeStart(), "开始时间不能为空"); @@ -75,34 +217,324 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { 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); + List indicators = new ArrayList(); for (String indicatorCode : indicatorCodes) { - SteadyTrendIndicatorDefinitionBO indicator = requireIndicator(indicatorCode); + indicators.add(requireIndicator(indicatorCode)); + } + log.info("数据校验新增检测开始,lineId={},indicatorCount={},timeStart={},timeEnd={},intervalMinutes={}", + lineId, indicatorCodes.size(), startTime, endTime, intervalMinutes); + prefetchNormalIndicatorPoints(lineId, indicators, startTime, endTime, intervalMinutes); + for (SteadyTrendIndicatorDefinitionBO indicator : indicators) { int itemIntervalMinutes = resolveIndicatorIntervalMinutes(indicator, intervalMinutes); List itemSlots = timeSlotCalculator.buildTimeSlots(startTime, endTime, itemIntervalMinutes); - result.getItems().addAll(buildIndicatorItems(lineId, indicator, harmonicOrders, startTime, endTime, itemSlots, itemIntervalMinutes)); + result.getItems().addAll(buildIndicatorItems(lineId, indicator, startTime, endTime, itemSlots, itemIntervalMinutes)); } - log.info("数据校验查询结束,lineId={},itemCount={},costMs={}", lineId, result.getItems().size(), System.currentTimeMillis() - startMillis); + log.info("数据校验新增检测结束,lineId={},itemCount={},costMs={}", lineId, result.getItems().size(), System.currentTimeMillis() - startMillis); return result; } + private SteadyChecksquareTaskPO saveResultInTransaction(SteadyChecksquareQueryParam param, SteadyChecksquareQueryVO result) { + if (transactionTemplate == null) { + return saveResult(param, result); + } + return transactionTemplate.execute(status -> saveResult(param, result)); + } + + private SteadyChecksquareTaskPO saveResult(SteadyChecksquareQueryParam param, SteadyChecksquareQueryVO result) { + LocalDateTime now = LocalDateTime.now(); + SteadyChecksquareTaskPO task = new SteadyChecksquareTaskPO(); + task.setId(SteadyChecksquareIdUtil.uuid()); + task.setTaskNo(SteadyChecksquareIdUtil.taskNo()); + task.setLineId(result.getLineId()); + task.setLineName(result.getLineName()); + task.setTimeStart(parseRequiredTime(result.getTimeStart(), "开始时间不能为空")); + task.setTimeEnd(parseRequiredTime(result.getTimeEnd(), "结束时间不能为空")); + task.setIntervalMinutes(result.getIntervalMinutes()); + List indicatorCodes = normalizeTextList(param.getIndicatorCodes()); + task.setIndicatorCodesJson(writeJson(indicatorCodes)); + task.setIndicatorCodesText(buildIndicatorCodesText(indicatorCodes)); + task.setTaskStatus(SteadyChecksquareConst.TASK_STATUS_SUCCESS); + task.setItemCount(result.getItems().size()); + task.setAbnormalItemCount(countAbnormalItems(result.getItems())); + task.setMaxMissingRate(maxMissingRate(result.getItems())); + task.setResultMessage("数据校验完成"); + task.setState(SteadyChecksquareConst.STATE_ENABLED); + task.setCreateTime(now); + task.setUpdateTime(now); + taskService.save(task); + + List itemPOs = new ArrayList(); + List summaryPOs = new ArrayList(); + List detailPOs = new ArrayList(); + for (SteadyChecksquareItemVO item : result.getItems()) { + SteadyChecksquareItemPO itemPO = buildItemPO(task.getId(), item, now); + item.setItemId(itemPO.getId()); + itemPOs.add(itemPO); + summaryPOs.addAll(buildSummaryPOs(itemPO.getId(), item, now)); + detailPOs.addAll(buildDetailPOs(itemPO.getId(), item, now)); + } + if (!itemPOs.isEmpty()) { + itemService.saveBatch(itemPOs); + } + if (!summaryPOs.isEmpty()) { + statSummaryService.saveBatch(summaryPOs); + } + if (!detailPOs.isEmpty()) { + detailService.saveBatch(detailPOs); + } + return task; + } + + private SteadyChecksquareItemPO buildItemPO(String taskId, SteadyChecksquareItemVO item, LocalDateTime now) { + SteadyChecksquareItemPO po = new SteadyChecksquareItemPO(); + po.setId(SteadyChecksquareIdUtil.uuid()); + po.setTaskId(taskId); + po.setItemKey(item.getItemKey()); + po.setIndicatorCode(item.getIndicatorCode()); + po.setIndicatorName(item.getIndicatorName()); + po.setHarmonicOrder(item.getHarmonicOrder()); + po.setIntervalMinutes(item.getIntervalMinutes()); + po.setHasData(toFlag(item.getHasData())); + po.setExpectedPointCount(nullToZero(item.getExpectedPointCount())); + po.setActualPointCount(nullToZero(item.getActualPointCount())); + po.setMissingPointCount(nullToZero(item.getMissingPointCount())); + po.setMissingRate(item.getMissingRate() == null ? BigDecimal.ZERO.setScale(6, RoundingMode.HALF_UP) : item.getMissingRate()); + po.setMissingRateText(item.getMissingRateText()); + po.setMaxContinuousMissingMinutes(nullToZero(item.getMaxContinuousMissingMinutes())); + po.setAbnormal(toFlag(item.getAbnormal())); + po.setAbnormalPointCount(nullToZero(item.getAbnormalPointCount())); + po.setHarmonicParityAbnormal(toFlag(item.getHarmonicParityAbnormal())); + po.setHarmonicParityAbnormalPointCount(nullToZero(item.getHarmonicParityAbnormalPointCount())); + po.setState(SteadyChecksquareConst.STATE_ENABLED); + po.setCreateTime(now); + po.setUpdateTime(now); + return po; + } + + private List buildSummaryPOs(String itemId, SteadyChecksquareItemVO item, LocalDateTime now) { + List result = new ArrayList(); + for (SteadyChecksquareStatSummaryVO summary : item.getStatSummaries()) { + SteadyChecksquareStatSummaryPO po = new SteadyChecksquareStatSummaryPO(); + po.setId(SteadyChecksquareIdUtil.uuid()); + po.setItemId(itemId); + po.setStatType(summary.getStatType()); + po.setSupported(toFlag(summary.getSupported())); + po.setHasData(toFlag(summary.getHasData())); + po.setExpectedPointCount(nullToZero(summary.getExpectedPointCount())); + po.setActualPointCount(nullToZero(summary.getActualPointCount())); + po.setMissingPointCount(nullToZero(summary.getMissingPointCount())); + po.setMissingRate(summary.getMissingRate() == null ? BigDecimal.ZERO.setScale(6, RoundingMode.HALF_UP) : summary.getMissingRate()); + po.setMissingRateText(summary.getMissingRateText()); + po.setMaxContinuousMissingMinutes(nullToZero(summary.getMaxContinuousMissingMinutes())); + po.setCreateTime(now); + result.add(po); + } + return result; + } + + private List buildDetailPOs(String itemId, SteadyChecksquareItemVO item, LocalDateTime now) { + List result = new ArrayList(); + for (SteadyChecksquareStatDetailVO detail : item.getStatDetails()) { + for (SteadyChecksquareSegmentVO segment : detail.getSegments()) { + SteadyChecksquareDetailPO po = new SteadyChecksquareDetailPO(); + po.setId(SteadyChecksquareIdUtil.uuid()); + po.setItemId(itemId); + po.setDetailType(SteadyChecksquareConst.DETAIL_TYPE_SEGMENT); + po.setStatType(detail.getStatType()); + po.setHarmonicOrder(segment.getHarmonicOrder()); + po.setStartTime(parseOptionalTime(segment.getStartTime())); + po.setEndTime(parseOptionalTime(segment.getEndTime())); + po.setSegmentStatus(segment.getStatus()); + po.setMissingPointCount(segment.getMissingPointCount()); + po.setDurationMinutes(segment.getDurationMinutes()); + po.setCreateTime(now); + result.add(po); + } + } + for (SteadyChecksquareValueOrderDetailVO detail : item.getAbnormalDetails()) { + SteadyChecksquareDetailPO po = new SteadyChecksquareDetailPO(); + po.setId(SteadyChecksquareIdUtil.uuid()); + po.setItemId(itemId); + po.setDetailType(SteadyChecksquareConst.DETAIL_TYPE_VALUE_ORDER); + po.setPointTime(parseOptionalTime(detail.getTime())); + po.setPhase(detail.getPhase()); + po.setHarmonicOrder(detail.getHarmonicOrder()); + po.setMaxValue(detail.getMaxValue()); + po.setMinValue(detail.getMinValue()); + po.setAvgValue(detail.getAvgValue()); + po.setCp95Value(detail.getCp95Value()); + po.setCreateTime(now); + result.add(po); + } + for (SteadyChecksquareHarmonicParityDetailVO detail : item.getHarmonicParityAbnormalDetails()) { + SteadyChecksquareDetailPO po = new SteadyChecksquareDetailPO(); + po.setId(SteadyChecksquareIdUtil.uuid()); + po.setItemId(itemId); + po.setDetailType(SteadyChecksquareConst.DETAIL_TYPE_HARMONIC_PARITY); + po.setPointTime(parseOptionalTime(detail.getTime())); + po.setPhase(detail.getPhase()); + po.setStatType(detail.getStatType()); + po.setEvenHarmonicOrder(detail.getEvenHarmonicOrder()); + po.setEvenValue(detail.getEvenValue()); + po.setOddHarmonicOrdersJson(writeJson(detail.getOddHarmonicOrders())); + po.setOddValuesJson(writeJson(detail.getOddValues())); + po.setOddMedianValue(detail.getOddMedianValue()); + po.setThresholdMultiplier(detail.getThresholdMultiplier()); + po.setCreateTime(now); + result.add(po); + } + return result; + } + + private void prefetchNormalIndicatorPoints(String lineId, List indicators, + LocalDateTime startTime, LocalDateTime endTime, int lineIntervalMinutes) { + Map> fieldMap = + new LinkedHashMap>(); + Map intervalMap = new LinkedHashMap(); + for (SteadyTrendIndicatorDefinitionBO indicator : indicators) { + if (Boolean.TRUE.equals(indicator.getHarmonic())) { + continue; + } + int intervalMinutes = resolveIndicatorIntervalMinutes(indicator, lineIntervalMinutes); + for (String statType : indicator.getSupportStats()) { + for (String phase : indicator.getPhaseCodes()) { + SteadyTrendResolvedFieldBO field = buildResolvedField(lineId, indicator, null, phase, statType); + String key = buildPrefetchKey(field, intervalMinutes); + List fields = fieldMap.get(key); + if (fields == null) { + fields = new ArrayList(); + fieldMap.put(key, fields); + intervalMap.put(key, intervalMinutes); + } + fields.add(field); + } + } + } + for (Map.Entry> entry : fieldMap.entrySet()) { + // 预取只依赖请求级缓存;后续缺数和规则校验复用同一批 Influx 结果。 + influxQueryComponent.queryValuePointMap(entry.getValue(), startTime, endTime, intervalMap.get(entry.getKey())); + } + } + + private String buildPrefetchKey(SteadyTrendResolvedFieldBO field, int intervalMinutes) { + return field.getMeasurement() + "|" + field.getLineId() + "|" + field.getPhase() + "|" + + field.getStatType() + "|" + intervalMinutes; + } + 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)) { + List harmonicOrders = buildAggregateHarmonicOrders(indicator); + prefetchHarmonicIndicatorPoints(lineId, indicator, harmonicOrders, startTime, endTime, intervalMinutes); + for (Integer order : harmonicOrders) { result.add(buildItem(lineId, indicator, order, startTime, endTime, slots, intervalMinutes)); } - return result; + fillHarmonicParityRuleResult(result, lineId, indicator, startTime, endTime, intervalMinutes); + return Collections.singletonList(aggregateHarmonicItems(lineId, indicator, result, intervalMinutes)); } result.add(buildItem(lineId, indicator, null, startTime, endTime, slots, intervalMinutes)); return result; } + private void prefetchHarmonicIndicatorPoints(String lineId, SteadyTrendIndicatorDefinitionBO indicator, + List harmonicOrders, LocalDateTime startTime, + LocalDateTime endTime, int intervalMinutes) { + if (harmonicOrders == null || harmonicOrders.isEmpty()) { + return; + } + for (String statType : indicator.getSupportStats()) { + for (String phase : indicator.getPhaseCodes()) { + List fields = new ArrayList(); + for (Integer order : harmonicOrders) { + fields.add(buildResolvedField(lineId, indicator, order, phase, statType)); + } + influxQueryComponent.queryValuePointMap(fields, startTime, endTime, intervalMinutes); + } + } + } + + private SteadyChecksquareItemVO aggregateHarmonicItems(String lineId, SteadyTrendIndicatorDefinitionBO indicator, + List orderItems, int intervalMinutes) { + SteadyChecksquareItemVO result = new SteadyChecksquareItemVO(); + result.setItemKey(buildItemKey(lineId, indicator, null)); + result.setIndicatorCode(indicator.getIndicatorCode()); + result.setIndicatorName(indicator.getName()); + result.setHarmonicOrder(null); + result.setIntervalMinutes(intervalMinutes); + result.setHasData(anyHasData(orderItems)); + result.setExpectedPointCount(averageInteger(orderItems, "expectedPointCount")); + result.setActualPointCount(averageInteger(orderItems, "actualPointCount")); + result.setMissingPointCount(averageInteger(orderItems, "missingPointCount")); + result.setMissingRate(averageRate(orderItems)); + result.setMissingRateText(formatRateText(result.getMissingRate())); + result.setMaxContinuousMissingMinutes(averageInteger(orderItems, "maxContinuousMissingMinutes")); + result.setAbnormal(anyAbnormal(orderItems)); + result.setAbnormalPointCount(averageAbnormalCount(orderItems, "abnormalPointCount", result.getAbnormal())); + result.setHarmonicParityAbnormal(anyHarmonicParityAbnormal(orderItems)); + result.setHarmonicParityAbnormalPointCount(averageAbnormalCount(orderItems, + "harmonicParityAbnormalPointCount", result.getHarmonicParityAbnormal())); + for (SteadyChecksquareItemVO orderItem : orderItems) { + result.getAbnormalDetails().addAll(orderItem.getAbnormalDetails()); + result.getHarmonicParityAbnormalDetails().addAll(orderItem.getHarmonicParityAbnormalDetails()); + } + result.setStatSummaries(aggregateStatSummaries(orderItems)); + result.setStatDetails(aggregateStatDetails(orderItems)); + return result; + } + + private List aggregateStatSummaries(List orderItems) { + Map> summaryMap = new LinkedHashMap>(); + for (SteadyChecksquareItemVO item : orderItems) { + for (SteadyChecksquareStatSummaryVO summary : item.getStatSummaries()) { + List summaries = summaryMap.get(summary.getStatType()); + if (summaries == null) { + summaries = new ArrayList(); + summaryMap.put(summary.getStatType(), summaries); + } + summaries.add(summary); + } + } + List result = new ArrayList(); + for (Map.Entry> entry : summaryMap.entrySet()) { + SteadyChecksquareStatSummaryVO summary = new SteadyChecksquareStatSummaryVO(); + summary.setStatType(entry.getKey()); + summary.setSupported(true); + summary.setHasData(anySummaryHasData(entry.getValue())); + summary.setExpectedPointCount(averageSummaryInteger(entry.getValue(), "expectedPointCount")); + summary.setActualPointCount(averageSummaryInteger(entry.getValue(), "actualPointCount")); + summary.setMissingPointCount(averageSummaryInteger(entry.getValue(), "missingPointCount")); + summary.setMissingRate(averageSummaryRate(entry.getValue())); + summary.setMissingRateText(formatRateText(summary.getMissingRate())); + summary.setMaxContinuousMissingMinutes(averageSummaryInteger(entry.getValue(), "maxContinuousMissingMinutes")); + result.add(summary); + } + return result; + } + + private List aggregateStatDetails(List orderItems) { + Map detailMap = new LinkedHashMap(); + for (SteadyChecksquareItemVO item : orderItems) { + for (SteadyChecksquareStatDetailVO detail : item.getStatDetails()) { + SteadyChecksquareStatDetailVO aggregateDetail = detailMap.get(detail.getStatType()); + if (aggregateDetail == null) { + aggregateDetail = new SteadyChecksquareStatDetailVO(); + aggregateDetail.setStatType(detail.getStatType()); + aggregateDetail.setSupported(true); + detailMap.put(detail.getStatType(), aggregateDetail); + } + if (item.getHarmonicOrder() != null) { + for (SteadyChecksquareSegmentVO segment : detail.getSegments()) { + segment.setHarmonicOrder(item.getHarmonicOrder()); + } + } + aggregateDetail.getSegments().addAll(detail.getSegments()); + } + } + return new ArrayList(detailMap.values()); + } + private SteadyChecksquareItemVO buildItem(String lineId, SteadyTrendIndicatorDefinitionBO indicator, Integer harmonicOrder, LocalDateTime startTime, LocalDateTime endTime, List slots, int intervalMinutes) { @@ -112,6 +544,8 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { item.setIndicatorName(indicator.getName()); item.setHarmonicOrder(harmonicOrder); item.setIntervalMinutes(intervalMinutes); + item.setHarmonicParityAbnormal(false); + item.setHarmonicParityAbnormalPointCount(0); int totalExpected = 0; int totalActual = 0; @@ -138,9 +572,56 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { item.setMissingRate(calculateRate(item.getMissingPointCount(), totalExpected)); item.setMissingRateText(formatRateText(item.getMissingRate())); item.setMaxContinuousMissingMinutes(maxContinuousMissingMinutes); + fillValueOrderRuleResult(item, lineId, indicator, harmonicOrder, startTime, endTime, intervalMinutes); return item; } + private void fillHarmonicParityRuleResult(List items, String lineId, + SteadyTrendIndicatorDefinitionBO indicator, + LocalDateTime startTime, LocalDateTime endTime, + int intervalMinutes) { + SteadyChecksquareHarmonicParityRuleVO ruleResult = harmonicParityRuleComponent.check(lineId, indicator, + startTime, endTime, intervalMinutes); + if (ruleResult == null || ruleResult.getAbnormalDetails() == null || ruleResult.getAbnormalDetails().isEmpty()) { + for (SteadyChecksquareItemVO item : items) { + item.setHarmonicParityAbnormal(false); + item.setHarmonicParityAbnormalPointCount(0); + } + return; + } + for (SteadyChecksquareItemVO item : items) { + List matchedDetails = + filterHarmonicParityDetails(ruleResult, item.getHarmonicOrder()); + item.setHarmonicParityAbnormal(!matchedDetails.isEmpty()); + item.setHarmonicParityAbnormalPointCount(matchedDetails.size()); + item.setHarmonicParityAbnormalDetails(matchedDetails); + } + } + + private List filterHarmonicParityDetails( + SteadyChecksquareHarmonicParityRuleVO ruleResult, Integer harmonicOrder) { + List result = new ArrayList(); + if (ruleResult == null || ruleResult.getAbnormalDetails() == null || harmonicOrder == null) { + return result; + } + for (SteadyChecksquareHarmonicParityDetailVO detail : ruleResult.getAbnormalDetails()) { + if (detail != null && harmonicOrder.equals(detail.getEvenHarmonicOrder())) { + result.add(detail); + } + } + return result; + } + + private void fillValueOrderRuleResult(SteadyChecksquareItemVO item, String lineId, SteadyTrendIndicatorDefinitionBO indicator, + Integer harmonicOrder, LocalDateTime startTime, LocalDateTime endTime, + int intervalMinutes) { + SteadyChecksquareValueOrderRuleVO ruleResult = valueOrderRuleComponent.check(lineId, indicator, harmonicOrder, + startTime, endTime, intervalMinutes); + item.setAbnormal(ruleResult.getAbnormal()); + item.setAbnormalPointCount(ruleResult.getAbnormalPointCount()); + item.setAbnormalDetails(ruleResult.getAbnormalDetails()); + } + private Set queryMergedActualSlots(String lineId, SteadyTrendIndicatorDefinitionBO indicator, Integer harmonicOrder, String statType, LocalDateTime startTime, LocalDateTime endTime, int intervalMinutes) { @@ -246,6 +727,14 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { } } + private LocalDateTime parseOptionalTime(String time) { + String text = trimToNull(time); + if (text == null) { + return null; + } + return parseRequiredTime(text, "时间不能为空"); + } + private AddLedgerLinePathVO requireLinePath(String lineId) { Map linePathMap = addLedgerService.listLinePathByLineIds(Collections.singletonList(lineId)); AddLedgerLinePathVO linePath = linePathMap.get(lineId); @@ -282,6 +771,324 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { return indicator; } + private List buildAggregateHarmonicOrders(SteadyTrendIndicatorDefinitionBO indicator) { + int start = Math.max(HARMONIC_AGGREGATE_ORDER_START, indicator.getHarmonicOrderStart()); + int end = Math.min(HARMONIC_AGGREGATE_ORDER_END, indicator.getHarmonicOrderEnd()); + if (start > end) { + throw fail("谐波次数只能在 " + HARMONIC_AGGREGATE_ORDER_START + " 到 " + HARMONIC_AGGREGATE_ORDER_END + " 之间"); + } + List result = new ArrayList(); + for (int order = start; order <= end; order++) { + result.add(order); + } + return result; + } + + private Map> loadSummaryMap(List items) { + Map> result = + new LinkedHashMap>(); + if (items == null || items.isEmpty()) { + return result; + } + List itemIds = new ArrayList(); + for (SteadyChecksquareItemPO item : items) { + itemIds.add(item.getId()); + result.put(item.getId(), new ArrayList()); + } + List summaries = statSummaryService.lambdaQuery() + .in(SteadyChecksquareStatSummaryPO::getItemId, itemIds) + .list(); + for (SteadyChecksquareStatSummaryPO summary : summaries) { + List itemSummaries = result.get(summary.getItemId()); + if (itemSummaries != null) { + itemSummaries.add(toSummaryVO(summary)); + } + } + return result; + } + + private void fillItemDetail(SteadyChecksquareItemDetailVO result, SteadyChecksquareDetailPO detail) { + if (SteadyChecksquareConst.DETAIL_TYPE_SEGMENT.equals(detail.getDetailType())) { + SteadyChecksquareSegmentVO segment = new SteadyChecksquareSegmentVO(); + segment.setStartTime(formatTime(detail.getStartTime())); + segment.setEndTime(formatTime(detail.getEndTime())); + segment.setStatus(detail.getSegmentStatus()); + segment.setHarmonicOrder(detail.getHarmonicOrder()); + segment.setMissingPointCount(detail.getMissingPointCount()); + segment.setDurationMinutes(detail.getDurationMinutes()); + result.getSegments().add(segment); + return; + } + if (SteadyChecksquareConst.DETAIL_TYPE_VALUE_ORDER.equals(detail.getDetailType())) { + SteadyChecksquareValueOrderDetailVO valueDetail = new SteadyChecksquareValueOrderDetailVO(); + valueDetail.setTime(formatTime(detail.getPointTime())); + valueDetail.setPhase(detail.getPhase()); + valueDetail.setHarmonicOrder(detail.getHarmonicOrder()); + valueDetail.setMaxValue(detail.getMaxValue()); + valueDetail.setMinValue(detail.getMinValue()); + valueDetail.setAvgValue(detail.getAvgValue()); + valueDetail.setCp95Value(detail.getCp95Value()); + result.getValueOrderDetails().add(valueDetail); + return; + } + if (SteadyChecksquareConst.DETAIL_TYPE_HARMONIC_PARITY.equals(detail.getDetailType())) { + SteadyChecksquareHarmonicParityDetailVO parityDetail = new SteadyChecksquareHarmonicParityDetailVO(); + parityDetail.setTime(formatTime(detail.getPointTime())); + parityDetail.setPhase(detail.getPhase()); + parityDetail.setStatType(detail.getStatType()); + parityDetail.setEvenHarmonicOrder(detail.getEvenHarmonicOrder()); + parityDetail.setEvenValue(detail.getEvenValue()); + parityDetail.setOddHarmonicOrders(readIntegerList(detail.getOddHarmonicOrdersJson())); + parityDetail.setOddValues(readBigDecimalList(detail.getOddValuesJson())); + parityDetail.setOddMedianValue(detail.getOddMedianValue()); + parityDetail.setThresholdMultiplier(detail.getThresholdMultiplier()); + result.getHarmonicParityDetails().add(parityDetail); + } + } + + private SteadyChecksquareTaskPO requireTask(String taskId) { + SteadyChecksquareTaskPO task = taskService.getById(taskId); + if (task == null || !Integer.valueOf(SteadyChecksquareConst.STATE_ENABLED).equals(task.getState())) { + throw fail("数据校验任务不存在或已删除"); + } + return task; + } + + private SteadyChecksquareItemPO requireItem(String itemId) { + SteadyChecksquareItemPO item = itemService.getById(itemId); + if (item == null || !Integer.valueOf(SteadyChecksquareConst.STATE_ENABLED).equals(item.getState())) { + throw fail("数据校验检测项不存在或已删除"); + } + return item; + } + + private boolean isSupportedDetailType(String detailType) { + return SteadyChecksquareConst.DETAIL_TYPE_SEGMENT.equals(detailType) + || SteadyChecksquareConst.DETAIL_TYPE_VALUE_ORDER.equals(detailType) + || SteadyChecksquareConst.DETAIL_TYPE_HARMONIC_PARITY.equals(detailType); + } + + private boolean isPaged(Integer pageNum, Integer pageSize) { + return pageNum != null && pageNum > 0 && pageSize != null && pageSize > 0; + } + + private SteadyChecksquareTaskVO toTaskVO(SteadyChecksquareTaskPO task) { + SteadyChecksquareTaskVO vo = new SteadyChecksquareTaskVO(); + vo.setTaskId(task.getId()); + vo.setTaskNo(task.getTaskNo()); + vo.setLineId(task.getLineId()); + vo.setLineName(task.getLineName()); + vo.setTimeStart(formatTime(task.getTimeStart())); + vo.setTimeEnd(formatTime(task.getTimeEnd())); + vo.setIntervalMinutes(task.getIntervalMinutes()); + vo.setTaskStatus(task.getTaskStatus()); + vo.setItemCount(task.getItemCount()); + vo.setAbnormalItemCount(task.getAbnormalItemCount()); + vo.setMaxMissingRate(task.getMaxMissingRate()); + vo.setCreateTime(formatTime(task.getCreateTime())); + return vo; + } + + private SteadyChecksquareCreateVO toCreateVO(SteadyChecksquareTaskPO task) { + SteadyChecksquareCreateVO vo = new SteadyChecksquareCreateVO(); + vo.setTaskId(task.getId()); + vo.setTaskNo(task.getTaskNo()); + vo.setLineId(task.getLineId()); + vo.setLineName(task.getLineName()); + vo.setTimeStart(formatTime(task.getTimeStart())); + vo.setTimeEnd(formatTime(task.getTimeEnd())); + vo.setIntervalMinutes(task.getIntervalMinutes()); + vo.setItemCount(task.getItemCount()); + vo.setAbnormalItemCount(task.getAbnormalItemCount()); + return vo; + } + + private SteadyChecksquareItemVO toItemVO(SteadyChecksquareItemPO item) { + SteadyChecksquareItemVO vo = new SteadyChecksquareItemVO(); + vo.setItemId(item.getId()); + vo.setItemKey(item.getItemKey()); + vo.setIndicatorCode(item.getIndicatorCode()); + vo.setIndicatorName(item.getIndicatorName()); + vo.setHarmonicOrder(item.getHarmonicOrder()); + vo.setIntervalMinutes(item.getIntervalMinutes()); + vo.setHasData(toBoolean(item.getHasData())); + vo.setExpectedPointCount(item.getExpectedPointCount()); + vo.setActualPointCount(item.getActualPointCount()); + vo.setMissingPointCount(item.getMissingPointCount()); + vo.setMissingRate(item.getMissingRate()); + vo.setMissingRateText(item.getMissingRateText()); + vo.setMaxContinuousMissingMinutes(item.getMaxContinuousMissingMinutes()); + vo.setAbnormal(toBoolean(item.getAbnormal())); + vo.setAbnormalPointCount(item.getAbnormalPointCount()); + vo.setHarmonicParityAbnormal(toBoolean(item.getHarmonicParityAbnormal())); + vo.setHarmonicParityAbnormalPointCount(item.getHarmonicParityAbnormalPointCount()); + return vo; + } + + private SteadyChecksquareStatSummaryVO toSummaryVO(SteadyChecksquareStatSummaryPO summary) { + SteadyChecksquareStatSummaryVO vo = new SteadyChecksquareStatSummaryVO(); + vo.setStatType(summary.getStatType()); + vo.setSupported(toBoolean(summary.getSupported())); + vo.setHasData(toBoolean(summary.getHasData())); + vo.setExpectedPointCount(summary.getExpectedPointCount()); + vo.setActualPointCount(summary.getActualPointCount()); + vo.setMissingPointCount(summary.getMissingPointCount()); + vo.setMissingRate(summary.getMissingRate()); + vo.setMissingRateText(summary.getMissingRateText()); + vo.setMaxContinuousMissingMinutes(summary.getMaxContinuousMissingMinutes()); + return vo; + } + + private int countAbnormalItems(List items) { + int count = 0; + for (SteadyChecksquareItemVO item : items) { + if (Boolean.TRUE.equals(item.getAbnormal()) || Boolean.TRUE.equals(item.getHarmonicParityAbnormal())) { + count++; + } + } + return count; + } + + private BigDecimal maxMissingRate(List items) { + BigDecimal max = BigDecimal.ZERO.setScale(6, RoundingMode.HALF_UP); + for (SteadyChecksquareItemVO item : items) { + if (item.getMissingRate() != null && item.getMissingRate().compareTo(max) > 0) { + max = item.getMissingRate(); + } + } + return max; + } + + private Boolean anyHasData(List items) { + for (SteadyChecksquareItemVO item : items) { + if (Boolean.TRUE.equals(item.getHasData())) { + return true; + } + } + return false; + } + + private Boolean anyAbnormal(List items) { + for (SteadyChecksquareItemVO item : items) { + if (Boolean.TRUE.equals(item.getAbnormal())) { + return true; + } + } + return false; + } + + private Boolean anyHarmonicParityAbnormal(List items) { + for (SteadyChecksquareItemVO item : items) { + if (Boolean.TRUE.equals(item.getHarmonicParityAbnormal())) { + return true; + } + } + return false; + } + + private Boolean anySummaryHasData(List summaries) { + for (SteadyChecksquareStatSummaryVO summary : summaries) { + if (Boolean.TRUE.equals(summary.getHasData())) { + return true; + } + } + return false; + } + + private Integer averageInteger(List items, String fieldName) { + if (items == null || items.isEmpty()) { + return 0; + } + int total = 0; + for (SteadyChecksquareItemVO item : items) { + total += valueOfItemInteger(item, fieldName); + } + return new BigDecimal(total).divide(new BigDecimal(items.size()), 0, RoundingMode.HALF_UP).intValue(); + } + + private Integer averageAbnormalCount(List items, String fieldName, Boolean abnormal) { + Integer average = averageInteger(items, fieldName); + if (Boolean.TRUE.equals(abnormal) && average <= 0) { + return 1; + } + return average; + } + + private Integer averageSummaryInteger(List summaries, String fieldName) { + if (summaries == null || summaries.isEmpty()) { + return 0; + } + int total = 0; + for (SteadyChecksquareStatSummaryVO summary : summaries) { + total += valueOfSummaryInteger(summary, fieldName); + } + return new BigDecimal(total).divide(new BigDecimal(summaries.size()), 0, RoundingMode.HALF_UP).intValue(); + } + + private BigDecimal averageRate(List items) { + if (items == null || items.isEmpty()) { + return BigDecimal.ZERO.setScale(6, RoundingMode.HALF_UP); + } + BigDecimal total = BigDecimal.ZERO; + for (SteadyChecksquareItemVO item : items) { + total = total.add(item.getMissingRate() == null ? BigDecimal.ZERO : item.getMissingRate()); + } + return total.divide(new BigDecimal(items.size()), 6, RoundingMode.HALF_UP); + } + + private BigDecimal averageSummaryRate(List summaries) { + if (summaries == null || summaries.isEmpty()) { + return BigDecimal.ZERO.setScale(6, RoundingMode.HALF_UP); + } + BigDecimal total = BigDecimal.ZERO; + for (SteadyChecksquareStatSummaryVO summary : summaries) { + total = total.add(summary.getMissingRate() == null ? BigDecimal.ZERO : summary.getMissingRate()); + } + return total.divide(new BigDecimal(summaries.size()), 6, RoundingMode.HALF_UP); + } + + private int valueOfItemInteger(SteadyChecksquareItemVO item, String fieldName) { + if ("expectedPointCount".equals(fieldName)) { + return nullToZero(item.getExpectedPointCount()); + } + if ("actualPointCount".equals(fieldName)) { + return nullToZero(item.getActualPointCount()); + } + if ("missingPointCount".equals(fieldName)) { + return nullToZero(item.getMissingPointCount()); + } + if ("maxContinuousMissingMinutes".equals(fieldName)) { + return nullToZero(item.getMaxContinuousMissingMinutes()); + } + if ("abnormalPointCount".equals(fieldName)) { + return nullToZero(item.getAbnormalPointCount()); + } + if ("harmonicParityAbnormalPointCount".equals(fieldName)) { + return nullToZero(item.getHarmonicParityAbnormalPointCount()); + } + return 0; + } + + private int valueOfSummaryInteger(SteadyChecksquareStatSummaryVO summary, String fieldName) { + if ("expectedPointCount".equals(fieldName)) { + return nullToZero(summary.getExpectedPointCount()); + } + if ("actualPointCount".equals(fieldName)) { + return nullToZero(summary.getActualPointCount()); + } + if ("missingPointCount".equals(fieldName)) { + return nullToZero(summary.getMissingPointCount()); + } + if ("maxContinuousMissingMinutes".equals(fieldName)) { + return nullToZero(summary.getMaxContinuousMissingMinutes()); + } + return 0; + } + + private int nullToZero(Integer value) { + return value == null ? 0 : value; + } + private BigDecimal calculateRate(int missingCount, int expectedCount) { if (expectedCount <= 0) { return BigDecimal.ZERO.setScale(6, RoundingMode.HALF_UP); @@ -296,6 +1103,21 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { return rate.multiply(new BigDecimal("100")).setScale(2, RoundingMode.HALF_UP).toPlainString() + "%"; } + private String formatTime(LocalDateTime time) { + if (time == null) { + return null; + } + return time.format(TIME_FORMATTER); + } + + private int toFlag(Boolean value) { + return Boolean.TRUE.equals(value) ? 1 : 0; + } + + private Boolean toBoolean(Integer value) { + return value != null && value == 1; + } + private List normalizeTextList(List values) { if (values == null || values.isEmpty()) { return new ArrayList(); @@ -310,31 +1132,6 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { 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; @@ -343,6 +1140,49 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { return trimmed.isEmpty() ? null : trimmed; } + private String writeJson(Object value) { + try { + return objectMapper.writeValueAsString(value); + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.JSON_CONVERT_EXCEPTION, exception.getMessage()); + } + } + + private String buildIndicatorCodesText(List indicatorCodes) { + if (indicatorCodes == null || indicatorCodes.isEmpty()) { + return null; + } + StringBuilder builder = new StringBuilder("|"); + for (String indicatorCode : indicatorCodes) { + builder.append(indicatorCode).append("|"); + } + return builder.toString(); + } + + private List readIntegerList(String json) { + if (trimToNull(json) == null) { + return new ArrayList(); + } + try { + Integer[] values = objectMapper.readValue(json, Integer[].class); + return new ArrayList(Arrays.asList(values)); + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.JSON_CONVERT_EXCEPTION, exception.getMessage()); + } + } + + private List readBigDecimalList(String json) { + if (trimToNull(json) == null) { + return new ArrayList(); + } + try { + BigDecimal[] values = objectMapper.readValue(json, BigDecimal[].class); + return new ArrayList(Arrays.asList(values)); + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.JSON_CONVERT_EXCEPTION, exception.getMessage()); + } + } + 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/service/impl/SteadyChecksquareStatSummaryServiceImpl.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareStatSummaryServiceImpl.java new file mode 100644 index 0000000..374a5d6 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareStatSummaryServiceImpl.java @@ -0,0 +1,16 @@ +package com.njcn.gather.steady.checksquare.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njcn.gather.steady.checksquare.mapper.SteadyChecksquareStatSummaryMapper; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareStatSummaryPO; +import com.njcn.gather.steady.checksquare.service.SteadyChecksquareStatSummaryService; +import org.springframework.stereotype.Service; + +/** + * 数据校验统计摘要服务实现。 + */ +@Service +public class SteadyChecksquareStatSummaryServiceImpl + extends ServiceImpl + implements SteadyChecksquareStatSummaryService { +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareTaskServiceImpl.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareTaskServiceImpl.java new file mode 100644 index 0000000..91c417c --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareTaskServiceImpl.java @@ -0,0 +1,15 @@ +package com.njcn.gather.steady.checksquare.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njcn.gather.steady.checksquare.mapper.SteadyChecksquareTaskMapper; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareTaskPO; +import com.njcn.gather.steady.checksquare.service.SteadyChecksquareTaskService; +import org.springframework.stereotype.Service; + +/** + * 数据校验任务服务实现。 + */ +@Service +public class SteadyChecksquareTaskServiceImpl extends ServiceImpl + implements SteadyChecksquareTaskService { +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/util/SteadyChecksquareIdUtil.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/util/SteadyChecksquareIdUtil.java new file mode 100644 index 0000000..f2d0a92 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/util/SteadyChecksquareIdUtil.java @@ -0,0 +1,24 @@ +package com.njcn.gather.steady.checksquare.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +/** + * 数据校验编号工具。 + */ +public final class SteadyChecksquareIdUtil { + + private static final DateTimeFormatter TASK_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"); + + private SteadyChecksquareIdUtil() { + } + + public static String uuid() { + return UUID.randomUUID().toString().replace("-", ""); + } + + public static String taskNo() { + return "CS" + LocalDateTime.now().format(TASK_FORMATTER); + } +} diff --git a/steady/steady-DataView/src/main/resources/sql/steady-DataView/steady-data-view-index.sql b/steady/steady-DataView/src/main/resources/sql/steady-DataView/steady-data-view-index.sql new file mode 100644 index 0000000..4880578 --- /dev/null +++ b/steady/steady-DataView/src/main/resources/sql/steady-DataView/steady-data-view-index.sql @@ -0,0 +1,41 @@ +-- 稳态数据查看建议索引。 +-- 本脚本不自动执行,请按数据库现状审阅后单独执行。 + +CREATE INDEX idx_data_v_time_line_phase +ON data_v (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_i_time_line_phase +ON data_i (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_flicker_time_line_phase +ON data_flicker (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_fluc_time_line_phase +ON data_fluc (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_harmphasic_i_time_line_phase +ON data_harmphasic_i (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_harmphasic_v_time_line_phase +ON data_harmphasic_v (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_harmpower_p_time_line_phase +ON data_harmpower_p (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_harmpower_q_time_line_phase +ON data_harmpower_q (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_harmpower_s_time_line_phase +ON data_harmpower_s (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_harmrate_i_time_line_phase +ON data_harmrate_i (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_harmrate_v_time_line_phase +ON data_harmrate_v (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_inharm_i_time_line_phase +ON data_inharm_i (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_plt_time_line_phase +ON data_plt (TIMEID, LINEID, PHASIC_TYPE); diff --git a/steady/steady-DataView/src/main/resources/sql/steady-DataView/steady-menu-icon-update_20260525.sql b/steady/steady-DataView/src/main/resources/sql/steady-DataView/steady-menu-icon-update_20260525.sql new file mode 100644 index 0000000..d4751de --- /dev/null +++ b/steady/steady-DataView/src/main/resources/sql/steady-DataView/steady-menu-icon-update_20260525.sql @@ -0,0 +1,42 @@ +-- 稳态模块菜单图标修正脚本。 +-- 本脚本不自动执行,请按数据库现状审阅后单独执行。 + +UPDATE sys_function +SET Icon = 'DataAnalysis' +WHERE State = 1 + AND Type = 0 + AND ( + Name = '稳态模块' + OR Code IN ('steady', 'steadyModule', 'steadyDataView') + OR Path IN ('/steady', '/steadyDataView', '/steady/data-view') + ); + +UPDATE sys_function +SET Icon = 'DataBoard' +WHERE State = 1 + AND Type = 0 + AND ( + Name = '稳态数据' + OR Code IN ('steadyData', 'steadyDataDetail') + OR Path IN ('/steady/data', '/steady/data-view/detail', '/steadyDataView/index') + ); + +UPDATE sys_function +SET Icon = 'TrendCharts' +WHERE State = 1 + AND Type = 0 + AND ( + Name = '稳态趋势' + OR Code IN ('steadyTrend', 'steadyDataTrend') + OR Path IN ('/steady/trend', '/steady/data-view/trend', '/steadyTrend/index') + ); + +UPDATE sys_function +SET Icon = 'CircleCheck' +WHERE State = 1 + AND Type = 0 + AND ( + Name = '数据验证' + OR Code IN ('dataValidation', 'steadyDataValidation') + OR Path IN ('/steady/data-validation', '/steady/data-view/validation', '/dataValidation/index') + ); diff --git a/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareHarmonicParityRuleComponentTest.java b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareHarmonicParityRuleComponentTest.java new file mode 100644 index 0000000..a35ca1b --- /dev/null +++ b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareHarmonicParityRuleComponentTest.java @@ -0,0 +1,147 @@ +package com.njcn.gather.steady.checksquare.component; + +import com.njcn.gather.steady.checksquare.pojo.bo.SteadyChecksquareValuePointBO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityDetailVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityRuleVO; +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 org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * 谐波奇偶关系规则测试。 + */ +class SteadyChecksquareHarmonicParityRuleComponentTest { + + @Test + void shouldRecordAbnormalWhenEvenHarmonicExceedsOddMedianThreshold() { + SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + SteadyChecksquareHarmonicParityRuleComponent component = new SteadyChecksquareHarmonicParityRuleComponent(influxQueryComponent); + LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0); + when(influxQueryComponent.queryValuePointMap(any(), + any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenAnswer(invocation -> { + Map> values = emptyBatchResult(invocation.getArgument(0)); + putPoint(values, "v_3", time, "10"); + putPoint(values, "v_4", time, "31"); + putPoint(values, "v_5", time, "12"); + putPoint(values, "v_7", time, "14"); + return values; + }); + SteadyTrendIndicatorDefinitionBO indicator = new SteadyTrendIndicatorCatalog().getIndicator("V_HARMONIC"); + + SteadyChecksquareHarmonicParityRuleVO result = component.check("line-001", indicator, + time, time, 1); + + Assertions.assertEquals(Boolean.TRUE, result.getAbnormal()); + Assertions.assertEquals(Integer.valueOf(1), result.getAbnormalPointCount()); + SteadyChecksquareHarmonicParityDetailVO detail = result.getAbnormalDetails().get(0); + Assertions.assertEquals("2026-05-01 00:00:00", detail.getTime()); + Assertions.assertEquals("A", detail.getPhase()); + Assertions.assertEquals("AVG", detail.getStatType()); + Assertions.assertEquals(Integer.valueOf(4), detail.getEvenHarmonicOrder()); + Assertions.assertEquals(new BigDecimal("31"), detail.getEvenValue()); + Assertions.assertEquals(Arrays.asList(3, 5, 7), detail.getOddHarmonicOrders()); + Assertions.assertEquals(new BigDecimal("12"), detail.getOddMedianValue()); + Assertions.assertEquals(new BigDecimal("2"), detail.getThresholdMultiplier()); + } + + @Test + void shouldSkipWhenOddReferenceCountLessThanTwo() { + SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + SteadyChecksquareHarmonicParityRuleComponent component = new SteadyChecksquareHarmonicParityRuleComponent(influxQueryComponent); + LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0); + when(influxQueryComponent.queryValuePointMap(any(), + any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenAnswer(invocation -> { + Map> values = emptyBatchResult(invocation.getArgument(0)); + putPoint(values, "v_2", time, "50"); + putPoint(values, "v_3", time, "10"); + return values; + }); + SteadyTrendIndicatorDefinitionBO indicator = new SteadyTrendIndicatorCatalog().getIndicator("V_HARMONIC"); + + SteadyChecksquareHarmonicParityRuleVO result = component.check("line-001", indicator, + time, time, 1); + + Assertions.assertEquals(Boolean.FALSE, result.getAbnormal()); + Assertions.assertEquals(Integer.valueOf(0), result.getAbnormalPointCount()); + Assertions.assertTrue(result.getAbnormalDetails().isEmpty()); + } + + @Test + void shouldSkipEvenHarmonicWhenValueNotGreaterThanDeadband() { + SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + SteadyChecksquareHarmonicParityRuleComponent component = new SteadyChecksquareHarmonicParityRuleComponent(influxQueryComponent); + LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0); + when(influxQueryComponent.queryValuePointMap(any(), + any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenAnswer(invocation -> { + Map> values = emptyBatchResult(invocation.getArgument(0)); + putPoint(values, "v_3", time, "0.01"); + putPoint(values, "v_4", time, "0.10"); + putPoint(values, "v_5", time, "0.02"); + return values; + }); + SteadyTrendIndicatorDefinitionBO indicator = new SteadyTrendIndicatorCatalog().getIndicator("V_HARMONIC"); + + SteadyChecksquareHarmonicParityRuleVO result = component.check("line-001", indicator, + time, time, 1); + + Assertions.assertEquals(Boolean.FALSE, result.getAbnormal()); + Assertions.assertEquals(Integer.valueOf(0), result.getAbnormalPointCount()); + Assertions.assertTrue(result.getAbnormalDetails().isEmpty()); + } + + @Test + void shouldSkipNonHarmonicIndicator() { + SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + SteadyChecksquareHarmonicParityRuleComponent component = new SteadyChecksquareHarmonicParityRuleComponent(influxQueryComponent); + SteadyTrendIndicatorDefinitionBO indicator = new SteadyTrendIndicatorCatalog().getIndicator("V_RMS"); + + SteadyChecksquareHarmonicParityRuleVO result = component.check("line-001", indicator, + LocalDateTime.of(2026, 5, 1, 0, 0), + LocalDateTime.of(2026, 5, 1, 0, 1), 1); + + Assertions.assertEquals(Boolean.FALSE, result.getAbnormal()); + Assertions.assertEquals(Integer.valueOf(0), result.getAbnormalPointCount()); + Assertions.assertTrue(result.getAbnormalDetails().isEmpty()); + } + + private SteadyChecksquareValuePointBO point(LocalDateTime time, String value) { + SteadyChecksquareValuePointBO point = new SteadyChecksquareValuePointBO(); + point.setTime(time); + point.setValue(new BigDecimal(value)); + return point; + } + + private Map> emptyBatchResult(List fields) { + Map> result = + new LinkedHashMap>(); + for (SteadyTrendResolvedFieldBO field : fields) { + result.put(field.getField(), Collections.emptyList()); + } + return result; + } + + private void putPoint(Map> values, String field, + LocalDateTime time, String value) { + if (values.containsKey(field)) { + values.put(field, Collections.singletonList(point(time, value))); + } + } +} 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 index 83de0ee..867adda 100644 --- 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 @@ -2,10 +2,16 @@ 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 com.sun.net.httpserver.HttpServer; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; /** * 数据校验 InfluxQL 构造契约测试。 @@ -33,4 +39,115 @@ class SteadyChecksquareInfluxQueryComponentTest { Assertions.assertFalse(query.contains("quality_flag")); Assertions.assertFalse(query.contains("GROUP BY time")); } + + @Test + void shouldBuildValuePointQueryWithStatTypeFilter() { + 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("CP95"); + + String query = component.buildValuePointQuery(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\" = 'CP95'")); + Assertions.assertTrue(query.endsWith("ORDER BY time ASC")); + } + + @Test + void shouldReuseValuePointQueryWithinRequestCache() throws Exception { + AtomicInteger requestCount = new AtomicInteger(); + HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + server.createContext("/query", exchange -> { + requestCount.incrementAndGet(); + byte[] body = ("{\"results\":[{\"series\":[{\"values\":[" + + "[\"2026-05-01T00:00:00Z\",1.23]," + + "[\"2026-05-01T00:01:00Z\",2.34]" + + "]}]}]}").getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, body.length); + exchange.getResponseBody().write(body); + exchange.close(); + }); + server.start(); + try { + SteadyInfluxDbProperties properties = new SteadyInfluxDbProperties(); + properties.setUrl("http://127.0.0.1:" + server.getAddress().getPort()); + properties.setDatabase("steady"); + SteadyChecksquareInfluxQueryComponent component = new SteadyChecksquareInfluxQueryComponent(properties); + SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO(); + field.setMeasurement("data_v"); + field.setField("rms"); + field.setLineId("line-001"); + field.setPhase("A"); + field.setStatType("AVG"); + + component.enableRequestCache(); + component.queryExistingSlots(field, + LocalDateTime.of(2026, 5, 1, 0, 0, 0), + LocalDateTime.of(2026, 5, 1, 0, 1, 0), 1); + component.queryValuePoints(field, + LocalDateTime.of(2026, 5, 1, 0, 0, 0), + LocalDateTime.of(2026, 5, 1, 0, 1, 0), 1); + component.clearRequestCache(); + + Assertions.assertEquals(1, requestCount.get()); + } finally { + server.stop(0); + } + } + + @Test + void shouldQueryMultipleValueFieldsOnce() throws Exception { + AtomicInteger requestCount = new AtomicInteger(); + HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + server.createContext("/query", exchange -> { + requestCount.incrementAndGet(); + byte[] body = ("{\"results\":[{\"series\":[{\"columns\":[\"time\",\"h_2\",\"h_3\"],\"values\":[" + + "[\"2026-05-01T00:00:00Z\",1.23,2.34]," + + "[\"2026-05-01T00:01:00Z\",3.45,null]" + + "]}]}]}").getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, body.length); + exchange.getResponseBody().write(body); + exchange.close(); + }); + server.start(); + try { + SteadyInfluxDbProperties properties = new SteadyInfluxDbProperties(); + properties.setUrl("http://127.0.0.1:" + server.getAddress().getPort()); + properties.setDatabase("steady"); + SteadyChecksquareInfluxQueryComponent component = new SteadyChecksquareInfluxQueryComponent(properties); + SteadyTrendResolvedFieldBO h2 = buildField("h_2"); + SteadyTrendResolvedFieldBO h3 = buildField("h_3"); + + component.enableRequestCache(); + Map> result = + component.queryValuePointMap(Arrays.asList(h2, h3), + LocalDateTime.of(2026, 5, 1, 0, 0, 0), + LocalDateTime.of(2026, 5, 1, 0, 1, 0), 1); + component.clearRequestCache(); + + Assertions.assertEquals(1, requestCount.get()); + Assertions.assertEquals(2, result.get("h_2").size()); + Assertions.assertEquals(1, result.get("h_3").size()); + } finally { + server.stop(0); + } + } + + private SteadyTrendResolvedFieldBO buildField(String fieldName) { + SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO(); + field.setMeasurement("data_harmonic"); + field.setField(fieldName); + field.setLineId("line-001"); + field.setPhase("A"); + field.setStatType("AVG"); + return field; + } } diff --git a/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponentTest.java b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponentTest.java new file mode 100644 index 0000000..748c4e4 --- /dev/null +++ b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponentTest.java @@ -0,0 +1,219 @@ +package com.njcn.gather.steady.checksquare.component; + +import com.njcn.gather.steady.checksquare.pojo.bo.SteadyChecksquareValuePointBO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderRuleVO; +import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendIndicatorDefinitionBO; +import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendSeriesFieldBO; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; + +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 SteadyChecksquareValueOrderRuleComponentTest { + + @Test + void shouldMarkIndicatorAbnormalWhenInvalidPointCountGreaterThanOne() { + SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent); + LocalDateTime firstTime = LocalDateTime.of(2026, 5, 1, 0, 0); + LocalDateTime secondTime = LocalDateTime.of(2026, 5, 1, 0, 1); + when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1))) + .thenAnswer(invocation -> { + String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType(); + if ("MAX".equals(statType)) { + return Arrays.asList(point(firstTime, "8"), point(secondTime, "9")); + } + if ("CP95".equals(statType)) { + return Arrays.asList(point(firstTime, "9"), point(secondTime, "10")); + } + if ("AVG".equals(statType)) { + return Arrays.asList(point(firstTime, "7"), point(secondTime, "8")); + } + if ("MIN".equals(statType)) { + return Arrays.asList(point(firstTime, "1"), point(secondTime, "9")); + } + return Collections.emptyList(); + }); + + SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator(), null, + LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 2), 1); + + Assertions.assertEquals(Integer.valueOf(2), result.getAbnormalPointCount()); + Assertions.assertEquals(Boolean.TRUE, result.getAbnormal()); + Assertions.assertEquals(2, result.getAbnormalDetails().size()); + Assertions.assertEquals("2026-05-01 00:00:00", result.getAbnormalDetails().get(0).getTime()); + Assertions.assertEquals("A", result.getAbnormalDetails().get(0).getPhase()); + Assertions.assertEquals(new BigDecimal("8"), result.getAbnormalDetails().get(0).getMaxValue()); + Assertions.assertEquals(new BigDecimal("1"), result.getAbnormalDetails().get(0).getMinValue()); + Assertions.assertEquals(new BigDecimal("7"), result.getAbnormalDetails().get(0).getAvgValue()); + Assertions.assertEquals(new BigDecimal("9"), result.getAbnormalDetails().get(0).getCp95Value()); + } + + @Test + void shouldTreatEqualAdjacentStatValuesAsNormal() { + SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent); + LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0); + when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1))) + .thenAnswer(invocation -> { + String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType(); + if ("MAX".equals(statType)) { + return Collections.singletonList(point(time, "10")); + } + if ("CP95".equals(statType)) { + return Collections.singletonList(point(time, "10")); + } + if ("AVG".equals(statType)) { + return Collections.singletonList(point(time, "8")); + } + if ("MIN".equals(statType)) { + return Collections.singletonList(point(time, "8")); + } + return Collections.emptyList(); + }); + + SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator(), null, + LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 1), 1); + + Assertions.assertEquals(Integer.valueOf(0), result.getAbnormalPointCount()); + Assertions.assertEquals(Boolean.FALSE, result.getAbnormal()); + Assertions.assertTrue(result.getAbnormalDetails().isEmpty()); + } + + @Test + void shouldNotMarkIndicatorAbnormalWhenOnlyOneInvalidPointExists() { + SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent); + LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0); + when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1))) + .thenAnswer(invocation -> { + String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType(); + if ("MAX".equals(statType)) { + return Collections.singletonList(point(time, "8")); + } + if ("CP95".equals(statType)) { + return Collections.singletonList(point(time, "10")); + } + if ("AVG".equals(statType)) { + return Collections.singletonList(point(time, "8")); + } + if ("MIN".equals(statType)) { + return Collections.singletonList(point(time, "1")); + } + return Collections.emptyList(); + }); + + SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator(), null, + LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 1), 1); + + Assertions.assertEquals(Integer.valueOf(1), result.getAbnormalPointCount()); + Assertions.assertEquals(Boolean.FALSE, result.getAbnormal()); + Assertions.assertEquals(1, result.getAbnormalDetails().size()); + } + + @Test + void shouldFillHarmonicOrderInAbnormalDetailForHarmonicIndicator() { + SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent); + LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0); + when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1))) + .thenAnswer(invocation -> { + String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType(); + if ("MAX".equals(statType)) { + return Collections.singletonList(point(time, "8")); + } + if ("CP95".equals(statType)) { + return Collections.singletonList(point(time, "10")); + } + if ("AVG".equals(statType)) { + return Collections.singletonList(point(time, "8")); + } + if ("MIN".equals(statType)) { + return Collections.singletonList(point(time, "1")); + } + return Collections.emptyList(); + }); + SteadyTrendIndicatorDefinitionBO indicator = indicator(); + indicator.setHarmonic(true); + indicator.setHarmonicFieldPrefix("v"); + + SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator, 5, + LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 1), 1); + + Assertions.assertEquals(1, result.getAbnormalDetails().size()); + Assertions.assertEquals(Integer.valueOf(5), result.getAbnormalDetails().get(0).getHarmonicOrder()); + } + + @Test + void shouldSkipPointWhenAnyRequiredStatValueMissing() { + SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent); + LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0); + when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1))) + .thenAnswer(invocation -> { + String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType(); + if ("MAX".equals(statType)) { + return Collections.singletonList(point(time, "10")); + } + if ("CP95".equals(statType)) { + return Collections.singletonList(point(time, "11")); + } + if ("MIN".equals(statType)) { + return Collections.singletonList(point(time, "1")); + } + return Collections.emptyList(); + }); + + SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator(), null, + LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 1), 1); + + Assertions.assertEquals(Integer.valueOf(0), result.getAbnormalPointCount()); + Assertions.assertEquals(Boolean.FALSE, result.getAbnormal()); + Assertions.assertTrue(result.getAbnormalDetails().isEmpty()); + } + + @Test + void shouldSkipIndicatorWhenNotAllFourStatsSupported() { + SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent); + SteadyTrendIndicatorDefinitionBO indicator = indicator(); + indicator.setSupportStats(Collections.singletonList("AVG")); + + SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator, null, + LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 1), 1); + + Assertions.assertEquals(Integer.valueOf(0), result.getAbnormalPointCount()); + Assertions.assertEquals(Boolean.FALSE, result.getAbnormal()); + Assertions.assertTrue(result.getAbnormalDetails().isEmpty()); + } + + private SteadyTrendIndicatorDefinitionBO indicator() { + SteadyTrendIndicatorDefinitionBO indicator = new SteadyTrendIndicatorDefinitionBO(); + indicator.setIndicatorCode("V_RMS"); + indicator.setName("相电压有效值"); + indicator.setTableName("data_v"); + indicator.setPhaseCodes(Collections.singletonList("A")); + indicator.setSeriesFields(Collections.singletonList(new SteadyTrendSeriesFieldBO("rms", "相电压有效值"))); + indicator.setSupportStats(Arrays.asList("AVG", "MAX", "MIN", "CP95")); + indicator.setUnit("V"); + return indicator; + } + + private SteadyChecksquareValuePointBO point(LocalDateTime time, String value) { + SteadyChecksquareValuePointBO point = new SteadyChecksquareValuePointBO(); + point.setTime(time); + point.setValue(new BigDecimal(value)); + return point; + } +} 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 index 0d1c597..d68542f 100644 --- 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 @@ -2,6 +2,7 @@ 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.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -17,8 +18,21 @@ class SteadyChecksquareControllerTest { 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()); + Method queryMethod = SteadyChecksquareController.class.getDeclaredMethod("query", com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareHistoryQueryParam.class); + PostMapping queryMapping = queryMethod.getAnnotation(PostMapping.class); + Assertions.assertArrayEquals(new String[]{"/query"}, queryMapping.value()); + + Method createMethod = SteadyChecksquareController.class.getDeclaredMethod("create", com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam.class); + PostMapping createMapping = createMethod.getAnnotation(PostMapping.class); + Assertions.assertArrayEquals(new String[]{"/create"}, createMapping.value()); + + Method detailMethod = SteadyChecksquareController.class.getDeclaredMethod("detail", String.class); + GetMapping detailMapping = detailMethod.getAnnotation(GetMapping.class); + Assertions.assertArrayEquals(new String[]{"/detail"}, detailMapping.value()); + + Method itemDetailMethod = SteadyChecksquareController.class.getDeclaredMethod("itemDetail", + String.class, String.class, String.class, Integer.class, Integer.class); + GetMapping itemDetailMapping = itemDetailMethod.getAnnotation(GetMapping.class); + Assertions.assertArrayEquals(new String[]{"/item-detail"}, itemDetailMapping.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 index d97ff41..eae9492 100644 --- 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 @@ -19,7 +19,7 @@ class SteadyChecksquareQueryParamTest { Assertions.assertNull(field("qualityFlag")); Assertions.assertNull(field("statTypes")); Assertions.assertNull(field("phases")); - Assertions.assertNotNull(field("harmonicOrders")); + Assertions.assertNull(field("harmonicOrders")); } private Field field(String name) { 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 index 57a49d8..421a010 100644 --- 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 @@ -1,26 +1,54 @@ package com.njcn.gather.steady.checksquare.service.impl; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.njcn.gather.steady.checksquare.component.SteadyChecksquareCalculator; +import com.njcn.gather.steady.checksquare.component.SteadyChecksquareHarmonicParityRuleComponent; import com.njcn.gather.steady.checksquare.component.SteadyChecksquareInfluxQueryComponent; +import com.njcn.gather.steady.checksquare.component.SteadyChecksquareValueOrderRuleComponent; import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareDetailPO; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareItemPO; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareStatSummaryPO; +import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareTaskPO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityDetailVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityRuleVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemDetailVO; 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.SteadyChecksquareStatSummaryVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderDetailVO; +import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderRuleVO; +import com.njcn.gather.steady.checksquare.service.SteadyChecksquareDetailService; +import com.njcn.gather.steady.checksquare.service.SteadyChecksquareItemService; +import com.njcn.gather.steady.checksquare.service.SteadyChecksquareStatSummaryService; +import com.njcn.gather.steady.checksquare.service.SteadyChecksquareTaskService; import com.njcn.gather.steady.datavie.component.SteadyTrendIndicatorCatalog; +import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO; 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 org.mockito.ArgumentCaptor; +import org.springframework.transaction.annotation.Transactional; +import java.lang.reflect.Method; +import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.ArrayList; 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.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** @@ -28,12 +56,28 @@ import static org.mockito.Mockito.when; */ class SteadyChecksquareServiceImplTest { + @Test + void shouldNotOpenTransactionAroundCreateCalculation() throws Exception { + Method createMethod = SteadyChecksquareServiceImpl.class.getMethod("create", SteadyChecksquareQueryParam.class); + + Assertions.assertNull(createMethod.getAnnotation(Transactional.class)); + } + @Test void shouldUseFixedFlickerIntervalsPerIndicator() { SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class); + SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class); AddLedgerService addLedgerService = mock(AddLedgerService.class); SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(), - influxQueryComponent, new SteadyChecksquareCalculator(), new AddDataTimeSlotCalculator(), addLedgerService); + influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, harmonicParityRuleComponent, + new AddDataTimeSlotCalculator(), addLedgerService, mock(SteadyChecksquareTaskService.class), + mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class), + mock(SteadyChecksquareDetailService.class), new ObjectMapper()); + when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(emptyRuleResult()); + when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(emptyHarmonicParityRuleResult()); AddLedgerLinePathVO linePath = new AddLedgerLinePathVO(); linePath.setLineId("line-001"); linePath.setLineName("进线一"); @@ -54,7 +98,7 @@ class SteadyChecksquareServiceImplTest { param.setTimeStart("2026-05-01 00:00:00"); param.setTimeEnd("2026-05-01 02:00:00"); - SteadyChecksquareQueryVO result = service.query(param); + SteadyChecksquareQueryVO result = calculate(service, param); Assertions.assertEquals(Integer.valueOf(1), result.getIntervalMinutes()); Assertions.assertEquals(3, result.getItems().size()); @@ -64,11 +108,20 @@ class SteadyChecksquareServiceImplTest { } @Test - void shouldOnlyQueryRequestedHarmonicOrders() { + void shouldAggregateAllHarmonicOrdersIntoIndicatorItem() { SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class); + SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class); AddLedgerService addLedgerService = mock(AddLedgerService.class); SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(), - influxQueryComponent, new SteadyChecksquareCalculator(), new AddDataTimeSlotCalculator(), addLedgerService); + influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, harmonicParityRuleComponent, + new AddDataTimeSlotCalculator(), addLedgerService, mock(SteadyChecksquareTaskService.class), + mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class), + mock(SteadyChecksquareDetailService.class), new ObjectMapper()); + when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(emptyRuleResult()); + when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(emptyHarmonicParityRuleResult()); AddLedgerLinePathVO linePath = new AddLedgerLinePathVO(); linePath.setLineId("line-001"); linePath.setLineName("进线一"); @@ -82,22 +135,32 @@ class SteadyChecksquareServiceImplTest { 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); + SteadyChecksquareQueryVO result = calculate(service, param); Assertions.assertEquals(1, result.getItems().size()); - Assertions.assertEquals(Integer.valueOf(5), result.getItems().get(0).getHarmonicOrder()); + Assertions.assertEquals("line-001|V_HARMONIC", result.getItems().get(0).getItemKey()); + Assertions.assertNull(result.getItems().get(0).getHarmonicOrder()); + Assertions.assertEquals(Integer.valueOf(2), result.getItems().get(0).getStatDetails().get(0).getSegments().get(0).getHarmonicOrder()); } @Test - void shouldKeepRequestedHarmonicOrdersDistinctAndOrdered() { + void shouldAverageHarmonicOrderResultsAndMarkAbnormalWhenAnyOrderAbnormal() { SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class); + SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class); AddLedgerService addLedgerService = mock(AddLedgerService.class); SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(), - influxQueryComponent, new SteadyChecksquareCalculator(), new AddDataTimeSlotCalculator(), addLedgerService); + influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, harmonicParityRuleComponent, + new AddDataTimeSlotCalculator(), addLedgerService, mock(SteadyChecksquareTaskService.class), + mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class), + mock(SteadyChecksquareDetailService.class), new ObjectMapper()); + when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(emptyRuleResult()); + when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(emptyHarmonicParityRuleResult()); AddLedgerLinePathVO linePath = new AddLedgerLinePathVO(); linePath.setLineId("line-001"); linePath.setLineName("进线一"); @@ -105,21 +168,328 @@ class SteadyChecksquareServiceImplTest { 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()); + .thenReturn(new HashSet(Collections.singletonList( + LocalDateTime.of(2026, 5, 1, 0, 0)))); + SteadyChecksquareValueOrderRuleVO normalRuleResult = new SteadyChecksquareValueOrderRuleVO(); + SteadyChecksquareValueOrderRuleVO abnormalRuleResult = new SteadyChecksquareValueOrderRuleVO(); + SteadyChecksquareValueOrderDetailVO abnormalDetail = new SteadyChecksquareValueOrderDetailVO(); + abnormalDetail.setTime("2026-05-01 00:00:00"); + abnormalDetail.setPhase("A"); + abnormalDetail.setHarmonicOrder(2); + abnormalRuleResult.setAbnormal(true); + abnormalRuleResult.setAbnormalPointCount(4); + abnormalRuleResult.setAbnormalDetails(Collections.singletonList(abnormalDetail)); + when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(normalRuleResult); + when(valueOrderRuleComponent.check(any(), any(), eq(2), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(abnormalRuleResult); + when(valueOrderRuleComponent.check(any(), any(), eq(3), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(normalRuleResult); 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); + SteadyChecksquareQueryVO result = calculate(service, 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()); + Assertions.assertEquals(1, items.size()); + Assertions.assertEquals(Boolean.TRUE, items.get(0).getAbnormal()); + Assertions.assertEquals(Integer.valueOf(1), items.get(0).getAbnormalPointCount()); + Assertions.assertEquals(1, items.get(0).getAbnormalDetails().size()); + Assertions.assertEquals(Integer.valueOf(2), items.get(0).getAbnormalDetails().get(0).getHarmonicOrder()); + Assertions.assertEquals(Integer.valueOf(8), items.get(0).getExpectedPointCount()); + Assertions.assertEquals(Integer.valueOf(4), items.get(0).getActualPointCount()); + } + + @Test + void shouldAssembleValueOrderRuleResultIntoItem() { + SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class); + SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class); + AddLedgerService addLedgerService = mock(AddLedgerService.class); + SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(), + influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, harmonicParityRuleComponent, + new AddDataTimeSlotCalculator(), addLedgerService, mock(SteadyChecksquareTaskService.class), + mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class), + mock(SteadyChecksquareDetailService.class), new ObjectMapper()); + 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)))); + SteadyChecksquareValueOrderRuleVO ruleResult = new SteadyChecksquareValueOrderRuleVO(); + SteadyChecksquareValueOrderDetailVO detail = new SteadyChecksquareValueOrderDetailVO(); + detail.setTime("2026-05-01 00:00:00"); + detail.setPhase("A"); + ruleResult.setAbnormalPointCount(2); + ruleResult.setAbnormal(true); + ruleResult.setAbnormalDetails(Collections.singletonList(detail)); + when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1))) + .thenReturn(ruleResult); + + SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam(); + param.setLineId("line-001"); + param.setIndicatorCodes(Collections.singletonList("V_RMS")); + param.setTimeStart("2026-05-01 00:00:00"); + param.setTimeEnd("2026-05-01 00:01:00"); + + SteadyChecksquareQueryVO result = calculate(service, param); + + SteadyChecksquareItemVO item = result.getItems().get(0); + Assertions.assertEquals(Boolean.TRUE, item.getAbnormal()); + Assertions.assertEquals(Integer.valueOf(2), item.getAbnormalPointCount()); + Assertions.assertEquals(1, item.getAbnormalDetails().size()); + Assertions.assertEquals("A", item.getAbnormalDetails().get(0).getPhase()); + } + + @Test + void shouldPrefetchNormalIndicatorFieldsByMeasurementPhaseAndStat() { + SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class); + SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class); + AddLedgerService addLedgerService = mock(AddLedgerService.class); + SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(), + influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, harmonicParityRuleComponent, + new AddDataTimeSlotCalculator(), addLedgerService, mock(SteadyChecksquareTaskService.class), + mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class), + mock(SteadyChecksquareDetailService.class), new ObjectMapper()); + when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(emptyRuleResult()); + when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(emptyHarmonicParityRuleResult()); + 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(Arrays.asList("V_RMS", "V_LINE_RMS")); + param.setTimeStart("2026-05-01 00:00:00"); + param.setTimeEnd("2026-05-01 00:01:00"); + + calculate(service, param); + + ArgumentCaptor captor = ArgumentCaptor.forClass(List.class); + verify(influxQueryComponent, times(12)).queryValuePointMap(captor.capture(), + any(LocalDateTime.class), any(LocalDateTime.class), eq(1)); + boolean foundBatch = false; + for (List fields : captor.getAllValues()) { + if (fields.size() == 2) { + List fieldNames = new ArrayList(); + for (Object field : fields) { + fieldNames.add(((SteadyTrendResolvedFieldBO) field).getField()); + } + foundBatch = fieldNames.contains("rms") && fieldNames.contains("rms_lvr"); + } + } + Assertions.assertTrue(foundBatch); + } + + @Test + void shouldAssembleHarmonicParityRuleResultIntoAggregateItem() { + SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class); + SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class); + AddLedgerService addLedgerService = mock(AddLedgerService.class); + SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(), + influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, harmonicParityRuleComponent, + new AddDataTimeSlotCalculator(), addLedgerService, mock(SteadyChecksquareTaskService.class), + mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class), + mock(SteadyChecksquareDetailService.class), new ObjectMapper()); + when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(emptyRuleResult()); + 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)))); + SteadyChecksquareHarmonicParityRuleVO ruleResult = new SteadyChecksquareHarmonicParityRuleVO(); + SteadyChecksquareHarmonicParityDetailVO detail = new SteadyChecksquareHarmonicParityDetailVO(); + detail.setTime("2026-05-01 00:00:00"); + detail.setPhase("A"); + detail.setStatType("AVG"); + detail.setEvenHarmonicOrder(4); + ruleResult.setAbnormal(true); + ruleResult.setAbnormalPointCount(1); + ruleResult.setAbnormalDetails(Collections.singletonList(detail)); + when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1))) + .thenReturn(ruleResult); + + SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam(); + param.setLineId("line-001"); + param.setIndicatorCodes(Collections.singletonList("V_HARMONIC")); + param.setTimeStart("2026-05-01 00:00:00"); + param.setTimeEnd("2026-05-01 00:01:00"); + + SteadyChecksquareQueryVO result = calculate(service, param); + + SteadyChecksquareItemVO item = result.getItems().get(0); + Assertions.assertNull(item.getHarmonicOrder()); + Assertions.assertEquals(Boolean.TRUE, item.getHarmonicParityAbnormal()); + Assertions.assertEquals(Integer.valueOf(1), item.getHarmonicParityAbnormalPointCount()); + Assertions.assertEquals("AVG", item.getHarmonicParityAbnormalDetails().get(0).getStatType()); + } + + @Test + void shouldRejectUnsupportedItemDetailType() { + SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class); + SteadyChecksquareItemPO item = new SteadyChecksquareItemPO(); + item.setId("item-001"); + item.setState(1); + when(itemService.getById("item-001")).thenReturn(item); + SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(), + mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(), + mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class), + new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), mock(SteadyChecksquareTaskService.class), + itemService, mock(SteadyChecksquareStatSummaryService.class), + mock(SteadyChecksquareDetailService.class), new ObjectMapper()); + + Assertions.assertThrows(RuntimeException.class, () -> service.itemDetail("item-001", "UNKNOWN", null)); + } + + @Test + void shouldLoadDetailSummariesInSingleBatch() { + SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class); + SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class); + SteadyChecksquareStatSummaryService statSummaryService = mock(SteadyChecksquareStatSummaryService.class); + LambdaQueryChainWrapper itemQuery = mock(LambdaQueryChainWrapper.class); + LambdaQueryChainWrapper summaryQuery = mock(LambdaQueryChainWrapper.class); + SteadyChecksquareTaskPO task = new SteadyChecksquareTaskPO(); + task.setId("task-001"); + task.setState(1); + task.setLineId("line-001"); + task.setLineName("进线一"); + task.setTimeStart(LocalDateTime.of(2026, 5, 1, 0, 0)); + task.setTimeEnd(LocalDateTime.of(2026, 5, 1, 0, 1)); + task.setIntervalMinutes(1); + SteadyChecksquareItemPO item1 = buildItemPO("item-001", "V_RMS"); + SteadyChecksquareItemPO item2 = buildItemPO("item-002", "FREQ"); + SteadyChecksquareStatSummaryPO summary1 = buildSummaryPO("item-001", "AVG"); + SteadyChecksquareStatSummaryPO summary2 = buildSummaryPO("item-002", "AVG"); + when(taskService.getById("task-001")).thenReturn(task); + when(itemService.lambdaQuery()).thenReturn(itemQuery); + when(itemQuery.eq(any(), any())).thenReturn(itemQuery); + when(itemQuery.list()).thenReturn(Arrays.asList(item1, item2)); + when(statSummaryService.lambdaQuery()).thenReturn(summaryQuery); + when(summaryQuery.in(any(), any(List.class))).thenReturn(summaryQuery); + when(summaryQuery.list()).thenReturn(Arrays.asList(summary1, summary2)); + SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(), + mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(), + mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class), + new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), taskService, + itemService, statSummaryService, mock(SteadyChecksquareDetailService.class), new ObjectMapper()); + + SteadyChecksquareQueryVO result = service.detail("task-001"); + + Assertions.assertEquals(2, result.getItems().size()); + Assertions.assertEquals(1, result.getItems().get(0).getStatSummaries().size()); + Assertions.assertEquals(1, result.getItems().get(1).getStatSummaries().size()); + verify(statSummaryService, times(1)).lambdaQuery(); + } + + @Test + void shouldPageItemDetailWhenPageArgumentsPresent() { + SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class); + SteadyChecksquareDetailService detailService = mock(SteadyChecksquareDetailService.class); + SteadyChecksquareItemPO item = new SteadyChecksquareItemPO(); + item.setId("item-001"); + item.setState(1); + SteadyChecksquareDetailPO detail = new SteadyChecksquareDetailPO(); + detail.setItemId("item-001"); + detail.setDetailType("VALUE_ORDER"); + detail.setPointTime(LocalDateTime.of(2026, 5, 1, 0, 0)); + detail.setPhase("A"); + Page page = new Page(2, 1); + page.setTotal(1); + page.setRecords(Collections.singletonList(detail)); + when(itemService.getById("item-001")).thenReturn(item); + when(detailService.page(any(Page.class), any())).thenReturn(page); + SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(), + mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(), + mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class), + new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), mock(SteadyChecksquareTaskService.class), + itemService, mock(SteadyChecksquareStatSummaryService.class), + detailService, new ObjectMapper()); + + SteadyChecksquareItemDetailVO result = service.itemDetail("item-001", "VALUE_ORDER", null, 2, 1); + + Assertions.assertEquals(Integer.valueOf(2), result.getPageNum()); + Assertions.assertEquals(Integer.valueOf(1), result.getPageSize()); + Assertions.assertEquals(Long.valueOf(1L), result.getTotal()); + Assertions.assertEquals(1, result.getValueOrderDetails().size()); + verify(detailService).page(any(Page.class), any()); + } + + @Test + void shouldSaveChecksquareResultsInBatch() { + SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class); + SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class); + SteadyChecksquareStatSummaryService statSummaryService = mock(SteadyChecksquareStatSummaryService.class); + SteadyChecksquareDetailService detailService = mock(SteadyChecksquareDetailService.class); + SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(), + mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(), + mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class), + new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), taskService, + itemService, statSummaryService, detailService, new ObjectMapper()); + SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam(); + param.setIndicatorCodes(Collections.singletonList("V_RMS")); + SteadyChecksquareQueryVO result = new SteadyChecksquareQueryVO(); + result.setLineId("line-001"); + result.setLineName("进线一"); + result.setTimeStart("2026-05-01 00:00:00"); + result.setTimeEnd("2026-05-01 00:01:00"); + result.setIntervalMinutes(1); + SteadyChecksquareItemVO item = new SteadyChecksquareItemVO(); + item.setItemKey("line-001|V_RMS"); + item.setIndicatorCode("V_RMS"); + item.setIndicatorName("相电压有效值"); + item.setIntervalMinutes(1); + item.setHasData(true); + item.setExpectedPointCount(2); + item.setActualPointCount(2); + item.setMissingPointCount(0); + item.setMissingRate(BigDecimal.ZERO.setScale(6)); + item.setMissingRateText("0.00%"); + item.setMaxContinuousMissingMinutes(0); + item.setAbnormal(false); + item.setAbnormalPointCount(0); + item.setHarmonicParityAbnormal(false); + item.setHarmonicParityAbnormalPointCount(0); + SteadyChecksquareStatSummaryVO summary = new SteadyChecksquareStatSummaryVO(); + summary.setStatType("AVG"); + summary.setSupported(true); + summary.setHasData(true); + summary.setExpectedPointCount(2); + summary.setActualPointCount(2); + summary.setMissingPointCount(0); + summary.setMissingRate(BigDecimal.ZERO.setScale(6)); + summary.setMissingRateText("0.00%"); + summary.setMaxContinuousMissingMinutes(0); + item.getStatSummaries().add(summary); + result.getItems().add(item); + + saveResult(service, param, result); + + verify(taskService).save(any()); + verify(itemService).saveBatch(any()); + verify(statSummaryService).saveBatch(any()); } private void assertItemInterval(SteadyChecksquareItemVO item, String indicatorCode, int intervalMinutes, int expectedPointCount) { @@ -127,4 +497,66 @@ class SteadyChecksquareServiceImplTest { Assertions.assertEquals(Integer.valueOf(intervalMinutes), item.getIntervalMinutes()); Assertions.assertEquals(Integer.valueOf(expectedPointCount), item.getExpectedPointCount()); } + + private SteadyChecksquareQueryVO calculate(SteadyChecksquareServiceImpl service, SteadyChecksquareQueryParam param) { + try { + Method method = SteadyChecksquareServiceImpl.class.getDeclaredMethod("calculate", SteadyChecksquareQueryParam.class); + method.setAccessible(true); + return (SteadyChecksquareQueryVO) method.invoke(service, param); + } catch (Exception exception) { + throw new RuntimeException(exception); + } + } + + private void saveResult(SteadyChecksquareServiceImpl service, SteadyChecksquareQueryParam param, SteadyChecksquareQueryVO result) { + try { + Method method = SteadyChecksquareServiceImpl.class.getDeclaredMethod("saveResult", + SteadyChecksquareQueryParam.class, SteadyChecksquareQueryVO.class); + method.setAccessible(true); + method.invoke(service, param, result); + } catch (Exception exception) { + throw new RuntimeException(exception); + } + } + + private SteadyChecksquareItemPO buildItemPO(String itemId, String indicatorCode) { + SteadyChecksquareItemPO item = new SteadyChecksquareItemPO(); + item.setId(itemId); + item.setIndicatorCode(indicatorCode); + item.setIndicatorName(indicatorCode); + item.setState(1); + item.setHasData(1); + item.setExpectedPointCount(1); + item.setActualPointCount(1); + item.setMissingPointCount(0); + item.setMissingRate(BigDecimal.ZERO.setScale(6)); + item.setMaxContinuousMissingMinutes(0); + item.setAbnormal(0); + item.setAbnormalPointCount(0); + item.setHarmonicParityAbnormal(0); + item.setHarmonicParityAbnormalPointCount(0); + return item; + } + + private SteadyChecksquareStatSummaryPO buildSummaryPO(String itemId, String statType) { + SteadyChecksquareStatSummaryPO summary = new SteadyChecksquareStatSummaryPO(); + summary.setItemId(itemId); + summary.setStatType(statType); + summary.setSupported(1); + summary.setHasData(1); + summary.setExpectedPointCount(1); + summary.setActualPointCount(1); + summary.setMissingPointCount(0); + summary.setMissingRate(BigDecimal.ZERO.setScale(6)); + summary.setMaxContinuousMissingMinutes(0); + return summary; + } + + private SteadyChecksquareValueOrderRuleVO emptyRuleResult() { + return new SteadyChecksquareValueOrderRuleVO(); + } + + private SteadyChecksquareHarmonicParityRuleVO emptyHarmonicParityRuleResult() { + return new SteadyChecksquareHarmonicParityRuleVO(); + } } diff --git a/system-ops/dbms/README.md b/system-ops/dbms/README.md index 6f63251..383359e 100644 --- a/system-ops/dbms/README.md +++ b/system-ops/dbms/README.md @@ -2,13 +2,100 @@ ## 模块定位 -`dbms` 是 `system-ops` 下的数据库监控模块,当前先提供数据库监控菜单对应的后端基础入口。 +`dbms` 是 `system-ops` 下的数据库运维模块,当前支持 Oracle、MySQL 两类数据库运维能力,其中 Oracle 支持 `DATA_PUMP`、`JDBC_EXPORT`,MySQL 当前支持 `JDBC_EXPORT`。 ## 当前接口 - `GET /database/overview` - - 查询数据库监控基础信息。 + - 查询数据库运维概览信息。 +- `POST /database/connections/list` + - 查询数据库连接配置。 +- `POST /database/connections/add` + - 新增 Oracle 数据库连接配置。 +- `POST /database/connections/update` + - 修改 Oracle 数据库连接配置。 +- `POST /database/connections/delete` + - 删除数据库连接配置。 +- `POST /database/connections/test` + - 测试 Oracle 数据库连接。 +- `POST /database/connections/tables` + - 查询 Oracle 表列表。 +- `POST /database/backups/create` + - 创建备份任务,默认使用 `DATA_PUMP`,可选 `JDBC_EXPORT`。 +- `POST /database/backups/tasks/list` + - 查询备份任务列表。 +- `GET /database/backups/tasks/status` + - 查询任务状态。 +- `POST /database/backups/files/list` + - 查询备份文件记录。 +- `POST /database/restores/create` + - 创建恢复任务。 +- `GET /database/restores/tasks/status` + - 查询恢复任务状态。 +- `POST /database/delete/backup-file` + - 删除备份文件,要求 `confirmText=确认删除`。 +- `POST /database/delete/task` + - 删除任务记录,要求 `confirmText=确认删除`。 + +## 数据脚本 + +- `src/main/resources/sql/system-ops/system-ops-init.sql` + - 系统运维菜单初始化脚本。 +- `src/main/resources/sql/system-ops/dbms-database-ops-init.sql` + - 数据库运维连接、任务、备份文件和恢复记录表结构。 + +## 配置项 + +建议通过环境配置覆盖: + +```yaml +dbms: + backup: + storage-path: D:/dbms-backup + default-max-file-size-mb: 512 + mysql-fetch-size: 1000 + tools: + expdp-path: + impdp-path: +``` + +说明: + +- `backup.storage-path` + - `JDBC_EXPORT` 生成的 CSV 和元数据 JSON 的受管根目录。 +- `backup.default-max-file-size-mb` + - MySQL `JDBC_EXPORT` 默认分片大小,前端可通过 `maxFileSizeMb` 覆盖,默认 512MB。 +- `backup.mysql-fetch-size` + - MySQL `JDBC_EXPORT` 流式读取批量大小,默认 1000。 +- `tools.expdp-path`、`tools.impdp-path` + - Oracle Data Pump 工具路径;为空时尝试走系统 `PATH`。 + +## 当前行为 + +- 当前能力矩阵如下: + +| 数据库类型 | 连接测试 | 表列表 | JDBC_EXPORT | DATA_PUMP | +| --- | --- | --- | --- | --- | +| ORACLE | 支持 | 支持 | 支持 | 支持 | +| MYSQL | 支持 | 支持 | 支持 | 不支持 | +- 备份和恢复只允许基于已保存且连接可用的连接配置发起。 +- 新增连接前的测试接口仍可传 `temporaryPassword` 做临时连通性测试。 +- 备份任务异步执行,只有实际文件生成成功后才会写入 `dbms_backup_file` 记录。 +- `JDBC_EXPORT` 当前会生成两类文件: +- MySQL `JDBC_EXPORT` 会按任务号创建独立备份目录,每张表独立 CSV,默认按 512MB 分片: + - 数据分片文件:`_part001_yyyyMMdd_.csv` + - 元数据文件:`mysql_jdbc_export_metadata_yyyyMMdd_.json` +- 备份任务支持停止和重新开始: + - `POST /database/backups/tasks/stop` + - `POST /database/backups/tasks/restart` +- `JDBC_EXPORT` 恢复依赖元数据文件,不再允许缺少元数据直接发起恢复。 +- 删除备份文件时,会校验目标路径必须位于受管备份目录下,避免误删非备份文件。 ## 当前限制 -- 当前只完成后端基础入口,不包含真实数据库连接状态、容量或性能指标探测逻辑。 +- `DATA_PUMP` 仍依赖部署机器可执行 `expdp`、`impdp`,并且 Oracle 侧已准备好 `directory` 对象和权限。 +- 当前代码要求 `DATA_PUMP` 连接配置里补齐可管理的 `directoryPath`,否则虽然 Oracle 端可能已导出成功,后端无法安全管理文件记录与删除。 +- `JDBC_EXPORT` 恢复当前仅覆盖表数据,不承诺恢复索引、约束、触发器、序列、存储过程、权限等数据库对象。 +- `TIME_RANGE` 模式当前只在 `JDBC_EXPORT` 场景真正参与查询过滤;`DATA_PUMP` 尚未接入 Oracle `QUERY` 参数。 +- MySQL `JDBC_EXPORT` 已实现按大小分片;Oracle `JDBC_EXPORT` 仍沿用原单文件导出路径。 +- 本轮仅完成代码路径和文档收口,未执行 `mvn` 编译、测试或真实库联调。 diff --git a/system-ops/dbms/pom.xml b/system-ops/dbms/pom.xml index 79df890..776b0c3 100644 --- a/system-ops/dbms/pom.xml +++ b/system-ops/dbms/pom.xml @@ -26,5 +26,19 @@ spingboot2.3.12 2.3.12 + + com.njcn + mybatis-plus + 0.0.1 + + + org.springframework + spring-tx + + + com.oracle + ojdbc6 + 11.2.0.3 + diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/DataPumpCommandExecutor.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/DataPumpCommandExecutor.java new file mode 100644 index 0000000..37e27c7 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/DataPumpCommandExecutor.java @@ -0,0 +1,111 @@ +package com.njcn.gather.systemops.database.component; + +import cn.hutool.core.util.StrUtil; +import com.njcn.gather.systemops.database.config.DbmsProperties; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +/** + * Oracle Data Pump 命令执行组件。 + */ +@Component +@RequiredArgsConstructor +public class DataPumpCommandExecutor { + + private final DbmsProperties dbmsProperties; + + public CommandResult expdp(DatabaseConnection connection, String password, String directoryName, + String dumpFileName, String logFileName, List tableNames) { + List command = new ArrayList<>(); + command.add(resolveTool(dbmsProperties.getTools().getExpdpPath(), "expdp")); + fillCommonArgs(command, connection, password, directoryName, dumpFileName, logFileName); + if (tableNames != null && !tableNames.isEmpty()) { + command.add("tables=" + connection.getSchemaName() + "." + String.join("," + connection.getSchemaName() + ".", tableNames)); + } + return execute(command); + } + + public CommandResult impdp(DatabaseConnection connection, String password, String directoryName, + String dumpFileName, String logFileName, String tableExistsAction) { + List command = new ArrayList<>(); + command.add(resolveTool(dbmsProperties.getTools().getImpdpPath(), "impdp")); + fillCommonArgs(command, connection, password, directoryName, dumpFileName, logFileName); + if (StrUtil.isNotBlank(tableExistsAction)) { + command.add("table_exists_action=" + tableExistsAction); + } + return execute(command); + } + + private void fillCommonArgs(List command, DatabaseConnection connection, String password, + String directoryName, String dumpFileName, String logFileName) { + command.add(connection.getUsername() + "/" + password + "@" + buildConnectIdentifier(connection)); + command.add("directory=" + directoryName); + command.add("dumpfile=" + dumpFileName); + command.add("logfile=" + logFileName); + } + + private String buildConnectIdentifier(DatabaseConnection connection) { + if (StrUtil.isNotBlank(connection.getServiceName())) { + return "//" + connection.getHost() + ":" + connection.getPort() + "/" + connection.getServiceName(); + } + return connection.getHost() + ":" + connection.getPort() + ":" + connection.getSid(); + } + + private String resolveTool(String configuredPath, String defaultName) { + return StrUtil.blankToDefault(configuredPath, defaultName); + } + + private CommandResult execute(List command) { + CommandResult result = new CommandResult(); + result.setCommand(maskPassword(command)); + try { + Process process = new ProcessBuilder(command).redirectErrorStream(true).start(); + StringBuilder output = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.defaultCharset()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append(System.lineSeparator()); + } + } + int exitCode = process.waitFor(); + result.setExitCode(exitCode); + result.setOutput(output.toString()); + result.setSuccess(exitCode == 0); + } catch (Exception exception) { + result.setExitCode(-1); + result.setOutput(exception.getMessage()); + result.setSuccess(false); + } + return result; + } + + private String maskPassword(List command) { + if (command.size() < 2) { + return String.join(" ", command); + } + List masked = new ArrayList<>(command); + String credential = masked.get(1); + int slashIndex = credential.indexOf('/'); + int atIndex = credential.indexOf('@'); + if (slashIndex > 0 && atIndex > slashIndex) { + masked.set(1, credential.substring(0, slashIndex + 1) + "******" + credential.substring(atIndex)); + } + return String.join(" ", masked); + } + + @Data + public static class CommandResult { + private Boolean success; + private Integer exitCode; + private String command; + private String output; + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/DatabasePasswordComponent.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/DatabasePasswordComponent.java new file mode 100644 index 0000000..22823af --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/DatabasePasswordComponent.java @@ -0,0 +1,28 @@ +package com.njcn.gather.systemops.database.component; + +import cn.hutool.core.util.StrUtil; +import org.springframework.stereotype.Component; + +/** + * 数据库连接密码处理组件。 + */ +@Component +public class DatabasePasswordComponent { + + public String encrypt(String plainText) { + if (StrUtil.isBlank(plainText)) { + return null; + } + return plainText; + } + + /** + * 优先使用本次请求传入的临时密码;否则复用已保存的数据库密码。 + */ + public String resolveRuntimePassword(String passwordCipher, String temporaryPassword) { + if (StrUtil.isNotBlank(temporaryPassword)) { + return temporaryPassword; + } + return StrUtil.isBlank(passwordCipher) ? null : passwordCipher; + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/JdbcExportComponent.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/JdbcExportComponent.java new file mode 100644 index 0000000..f39a270 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/JdbcExportComponent.java @@ -0,0 +1,541 @@ +package com.njcn.gather.systemops.database.component; + +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.njcn.gather.systemops.database.constant.DatabaseOpsConst; +import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam; +import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.Statement; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.function.BooleanSupplier; + +/** + * JDBC 表数据导出与恢复组件。 + */ +@Component +@RequiredArgsConstructor +public class JdbcExportComponent { + + private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("^[A-Za-z][A-Za-z0-9_#$]*$"); + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private final ObjectMapper objectMapper; + + public void exportCsv(Connection jdbcConnection, String ownerName, DatabaseBackupParam.CreateParam param, + Path dataFilePath, Path metadataFilePath) throws Exception { + Files.createDirectories(dataFilePath.getParent()); + if (metadataFilePath.getParent() != null) { + Files.createDirectories(metadataFilePath.getParent()); + } + List metadataList = new ArrayList<>(); + try (BufferedWriter writer = Files.newBufferedWriter(dataFilePath, StandardCharsets.UTF_8)) { + for (String tableName : param.getTargetNames()) { + metadataList.add(exportTable(jdbcConnection, ownerName, tableName, param, writer)); + } + } + try (BufferedWriter metadataWriter = Files.newBufferedWriter(metadataFilePath, StandardCharsets.UTF_8)) { + objectMapper.writeValue(metadataWriter, metadataList); + } + } + + public void importCsv(Connection jdbcConnection, Path dataFilePath, Path metadataFilePath, String dbType, + String restoreMode, String targetOwnerName) throws Exception { + String metadataText = new String(Files.readAllBytes(metadataFilePath), StandardCharsets.UTF_8); + if (metadataText.trim().startsWith("{")) { + importCsvV2(jdbcConnection, metadataFilePath, dbType, restoreMode, targetOwnerName); + return; + } + List metadataList = Arrays.asList(objectMapper.readValue(metadataFilePath.toFile(), TableMetadata[].class)); + Map metadataMap = new LinkedHashMap<>(); + for (TableMetadata metadata : metadataList) { + metadataMap.put(metadata.getFullTableName(), metadata); + } + jdbcConnection.setAutoCommit(false); + try (BufferedReader reader = Files.newBufferedReader(dataFilePath, StandardCharsets.UTF_8)) { + try { + String line; + TableMetadata currentMetadata = null; + List currentColumns = null; + while ((line = reader.readLine()) != null) { + if (line.startsWith("-- TABLE ")) { + currentMetadata = metadataMap.get(line.substring("-- TABLE ".length()).trim()); + if (currentMetadata == null) { + throw new IllegalArgumentException("未找到表元数据:" + line); + } + currentColumns = null; + prepareTargetTable(jdbcConnection, currentMetadata, dbType, restoreMode, targetOwnerName); + continue; + } + if (currentMetadata == null) { + continue; + } + if (currentColumns == null) { + currentColumns = parseCsvLine(line); + continue; + } + List values = parseCsvLine(line); + insertRow(jdbcConnection, currentMetadata, currentColumns, values, dbType, restoreMode, targetOwnerName); + } + jdbcConnection.commit(); + } catch (Exception exception) { + jdbcConnection.rollback(); + throw exception; + } + } + } + + public ExportManifest exportMysqlCsvV2(Connection jdbcConnection, String databaseName, String taskNo, + DatabaseBackupParam.CreateParam param, Path backupDirectory, + Path metadataFilePath, int fetchSize, long maxPartBytes, + BooleanSupplier cancelled) throws Exception { + Files.createDirectories(backupDirectory); + ExportManifest manifest = new ExportManifest(); + manifest.setVersion(2); + manifest.setDbType("MYSQL"); + manifest.setBackupStrategy("JDBC_EXPORT"); + manifest.setTaskNo(taskNo); + manifest.setDatabaseName(databaseName); + List tableMetadataList = new ArrayList<>(); + manifest.setTables(tableMetadataList); + for (String tableName : param.getTargetNames()) { + checkCancelled(cancelled, backupDirectory); + tableMetadataList.add(exportMysqlTableV2(jdbcConnection, tableName, param, backupDirectory, taskNo, + fetchSize, maxPartBytes, cancelled)); + } + try (BufferedWriter metadataWriter = Files.newBufferedWriter(metadataFilePath, StandardCharsets.UTF_8)) { + objectMapper.writeValue(metadataWriter, manifest); + } + return manifest; + } + + private TableMetadata exportTable(Connection connection, String ownerName, String tableName, + DatabaseBackupParam.CreateParam param, BufferedWriter writer) throws Exception { + String normalizedOwner = normalizeOwner(ownerName); + String normalizedTable = normalizeMysqlIdentifier(tableName); + String fullTableName = buildFullTableName(normalizedOwner, normalizedTable); + String querySql = buildQuerySql(fullTableName, param); + TableMetadata metadata = new TableMetadata(); + metadata.setOwnerName(normalizedOwner); + metadata.setTableName(normalizedTable); + metadata.setFullTableName(fullTableName); + metadata.setTimeColumn(StrUtil.isBlank(param.getTimeColumn()) ? null : normalizeMysqlIdentifier(param.getTimeColumn())); + metadata.setStartTime(param.getStartTime() == null ? null : param.getStartTime().format(DATE_TIME_FORMATTER)); + metadata.setEndTime(param.getEndTime() == null ? null : param.getEndTime().format(DATE_TIME_FORMATTER)); + writer.write("-- TABLE " + fullTableName); + writer.newLine(); + try (PreparedStatement statement = connection.prepareStatement(querySql)) { + fillQueryParams(statement, param); + try (ResultSet resultSet = statement.executeQuery()) { + ResultSetMetaData resultSetMetaData = resultSet.getMetaData(); + int columnCount = resultSetMetaData.getColumnCount(); + List columnNames = new ArrayList<>(); + List columnTypes = new ArrayList<>(); + for (int i = 1; i <= columnCount; i++) { + String columnName = resultSetMetaData.getColumnName(i); + columnNames.add(normalizeMysqlIdentifier(columnName)); + columnTypes.add(resultSetMetaData.getColumnTypeName(i)); + if (i > 1) { + writer.write(","); + } + writer.write(escape(columnName)); + } + writer.newLine(); + long rowCount = 0L; + while (resultSet.next()) { + for (int i = 1; i <= columnCount; i++) { + if (i > 1) { + writer.write(","); + } + writer.write(escape(resultSet.getString(i))); + } + writer.newLine(); + rowCount++; + } + metadata.setColumnNames(columnNames); + metadata.setColumnTypes(columnTypes); + metadata.setRowCount(rowCount); + return metadata; + } + } + } + + private TableExportMetadata exportMysqlTableV2(Connection connection, String tableName, + DatabaseBackupParam.CreateParam param, Path backupDirectory, + String taskNo, int fetchSize, long maxPartBytes, + BooleanSupplier cancelled) throws Exception { + String normalizedTable = normalizeIdentifier(tableName); + String querySql = buildQuerySql(normalizedTable, param); + TableExportMetadata metadata = new TableExportMetadata(); + metadata.setTableName(normalizedTable); + metadata.setFullTableName(normalizedTable); + metadata.setTimeColumn(StrUtil.isBlank(param.getTimeColumn()) ? null : normalizeIdentifier(param.getTimeColumn())); + metadata.setStartTime(param.getStartTime() == null ? null : param.getStartTime().format(DATE_TIME_FORMATTER)); + metadata.setEndTime(param.getEndTime() == null ? null : param.getEndTime().format(DATE_TIME_FORMATTER)); + metadata.setColumns(new ArrayList<>()); + metadata.setParts(new ArrayList<>()); + try (PreparedStatement statement = connection.prepareStatement(querySql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) { + statement.setFetchSize(fetchSize); + fillQueryParams(statement, param); + try (ResultSet resultSet = statement.executeQuery()) { + ResultSetMetaData resultSetMetaData = resultSet.getMetaData(); + int columnCount = resultSetMetaData.getColumnCount(); + List columnNames = new ArrayList<>(); + for (int i = 1; i <= columnCount; i++) { + String columnName = resultSetMetaData.getColumnName(i); + columnNames.add(columnName); + ColumnMetadata columnMetadata = new ColumnMetadata(); + columnMetadata.setName(columnName); + columnMetadata.setType(resultSetMetaData.getColumnTypeName(i)); + metadata.getColumns().add(columnMetadata); + } + PartWriter partWriter = openPartWriter(backupDirectory, normalizedTable, taskNo, + metadata.getParts().size() + 1, columnNames); + metadata.getParts().add(partWriter.getPart()); + long totalRows = 0L; + try { + while (resultSet.next()) { + checkCancelled(cancelled, backupDirectory); + if (partWriter.shouldRotate(maxPartBytes)) { + partWriter.close(); + partWriter = openPartWriter(backupDirectory, normalizedTable, taskNo, + metadata.getParts().size() + 1, columnNames); + metadata.getParts().add(partWriter.getPart()); + } + partWriter.writeRow(resultSet, columnCount); + totalRows++; + } + } finally { + partWriter.close(); + } + metadata.setRowCount(totalRows); + return metadata; + } + } + } + + private String buildQuerySql(String fullTableName, DatabaseBackupParam.CreateParam param) { + StringBuilder sql = new StringBuilder("SELECT * FROM ").append(fullTableName); + if (param.getStartTime() != null && param.getEndTime() != null && StrUtil.isNotBlank(param.getTimeColumn())) { + sql.append(" WHERE ").append(normalizeIdentifier(param.getTimeColumn())).append(" BETWEEN ? AND ?"); + } + return sql.toString(); + } + + private void fillQueryParams(PreparedStatement statement, DatabaseBackupParam.CreateParam param) throws Exception { + if (param.getStartTime() != null && param.getEndTime() != null && StrUtil.isNotBlank(param.getTimeColumn())) { + statement.setString(1, param.getStartTime().format(DATE_TIME_FORMATTER)); + statement.setString(2, param.getEndTime().format(DATE_TIME_FORMATTER)); + } + } + + private void prepareTargetTable(Connection connection, TableMetadata metadata, String dbType, String restoreMode, + String targetOwnerName) throws Exception { + if (!"TRUNCATE".equalsIgnoreCase(restoreMode) + && !("REPLACE".equalsIgnoreCase(restoreMode) && !isMysql(dbType))) { + return; + } + String fullTargetName = buildTargetTableName(metadata, targetOwnerName); + try (Statement statement = connection.createStatement()) { + statement.execute("TRUNCATE TABLE " + fullTargetName); + } + } + + private void insertRow(Connection connection, TableMetadata metadata, List columns, + List values, String dbType, String restoreMode, String targetOwnerName) throws Exception { + String fullTargetName = buildTargetTableName(metadata, targetOwnerName); + StringBuilder placeholders = new StringBuilder(); + for (int i = 0; i < columns.size(); i++) { + if (i > 0) { + placeholders.append(","); + } + placeholders.append("?"); + } + String sql = buildInsertSql(dbType, restoreMode, fullTargetName, columns, placeholders.toString()); + try (PreparedStatement statement = connection.prepareStatement(sql)) { + for (int i = 0; i < columns.size(); i++) { + statement.setString(i + 1, i < values.size() ? values.get(i) : null); + } + statement.executeUpdate(); + } + } + + private String buildInsertSql(String dbType, String restoreMode, String fullTargetName, List columns, + String placeholders) { + String command = "INSERT INTO"; + if (isMysql(dbType) && "SKIP".equalsIgnoreCase(restoreMode)) { + // MySQL 跳过重复主键行,避免普通恢复因历史数据重复而整体失败。 + command = "INSERT IGNORE INTO"; + } else if (isMysql(dbType) && "REPLACE".equalsIgnoreCase(restoreMode)) { + command = "REPLACE INTO"; + } + return command + " " + fullTargetName + " (" + String.join(",", columns) + ") VALUES (" + placeholders + ")"; + } + + private void importCsvV2(Connection jdbcConnection, Path metadataFilePath, String dbType, String restoreMode, + String targetOwnerName) throws Exception { + ExportManifest manifest = objectMapper.readValue(metadataFilePath.toFile(), ExportManifest.class); + jdbcConnection.setAutoCommit(false); + try { + for (TableExportMetadata tableMetadata : manifest.getTables()) { + prepareTargetTable(jdbcConnection, toLegacyMetadata(tableMetadata), dbType, restoreMode, targetOwnerName); + for (FilePartMetadata part : tableMetadata.getParts()) { + importPart(jdbcConnection, metadataFilePath.getParent(), tableMetadata, part, dbType, restoreMode, targetOwnerName); + } + } + jdbcConnection.commit(); + } catch (Exception exception) { + jdbcConnection.rollback(); + throw exception; + } + } + + private void importPart(Connection jdbcConnection, Path backupDirectory, TableExportMetadata tableMetadata, + FilePartMetadata part, String dbType, String restoreMode, String targetOwnerName) throws Exception { + Path partPath = backupDirectory.resolve(part.getFileName()).normalize(); + if (!partPath.startsWith(backupDirectory.normalize())) { + throw new IllegalArgumentException("备份分片路径不在元数据目录内:" + part.getFileName()); + } + try (BufferedReader reader = Files.newBufferedReader(partPath, StandardCharsets.UTF_8)) { + List columns = null; + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("-- TABLE ")) { + continue; + } + if (columns == null) { + columns = parseCsvLine(line); + continue; + } + List values = parseCsvLine(line); + insertRow(jdbcConnection, toLegacyMetadata(tableMetadata), columns, values, dbType, restoreMode, targetOwnerName); + } + } + } + + private boolean isMysql(String dbType) { + return DatabaseOpsConst.DB_TYPE_MYSQL.equalsIgnoreCase(dbType); + } + + private PartWriter openPartWriter(Path backupDirectory, String tableName, String taskNo, int partIndex, + List columnNames) throws IOException { + String rawName = tableName.toLowerCase(Locale.ROOT) + "_part" + String.format("%03d", partIndex) + ".csv"; + String fileName = DatabaseFileNameUtil.appendTodayWithTask(rawName, taskNo); + Path filePath = backupDirectory.resolve(fileName).normalize(); + BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8); + writer.write("-- TABLE " + tableName); + writer.newLine(); + for (int i = 0; i < columnNames.size(); i++) { + if (i > 0) { + writer.write(","); + } + writer.write(escape(columnNames.get(i))); + } + writer.newLine(); + FilePartMetadata part = new FilePartMetadata(); + part.setFileName(fileName); + part.setFilePath(filePath.toString()); + part.setRowCount(0L); + part.setFileSize(0L); + return new PartWriter(writer, filePath, part); + } + + private TableMetadata toLegacyMetadata(TableExportMetadata metadata) { + TableMetadata legacy = new TableMetadata(); + legacy.setOwnerName(null); + legacy.setTableName(metadata.getTableName()); + legacy.setFullTableName(metadata.getFullTableName()); + legacy.setTimeColumn(metadata.getTimeColumn()); + legacy.setStartTime(metadata.getStartTime()); + legacy.setEndTime(metadata.getEndTime()); + legacy.setRowCount(metadata.getRowCount()); + List columnNames = new ArrayList<>(); + List columnTypes = new ArrayList<>(); + for (ColumnMetadata column : metadata.getColumns()) { + columnNames.add(column.getName()); + columnTypes.add(column.getType()); + } + legacy.setColumnNames(columnNames); + legacy.setColumnTypes(columnTypes); + return legacy; + } + + private void checkCancelled(BooleanSupplier cancelled, Path backupDirectory) { + if (cancelled != null && cancelled.getAsBoolean()) { + throw new IllegalStateException("备份任务已停止,已生成文件保留在:" + backupDirectory); + } + } + + private String buildTargetTableName(TableMetadata metadata, String targetOwnerName) { + String owner = normalizeOwner(StrUtil.blankToDefault(targetOwnerName, metadata.getOwnerName())); + return buildFullTableName(owner, metadata.getTableName()); + } + + private String buildFullTableName(String ownerName, String tableName) { + if (StrUtil.isBlank(ownerName)) { + return tableName; + } + return ownerName + "." + tableName; + } + + private String normalizeOwner(String ownerName) { + if (StrUtil.isBlank(ownerName)) { + return null; + } + return normalizeIdentifier(ownerName); + } + + private List parseCsvLine(String line) { + List result = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean quoted = false; + for (int i = 0; i < line.length(); i++) { + char currentChar = line.charAt(i); + if (currentChar == '"') { + if (quoted && i + 1 < line.length() && line.charAt(i + 1) == '"') { + current.append('"'); + i++; + } else { + quoted = !quoted; + } + continue; + } + if (currentChar == ',' && !quoted) { + result.add(current.toString()); + current.setLength(0); + continue; + } + current.append(currentChar); + } + result.add(current.toString()); + return result; + } + + private String escape(String value) { + if (value == null) { + return ""; + } + return "\"" + value.replace("\"", "\"\"") + "\""; + } + + private String normalizeIdentifier(String value) { + if (value == null || !IDENTIFIER_PATTERN.matcher(value).matches()) { + throw new IllegalArgumentException("数据库对象名称格式不正确:" + value); + } + return value.trim().toUpperCase(Locale.ROOT); + } + + private String normalizeMysqlIdentifier(String value) { + if (value == null || !IDENTIFIER_PATTERN.matcher(value).matches()) { + throw new IllegalArgumentException("数据库对象名称格式不正确:" + value); + } + return value.trim(); + } + + @Data + public static class TableMetadata { + private String ownerName; + private String tableName; + private String fullTableName; + private List columnNames; + private List columnTypes; + private String timeColumn; + private String startTime; + private String endTime; + private Long rowCount; + } + + @Data + public static class ExportManifest { + private Integer version; + private String dbType; + private String backupStrategy; + private String taskNo; + private String databaseName; + private List tables; + } + + @Data + public static class TableExportMetadata { + private String tableName; + private String fullTableName; + private String timeColumn; + private String startTime; + private String endTime; + private List columns; + private Long rowCount; + private List parts; + } + + @Data + public static class ColumnMetadata { + private String name; + private String type; + } + + @Data + public static class FilePartMetadata { + private String fileName; + private String filePath; + private Long rowCount; + private Long fileSize; + } + + private class PartWriter { + private final BufferedWriter writer; + private final Path filePath; + private final FilePartMetadata part; + + private PartWriter(BufferedWriter writer, Path filePath, FilePartMetadata part) { + this.writer = writer; + this.filePath = filePath; + this.part = part; + } + + private FilePartMetadata getPart() { + return part; + } + + private boolean shouldRotate(long maxPartBytes) throws IOException { + writer.flush(); + return part.getRowCount() > 0 && Files.size(filePath) >= maxPartBytes; + } + + private void writeRow(ResultSet resultSet, int columnCount) throws Exception { + for (int i = 1; i <= columnCount; i++) { + if (i > 1) { + writer.write(","); + } + writer.write(escape(resultSet.getString(i))); + } + writer.newLine(); + part.setRowCount(part.getRowCount() + 1); + } + + private void close() throws IOException { + writer.close(); + part.setFileSize(Files.exists(filePath) ? Files.size(filePath) : 0L); + } + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/OracleJdbcComponent.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/OracleJdbcComponent.java new file mode 100644 index 0000000..c5cb533 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/OracleJdbcComponent.java @@ -0,0 +1,89 @@ +package com.njcn.gather.systemops.database.component; + +import cn.hutool.core.util.StrUtil; +import com.njcn.gather.systemops.database.constant.DatabaseOpsConst; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTableVO; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTestResultVO; +import org.springframework.stereotype.Component; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Oracle JDBC 连接与元数据探测组件。 + */ +@Component +public class OracleJdbcComponent { + + public DatabaseTestResultVO test(DatabaseConnection connection, String password) { + DatabaseTestResultVO result = new DatabaseTestResultVO(); + try (Connection ignored = openConnection(connection, password)) { + result.setSuccess(true); + result.setMessage("连接成功"); + } catch (Exception exception) { + result.setSuccess(false); + result.setMessage(exception.getMessage()); + } + return result; + } + + public List listTables(DatabaseConnection connection, String password, String schemaName) throws Exception { + String owner = StrUtil.blankToDefault(schemaName, connection.getSchemaName()); + if (StrUtil.isBlank(owner)) { + owner = connection.getUsername(); + } + owner = owner.trim().toUpperCase(Locale.ROOT); + String sql = "SELECT t.owner, t.table_name, t.num_rows, o.last_ddl_time, c.comments " + + "FROM all_tables t " + + "LEFT JOIN all_tab_comments c " + + "ON t.owner = c.owner AND t.table_name = c.table_name " + + "LEFT JOIN all_objects o " + + "ON t.owner = o.owner AND t.table_name = o.object_name AND o.object_type = 'TABLE' " + + "WHERE t.owner = ? ORDER BY t.table_name"; + try (Connection jdbcConnection = openConnection(connection, password); + PreparedStatement statement = jdbcConnection.prepareStatement(sql)) { + statement.setString(1, owner); + try (ResultSet resultSet = statement.executeQuery()) { + List result = new ArrayList<>(); + while (resultSet.next()) { + DatabaseTableVO table = new DatabaseTableVO(); + table.setOwner(resultSet.getString("owner")); + table.setTableName(resultSet.getString("table_name")); + table.setEngine(DatabaseOpsConst.DB_TYPE_ORACLE); + table.setTableRows(getLongValue(resultSet, "num_rows")); + Timestamp updateTime = resultSet.getTimestamp("last_ddl_time"); + table.setUpdateTime(updateTime == null ? null : updateTime.toLocalDateTime()); + table.setComments(resultSet.getString("comments")); + result.add(table); + } + return result; + } + } + } + + private Long getLongValue(ResultSet resultSet, String columnName) throws Exception { + long value = resultSet.getLong(columnName); + return resultSet.wasNull() ? null : value; + } + + public Connection openConnection(DatabaseConnection connection, String password) throws Exception { + if (StrUtil.isBlank(password)) { + throw new IllegalArgumentException("数据库密码不能为空"); + } + return DriverManager.getConnection(buildJdbcUrl(connection), connection.getUsername(), password); + } + + public String buildJdbcUrl(DatabaseConnection connection) { + if (DatabaseOpsConst.CONNECT_TYPE_SID.equalsIgnoreCase(connection.getConnectType())) { + return "jdbc:oracle:thin:@" + connection.getHost() + ":" + connection.getPort() + ":" + connection.getSid(); + } + return "jdbc:oracle:thin:@//" + connection.getHost() + ":" + connection.getPort() + "/" + connection.getServiceName(); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/config/DbmsExecutorConfig.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/config/DbmsExecutorConfig.java new file mode 100644 index 0000000..5fc486a --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/config/DbmsExecutorConfig.java @@ -0,0 +1,37 @@ +package com.njcn.gather.systemops.database.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 数据库运维后台任务线程池。 + */ +@Slf4j +@Configuration +public class DbmsExecutorConfig { + + @Bean(name = "dbmsTaskExecutorService", destroyMethod = "shutdown") + public ExecutorService dbmsTaskExecutorService() { + AtomicInteger threadIndex = new AtomicInteger(1); + return new ThreadPoolExecutor( + 1, + 1, + 30, + TimeUnit.SECONDS, + new LinkedBlockingQueue(8), + runnable -> { + Thread thread = new Thread(runnable); + thread.setName("dbms-task-" + threadIndex.getAndIncrement()); + return thread; + }, + (runnable, executor) -> log.warn("数据库运维任务线程池已满,拒绝新的任务") + ); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/config/DbmsProperties.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/config/DbmsProperties.java new file mode 100644 index 0000000..59324d0 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/config/DbmsProperties.java @@ -0,0 +1,30 @@ +package com.njcn.gather.systemops.database.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 数据库运维配置。 + */ +@Data +@Component +@ConfigurationProperties(prefix = "dbms") +public class DbmsProperties { + + private Backup backup = new Backup(); + private Tools tools = new Tools(); + + @Data + public static class Backup { + private String storagePath = "D:/dbms-backup"; + private Integer defaultMaxFileSizeMb = 512; + private Integer mysqlFetchSize = 1000; + } + + @Data + public static class Tools { + private String expdpPath; + private String impdpPath; + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/constant/DatabaseOpsConst.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/constant/DatabaseOpsConst.java new file mode 100644 index 0000000..41322ce --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/constant/DatabaseOpsConst.java @@ -0,0 +1,21 @@ +package com.njcn.gather.systemops.database.constant; + +/** + * 数据库运维常量。 + */ +public final class DatabaseOpsConst { + + public static final String DB_TYPE_ORACLE = "ORACLE"; + public static final String DB_TYPE_MYSQL = "MYSQL"; + public static final String CONNECT_TYPE_SERVICE_NAME = "SERVICE_NAME"; + public static final String CONNECT_TYPE_SID = "SID"; + public static final String CONFIRM_DELETE = "确认删除"; + public static final String CONFIRM_OVERWRITE = "确认覆盖"; + public static final int STATE_DELETED = 0; + public static final int STATE_ENABLED = 1; + public static final int SAVE_PASSWORD_YES = 1; + public static final int SAVE_PASSWORD_NO = 0; + + private DatabaseOpsConst() { + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/controller/DatabaseBackupController.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/controller/DatabaseBackupController.java new file mode 100644 index 0000000..092c59a --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/controller/DatabaseBackupController.java @@ -0,0 +1,87 @@ +package com.njcn.gather.systemops.database.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.constant.OperateType; +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.gather.systemops.database.pojo.param.DatabaseBackupParam; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseBackupFileVO; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTaskCreateVO; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTaskVO; +import com.njcn.gather.systemops.database.service.DatabaseBackupFileService; +import com.njcn.gather.systemops.database.service.DatabaseOperationTaskService; +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 org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 数据库备份接口。 + */ +@Api(tags = "数据库备份") +@RestController +@RequestMapping("/database/backups") +@RequiredArgsConstructor +public class DatabaseBackupController extends BaseController { + + private final DatabaseOperationTaskService databaseOperationTaskService; + private final DatabaseBackupFileService databaseBackupFileService; + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD) + @ApiOperation("创建备份任务") + @PostMapping("/create") + public HttpResult create(@RequestBody @Validated DatabaseBackupParam.CreateParam param) { + String methodDescribe = getMethodDescribe("create"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseOperationTaskService.createBackupTask(param), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询备份任务") + @PostMapping("/tasks/list") + public HttpResult> listTasks(@RequestBody @Validated DatabaseBackupParam.TaskQueryParam param) { + String methodDescribe = getMethodDescribe("listTasks"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseOperationTaskService.listBackupTasks(param), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询任务状态") + @GetMapping("/tasks/status") + public HttpResult status(@RequestParam("taskId") String taskId) { + String methodDescribe = getMethodDescribe("status"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseOperationTaskService.getStatus(taskId), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE) + @ApiOperation("停止备份任务") + @PostMapping("/tasks/stop") + public HttpResult stop(@RequestBody @Validated DatabaseBackupParam.StopParam param) { + String methodDescribe = getMethodDescribe("stop"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseOperationTaskService.stopBackupTask(param), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD) + @ApiOperation("重新开始备份任务") + @PostMapping("/tasks/restart") + public HttpResult restart(@RequestBody @Validated DatabaseBackupParam.RestartParam param) { + String methodDescribe = getMethodDescribe("restart"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseOperationTaskService.restartBackupTask(param), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询备份文件") + @PostMapping("/files/list") + public HttpResult> listFiles(@RequestBody @Validated DatabaseBackupParam.FileQueryParam param) { + String methodDescribe = getMethodDescribe("listFiles"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseBackupFileService.listFiles(param), methodDescribe); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/controller/DatabaseConnectionController.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/controller/DatabaseConnectionController.java new file mode 100644 index 0000000..6330671 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/controller/DatabaseConnectionController.java @@ -0,0 +1,88 @@ +package com.njcn.gather.systemops.database.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.constant.OperateType; +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.gather.systemops.database.pojo.param.DatabaseConnectionParam; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseConnectionVO; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTableVO; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTestResultVO; +import com.njcn.gather.systemops.database.service.DatabaseConnectionService; +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 org.springframework.validation.annotation.Validated; +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; + +import java.util.List; + +/** + * 数据库连接配置接口。 + */ +@Api(tags = "数据库连接配置") +@RestController +@RequestMapping("/database/connections") +@RequiredArgsConstructor +public class DatabaseConnectionController extends BaseController { + + private final DatabaseConnectionService databaseConnectionService; + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询数据库连接配置") + @PostMapping("/list") + public HttpResult> list(@RequestBody @Validated DatabaseConnectionParam.QueryParam param) { + String methodDescribe = getMethodDescribe("list"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseConnectionService.listConnections(param), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD) + @ApiOperation("新增数据库连接配置") + @PostMapping("/add") + public HttpResult add(@RequestBody @Validated DatabaseConnectionParam param) { + String methodDescribe = getMethodDescribe("add"); + boolean result = databaseConnectionService.addConnection(param); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE) + @ApiOperation("修改数据库连接配置") + @PostMapping("/update") + public HttpResult update(@RequestBody @Validated DatabaseConnectionParam.UpdateParam param) { + String methodDescribe = getMethodDescribe("update"); + boolean result = databaseConnectionService.updateConnection(param); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DELETE) + @ApiOperation("删除数据库连接配置") + @PostMapping("/delete") + public HttpResult delete(@RequestBody @Validated DatabaseConnectionParam.DeleteParam param) { + String methodDescribe = getMethodDescribe("delete"); + boolean result = databaseConnectionService.deleteConnection(param); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("测试数据库连接") + @PostMapping("/test") + public HttpResult test(@RequestBody @Validated DatabaseConnectionParam.TestParam param) { + String methodDescribe = getMethodDescribe("test"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseConnectionService.testConnection(param), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询 Oracle 表列表") + @PostMapping("/tables") + public HttpResult> tables(@RequestBody @Validated DatabaseConnectionParam.TablesParam param) { + String methodDescribe = getMethodDescribe("tables"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseConnectionService.listTables(param), methodDescribe); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/controller/DatabaseDeleteController.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/controller/DatabaseDeleteController.java new file mode 100644 index 0000000..78bee9e --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/controller/DatabaseDeleteController.java @@ -0,0 +1,51 @@ +package com.njcn.gather.systemops.database.controller; + +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.constant.OperateType; +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.gather.systemops.database.pojo.param.DatabaseDeleteParam; +import com.njcn.gather.systemops.database.service.DatabaseBackupFileService; +import com.njcn.gather.systemops.database.service.DatabaseOperationTaskService; +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 org.springframework.validation.annotation.Validated; +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; + +/** + * 数据库运维删除接口。 + */ +@Api(tags = "数据库运维删除") +@RestController +@RequestMapping("/database/delete") +@RequiredArgsConstructor +public class DatabaseDeleteController extends BaseController { + + private final DatabaseBackupFileService databaseBackupFileService; + private final DatabaseOperationTaskService databaseOperationTaskService; + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DELETE) + @ApiOperation("删除备份文件") + @PostMapping("/backup-file") + public HttpResult deleteBackupFile(@RequestBody @Validated DatabaseDeleteParam.BackupFileParam param) { + String methodDescribe = getMethodDescribe("deleteBackupFile"); + boolean result = databaseBackupFileService.deleteBackupFile(param.getBackupFileId(), param.getConfirmText()); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DELETE) + @ApiOperation("删除任务记录") + @PostMapping("/task") + public HttpResult deleteTask(@RequestBody @Validated DatabaseDeleteParam.TaskParam param) { + String methodDescribe = getMethodDescribe("deleteTask"); + boolean result = databaseOperationTaskService.deleteTask(param.getTaskId(), param.getConfirmText()); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, result, methodDescribe); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/controller/DatabaseRestoreController.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/controller/DatabaseRestoreController.java new file mode 100644 index 0000000..7add150 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/controller/DatabaseRestoreController.java @@ -0,0 +1,53 @@ +package com.njcn.gather.systemops.database.controller; + +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.constant.OperateType; +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.gather.systemops.database.pojo.param.DatabaseRestoreParam; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTaskCreateVO; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTaskVO; +import com.njcn.gather.systemops.database.service.DatabaseOperationTaskService; +import com.njcn.gather.systemops.database.service.DatabaseRestoreService; +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 org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 数据库恢复接口。 + */ +@Api(tags = "数据库恢复") +@RestController +@RequestMapping("/database/restores") +@RequiredArgsConstructor +public class DatabaseRestoreController extends BaseController { + + private final DatabaseRestoreService databaseRestoreService; + private final DatabaseOperationTaskService databaseOperationTaskService; + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD) + @ApiOperation("创建恢复任务") + @PostMapping("/create") + public HttpResult create(@RequestBody @Validated DatabaseRestoreParam.CreateParam param) { + String methodDescribe = getMethodDescribe("create"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseRestoreService.createRestoreTask(param), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询恢复任务状态") + @GetMapping("/tasks/status") + public HttpResult status(@RequestParam("taskId") String taskId) { + String methodDescribe = getMethodDescribe("status"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseOperationTaskService.getStatus(taskId), methodDescribe); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/mapper/DatabaseBackupFileMapper.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/mapper/DatabaseBackupFileMapper.java new file mode 100644 index 0000000..b7539ea --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/mapper/DatabaseBackupFileMapper.java @@ -0,0 +1,10 @@ +package com.njcn.gather.systemops.database.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; + +/** + * 数据库备份文件 Mapper。 + */ +public interface DatabaseBackupFileMapper extends BaseMapper { +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/mapper/DatabaseConnectionMapper.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/mapper/DatabaseConnectionMapper.java new file mode 100644 index 0000000..48addbc --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/mapper/DatabaseConnectionMapper.java @@ -0,0 +1,10 @@ +package com.njcn.gather.systemops.database.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; + +/** + * 数据库连接配置 Mapper。 + */ +public interface DatabaseConnectionMapper extends BaseMapper { +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/mapper/DatabaseOperationTaskMapper.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/mapper/DatabaseOperationTaskMapper.java new file mode 100644 index 0000000..83fa09b --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/mapper/DatabaseOperationTaskMapper.java @@ -0,0 +1,10 @@ +package com.njcn.gather.systemops.database.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; + +/** + * 数据库运维任务 Mapper。 + */ +public interface DatabaseOperationTaskMapper extends BaseMapper { +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/mapper/DatabaseRestoreRecordMapper.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/mapper/DatabaseRestoreRecordMapper.java new file mode 100644 index 0000000..99b2e33 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/mapper/DatabaseRestoreRecordMapper.java @@ -0,0 +1,10 @@ +package com.njcn.gather.systemops.database.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njcn.gather.systemops.database.pojo.po.DatabaseRestoreRecord; + +/** + * 数据库恢复记录 Mapper。 + */ +public interface DatabaseRestoreRecordMapper extends BaseMapper { +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/BackupModeEnum.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/BackupModeEnum.java new file mode 100644 index 0000000..9671cc6 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/BackupModeEnum.java @@ -0,0 +1,10 @@ +package com.njcn.gather.systemops.database.pojo.enums; + +/** + * 备份模式。 + */ +public enum BackupModeEnum { + FULL_TABLE, + TIME_RANGE, + SIZE_SPLIT +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/BackupStrategyEnum.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/BackupStrategyEnum.java new file mode 100644 index 0000000..6d51df7 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/BackupStrategyEnum.java @@ -0,0 +1,9 @@ +package com.njcn.gather.systemops.database.pojo.enums; + +/** + * 备份策略。 + */ +public enum BackupStrategyEnum { + DATA_PUMP, + JDBC_EXPORT +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/FileFormatEnum.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/FileFormatEnum.java new file mode 100644 index 0000000..62ac2b2 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/FileFormatEnum.java @@ -0,0 +1,10 @@ +package com.njcn.gather.systemops.database.pojo.enums; + +/** + * 备份文件格式。 + */ +public enum FileFormatEnum { + DMP, + SQL, + CSV +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/OperationTypeEnum.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/OperationTypeEnum.java new file mode 100644 index 0000000..74a02c8 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/OperationTypeEnum.java @@ -0,0 +1,10 @@ +package com.njcn.gather.systemops.database.pojo.enums; + +/** + * 数据库运维操作类型。 + */ +public enum OperationTypeEnum { + BACKUP, + RESTORE, + DELETE +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/RestoreModeEnum.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/RestoreModeEnum.java new file mode 100644 index 0000000..65c3c82 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/RestoreModeEnum.java @@ -0,0 +1,11 @@ +package com.njcn.gather.systemops.database.pojo.enums; + +/** + * 恢复模式。 + */ +public enum RestoreModeEnum { + SKIP, + APPEND, + TRUNCATE, + REPLACE +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/TaskStatusEnum.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/TaskStatusEnum.java new file mode 100644 index 0000000..f132981 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/TaskStatusEnum.java @@ -0,0 +1,12 @@ +package com.njcn.gather.systemops.database.pojo.enums; + +/** + * 运维任务状态。 + */ +public enum TaskStatusEnum { + WAITING, + RUNNING, + SUCCESS, + FAIL, + CANCELLED +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseBackupParam.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseBackupParam.java new file mode 100644 index 0000000..00e6eb9 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseBackupParam.java @@ -0,0 +1,85 @@ +package com.njcn.gather.systemops.database.pojo.param; + +import com.njcn.web.pojo.param.BaseParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 数据库备份参数。 + */ +public class DatabaseBackupParam { + + @Data + @ApiModel("创建备份任务参数") + public static class CreateParam { + @ApiModelProperty("连接 ID") + @NotBlank(message = "连接 ID 不能为空") + private String connectionId; + @ApiModelProperty("备份策略:DATA_PUMP、JDBC_EXPORT,默认 DATA_PUMP") + private String backupStrategy; + @ApiModelProperty("Schema") + private String schemaName; + @ApiModelProperty("表名列表") + private List targetNames; + @ApiModelProperty("备份模式:FULL_TABLE、TIME_RANGE、SIZE_SPLIT") + private String backupMode; + @ApiModelProperty("时间字段") + private String timeColumn; + @ApiModelProperty("开始时间") + private LocalDateTime startTime; + @ApiModelProperty("结束时间") + private LocalDateTime endTime; + @ApiModelProperty("最大文件大小 MB") + private Integer maxFileSizeMb; + @ApiModelProperty("Oracle Directory 名称") + private String directoryName; + @ApiModelProperty("临时密码,不保存密码时传入") + private String temporaryPassword; + } + + @Data + @EqualsAndHashCode(callSuper = true) + @ApiModel("备份任务查询参数") + public static class TaskQueryParam extends BaseParam { + @ApiModelProperty("连接 ID") + private String connectionId; + @ApiModelProperty("任务状态") + private String taskStatus; + } + + @Data + @EqualsAndHashCode(callSuper = true) + @ApiModel("备份文件查询参数") + public static class FileQueryParam extends BaseParam { + @ApiModelProperty("连接 ID") + private String connectionId; + @ApiModelProperty("任务 ID") + private String taskId; + @ApiModelProperty("备份策略") + private String backupStrategy; + } + + @Data + @ApiModel("停止备份任务参数") + public static class StopParam { + @ApiModelProperty("备份任务 ID") + @NotBlank(message = "备份任务 ID 不能为空") + private String taskId; + } + + @Data + @ApiModel("重新开始备份任务参数") + public static class RestartParam { + @ApiModelProperty("备份任务 ID") + @NotBlank(message = "备份任务 ID 不能为空") + private String taskId; + @ApiModelProperty("临时密码,原连接未保存密码时传入") + private String temporaryPassword; + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseConnectionParam.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseConnectionParam.java new file mode 100644 index 0000000..b640370 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseConnectionParam.java @@ -0,0 +1,130 @@ +package com.njcn.gather.systemops.database.pojo.param; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.njcn.web.pojo.param.BaseParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * 数据库连接配置参数。 + */ +@Data +@ApiModel("数据库连接配置参数") +public class DatabaseConnectionParam { + + @ApiModelProperty("连接名称") + @NotBlank(message = "连接名称不能为空") + private String connectionName; + + @ApiModelProperty("数据库类型:ORACLE、MYSQL") + private String dbType; + + @ApiModelProperty("数据库主机地址") + @NotBlank(message = "数据库主机地址不能为空") + private String host; + + @ApiModelProperty("数据库端口") + @NotNull(message = "数据库端口不能为空") + private Integer port; + + @ApiModelProperty("连接类型:SERVICE_NAME、SID") + private String connectType; + + @ApiModelProperty("服务名") + private String serviceName; + + @ApiModelProperty("SID") + private String sid; + + @ApiModelProperty("数据库名,MySQL 使用") + private String databaseName; + + @ApiModelProperty("Schema") + private String schemaName; + + @ApiModelProperty("用户名") + @NotBlank(message = "用户名不能为空") + private String username; + + @ApiModelProperty("密码") + private String password; + + @ApiModelProperty("是否保存密码:0-否,1-是") + private Integer savePassword; + + @ApiModelProperty("Oracle Directory 名称") + private String directoryName; + + @ApiModelProperty("Oracle Directory 物理路径") + private String directoryPath; + + @ApiModelProperty("扩展配置 JSON") + private String extraConfigJson; + + @ApiModelProperty("备注") + private String remark; + + @Data + @EqualsAndHashCode(callSuper = true) + @ApiModel("数据库连接更新参数") + public static class UpdateParam extends DatabaseConnectionParam { + @ApiModelProperty("连接 ID") + @NotBlank(message = "连接 ID 不能为空") + private String id; + } + + @Data + @EqualsAndHashCode(callSuper = true) + @ApiModel("数据库连接查询参数") + public static class QueryParam extends BaseParam { + @ApiModelProperty("连接名称") + private String connectionName; + @ApiModelProperty("数据库类型") + private String dbType; + @ApiModelProperty("Schema") + private String schemaName; + } + + @Data + @ApiModel("数据库连接删除参数") + public static class DeleteParam { + @ApiModelProperty("连接 ID") + @NotBlank(message = "连接 ID 不能为空") + private String id; + } + + @Data + @ApiModel("数据库连接测试参数") + public static class TestParam { + @ApiModelProperty("连接 ID,已有连接测试时传入") + private String connectionId; + @ApiModelProperty("临时连接参数,新增前测试时传入") + private DatabaseConnectionParam connection; + @ApiModelProperty("临时密码,测试时允许只传该字段而不写入 connection.password") + private String temporaryPassword; + } + + @Data + @ApiModel("数据库表查询参数") + public static class TablesParam { + @ApiModelProperty("连接 ID") + @NotBlank(message = "连接 ID 不能为空") + private String connectionId; + @ApiModelProperty("临时密码,不保存密码时传入") + private String temporaryPassword; + @ApiModelProperty("兼容前端传入的运行时密码;为空时复用数据库 password_cipher") + private String password; + @JsonAlias("password_cipher") + @ApiModelProperty("兼容前端传入的已保存密码") + private String passwordCipher; + @ApiModelProperty("兼容前端传入的临时连接参数") + private DatabaseConnectionParam connection; + @ApiModelProperty("Schema 或数据库名,不传则使用连接默认值") + private String schemaName; + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseDeleteParam.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseDeleteParam.java new file mode 100644 index 0000000..e094c9b --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseDeleteParam.java @@ -0,0 +1,33 @@ +package com.njcn.gather.systemops.database.pojo.param; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +/** + * 数据库运维删除参数。 + */ +public class DatabaseDeleteParam { + + @Data + @ApiModel("删除备份文件参数") + public static class BackupFileParam { + @ApiModelProperty("备份文件 ID") + @NotBlank(message = "备份文件 ID 不能为空") + private String backupFileId; + @ApiModelProperty("确认文案") + private String confirmText; + } + + @Data + @ApiModel("删除任务参数") + public static class TaskParam { + @ApiModelProperty("任务 ID") + @NotBlank(message = "任务 ID 不能为空") + private String taskId; + @ApiModelProperty("确认文案") + private String confirmText; + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseRestoreParam.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseRestoreParam.java new file mode 100644 index 0000000..bbc93a3 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseRestoreParam.java @@ -0,0 +1,32 @@ +package com.njcn.gather.systemops.database.pojo.param; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +/** + * 数据库恢复参数。 + */ +public class DatabaseRestoreParam { + + @Data + @ApiModel("创建恢复任务参数") + public static class CreateParam { + @ApiModelProperty("目标连接 ID") + @NotBlank(message = "连接 ID 不能为空") + private String connectionId; + @ApiModelProperty("备份文件 ID") + @NotBlank(message = "备份文件 ID 不能为空") + private String backupFileId; + @ApiModelProperty("恢复模式:SKIP、APPEND、TRUNCATE、REPLACE") + private String restoreMode; + @ApiModelProperty("目标 Schema") + private String targetSchemaName; + @ApiModelProperty("临时密码,不保存密码时传入") + private String temporaryPassword; + @ApiModelProperty("覆盖确认文案") + private String overwriteConfirmText; + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/po/DatabaseBackupFile.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/po/DatabaseBackupFile.java new file mode 100644 index 0000000..e1d3ec9 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/po/DatabaseBackupFile.java @@ -0,0 +1,71 @@ +package com.njcn.gather.systemops.database.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 数据库备份文件记录。 + */ +@Data +@TableName("dbms_backup_file") +public class DatabaseBackupFile implements Serializable { + private static final long serialVersionUID = 3119981982091873277L; + + @TableId("id") + private String id; + @TableField("task_id") + private String taskId; + @TableField("connection_id") + private String connectionId; + @TableField("db_type") + private String dbType; + @TableField("backup_strategy") + private String backupStrategy; + @TableField("file_format") + private String fileFormat; + @TableField("schema_name") + private String schemaName; + @TableField("target_names_json") + private String targetNamesJson; + @TableField("backup_mode") + private String backupMode; + @TableField("backup_start_time") + private LocalDateTime backupStartTime; + @TableField("backup_end_time") + private LocalDateTime backupEndTime; + @TableField("time_column") + private String timeColumn; + @TableField("directory_name") + private String directoryName; + @TableField("dump_file_name") + private String dumpFileName; + @TableField("log_file_name") + private String logFileName; + @TableField("file_name") + private String fileName; + @TableField("file_path") + private String filePath; + @TableField("log_file_path") + private String logFilePath; + @TableField("metadata_file_path") + private String metadataFilePath; + @TableField("file_size") + private Long fileSize; + @TableField("checksum") + private String checksum; + @TableField("state") + private Integer state; + @TableField("create_by") + private String createBy; + @TableField("create_time") + private LocalDateTime createTime; + @TableField("update_by") + private String updateBy; + @TableField("update_time") + private LocalDateTime updateTime; +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/po/DatabaseConnection.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/po/DatabaseConnection.java new file mode 100644 index 0000000..5da7c57 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/po/DatabaseConnection.java @@ -0,0 +1,69 @@ +package com.njcn.gather.systemops.database.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 数据库连接配置。 + */ +@Data +@TableName("dbms_connection") +public class DatabaseConnection implements Serializable { + private static final long serialVersionUID = -5821519248914313778L; + + @TableId("id") + private String id; + @TableField("connection_name") + private String connectionName; + @TableField("db_type") + private String dbType; + @TableField("host") + private String host; + @TableField("port") + private Integer port; + @TableField("connect_type") + private String connectType; + @TableField("service_name") + private String serviceName; + @TableField("sid") + private String sid; + @TableField("database_name") + private String databaseName; + @TableField("schema_name") + private String schemaName; + @TableField("username") + private String username; + @TableField("password_cipher") + private String passwordCipher; + @TableField("save_password") + private Integer savePassword; + @TableField("directory_name") + private String directoryName; + @TableField("directory_path") + private String directoryPath; + @TableField("extra_config_json") + private String extraConfigJson; + @TableField("remark") + private String remark; + @TableField("last_test_status") + private String lastTestStatus; + @TableField("last_test_message") + private String lastTestMessage; + @TableField("last_test_time") + private LocalDateTime lastTestTime; + @TableField("state") + private Integer state; + @TableField("create_by") + private String createBy; + @TableField("create_time") + private LocalDateTime createTime; + @TableField("update_by") + private String updateBy; + @TableField("update_time") + private LocalDateTime updateTime; +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/po/DatabaseOperationTask.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/po/DatabaseOperationTask.java new file mode 100644 index 0000000..6c4cdfd --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/po/DatabaseOperationTask.java @@ -0,0 +1,58 @@ +package com.njcn.gather.systemops.database.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 数据库运维任务。 + */ +@Data +@TableName("dbms_operation_task") +public class DatabaseOperationTask implements Serializable { + private static final long serialVersionUID = 1831235987236858769L; + + @TableId("id") + private String id; + @TableField("task_no") + private String taskNo; + @TableField("connection_id") + private String connectionId; + @TableField("db_type") + private String dbType; + @TableField("operation_type") + private String operationType; + @TableField("backup_strategy") + private String backupStrategy; + @TableField("task_status") + private String taskStatus; + @TableField("schema_name") + private String schemaName; + @TableField("target_names_json") + private String targetNamesJson; + @TableField("request_param_json") + private String requestParamJson; + @TableField("result_message") + private String resultMessage; + @TableField("progress_percent") + private BigDecimal progressPercent; + @TableField("started_at") + private LocalDateTime startedAt; + @TableField("finished_at") + private LocalDateTime finishedAt; + @TableField("state") + private Integer state; + @TableField("create_by") + private String createBy; + @TableField("create_time") + private LocalDateTime createTime; + @TableField("update_by") + private String updateBy; + @TableField("update_time") + private LocalDateTime updateTime; +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/po/DatabaseRestoreRecord.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/po/DatabaseRestoreRecord.java new file mode 100644 index 0000000..c247e76 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/po/DatabaseRestoreRecord.java @@ -0,0 +1,51 @@ +package com.njcn.gather.systemops.database.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 数据库恢复记录。 + */ +@Data +@TableName("dbms_restore_record") +public class DatabaseRestoreRecord implements Serializable { + private static final long serialVersionUID = -5638979151924581277L; + + @TableId("id") + private String id; + @TableField("task_id") + private String taskId; + @TableField("backup_file_id") + private String backupFileId; + @TableField("connection_id") + private String connectionId; + @TableField("db_type") + private String dbType; + @TableField("restore_mode") + private String restoreMode; + @TableField("target_schema_name") + private String targetSchemaName; + @TableField("target_names_json") + private String targetNamesJson; + @TableField("table_exists_action") + private String tableExistsAction; + @TableField("overwrite_confirmed") + private Integer overwriteConfirmed; + @TableField("result_message") + private String resultMessage; + @TableField("state") + private Integer state; + @TableField("create_by") + private String createBy; + @TableField("create_time") + private LocalDateTime createTime; + @TableField("update_by") + private String updateBy; + @TableField("update_time") + private LocalDateTime updateTime; +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseBackupFileVO.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseBackupFileVO.java new file mode 100644 index 0000000..00b8aaa --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseBackupFileVO.java @@ -0,0 +1,29 @@ +package com.njcn.gather.systemops.database.pojo.vo; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 数据库备份文件响应。 + */ +@Data +public class DatabaseBackupFileVO { + private String id; + private String taskId; + private String connectionId; + private String dbType; + private String backupStrategy; + private String fileFormat; + private String schemaName; + private String targetNamesJson; + private String backupMode; + private String fileName; + private String filePath; + private String logFileName; + private String logFilePath; + private Long fileSize; + private String checksum; + private Integer state; + private LocalDateTime createTime; +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseConnectionVO.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseConnectionVO.java new file mode 100644 index 0000000..a7a6f8d --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseConnectionVO.java @@ -0,0 +1,34 @@ +package com.njcn.gather.systemops.database.pojo.vo; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 数据库连接配置响应。 + */ +@Data +public class DatabaseConnectionVO { + private String id; + private String connectionName; + private String dbType; + private String host; + private Integer port; + private String connectType; + private String serviceName; + private String sid; + private String databaseName; + private String schemaName; + private String username; + private Integer savePassword; + private String directoryName; + private String directoryPath; + private String extraConfigJson; + private String remark; + private String lastTestStatus; + private String lastTestMessage; + private LocalDateTime lastTestTime; + private Integer state; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseTableVO.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseTableVO.java new file mode 100644 index 0000000..c4cb609 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseTableVO.java @@ -0,0 +1,21 @@ +package com.njcn.gather.systemops.database.pojo.vo; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 数据库表信息。 + */ +@Data +public class DatabaseTableVO { + private String owner; + private String tableName; + private Long autoIncrementValue = 0L; + private Long autoIncrement = 0L; + private LocalDateTime updateTime; + private Long dataLength; + private String engine; + private Long tableRows; + private String comments; +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseTaskCreateVO.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseTaskCreateVO.java new file mode 100644 index 0000000..97ac094 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseTaskCreateVO.java @@ -0,0 +1,13 @@ +package com.njcn.gather.systemops.database.pojo.vo; + +import lombok.Data; + +/** + * 运维任务创建结果。 + */ +@Data +public class DatabaseTaskCreateVO { + private String taskId; + private String taskNo; + private String taskStatus; +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseTaskVO.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseTaskVO.java new file mode 100644 index 0000000..2745759 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseTaskVO.java @@ -0,0 +1,28 @@ +package com.njcn.gather.systemops.database.pojo.vo; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 数据库运维任务响应。 + */ +@Data +public class DatabaseTaskVO { + private String id; + private String taskNo; + private String connectionId; + private String dbType; + private String operationType; + private String backupStrategy; + private String taskStatus; + private String schemaName; + private String targetNamesJson; + private String resultMessage; + private BigDecimal progressPercent; + private LocalDateTime startedAt; + private LocalDateTime finishedAt; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseTestResultVO.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseTestResultVO.java new file mode 100644 index 0000000..60aa171 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseTestResultVO.java @@ -0,0 +1,12 @@ +package com.njcn.gather.systemops.database.pojo.vo; + +import lombok.Data; + +/** + * 数据库连接测试结果。 + */ +@Data +public class DatabaseTestResultVO { + private Boolean success; + private String message; +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/DatabaseBackupFileService.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/DatabaseBackupFileService.java new file mode 100644 index 0000000..1a502c6 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/DatabaseBackupFileService.java @@ -0,0 +1,23 @@ +package com.njcn.gather.systemops.database.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseBackupFileVO; + +import java.nio.file.Path; + +/** + * 数据库备份文件服务。 + */ +public interface DatabaseBackupFileService extends IService { + + Page listFiles(DatabaseBackupParam.FileQueryParam param); + + boolean deleteBackupFile(String backupFileId, String confirmText); + + void validateBackupFileReadable(DatabaseBackupFile backupFile); + + Path resolveManagedPath(DatabaseBackupFile backupFile, String filePath); +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/DatabaseConnectionService.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/DatabaseConnectionService.java new file mode 100644 index 0000000..84fd418 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/DatabaseConnectionService.java @@ -0,0 +1,33 @@ +package com.njcn.gather.systemops.database.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.systemops.database.pojo.param.DatabaseConnectionParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseConnectionVO; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTableVO; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTestResultVO; + +import java.util.List; + +/** + * 数据库连接配置服务。 + */ +public interface DatabaseConnectionService extends IService { + + Page listConnections(DatabaseConnectionParam.QueryParam queryParam); + + boolean addConnection(DatabaseConnectionParam param); + + boolean updateConnection(DatabaseConnectionParam.UpdateParam param); + + boolean deleteConnection(DatabaseConnectionParam.DeleteParam param); + + DatabaseTestResultVO testConnection(DatabaseConnectionParam.TestParam param); + + List listTables(DatabaseConnectionParam.TablesParam param); + + DatabaseConnection requireEnabled(String connectionId); + + String resolvePassword(DatabaseConnection connection, String temporaryPassword); +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/DatabaseOperationTaskService.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/DatabaseOperationTaskService.java new file mode 100644 index 0000000..ef12b43 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/DatabaseOperationTaskService.java @@ -0,0 +1,28 @@ +package com.njcn.gather.systemops.database.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTaskCreateVO; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTaskVO; + +/** + * 数据库运维任务服务。 + */ +public interface DatabaseOperationTaskService extends IService { + + DatabaseTaskCreateVO createBackupTask(DatabaseBackupParam.CreateParam param); + + Page listBackupTasks(DatabaseBackupParam.TaskQueryParam param); + + DatabaseTaskVO getStatus(String taskId); + + boolean stopBackupTask(DatabaseBackupParam.StopParam param); + + DatabaseTaskCreateVO restartBackupTask(DatabaseBackupParam.RestartParam param); + + boolean deleteTask(String taskId, String confirmText); + + boolean existsRunningTask(String connectionId); +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/DatabaseRestoreService.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/DatabaseRestoreService.java new file mode 100644 index 0000000..457e32a --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/DatabaseRestoreService.java @@ -0,0 +1,14 @@ +package com.njcn.gather.systemops.database.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.systemops.database.pojo.param.DatabaseRestoreParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseRestoreRecord; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTaskCreateVO; + +/** + * 数据库恢复服务。 + */ +public interface DatabaseRestoreService extends IService { + + DatabaseTaskCreateVO createRestoreTask(DatabaseRestoreParam.CreateParam param); +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/impl/DatabaseBackupFileServiceImpl.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/impl/DatabaseBackupFileServiceImpl.java new file mode 100644 index 0000000..d0dd0d0 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/impl/DatabaseBackupFileServiceImpl.java @@ -0,0 +1,179 @@ +package com.njcn.gather.systemops.database.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.systemops.database.config.DbmsProperties; +import com.njcn.gather.systemops.database.constant.DatabaseOpsConst; +import com.njcn.gather.systemops.database.mapper.DatabaseBackupFileMapper; +import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseBackupFileVO; +import com.njcn.gather.systemops.database.service.DatabaseBackupFileService; +import com.njcn.gather.systemops.database.util.DatabaseChecksumUtil; +import com.njcn.gather.systemops.database.util.DatabasePathUtil; +import com.njcn.web.factory.PageFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 数据库备份文件服务实现。 + */ +@Service +@RequiredArgsConstructor +public class DatabaseBackupFileServiceImpl extends ServiceImpl implements DatabaseBackupFileService { + + private final DbmsProperties dbmsProperties; + + @Override + public Page listFiles(DatabaseBackupParam.FileQueryParam param) { + DatabaseBackupParam.FileQueryParam query = param == null ? new DatabaseBackupParam.FileQueryParam() : param; + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(DatabaseBackupFile::getState, DatabaseOpsConst.STATE_ENABLED) + .eq(StrUtil.isNotBlank(query.getConnectionId()), DatabaseBackupFile::getConnectionId, query.getConnectionId()) + .eq(StrUtil.isNotBlank(query.getTaskId()), DatabaseBackupFile::getTaskId, query.getTaskId()) + .eq(StrUtil.isNotBlank(query.getBackupStrategy()), DatabaseBackupFile::getBackupStrategy, query.getBackupStrategy()) + .orderByDesc(DatabaseBackupFile::getCreateTime); + Page page = this.page(new Page<>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), wrapper); + Page result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal()); + result.setRecords(page.getRecords().stream().map(this::toVO).collect(Collectors.toList())); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean deleteBackupFile(String backupFileId, String confirmText) { + if (!DatabaseOpsConst.CONFIRM_DELETE.equals(confirmText)) { + throw new BusinessException(CommonResponseEnum.FAIL, "确认文案不正确"); + } + DatabaseBackupFile file = this.lambdaQuery() + .eq(DatabaseBackupFile::getId, backupFileId) + .eq(DatabaseBackupFile::getState, DatabaseOpsConst.STATE_ENABLED) + .one(); + if (file == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份文件不存在或已删除"); + } + deletePhysicalPath(file, file.getFilePath()); + deletePhysicalPath(file, file.getLogFilePath()); + deletePhysicalPath(file, file.getMetadataFilePath()); + file.setState(DatabaseOpsConst.STATE_DELETED); + file.setUpdateTime(LocalDateTime.now()); + return this.updateById(file); + } + + @Override + public void validateBackupFileReadable(DatabaseBackupFile backupFile) { + validateReadablePath(backupFile, backupFile.getFilePath(), "备份文件", false, true); + validateReadablePath(backupFile, backupFile.getMetadataFilePath(), "备份元数据文件", + StrUtil.isBlank(backupFile.getMetadataFilePath()), false); + if (StrUtil.isBlank(backupFile.getChecksum())) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份文件缺少校验值"); + } + Path checksumPath = resolveChecksumPath(backupFile); + String actualChecksum = DatabaseChecksumUtil.sha256(checksumPath); + if (!backupFile.getChecksum().equalsIgnoreCase(actualChecksum)) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份文件校验失败"); + } + } + + @Override + public Path resolveManagedPath(DatabaseBackupFile backupFile, String filePath) { + if (StrUtil.isBlank(filePath)) { + return null; + } + Path path = DatabasePathUtil.normalize(filePath); + if (path == null) { + return null; + } + Path storageRoot = DatabasePathUtil.normalize(dbmsProperties.getBackup().getStoragePath()); + if (DatabasePathUtil.isUnder(path, storageRoot)) { + return path; + } + Path primaryFilePath = DatabasePathUtil.normalize(backupFile.getFilePath()); + if (primaryFilePath != null) { + Path allowedRoot = Files.isDirectory(primaryFilePath) ? primaryFilePath : primaryFilePath.getParent(); + if (allowedRoot != null && DatabasePathUtil.isUnder(path, allowedRoot)) { + return path; + } + } + throw new BusinessException(CommonResponseEnum.FAIL, "文件路径不在允许的备份目录内"); + } + + private Path resolveChecksumPath(DatabaseBackupFile backupFile) { + Path metadataPath = resolveManagedPath(backupFile, backupFile.getMetadataFilePath()); + if (metadataPath != null && Files.exists(metadataPath) && !Files.isDirectory(metadataPath)) { + return metadataPath; + } + Path filePath = resolveManagedPath(backupFile, backupFile.getFilePath()); + if (filePath == null || !Files.exists(filePath) || Files.isDirectory(filePath)) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份校验文件不存在"); + } + return filePath; + } + + private void deletePhysicalPath(DatabaseBackupFile backupFile, String filePath) { + if (StrUtil.isBlank(filePath)) { + return; + } + try { + Path path = resolveManagedPath(backupFile, filePath); + if (path == null || !Files.exists(path)) { + return; + } + if (Files.isDirectory(path)) { + try (Stream paths = Files.walk(path)) { + paths.sorted(Comparator.reverseOrder()).forEach(this::deleteSinglePath); + } + } else { + Files.delete(path); + } + } catch (BusinessException exception) { + throw exception; + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "删除物理文件失败:" + exception.getMessage()); + } + } + + private void deleteSinglePath(Path path) { + try { + Files.deleteIfExists(path); + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "删除物理文件失败:" + exception.getMessage()); + } + } + + private void validateReadablePath(DatabaseBackupFile backupFile, String filePath, String fileType, + boolean allowBlank, boolean allowDirectory) { + if (StrUtil.isBlank(filePath)) { + if (allowBlank) { + return; + } + throw new BusinessException(CommonResponseEnum.FAIL, fileType + "路径不能为空"); + } + Path path = resolveManagedPath(backupFile, filePath); + if (path == null || !Files.exists(path)) { + throw new BusinessException(CommonResponseEnum.FAIL, fileType + "不存在"); + } + if (Files.isDirectory(path) && !allowDirectory) { + throw new BusinessException(CommonResponseEnum.FAIL, fileType + "不能是目录"); + } + } + + private DatabaseBackupFileVO toVO(DatabaseBackupFile file) { + DatabaseBackupFileVO vo = new DatabaseBackupFileVO(); + BeanUtil.copyProperties(file, vo); + return vo; + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/impl/DatabaseConnectionServiceImpl.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/impl/DatabaseConnectionServiceImpl.java new file mode 100644 index 0000000..fd62e35 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/impl/DatabaseConnectionServiceImpl.java @@ -0,0 +1,287 @@ +package com.njcn.gather.systemops.database.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.systemops.database.component.DatabasePasswordComponent; +import com.njcn.gather.systemops.database.constant.DatabaseOpsConst; +import com.njcn.gather.systemops.database.mapper.DatabaseConnectionMapper; +import com.njcn.gather.systemops.database.pojo.param.DatabaseConnectionParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseConnectionVO; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTableVO; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTestResultVO; +import com.njcn.gather.systemops.database.service.DatabaseConnectionService; +import com.njcn.gather.systemops.database.service.DatabaseOperationTaskService; +import com.njcn.gather.systemops.database.support.spi.DatabaseConnectionOperator; +import com.njcn.gather.systemops.database.support.spi.DatabaseOperatorRegistry; +import com.njcn.gather.systemops.database.util.DatabaseOpsIdUtil; +import com.njcn.web.factory.PageFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +/** + * 数据库连接配置服务实现。 + */ +@Service +@RequiredArgsConstructor +public class DatabaseConnectionServiceImpl extends ServiceImpl implements DatabaseConnectionService { + + private final DatabasePasswordComponent databasePasswordComponent; + private final DatabaseOperatorRegistry databaseOperatorRegistry; + private final ObjectProvider databaseOperationTaskServiceProvider; + + @Override + public Page listConnections(DatabaseConnectionParam.QueryParam queryParam) { + DatabaseConnectionParam.QueryParam query = queryParam == null ? new DatabaseConnectionParam.QueryParam() : queryParam; + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(DatabaseConnection::getState, DatabaseOpsConst.STATE_ENABLED) + .like(StrUtil.isNotBlank(query.getConnectionName()), DatabaseConnection::getConnectionName, query.getConnectionName()) + .eq(StrUtil.isNotBlank(query.getDbType()), DatabaseConnection::getDbType, query.getDbType()) + .like(StrUtil.isNotBlank(query.getSchemaName()), DatabaseConnection::getSchemaName, query.getSchemaName()) + .orderByDesc(DatabaseConnection::getUpdateTime); + Page page = this.page(new Page<>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), wrapper); + Page result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal()); + result.setRecords(page.getRecords().stream().map(this::toVO).collect(Collectors.toList())); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean addConnection(DatabaseConnectionParam param) { + DatabaseConnection connection = new DatabaseConnection(); + fillConnection(connection, param, true); + checkConnectionNameUnique(connection.getConnectionName()); + connection.setId(DatabaseOpsIdUtil.uuid()); + connection.setState(DatabaseOpsConst.STATE_ENABLED); + connection.setCreateTime(LocalDateTime.now()); + connection.setUpdateTime(LocalDateTime.now()); + return this.save(connection); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateConnection(DatabaseConnectionParam.UpdateParam param) { + DatabaseConnection connection = requireEnabled(param.getId()); + fillConnection(connection, param, false); + connection.setUpdateTime(LocalDateTime.now()); + return this.updateById(connection); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean deleteConnection(DatabaseConnectionParam.DeleteParam param) { + requireEnabled(param.getId()); + if (databaseOperationTaskServiceProvider.getObject().existsRunningTask(param.getId())) { + throw new BusinessException(CommonResponseEnum.FAIL, "存在运行中的任务,不能删除连接"); + } + return this.lambdaUpdate() + .set(DatabaseConnection::getState, DatabaseOpsConst.STATE_DELETED) + .set(DatabaseConnection::getUpdateTime, LocalDateTime.now()) + .eq(DatabaseConnection::getId, param.getId()) + .update(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public DatabaseTestResultVO testConnection(DatabaseConnectionParam.TestParam param) { + DatabaseConnection connection = resolveTestConnection(param); + DatabaseConnectionOperator operator = databaseOperatorRegistry.getConnectionOperator(connection.getDbType()); + DatabaseTestResultVO result = operator.test(connection, resolvePassword(connection, param.getTemporaryPassword())); + if (StrUtil.isNotBlank(connection.getId())) { + updateLastTestResult(connection.getId(), result); + } + return result; + } + + @Override + public List listTables(DatabaseConnectionParam.TablesParam param) { + DatabaseConnection connection = requireEnabled(param.getConnectionId()); + try { + DatabaseConnectionOperator operator = databaseOperatorRegistry.getConnectionOperator(connection.getDbType()); + String password = resolveTablesPassword(connection, param); + return operator.listTables(connection, password, + resolveSchemaOrDatabase(param, connection)); + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.FAIL, exception.getMessage()); + } + } + + @Override + public DatabaseConnection requireEnabled(String connectionId) { + if (StrUtil.isBlank(connectionId)) { + throw new BusinessException(CommonResponseEnum.FAIL, "连接 ID 不能为空"); + } + DatabaseConnection connection = this.lambdaQuery() + .eq(DatabaseConnection::getId, connectionId) + .eq(DatabaseConnection::getState, DatabaseOpsConst.STATE_ENABLED) + .one(); + if (connection == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "数据库连接不存在或已删除"); + } + return connection; + } + + @Override + public String resolvePassword(DatabaseConnection connection, String temporaryPassword) { + try { + return databasePasswordComponent.resolveRuntimePassword(connection.getPasswordCipher(), temporaryPassword); + } catch (IllegalArgumentException exception) { + throw new BusinessException(CommonResponseEnum.FAIL, exception.getMessage()); + } + } + + private DatabaseConnection resolveTestConnection(DatabaseConnectionParam.TestParam param) { + if (StrUtil.isNotBlank(param.getConnectionId())) { + DatabaseConnection savedConnection = requireEnabled(param.getConnectionId()); + if (param.getConnection() == null) { + return savedConnection; + } + DatabaseConnection connection = new DatabaseConnection(); + fillConnection(connection, param.getConnection(), true, true); + connection.setId(savedConnection.getId()); + if (StrUtil.isBlank(param.getConnection().getPassword())) { + // 已有连接测试编辑后参数时,未传密码则复用库里保存的密码。 + connection.setPasswordCipher(savedConnection.getPasswordCipher()); + } + return connection; + } + if (param.getConnection() == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "连接测试参数不能为空"); + } + DatabaseConnection connection = new DatabaseConnection(); + fillConnection(connection, param.getConnection(), true, StrUtil.isNotBlank(param.getTemporaryPassword())); + return connection; + } + + private void updateLastTestResult(String connectionId, DatabaseTestResultVO result) { + this.lambdaUpdate() + .set(DatabaseConnection::getLastTestStatus, Boolean.TRUE.equals(result.getSuccess()) ? "SUCCESS" : "FAIL") + .set(DatabaseConnection::getLastTestMessage, result.getMessage()) + .set(DatabaseConnection::getLastTestTime, LocalDateTime.now()) + .eq(DatabaseConnection::getId, connectionId) + .update(); + } + + private String resolveTablesPassword(DatabaseConnection connection, DatabaseConnectionParam.TablesParam param) { + if (StrUtil.isNotBlank(param.getTemporaryPassword())) { + return param.getTemporaryPassword(); + } + if (StrUtil.isNotBlank(param.getPassword())) { + return param.getPassword(); + } + if (param.getConnection() != null && StrUtil.isNotBlank(param.getConnection().getPassword())) { + return param.getConnection().getPassword(); + } + if (StrUtil.isNotBlank(param.getPasswordCipher())) { + return databasePasswordComponent.resolveRuntimePassword(param.getPasswordCipher(), null); + } + return resolvePassword(connection, null); + } + + private void fillConnection(DatabaseConnection connection, DatabaseConnectionParam param, boolean create) { + fillConnection(connection, param, create, false); + } + + private void fillConnection(DatabaseConnection connection, DatabaseConnectionParam param, boolean create, + boolean allowTemporaryPasswordOnly) { + String dbType = resolveDbType(param.getDbType()); + validateConnectionParam(param, dbType); + connection.setConnectionName(param.getConnectionName().trim()); + connection.setDbType(dbType); + connection.setHost(param.getHost().trim()); + connection.setPort(param.getPort()); + connection.setConnectType(DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType) ? resolveConnectType(param.getConnectType()) : null); + connection.setServiceName(DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType) ? trimToNull(param.getServiceName()) : null); + connection.setSid(DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType) ? trimToNull(param.getSid()) : null); + connection.setDatabaseName(DatabaseOpsConst.DB_TYPE_MYSQL.equals(dbType) ? trimToNull(param.getDatabaseName()) : null); + connection.setSchemaName(trimToNull(param.getSchemaName())); + connection.setUsername(param.getUsername().trim()); + connection.setSavePassword(param.getSavePassword() == null ? DatabaseOpsConst.SAVE_PASSWORD_YES : param.getSavePassword()); + if (connection.getSavePassword() != DatabaseOpsConst.SAVE_PASSWORD_YES + && connection.getSavePassword() != DatabaseOpsConst.SAVE_PASSWORD_NO) { + throw new BusinessException(CommonResponseEnum.FAIL, "savePassword 只能是 0 或 1"); + } + if (DatabaseOpsConst.SAVE_PASSWORD_YES == connection.getSavePassword() && StrUtil.isNotBlank(param.getPassword())) { + connection.setPasswordCipher(databasePasswordComponent.encrypt(param.getPassword())); + } + if (DatabaseOpsConst.SAVE_PASSWORD_NO == connection.getSavePassword()) { + connection.setPasswordCipher(null); + } else if (create && StrUtil.isBlank(param.getPassword()) && !allowTemporaryPasswordOnly) { + throw new BusinessException(CommonResponseEnum.FAIL, "保存密码时密码不能为空"); + } + connection.setDirectoryName(DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType) ? trimToNull(param.getDirectoryName()) : null); + connection.setDirectoryPath(DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType) ? trimToNull(param.getDirectoryPath()) : null); + connection.setExtraConfigJson(trimToNull(param.getExtraConfigJson())); + connection.setRemark(param.getRemark()); + } + + private void validateConnectionParam(DatabaseConnectionParam param, String dbType) { + if (DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType)) { + String connectType = resolveConnectType(param.getConnectType()); + if (DatabaseOpsConst.CONNECT_TYPE_SERVICE_NAME.equals(connectType) && StrUtil.isBlank(param.getServiceName())) { + throw new BusinessException(CommonResponseEnum.FAIL, "SERVICE_NAME 连接方式下服务名不能为空"); + } + if (DatabaseOpsConst.CONNECT_TYPE_SID.equals(connectType) && StrUtil.isBlank(param.getSid())) { + throw new BusinessException(CommonResponseEnum.FAIL, "SID 连接方式下 SID 不能为空"); + } + return; + } + if (StrUtil.isBlank(param.getDatabaseName())) { + throw new BusinessException(CommonResponseEnum.FAIL, "MYSQL 数据库名不能为空"); + } + } + + /** + * 新增连接时,连接名称在有效记录中必须唯一。 + */ + private void checkConnectionNameUnique(String connectionName) { + long count = this.lambdaQuery() + .eq(DatabaseConnection::getConnectionName, connectionName) + .eq(DatabaseConnection::getState, DatabaseOpsConst.STATE_ENABLED) + .count(); + if (count > 0) { + throw new BusinessException(CommonResponseEnum.FAIL, "连接名称已存在"); + } + } + + private String resolveConnectType(String connectType) { + return StrUtil.blankToDefault(connectType, DatabaseOpsConst.CONNECT_TYPE_SERVICE_NAME).trim().toUpperCase(Locale.ROOT); + } + + private String resolveDbType(String dbType) { + String resolved = StrUtil.blankToDefault(dbType, DatabaseOpsConst.DB_TYPE_ORACLE).trim().toUpperCase(Locale.ROOT); + if (!DatabaseOpsConst.DB_TYPE_ORACLE.equals(resolved) && !DatabaseOpsConst.DB_TYPE_MYSQL.equals(resolved)) { + throw new BusinessException(CommonResponseEnum.FAIL, "不支持的数据库类型:" + dbType); + } + return resolved; + } + + private String resolveSchemaOrDatabase(DatabaseConnectionParam.TablesParam param, DatabaseConnection connection) { + if (DatabaseOpsConst.DB_TYPE_MYSQL.equals(connection.getDbType())) { + return StrUtil.blankToDefault(param.getSchemaName(), connection.getDatabaseName()); + } + return param.getSchemaName(); + } + + private String trimToNull(String value) { + return StrUtil.isBlank(value) ? null : value.trim(); + } + + private DatabaseConnectionVO toVO(DatabaseConnection connection) { + DatabaseConnectionVO vo = new DatabaseConnectionVO(); + BeanUtil.copyProperties(connection, vo); + return vo; + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/impl/DatabaseOperationTaskServiceImpl.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/impl/DatabaseOperationTaskServiceImpl.java new file mode 100644 index 0000000..e59d7c4 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/impl/DatabaseOperationTaskServiceImpl.java @@ -0,0 +1,333 @@ +package com.njcn.gather.systemops.database.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +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.systemops.database.constant.DatabaseOpsConst; +import com.njcn.gather.systemops.database.mapper.DatabaseOperationTaskMapper; +import com.njcn.gather.systemops.database.pojo.enums.BackupModeEnum; +import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum; +import com.njcn.gather.systemops.database.pojo.enums.OperationTypeEnum; +import com.njcn.gather.systemops.database.pojo.enums.TaskStatusEnum; +import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTaskCreateVO; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTaskVO; +import com.njcn.gather.systemops.database.service.DatabaseBackupFileService; +import com.njcn.gather.systemops.database.service.DatabaseConnectionService; +import com.njcn.gather.systemops.database.service.DatabaseOperationTaskService; +import com.njcn.gather.systemops.database.support.spi.DatabaseBackupOperator; +import com.njcn.gather.systemops.database.support.spi.DatabaseOperatorRegistry; +import com.njcn.gather.systemops.database.util.DatabaseOpsIdUtil; +import com.njcn.web.factory.PageFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Locale; +import java.util.concurrent.ExecutorService; +import java.util.stream.Collectors; + +/** + * 数据库运维任务服务实现。 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class DatabaseOperationTaskServiceImpl extends ServiceImpl implements DatabaseOperationTaskService { + + private final DatabaseConnectionService databaseConnectionService; + private final DatabaseBackupFileService databaseBackupFileService; + private final DatabaseOperatorRegistry databaseOperatorRegistry; + private final ObjectMapper objectMapper; + @Resource(name = "dbmsTaskExecutorService") + private ExecutorService dbmsTaskExecutorService; + + @Override + @Transactional(rollbackFor = Exception.class) + public DatabaseTaskCreateVO createBackupTask(DatabaseBackupParam.CreateParam param) { + DatabaseConnection connection = databaseConnectionService.requireEnabled(param.getConnectionId()); + validateBackupParam(param, connection); + if (existsRunningTask(connection.getId())) { + throw new BusinessException(CommonResponseEnum.FAIL, "当前连接存在运行中的任务"); + } + DatabaseOperationTask task = buildBackupTask(param, connection); + this.save(task); + dbmsTaskExecutorService.submit(() -> executeBackupTask(task.getId(), param)); + return toCreateVO(task); + } + + @Override + public Page listBackupTasks(DatabaseBackupParam.TaskQueryParam param) { + DatabaseBackupParam.TaskQueryParam query = param == null ? new DatabaseBackupParam.TaskQueryParam() : param; + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(DatabaseOperationTask::getState, DatabaseOpsConst.STATE_ENABLED) + .eq(DatabaseOperationTask::getOperationType, OperationTypeEnum.BACKUP.name()) + .eq(StrUtil.isNotBlank(query.getConnectionId()), DatabaseOperationTask::getConnectionId, query.getConnectionId()) + .eq(StrUtil.isNotBlank(query.getTaskStatus()), DatabaseOperationTask::getTaskStatus, query.getTaskStatus()) + .orderByDesc(DatabaseOperationTask::getCreateTime); + Page page = this.page(new Page<>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), wrapper); + Page result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal()); + result.setRecords(page.getRecords().stream().map(this::toVO).collect(Collectors.toList())); + return result; + } + + @Override + public DatabaseTaskVO getStatus(String taskId) { + return toVO(requireEnabledTask(taskId)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean stopBackupTask(DatabaseBackupParam.StopParam param) { + DatabaseOperationTask task = requireEnabledTask(param.getTaskId()); + if (!OperationTypeEnum.BACKUP.name().equals(task.getOperationType())) { + throw new BusinessException(CommonResponseEnum.FAIL, "仅支持停止备份任务"); + } + if (!TaskStatusEnum.WAITING.name().equals(task.getTaskStatus()) + && !TaskStatusEnum.RUNNING.name().equals(task.getTaskStatus())) { + throw new BusinessException(CommonResponseEnum.FAIL, "仅等待中或运行中的任务允许停止"); + } + task.setTaskStatus(TaskStatusEnum.CANCELLED.name()); + task.setResultMessage("用户请求停止备份任务"); + task.setFinishedAt(LocalDateTime.now()); + task.setUpdateTime(LocalDateTime.now()); + return this.updateById(task); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public DatabaseTaskCreateVO restartBackupTask(DatabaseBackupParam.RestartParam param) { + DatabaseOperationTask sourceTask = requireEnabledTask(param.getTaskId()); + if (!OperationTypeEnum.BACKUP.name().equals(sourceTask.getOperationType())) { + throw new BusinessException(CommonResponseEnum.FAIL, "仅支持重新开始备份任务"); + } + if (!TaskStatusEnum.FAIL.name().equals(sourceTask.getTaskStatus()) + && !TaskStatusEnum.CANCELLED.name().equals(sourceTask.getTaskStatus())) { + throw new BusinessException(CommonResponseEnum.FAIL, "仅失败或已取消的任务允许重新开始"); + } + DatabaseBackupParam.CreateParam createParam = readCreateParam(sourceTask.getRequestParamJson()); + createParam.setTemporaryPassword(param.getTemporaryPassword()); + return createBackupTask(createParam); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean deleteTask(String taskId, String confirmText) { + if (!DatabaseOpsConst.CONFIRM_DELETE.equals(confirmText)) { + throw new BusinessException(CommonResponseEnum.FAIL, "确认文案不正确"); + } + DatabaseOperationTask task = requireEnabledTask(taskId); + if (TaskStatusEnum.RUNNING.name().equals(task.getTaskStatus()) || TaskStatusEnum.WAITING.name().equals(task.getTaskStatus())) { + throw new BusinessException(CommonResponseEnum.FAIL, "运行中的任务不能删除"); + } + task.setState(DatabaseOpsConst.STATE_DELETED); + task.setUpdateTime(LocalDateTime.now()); + return this.updateById(task); + } + + @Override + public boolean existsRunningTask(String connectionId) { + return this.lambdaQuery() + .eq(DatabaseOperationTask::getConnectionId, connectionId) + .eq(DatabaseOperationTask::getState, DatabaseOpsConst.STATE_ENABLED) + .in(DatabaseOperationTask::getTaskStatus, Arrays.asList(TaskStatusEnum.WAITING.name(), TaskStatusEnum.RUNNING.name())) + .count() > 0; + } + + private void executeBackupTask(String taskId, DatabaseBackupParam.CreateParam param) { + DatabaseOperationTask task = this.getById(taskId); + try { + if (task == null || TaskStatusEnum.CANCELLED.name().equals(task.getTaskStatus())) { + return; + } + markRunning(task); + DatabaseConnection connection = databaseConnectionService.requireEnabled(task.getConnectionId()); + connection.setSchemaName(task.getSchemaName()); + String password = databaseConnectionService.resolvePassword(connection, param.getTemporaryPassword()); + DatabaseBackupOperator operator = databaseOperatorRegistry.getBackupOperator(connection.getDbType(), task.getBackupStrategy()); + DatabaseBackupFile backupFile = operator.executeBackup(task, connection, password, param); + task = this.getById(taskId); + if (task == null || TaskStatusEnum.CANCELLED.name().equals(task.getTaskStatus())) { + return; + } + databaseBackupFileService.save(backupFile); + markSuccess(task, "备份任务执行成功"); + } catch (Exception exception) { + log.error("数据库备份任务失败,taskId={}", taskId, exception); + task = this.getById(taskId); + if (task != null && TaskStatusEnum.CANCELLED.name().equals(task.getTaskStatus())) { + markCancelled(task, exception.getMessage()); + return; + } + markFail(task, exception.getMessage()); + } + } + + private DatabaseOperationTask buildBackupTask(DatabaseBackupParam.CreateParam param, DatabaseConnection connection) { + DatabaseOperationTask task = new DatabaseOperationTask(); + task.setId(DatabaseOpsIdUtil.uuid()); + task.setTaskNo(DatabaseOpsIdUtil.taskNo("DBMSB")); + task.setConnectionId(connection.getId()); + task.setDbType(connection.getDbType()); + task.setOperationType(OperationTypeEnum.BACKUP.name()); + task.setBackupStrategy(resolveBackupStrategy(param.getBackupStrategy(), connection.getDbType())); + task.setTaskStatus(TaskStatusEnum.WAITING.name()); + task.setSchemaName(resolveSchemaName(param, connection)); + task.setTargetNamesJson(writeJson(param.getTargetNames())); + task.setRequestParamJson(writeJsonWithoutPassword(param)); + task.setProgressPercent(BigDecimal.ZERO); + task.setState(DatabaseOpsConst.STATE_ENABLED); + task.setCreateTime(LocalDateTime.now()); + task.setUpdateTime(LocalDateTime.now()); + return task; + } + + private void validateBackupParam(DatabaseBackupParam.CreateParam param, DatabaseConnection connection) { + if (param.getTargetNames() == null || param.getTargetNames().isEmpty()) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份表不能为空"); + } + if (DatabaseOpsConst.DB_TYPE_ORACLE.equals(connection.getDbType()) + && StrUtil.isBlank(StrUtil.blankToDefault(param.getSchemaName(), connection.getSchemaName()))) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份 Schema 不能为空"); + } + if (DatabaseOpsConst.DB_TYPE_MYSQL.equals(connection.getDbType()) && StrUtil.isBlank(connection.getDatabaseName())) { + throw new BusinessException(CommonResponseEnum.FAIL, "MYSQL 数据库名不能为空"); + } + String backupMode = StrUtil.blankToDefault(param.getBackupMode(), BackupModeEnum.FULL_TABLE.name()).toUpperCase(Locale.ROOT); + if (BackupModeEnum.TIME_RANGE.name().equals(backupMode) + && (param.getStartTime() == null || param.getEndTime() == null)) { + throw new BusinessException(CommonResponseEnum.FAIL, "按时间备份必须传入开始时间和结束时间"); + } + if (BackupModeEnum.TIME_RANGE.name().equals(backupMode) + && param.getStartTime() != null && param.getEndTime() != null + && param.getStartTime().isAfter(param.getEndTime())) { + throw new BusinessException(CommonResponseEnum.FAIL, "开始时间不能晚于结束时间"); + } + if (BackupModeEnum.SIZE_SPLIT.name().equals(backupMode) + && (param.getMaxFileSizeMb() == null || param.getMaxFileSizeMb() <= 0)) { + throw new BusinessException(CommonResponseEnum.FAIL, "按大小分片必须传入大于 0 的文件大小"); + } + if (BackupModeEnum.TIME_RANGE.name().equals(backupMode) && StrUtil.isBlank(param.getTimeColumn())) { + throw new BusinessException(CommonResponseEnum.FAIL, "按时间备份必须传入时间字段"); + } + resolveBackupStrategy(param.getBackupStrategy(), connection.getDbType()); + } + + private String resolveBackupStrategy(String backupStrategy, String dbType) { + String value = StrUtil.blankToDefault(backupStrategy, + DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType) ? BackupStrategyEnum.DATA_PUMP.name() : BackupStrategyEnum.JDBC_EXPORT.name()) + .trim() + .toUpperCase(Locale.ROOT); + try { + BackupStrategyEnum strategyEnum = BackupStrategyEnum.valueOf(value); + if (DatabaseOpsConst.DB_TYPE_MYSQL.equals(dbType) && BackupStrategyEnum.DATA_PUMP == strategyEnum) { + throw new BusinessException(CommonResponseEnum.FAIL, "MYSQL 不支持 DATA_PUMP"); + } + return strategyEnum.name(); + } catch (BusinessException exception) { + throw exception; + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "不支持的备份策略:" + backupStrategy); + } + } + + private String resolveSchemaName(DatabaseBackupParam.CreateParam param, DatabaseConnection connection) { + if (DatabaseOpsConst.DB_TYPE_MYSQL.equals(connection.getDbType())) { + return StrUtil.blankToDefault(param.getSchemaName(), connection.getDatabaseName()); + } + return StrUtil.blankToDefault(param.getSchemaName(), connection.getSchemaName()); + } + + private void markRunning(DatabaseOperationTask task) { + task.setTaskStatus(TaskStatusEnum.RUNNING.name()); + task.setStartedAt(LocalDateTime.now()); + task.setUpdateTime(LocalDateTime.now()); + this.updateById(task); + } + + private void markSuccess(DatabaseOperationTask task, String message) { + task.setTaskStatus(TaskStatusEnum.SUCCESS.name()); + task.setResultMessage(message); + task.setProgressPercent(new BigDecimal("100.00")); + task.setFinishedAt(LocalDateTime.now()); + task.setUpdateTime(LocalDateTime.now()); + this.updateById(task); + } + + private void markFail(DatabaseOperationTask task, String message) { + if (task == null) { + return; + } + task.setTaskStatus(TaskStatusEnum.FAIL.name()); + task.setResultMessage(message); + task.setFinishedAt(LocalDateTime.now()); + task.setUpdateTime(LocalDateTime.now()); + this.updateById(task); + } + + private void markCancelled(DatabaseOperationTask task, String message) { + task.setTaskStatus(TaskStatusEnum.CANCELLED.name()); + task.setResultMessage(StrUtil.blankToDefault(message, "备份任务已停止")); + task.setFinishedAt(LocalDateTime.now()); + task.setUpdateTime(LocalDateTime.now()); + this.updateById(task); + } + + private DatabaseOperationTask requireEnabledTask(String taskId) { + DatabaseOperationTask task = this.getById(taskId); + if (task == null || !Integer.valueOf(DatabaseOpsConst.STATE_ENABLED).equals(task.getState())) { + throw new BusinessException(CommonResponseEnum.FAIL, "任务不存在或已删除"); + } + return task; + } + + private String writeJson(Object value) { + try { + return objectMapper.writeValueAsString(value); + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.JSON_CONVERT_EXCEPTION, exception.getMessage()); + } + } + + private String writeJsonWithoutPassword(DatabaseBackupParam.CreateParam param) { + DatabaseBackupParam.CreateParam copy = new DatabaseBackupParam.CreateParam(); + BeanUtil.copyProperties(param, copy); + copy.setTemporaryPassword(null); + return writeJson(copy); + } + + private DatabaseBackupParam.CreateParam readCreateParam(String requestParamJson) { + try { + return objectMapper.readValue(requestParamJson, DatabaseBackupParam.CreateParam.class); + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.JSON_CONVERT_EXCEPTION, exception.getMessage()); + } + } + + private DatabaseTaskCreateVO toCreateVO(DatabaseOperationTask task) { + DatabaseTaskCreateVO vo = new DatabaseTaskCreateVO(); + vo.setTaskId(task.getId()); + vo.setTaskNo(task.getTaskNo()); + vo.setTaskStatus(task.getTaskStatus()); + return vo; + } + + private DatabaseTaskVO toVO(DatabaseOperationTask task) { + DatabaseTaskVO vo = new DatabaseTaskVO(); + BeanUtil.copyProperties(task, vo); + return vo; + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/impl/DatabaseRestoreServiceImpl.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/impl/DatabaseRestoreServiceImpl.java new file mode 100644 index 0000000..d54c52d --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/impl/DatabaseRestoreServiceImpl.java @@ -0,0 +1,226 @@ +package com.njcn.gather.systemops.database.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +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.systemops.database.constant.DatabaseOpsConst; +import com.njcn.gather.systemops.database.mapper.DatabaseRestoreRecordMapper; +import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum; +import com.njcn.gather.systemops.database.pojo.enums.OperationTypeEnum; +import com.njcn.gather.systemops.database.pojo.enums.RestoreModeEnum; +import com.njcn.gather.systemops.database.pojo.enums.TaskStatusEnum; +import com.njcn.gather.systemops.database.pojo.param.DatabaseRestoreParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; +import com.njcn.gather.systemops.database.pojo.po.DatabaseRestoreRecord; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTaskCreateVO; +import com.njcn.gather.systemops.database.service.DatabaseBackupFileService; +import com.njcn.gather.systemops.database.service.DatabaseConnectionService; +import com.njcn.gather.systemops.database.service.DatabaseOperationTaskService; +import com.njcn.gather.systemops.database.service.DatabaseRestoreService; +import com.njcn.gather.systemops.database.support.spi.DatabaseConnectionOperator; +import com.njcn.gather.systemops.database.support.spi.DatabaseOperatorRegistry; +import com.njcn.gather.systemops.database.support.spi.DatabaseRestoreOperator; +import com.njcn.gather.systemops.database.util.DatabaseOpsIdUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Locale; +import java.util.concurrent.ExecutorService; + +/** + * 数据库恢复服务实现。 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class DatabaseRestoreServiceImpl extends ServiceImpl implements DatabaseRestoreService { + + private final DatabaseConnectionService databaseConnectionService; + private final DatabaseOperationTaskService databaseOperationTaskService; + private final DatabaseBackupFileService databaseBackupFileService; + private final DatabaseOperatorRegistry databaseOperatorRegistry; + private final ObjectMapper objectMapper; + @Resource(name = "dbmsTaskExecutorService") + private ExecutorService dbmsTaskExecutorService; + + @Override + @Transactional(rollbackFor = Exception.class) + public DatabaseTaskCreateVO createRestoreTask(DatabaseRestoreParam.CreateParam param) { + DatabaseConnection connection = databaseConnectionService.requireEnabled(param.getConnectionId()); + DatabaseBackupFile backupFile = requireBackupFile(param.getBackupFileId()); + validateRestoreParam(param, connection, backupFile); + if (databaseOperationTaskService.existsRunningTask(connection.getId())) { + throw new BusinessException(CommonResponseEnum.FAIL, "当前连接存在运行中的任务"); + } + DatabaseOperationTask task = buildRestoreTask(param, connection, backupFile); + databaseOperationTaskService.save(task); + DatabaseRestoreRecord record = buildRestoreRecord(param, connection, backupFile, task); + this.save(record); + dbmsTaskExecutorService.submit(() -> executeRestoreTask(task.getId(), record.getId(), param)); + DatabaseTaskCreateVO vo = new DatabaseTaskCreateVO(); + vo.setTaskId(task.getId()); + vo.setTaskNo(task.getTaskNo()); + vo.setTaskStatus(task.getTaskStatus()); + return vo; + } + + private void executeRestoreTask(String taskId, String recordId, DatabaseRestoreParam.CreateParam param) { + DatabaseOperationTask task = databaseOperationTaskService.getById(taskId); + DatabaseRestoreRecord record = this.getById(recordId); + try { + markRunning(task); + DatabaseConnection connection = databaseConnectionService.requireEnabled(task.getConnectionId()); + DatabaseBackupFile backupFile = requireBackupFile(record.getBackupFileId()); + databaseBackupFileService.validateBackupFileReadable(backupFile); + String password = databaseConnectionService.resolvePassword(connection, param.getTemporaryPassword()); + DatabaseRestoreOperator operator = databaseOperatorRegistry.getRestoreOperator(connection.getDbType(), backupFile.getBackupStrategy()); + operator.executeRestore(task, record, backupFile, connection, password, param); + record.setResultMessage("恢复任务执行成功"); + record.setUpdateTime(LocalDateTime.now()); + this.updateById(record); + markSuccess(task, "恢复任务执行成功"); + } catch (Exception exception) { + log.error("数据库恢复任务失败,taskId={}", taskId, exception); + record.setResultMessage(exception.getMessage()); + record.setUpdateTime(LocalDateTime.now()); + this.updateById(record); + markFail(task, exception.getMessage()); + } + } + + private void validateRestoreParam(DatabaseRestoreParam.CreateParam param, DatabaseConnection connection, DatabaseBackupFile backupFile) { + if (!connection.getDbType().equals(backupFile.getDbType())) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份文件数据库类型和目标连接数据库类型不一致"); + } + String restoreMode = resolveRestoreMode(param.getRestoreMode()); + if ((RestoreModeEnum.TRUNCATE.name().equals(restoreMode) || RestoreModeEnum.REPLACE.name().equals(restoreMode)) + && !DatabaseOpsConst.CONFIRM_OVERWRITE.equals(param.getOverwriteConfirmText())) { + throw new BusinessException(CommonResponseEnum.FAIL, "覆盖类恢复必须输入确认覆盖"); + } + databaseBackupFileService.validateBackupFileReadable(backupFile); + String password = databaseConnectionService.resolvePassword(connection, param.getTemporaryPassword()); + DatabaseConnectionOperator operator = databaseOperatorRegistry.getConnectionOperator(connection.getDbType()); + if (!Boolean.TRUE.equals(operator.test(connection, password).getSuccess())) { + throw new BusinessException(CommonResponseEnum.FAIL, "目标连接测试失败,不能创建恢复任务"); + } + if (BackupStrategyEnum.DATA_PUMP.name().equals(backupFile.getBackupStrategy())) { + if (StrUtil.isBlank(backupFile.getDirectoryName()) || StrUtil.isBlank(backupFile.getDumpFileName())) { + throw new BusinessException(CommonResponseEnum.FAIL, "Data Pump 备份记录缺少目录或文件名"); + } + } + if (BackupStrategyEnum.JDBC_EXPORT.name().equals(backupFile.getBackupStrategy()) + && StrUtil.isBlank(backupFile.getMetadataFilePath())) { + throw new BusinessException(CommonResponseEnum.FAIL, "JDBC_EXPORT 备份缺少元数据文件,不能恢复"); + } + } + + private DatabaseOperationTask buildRestoreTask(DatabaseRestoreParam.CreateParam param, DatabaseConnection connection, DatabaseBackupFile backupFile) { + DatabaseOperationTask task = new DatabaseOperationTask(); + task.setId(DatabaseOpsIdUtil.uuid()); + task.setTaskNo(DatabaseOpsIdUtil.taskNo("DBMSR")); + task.setConnectionId(connection.getId()); + task.setDbType(connection.getDbType()); + task.setOperationType(OperationTypeEnum.RESTORE.name()); + task.setBackupStrategy(backupFile.getBackupStrategy()); + task.setTaskStatus(TaskStatusEnum.WAITING.name()); + task.setSchemaName(resolveTargetSchemaName(param, connection)); + task.setTargetNamesJson(backupFile.getTargetNamesJson()); + task.setRequestParamJson(writeJsonWithoutPassword(param)); + task.setProgressPercent(BigDecimal.ZERO); + task.setState(DatabaseOpsConst.STATE_ENABLED); + task.setCreateTime(LocalDateTime.now()); + task.setUpdateTime(LocalDateTime.now()); + return task; + } + + private DatabaseRestoreRecord buildRestoreRecord(DatabaseRestoreParam.CreateParam param, DatabaseConnection connection, + DatabaseBackupFile backupFile, DatabaseOperationTask task) { + String restoreMode = resolveRestoreMode(param.getRestoreMode()); + DatabaseRestoreRecord record = new DatabaseRestoreRecord(); + record.setId(DatabaseOpsIdUtil.uuid()); + record.setTaskId(task.getId()); + record.setBackupFileId(backupFile.getId()); + record.setConnectionId(connection.getId()); + record.setDbType(connection.getDbType()); + record.setRestoreMode(restoreMode); + record.setTargetSchemaName(resolveTargetSchemaName(param, connection)); + record.setTargetNamesJson(backupFile.getTargetNamesJson()); + record.setTableExistsAction(restoreMode); + record.setOverwriteConfirmed(DatabaseOpsConst.CONFIRM_OVERWRITE.equals(param.getOverwriteConfirmText()) ? 1 : 0); + record.setState(DatabaseOpsConst.STATE_ENABLED); + record.setCreateTime(LocalDateTime.now()); + record.setUpdateTime(LocalDateTime.now()); + return record; + } + + private String resolveTargetSchemaName(DatabaseRestoreParam.CreateParam param, DatabaseConnection connection) { + if (DatabaseOpsConst.DB_TYPE_MYSQL.equals(connection.getDbType())) { + return StrUtil.blankToDefault(param.getTargetSchemaName(), connection.getDatabaseName()); + } + return StrUtil.blankToDefault(param.getTargetSchemaName(), connection.getSchemaName()); + } + + private DatabaseBackupFile requireBackupFile(String backupFileId) { + DatabaseBackupFile backupFile = databaseBackupFileService.getById(backupFileId); + if (backupFile == null || !Integer.valueOf(DatabaseOpsConst.STATE_ENABLED).equals(backupFile.getState())) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份文件不存在或已删除"); + } + return backupFile; + } + + private String resolveRestoreMode(String restoreMode) { + String value = StrUtil.blankToDefault(restoreMode, RestoreModeEnum.SKIP.name()).trim().toUpperCase(Locale.ROOT); + try { + return RestoreModeEnum.valueOf(value).name(); + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "不支持的恢复模式:" + restoreMode); + } + } + + private void markRunning(DatabaseOperationTask task) { + task.setTaskStatus(TaskStatusEnum.RUNNING.name()); + task.setStartedAt(LocalDateTime.now()); + task.setUpdateTime(LocalDateTime.now()); + databaseOperationTaskService.updateById(task); + } + + private void markSuccess(DatabaseOperationTask task, String message) { + task.setTaskStatus(TaskStatusEnum.SUCCESS.name()); + task.setResultMessage(message); + task.setProgressPercent(new BigDecimal("100.00")); + task.setFinishedAt(LocalDateTime.now()); + task.setUpdateTime(LocalDateTime.now()); + databaseOperationTaskService.updateById(task); + } + + private void markFail(DatabaseOperationTask task, String message) { + task.setTaskStatus(TaskStatusEnum.FAIL.name()); + task.setResultMessage(message); + task.setFinishedAt(LocalDateTime.now()); + task.setUpdateTime(LocalDateTime.now()); + databaseOperationTaskService.updateById(task); + } + + private String writeJsonWithoutPassword(DatabaseRestoreParam.CreateParam param) { + try { + DatabaseRestoreParam.CreateParam copy = new DatabaseRestoreParam.CreateParam(); + copy.setConnectionId(param.getConnectionId()); + copy.setBackupFileId(param.getBackupFileId()); + copy.setRestoreMode(param.getRestoreMode()); + copy.setTargetSchemaName(param.getTargetSchemaName()); + copy.setOverwriteConfirmText(param.getOverwriteConfirmText()); + return objectMapper.writeValueAsString(copy); + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.JSON_CONVERT_EXCEPTION, exception.getMessage()); + } + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlConnectionOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlConnectionOperator.java new file mode 100644 index 0000000..a53f34e --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlConnectionOperator.java @@ -0,0 +1,102 @@ +package com.njcn.gather.systemops.database.support.mysql; + +import cn.hutool.core.util.StrUtil; +import com.njcn.gather.systemops.database.constant.DatabaseOpsConst; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTableVO; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTestResultVO; +import com.njcn.gather.systemops.database.support.spi.DatabaseConnectionOperator; +import org.springframework.stereotype.Component; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * MySQL 连接能力实现。 + */ +@Component +public class MysqlConnectionOperator implements DatabaseConnectionOperator { + + @Override + public boolean support(String dbType) { + return DatabaseOpsConst.DB_TYPE_MYSQL.equalsIgnoreCase(dbType); + } + + @Override + public DatabaseTestResultVO test(DatabaseConnection connection, String password) { + DatabaseTestResultVO result = new DatabaseTestResultVO(); + try (Connection ignored = openConnection(connection, password)) { + result.setSuccess(true); + result.setMessage("连接成功"); + } catch (Exception exception) { + result.setSuccess(false); + result.setMessage(exception.getMessage()); + } + return result; + } + + @Override + public List listTables(DatabaseConnection connection, String password, String schemaOrDatabaseName) throws Exception { + String databaseName = StrUtil.blankToDefault(schemaOrDatabaseName, connection.getDatabaseName()); + String sql = "SELECT t.table_schema, t.table_name, t.auto_increment, t.update_time, " + + "t.data_length, t.engine, t.table_rows, t.table_comment, " + + "MAX(CASE WHEN c.extra LIKE '%auto_increment%' THEN 1 ELSE 0 END) AS has_auto_increment " + + "FROM information_schema.tables t " + + "LEFT JOIN information_schema.columns c " + + "ON t.table_schema = c.table_schema AND t.table_name = c.table_name " + + "WHERE t.table_schema = ? AND t.table_type = 'BASE TABLE' " + + "GROUP BY t.table_schema, t.table_name, t.auto_increment, t.update_time, " + + "t.data_length, t.engine, t.table_rows, t.table_comment " + + "ORDER BY t.table_name"; + try (Connection jdbcConnection = openConnection(connection, password); + PreparedStatement statement = jdbcConnection.prepareStatement(sql)) { + statement.setString(1, databaseName); + try (ResultSet resultSet = statement.executeQuery()) { + List result = new ArrayList<>(); + while (resultSet.next()) { + DatabaseTableVO table = new DatabaseTableVO(); + table.setOwner(resultSet.getString("table_schema").toUpperCase(Locale.ROOT)); + table.setTableName(resultSet.getString("table_name")); + if (resultSet.getInt("has_auto_increment") == 1) { + fillAutoIncrement(table, defaultZero(getLongValue(resultSet, "auto_increment"))); + } + Timestamp updateTime = resultSet.getTimestamp("update_time"); + table.setUpdateTime(updateTime == null ? null : updateTime.toLocalDateTime()); + table.setDataLength(getLongValue(resultSet, "data_length")); + table.setEngine(resultSet.getString("engine")); + table.setTableRows(getLongValue(resultSet, "table_rows")); + table.setComments(resultSet.getString("table_comment")); + result.add(table); + } + return result; + } + } + } + + private Long getLongValue(ResultSet resultSet, String columnName) throws Exception { + long value = resultSet.getLong(columnName); + return resultSet.wasNull() ? null : value; + } + + private Long defaultZero(Long value) { + return value == null ? 0L : value; + } + + private void fillAutoIncrement(DatabaseTableVO table, Long autoIncrement) { + table.setAutoIncrementValue(autoIncrement); + table.setAutoIncrement(autoIncrement); + } + + private Connection openConnection(DatabaseConnection connection, String password) throws Exception { + if (StrUtil.isBlank(password)) { + throw new IllegalArgumentException("数据库密码不能为空"); + } + return DriverManager.getConnection(MysqlJdbcUrlUtil.build(connection), connection.getUsername(), password); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcExportBackupOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcExportBackupOperator.java new file mode 100644 index 0000000..961ce2a --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcExportBackupOperator.java @@ -0,0 +1,146 @@ +package com.njcn.gather.systemops.database.support.mysql; + +import cn.hutool.core.util.StrUtil; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.systemops.database.component.JdbcExportComponent; +import com.njcn.gather.systemops.database.config.DbmsProperties; +import com.njcn.gather.systemops.database.mapper.DatabaseOperationTaskMapper; +import com.njcn.gather.systemops.database.pojo.enums.BackupModeEnum; +import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum; +import com.njcn.gather.systemops.database.pojo.enums.FileFormatEnum; +import com.njcn.gather.systemops.database.pojo.enums.TaskStatusEnum; +import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; +import com.njcn.gather.systemops.database.support.spi.DatabaseBackupOperator; +import com.njcn.gather.systemops.database.util.DatabaseChecksumUtil; +import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil; +import com.njcn.gather.systemops.database.util.DatabaseOpsIdUtil; +import com.njcn.gather.systemops.database.util.DatabasePathUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.time.LocalDateTime; +import java.util.Locale; +import java.util.stream.Stream; + +/** + * MySQL JDBC_EXPORT 大数据量备份实现。 + */ +@Component +@RequiredArgsConstructor +public class MysqlJdbcExportBackupOperator implements DatabaseBackupOperator { + + private final JdbcExportComponent jdbcExportComponent; + private final DbmsProperties dbmsProperties; + private final DatabaseOperationTaskMapper databaseOperationTaskMapper; + + @Override + public boolean support(String dbType, String backupStrategy) { + return "MYSQL".equalsIgnoreCase(dbType) && BackupStrategyEnum.JDBC_EXPORT.name().equals(backupStrategy); + } + + @Override + public DatabaseBackupFile executeBackup(DatabaseOperationTask task, DatabaseConnection connection, String password, + DatabaseBackupParam.CreateParam param) throws Exception { + Path backupDirectory = buildManagedPath(dbmsProperties.getBackup().getStoragePath(), task.getTaskNo()); + String metadataFileName = DatabaseFileNameUtil.appendTodayWithTask("mysql_jdbc_export_metadata.json", task.getTaskNo()); + Path metadataFilePath = backupDirectory.resolve(metadataFileName).normalize(); + int fetchSize = positiveOrDefault(dbmsProperties.getBackup().getMysqlFetchSize(), 1000); + long maxPartBytes = resolveMaxPartBytes(param); + try (Connection jdbcConnection = DriverManager.getConnection(MysqlJdbcUrlUtil.build(connection), connection.getUsername(), password)) { + jdbcExportComponent.exportMysqlCsvV2(jdbcConnection, connection.getDatabaseName(), task.getTaskNo(), param, + backupDirectory, metadataFilePath, fetchSize, maxPartBytes, () -> isTaskCancelled(task.getId())); + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.FAIL, exception.getMessage() + ",导出目录:" + backupDirectory); + } + return buildBackupFile(task, connection, param, backupDirectory, metadataFilePath); + } + + private boolean isTaskCancelled(String taskId) { + DatabaseOperationTask task = databaseOperationTaskMapper.selectById(taskId); + return task != null && TaskStatusEnum.CANCELLED.name().equals(task.getTaskStatus()); + } + + private DatabaseBackupFile buildBackupFile(DatabaseOperationTask task, DatabaseConnection connection, + DatabaseBackupParam.CreateParam param, Path backupDirectory, + Path metadataFilePath) throws Exception { + if (!Files.exists(metadataFilePath)) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份元数据文件未生成"); + } + DatabaseBackupFile file = new DatabaseBackupFile(); + file.setId(DatabaseOpsIdUtil.uuid()); + file.setTaskId(task.getId()); + file.setConnectionId(connection.getId()); + file.setDbType(connection.getDbType()); + file.setBackupStrategy(task.getBackupStrategy()); + file.setFileFormat(FileFormatEnum.CSV.name()); + file.setSchemaName(task.getSchemaName()); + file.setTargetNamesJson(task.getTargetNamesJson()); + file.setBackupMode(StrUtil.blankToDefault(param.getBackupMode(), BackupModeEnum.FULL_TABLE.name()).toUpperCase(Locale.ROOT)); + file.setBackupStartTime(param.getStartTime()); + file.setBackupEndTime(param.getEndTime()); + file.setTimeColumn(param.getTimeColumn()); + file.setDirectoryName(null); + file.setDumpFileName(null); + file.setLogFileName(null); + file.setFileName(backupDirectory.getFileName().toString()); + file.setFilePath(backupDirectory.toString()); + file.setLogFilePath(null); + file.setMetadataFilePath(metadataFilePath.toString()); + file.setFileSize(readDirectoryFileSize(backupDirectory)); + file.setChecksum(DatabaseChecksumUtil.sha256(metadataFilePath)); + file.setState(1); + file.setCreateTime(LocalDateTime.now()); + file.setUpdateTime(LocalDateTime.now()); + return file; + } + + private long resolveMaxPartBytes(DatabaseBackupParam.CreateParam param) { + Integer maxFileSizeMb = param.getMaxFileSizeMb(); + if (maxFileSizeMb == null || maxFileSizeMb <= 0) { + maxFileSizeMb = positiveOrDefault(dbmsProperties.getBackup().getDefaultMaxFileSizeMb(), 512); + } + return maxFileSizeMb.longValue() * 1024L * 1024L; + } + + private int positiveOrDefault(Integer value, int defaultValue) { + return value == null || value <= 0 ? defaultValue : value; + } + + private Long readDirectoryFileSize(Path directory) { + try { + if (directory != null && Files.exists(directory) && Files.isDirectory(directory)) { + final long[] total = new long[]{0L}; + try (Stream paths = Files.walk(directory)) { + paths.filter(path -> Files.exists(path) && !Files.isDirectory(path)) + .forEach(path -> { + try { + total[0] += Files.size(path); + } catch (Exception ignored) { + // 忽略单个文件大小读取失败,避免影响备份记录生成。 + } + }); + } + return total[0]; + } + } catch (Exception ignored) { + return null; + } + return null; + } + + private Path buildManagedPath(String rootPath, String directoryName) { + Path root = DatabasePathUtil.normalize(rootPath); + if (root == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份目录未配置"); + } + return root.resolve(directoryName).normalize(); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcExportRestoreOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcExportRestoreOperator.java new file mode 100644 index 0000000..ecf68d5 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcExportRestoreOperator.java @@ -0,0 +1,48 @@ +package com.njcn.gather.systemops.database.support.mysql; + +import com.njcn.gather.systemops.database.component.JdbcExportComponent; +import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum; +import com.njcn.gather.systemops.database.pojo.param.DatabaseRestoreParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; +import com.njcn.gather.systemops.database.pojo.po.DatabaseRestoreRecord; +import com.njcn.gather.systemops.database.service.DatabaseBackupFileService; +import com.njcn.gather.systemops.database.support.spi.DatabaseRestoreOperator; +import org.springframework.stereotype.Component; + +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; + +/** + * MySQL JDBC_EXPORT 恢复实现。 + */ +@Component +public class MysqlJdbcExportRestoreOperator implements DatabaseRestoreOperator { + + private final JdbcExportComponent jdbcExportComponent; + private final DatabaseBackupFileService databaseBackupFileService; + + public MysqlJdbcExportRestoreOperator(JdbcExportComponent jdbcExportComponent, + DatabaseBackupFileService databaseBackupFileService) { + this.jdbcExportComponent = jdbcExportComponent; + this.databaseBackupFileService = databaseBackupFileService; + } + + @Override + public boolean support(String dbType, String backupStrategy) { + return "MYSQL".equalsIgnoreCase(dbType) && BackupStrategyEnum.JDBC_EXPORT.name().equals(backupStrategy); + } + + @Override + public void executeRestore(DatabaseOperationTask task, DatabaseRestoreRecord record, DatabaseBackupFile backupFile, + DatabaseConnection connection, String password, DatabaseRestoreParam.CreateParam param) throws Exception { + Path dataFilePath = databaseBackupFileService.resolveManagedPath(backupFile, backupFile.getFilePath()); + Path metadataFilePath = databaseBackupFileService.resolveManagedPath(backupFile, backupFile.getMetadataFilePath()); + try (Connection jdbcConnection = DriverManager.getConnection(MysqlJdbcUrlUtil.build(connection), connection.getUsername(), password)) { + jdbcExportComponent.importCsv(jdbcConnection, dataFilePath, metadataFilePath, + connection.getDbType(), record.getRestoreMode(), null); + } + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcUrlUtil.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcUrlUtil.java new file mode 100644 index 0000000..8dd8eb7 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcUrlUtil.java @@ -0,0 +1,18 @@ +package com.njcn.gather.systemops.database.support.mysql; + +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; + +/** + * MySQL JDBC URL 构造工具。 + */ +public final class MysqlJdbcUrlUtil { + + private MysqlJdbcUrlUtil() { + } + + public static String build(DatabaseConnection connection) { + return "jdbc:mysql://" + connection.getHost() + ":" + connection.getPort() + "/" + connection.getDatabaseName() + + "?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai" + + "&useCursorFetch=true&connectTimeout=5000&socketTimeout=30000"; + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleConnectionOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleConnectionOperator.java new file mode 100644 index 0000000..db21bca --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleConnectionOperator.java @@ -0,0 +1,37 @@ +package com.njcn.gather.systemops.database.support.oracle; + +import com.njcn.gather.systemops.database.component.OracleJdbcComponent; +import com.njcn.gather.systemops.database.constant.DatabaseOpsConst; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTableVO; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTestResultVO; +import com.njcn.gather.systemops.database.support.spi.DatabaseConnectionOperator; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * Oracle 连接能力实现。 + */ +@Component +@RequiredArgsConstructor +public class OracleConnectionOperator implements DatabaseConnectionOperator { + + private final OracleJdbcComponent oracleJdbcComponent; + + @Override + public boolean support(String dbType) { + return DatabaseOpsConst.DB_TYPE_ORACLE.equalsIgnoreCase(dbType); + } + + @Override + public DatabaseTestResultVO test(DatabaseConnection connection, String password) { + return oracleJdbcComponent.test(connection, password); + } + + @Override + public List listTables(DatabaseConnection connection, String password, String schemaOrDatabaseName) throws Exception { + return oracleJdbcComponent.listTables(connection, password, schemaOrDatabaseName); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleDataPumpBackupOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleDataPumpBackupOperator.java new file mode 100644 index 0000000..daa3ca0 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleDataPumpBackupOperator.java @@ -0,0 +1,122 @@ +package com.njcn.gather.systemops.database.support.oracle; + +import cn.hutool.core.util.StrUtil; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.systemops.database.component.DataPumpCommandExecutor; +import com.njcn.gather.systemops.database.pojo.enums.BackupModeEnum; +import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum; +import com.njcn.gather.systemops.database.pojo.enums.FileFormatEnum; +import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; +import com.njcn.gather.systemops.database.service.DatabaseBackupFileService; +import com.njcn.gather.systemops.database.support.spi.DatabaseBackupOperator; +import com.njcn.gather.systemops.database.util.DatabaseChecksumUtil; +import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil; +import com.njcn.gather.systemops.database.util.DatabaseOpsIdUtil; +import com.njcn.gather.systemops.database.util.DatabasePathUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.Locale; + +/** + * Oracle DATA_PUMP 备份实现。 + */ +@Component +@RequiredArgsConstructor +public class OracleDataPumpBackupOperator implements DatabaseBackupOperator { + + private final DataPumpCommandExecutor dataPumpCommandExecutor; + private final DatabaseBackupFileService databaseBackupFileService; + + @Override + public boolean support(String dbType, String backupStrategy) { + return "ORACLE".equalsIgnoreCase(dbType) && BackupStrategyEnum.DATA_PUMP.name().equals(backupStrategy); + } + + @Override + public DatabaseBackupFile executeBackup(DatabaseOperationTask task, DatabaseConnection connection, String password, + DatabaseBackupParam.CreateParam param) { + String directoryName = StrUtil.blankToDefault(param.getDirectoryName(), connection.getDirectoryName()); + if (StrUtil.isBlank(directoryName)) { + throw new BusinessException(CommonResponseEnum.FAIL, "DATA_PUMP 备份需要 Oracle Directory 名称"); + } + String baseName = buildBaseFileName(connection, task); + String dumpFileName = DatabaseFileNameUtil.appendTodayWithTask(baseName + ".dmp", task.getTaskNo()); + String logFileName = DatabaseFileNameUtil.appendTodayWithTask(baseName + ".log", task.getTaskNo()); + DataPumpCommandExecutor.CommandResult commandResult = dataPumpCommandExecutor.expdp(connection, password, + directoryName, dumpFileName, logFileName, param.getTargetNames()); + if (!Boolean.TRUE.equals(commandResult.getSuccess())) { + throw new BusinessException(CommonResponseEnum.FAIL, "Data Pump 执行失败:" + commandResult.getOutput()); + } + if (StrUtil.isBlank(connection.getDirectoryPath())) { + throw new BusinessException(CommonResponseEnum.FAIL, "Data Pump 备份需要配置可管理的 directoryPath"); + } + Path dumpPath = buildManagedPath(connection.getDirectoryPath(), dumpFileName); + Path logPath = buildManagedPath(connection.getDirectoryPath(), logFileName); + return buildBackupFile(task, connection, param, FileFormatEnum.DMP.name(), dumpFileName, dumpPath, logFileName, logPath, null); + } + + private DatabaseBackupFile buildBackupFile(DatabaseOperationTask task, DatabaseConnection connection, + DatabaseBackupParam.CreateParam param, String fileFormat, String fileName, + Path filePath, String logFileName, Path logFilePath, Path metadataFilePath) { + if (filePath == null || !Files.exists(filePath)) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份文件未生成"); + } + DatabaseBackupFile file = new DatabaseBackupFile(); + file.setId(DatabaseOpsIdUtil.uuid()); + file.setTaskId(task.getId()); + file.setConnectionId(connection.getId()); + file.setDbType(connection.getDbType()); + file.setBackupStrategy(task.getBackupStrategy()); + file.setFileFormat(fileFormat); + file.setSchemaName(task.getSchemaName()); + file.setTargetNamesJson(task.getTargetNamesJson()); + file.setBackupMode(StrUtil.blankToDefault(param.getBackupMode(), BackupModeEnum.FULL_TABLE.name()).toUpperCase(Locale.ROOT)); + file.setBackupStartTime(param.getStartTime()); + file.setBackupEndTime(param.getEndTime()); + file.setTimeColumn(param.getTimeColumn()); + file.setDirectoryName(StrUtil.blankToDefault(param.getDirectoryName(), connection.getDirectoryName())); + file.setDumpFileName(FileFormatEnum.DMP.name().equals(fileFormat) ? fileName : null); + file.setLogFileName(logFileName); + file.setFileName(fileName); + file.setFilePath(filePath.toString()); + file.setLogFilePath(logFilePath == null ? null : logFilePath.toString()); + file.setMetadataFilePath(metadataFilePath == null ? null : metadataFilePath.toString()); + file.setFileSize(readFileSize(filePath)); + file.setChecksum(DatabaseChecksumUtil.sha256(filePath)); + file.setState(1); + file.setCreateTime(LocalDateTime.now()); + file.setUpdateTime(LocalDateTime.now()); + return file; + } + + private Long readFileSize(Path filePath) { + try { + if (filePath != null && Files.exists(filePath) && !Files.isDirectory(filePath)) { + return Files.size(filePath); + } + } catch (Exception ignored) { + return null; + } + return null; + } + + private Path buildManagedPath(String rootPath, String fileName) { + Path root = DatabasePathUtil.normalize(rootPath); + if (root == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份目录未配置"); + } + return root.resolve(fileName).normalize(); + } + + private String buildBaseFileName(DatabaseConnection connection, DatabaseOperationTask task) { + return connection.getSchemaName() + "_" + task.getBackupStrategy().toLowerCase(Locale.ROOT); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleDataPumpRestoreOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleDataPumpRestoreOperator.java new file mode 100644 index 0000000..e23096d --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleDataPumpRestoreOperator.java @@ -0,0 +1,47 @@ +package com.njcn.gather.systemops.database.support.oracle; + +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.systemops.database.component.DataPumpCommandExecutor; +import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum; +import com.njcn.gather.systemops.database.pojo.param.DatabaseRestoreParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; +import com.njcn.gather.systemops.database.pojo.po.DatabaseRestoreRecord; +import com.njcn.gather.systemops.database.support.spi.DatabaseRestoreOperator; +import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil; +import org.springframework.stereotype.Component; + +/** + * Oracle DATA_PUMP 恢复实现。 + */ +@Component +public class OracleDataPumpRestoreOperator implements DatabaseRestoreOperator { + + private final DataPumpCommandExecutor dataPumpCommandExecutor; + + public OracleDataPumpRestoreOperator(DataPumpCommandExecutor dataPumpCommandExecutor) { + this.dataPumpCommandExecutor = dataPumpCommandExecutor; + } + + @Override + public boolean support(String dbType, String backupStrategy) { + return "ORACLE".equalsIgnoreCase(dbType) && BackupStrategyEnum.DATA_PUMP.name().equals(backupStrategy); + } + + @Override + public void executeRestore(DatabaseOperationTask task, DatabaseRestoreRecord record, DatabaseBackupFile backupFile, + DatabaseConnection connection, String password, DatabaseRestoreParam.CreateParam param) { + DataPumpCommandExecutor.CommandResult result = dataPumpCommandExecutor.impdp(connection, password, + backupFile.getDirectoryName(), backupFile.getDumpFileName(), buildRestoreLogName(task), + record.getTableExistsAction()); + if (!Boolean.TRUE.equals(result.getSuccess())) { + throw new BusinessException(CommonResponseEnum.FAIL, "Data Pump 恢复失败:" + result.getOutput()); + } + } + + private String buildRestoreLogName(DatabaseOperationTask task) { + return DatabaseFileNameUtil.appendTodayWithTask(task.getTaskNo() + "_restore.log", task.getTaskNo()); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleJdbcExportBackupOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleJdbcExportBackupOperator.java new file mode 100644 index 0000000..d4f73b5 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleJdbcExportBackupOperator.java @@ -0,0 +1,116 @@ +package com.njcn.gather.systemops.database.support.oracle; + +import cn.hutool.core.util.StrUtil; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.systemops.database.component.JdbcExportComponent; +import com.njcn.gather.systemops.database.component.OracleJdbcComponent; +import com.njcn.gather.systemops.database.config.DbmsProperties; +import com.njcn.gather.systemops.database.pojo.enums.BackupModeEnum; +import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum; +import com.njcn.gather.systemops.database.pojo.enums.FileFormatEnum; +import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; +import com.njcn.gather.systemops.database.support.spi.DatabaseBackupOperator; +import com.njcn.gather.systemops.database.util.DatabaseChecksumUtil; +import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil; +import com.njcn.gather.systemops.database.util.DatabaseOpsIdUtil; +import com.njcn.gather.systemops.database.util.DatabasePathUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.time.LocalDateTime; +import java.util.Locale; + +/** + * Oracle JDBC_EXPORT 备份实现。 + */ +@Component +@RequiredArgsConstructor +public class OracleJdbcExportBackupOperator implements DatabaseBackupOperator { + + private final JdbcExportComponent jdbcExportComponent; + private final OracleJdbcComponent oracleJdbcComponent; + private final DbmsProperties dbmsProperties; + + @Override + public boolean support(String dbType, String backupStrategy) { + return "ORACLE".equalsIgnoreCase(dbType) && BackupStrategyEnum.JDBC_EXPORT.name().equals(backupStrategy); + } + + @Override + public DatabaseBackupFile executeBackup(DatabaseOperationTask task, DatabaseConnection connection, String password, + DatabaseBackupParam.CreateParam param) throws Exception { + String baseName = buildBaseFileName(connection, task); + String fileName = DatabaseFileNameUtil.appendTodayWithTask(baseName + ".csv", task.getTaskNo()); + String metadataFileName = DatabaseFileNameUtil.appendTodayWithTask(baseName + "_metadata.json", task.getTaskNo()); + Path dataFilePath = buildManagedPath(dbmsProperties.getBackup().getStoragePath(), fileName); + Path metadataFilePath = buildManagedPath(dbmsProperties.getBackup().getStoragePath(), metadataFileName); + try (Connection jdbcConnection = oracleJdbcComponent.openConnection(connection, password)) { + jdbcExportComponent.exportCsv(jdbcConnection, connection.getSchemaName(), param, dataFilePath, metadataFilePath); + } + return buildBackupFile(task, connection, param, fileName, dataFilePath, metadataFilePath); + } + + private DatabaseBackupFile buildBackupFile(DatabaseOperationTask task, DatabaseConnection connection, + DatabaseBackupParam.CreateParam param, String fileName, Path filePath, + Path metadataFilePath) { + if (filePath == null || !Files.exists(filePath)) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份文件未生成"); + } + DatabaseBackupFile file = new DatabaseBackupFile(); + file.setId(DatabaseOpsIdUtil.uuid()); + file.setTaskId(task.getId()); + file.setConnectionId(connection.getId()); + file.setDbType(connection.getDbType()); + file.setBackupStrategy(task.getBackupStrategy()); + file.setFileFormat(FileFormatEnum.CSV.name()); + file.setSchemaName(task.getSchemaName()); + file.setTargetNamesJson(task.getTargetNamesJson()); + file.setBackupMode(StrUtil.blankToDefault(param.getBackupMode(), BackupModeEnum.FULL_TABLE.name()).toUpperCase(Locale.ROOT)); + file.setBackupStartTime(param.getStartTime()); + file.setBackupEndTime(param.getEndTime()); + file.setTimeColumn(param.getTimeColumn()); + file.setDirectoryName(null); + file.setDumpFileName(null); + file.setLogFileName(null); + file.setFileName(fileName); + file.setFilePath(filePath.toString()); + file.setLogFilePath(null); + file.setMetadataFilePath(metadataFilePath.toString()); + file.setFileSize(readFileSize(filePath)); + file.setChecksum(DatabaseChecksumUtil.sha256(filePath)); + file.setState(1); + file.setCreateTime(LocalDateTime.now()); + file.setUpdateTime(LocalDateTime.now()); + return file; + } + + private Long readFileSize(Path filePath) { + try { + if (filePath != null && Files.exists(filePath) && !Files.isDirectory(filePath)) { + return Files.size(filePath); + } + } catch (Exception ignored) { + return null; + } + return null; + } + + private Path buildManagedPath(String rootPath, String fileName) { + Path root = DatabasePathUtil.normalize(rootPath); + if (root == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份目录未配置"); + } + return root.resolve(fileName).normalize(); + } + + private String buildBaseFileName(DatabaseConnection connection, DatabaseOperationTask task) { + return connection.getSchemaName() + "_" + task.getBackupStrategy().toLowerCase(Locale.ROOT); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleJdbcExportRestoreOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleJdbcExportRestoreOperator.java new file mode 100644 index 0000000..bafa664 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleJdbcExportRestoreOperator.java @@ -0,0 +1,51 @@ +package com.njcn.gather.systemops.database.support.oracle; + +import com.njcn.gather.systemops.database.component.JdbcExportComponent; +import com.njcn.gather.systemops.database.component.OracleJdbcComponent; +import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum; +import com.njcn.gather.systemops.database.pojo.param.DatabaseRestoreParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; +import com.njcn.gather.systemops.database.pojo.po.DatabaseRestoreRecord; +import com.njcn.gather.systemops.database.service.DatabaseBackupFileService; +import com.njcn.gather.systemops.database.support.spi.DatabaseRestoreOperator; +import org.springframework.stereotype.Component; + +import java.nio.file.Path; +import java.sql.Connection; + +/** + * Oracle JDBC_EXPORT 恢复实现。 + */ +@Component +public class OracleJdbcExportRestoreOperator implements DatabaseRestoreOperator { + + private final JdbcExportComponent jdbcExportComponent; + private final OracleJdbcComponent oracleJdbcComponent; + private final DatabaseBackupFileService databaseBackupFileService; + + public OracleJdbcExportRestoreOperator(JdbcExportComponent jdbcExportComponent, + OracleJdbcComponent oracleJdbcComponent, + DatabaseBackupFileService databaseBackupFileService) { + this.jdbcExportComponent = jdbcExportComponent; + this.oracleJdbcComponent = oracleJdbcComponent; + this.databaseBackupFileService = databaseBackupFileService; + } + + @Override + public boolean support(String dbType, String backupStrategy) { + return "ORACLE".equalsIgnoreCase(dbType) && BackupStrategyEnum.JDBC_EXPORT.name().equals(backupStrategy); + } + + @Override + public void executeRestore(DatabaseOperationTask task, DatabaseRestoreRecord record, DatabaseBackupFile backupFile, + DatabaseConnection connection, String password, DatabaseRestoreParam.CreateParam param) throws Exception { + Path dataFilePath = databaseBackupFileService.resolveManagedPath(backupFile, backupFile.getFilePath()); + Path metadataFilePath = databaseBackupFileService.resolveManagedPath(backupFile, backupFile.getMetadataFilePath()); + try (Connection jdbcConnection = oracleJdbcComponent.openConnection(connection, password)) { + jdbcExportComponent.importCsv(jdbcConnection, dataFilePath, metadataFilePath, + connection.getDbType(), record.getRestoreMode(), record.getTargetSchemaName()); + } + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseBackupOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseBackupOperator.java new file mode 100644 index 0000000..2b77599 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseBackupOperator.java @@ -0,0 +1,17 @@ +package com.njcn.gather.systemops.database.support.spi; + +import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; + +/** + * 按数据库类型与备份策略隔离备份执行能力。 + */ +public interface DatabaseBackupOperator { + + boolean support(String dbType, String backupStrategy); + + DatabaseBackupFile executeBackup(DatabaseOperationTask task, DatabaseConnection connection, String password, + DatabaseBackupParam.CreateParam param) throws Exception; +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseConnectionOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseConnectionOperator.java new file mode 100644 index 0000000..dd2075c --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseConnectionOperator.java @@ -0,0 +1,19 @@ +package com.njcn.gather.systemops.database.support.spi; + +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTableVO; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTestResultVO; + +import java.util.List; + +/** + * 按数据库类型隔离连接测试与表查询能力。 + */ +public interface DatabaseConnectionOperator { + + boolean support(String dbType); + + DatabaseTestResultVO test(DatabaseConnection connection, String password); + + List listTables(DatabaseConnection connection, String password, String schemaOrDatabaseName) throws Exception; +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseOperatorRegistry.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseOperatorRegistry.java new file mode 100644 index 0000000..dc68664 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseOperatorRegistry.java @@ -0,0 +1,43 @@ +package com.njcn.gather.systemops.database.support.spi; + +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 数据库能力路由注册器。 + */ +@Component +@RequiredArgsConstructor +public class DatabaseOperatorRegistry { + + private final List connectionOperators; + private final List backupOperators; + private final List restoreOperators; + + public DatabaseConnectionOperator getConnectionOperator(String dbType) { + return connectionOperators.stream() + .filter(operator -> operator.support(dbType)) + .findFirst() + .orElseThrow(() -> new BusinessException(CommonResponseEnum.FAIL, "暂不支持的数据库类型:" + dbType)); + } + + public DatabaseBackupOperator getBackupOperator(String dbType, String backupStrategy) { + return backupOperators.stream() + .filter(operator -> operator.support(dbType, backupStrategy)) + .findFirst() + .orElseThrow(() -> new BusinessException(CommonResponseEnum.FAIL, + "暂不支持的备份能力:" + dbType + "/" + backupStrategy)); + } + + public DatabaseRestoreOperator getRestoreOperator(String dbType, String backupStrategy) { + return restoreOperators.stream() + .filter(operator -> operator.support(dbType, backupStrategy)) + .findFirst() + .orElseThrow(() -> new BusinessException(CommonResponseEnum.FAIL, + "暂不支持的恢复能力:" + dbType + "/" + backupStrategy)); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseRestoreOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseRestoreOperator.java new file mode 100644 index 0000000..ed433ba --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseRestoreOperator.java @@ -0,0 +1,18 @@ +package com.njcn.gather.systemops.database.support.spi; + +import com.njcn.gather.systemops.database.pojo.param.DatabaseRestoreParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; +import com.njcn.gather.systemops.database.pojo.po.DatabaseRestoreRecord; + +/** + * 按数据库类型与备份策略隔离恢复执行能力。 + */ +public interface DatabaseRestoreOperator { + + boolean support(String dbType, String backupStrategy); + + void executeRestore(DatabaseOperationTask task, DatabaseRestoreRecord record, DatabaseBackupFile backupFile, + DatabaseConnection connection, String password, DatabaseRestoreParam.CreateParam param) throws Exception; +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/util/DatabaseChecksumUtil.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/util/DatabaseChecksumUtil.java new file mode 100644 index 0000000..26a2298 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/util/DatabaseChecksumUtil.java @@ -0,0 +1,37 @@ +package com.njcn.gather.systemops.database.util; + +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; + +/** + * 文件校验工具。 + */ +public final class DatabaseChecksumUtil { + + private DatabaseChecksumUtil() { + } + + public static String sha256(Path path) { + if (path == null || !Files.exists(path) || Files.isDirectory(path)) { + return null; + } + try (InputStream inputStream = Files.newInputStream(path)) { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] buffer = new byte[8192]; + int length; + while ((length = inputStream.read(buffer)) != -1) { + digest.update(buffer, 0, length); + } + byte[] bytes = digest.digest(); + StringBuilder builder = new StringBuilder(); + for (byte item : bytes) { + builder.append(String.format("%02x", item)); + } + return builder.toString(); + } catch (Exception exception) { + return null; + } + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/util/DatabaseFileNameUtil.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/util/DatabaseFileNameUtil.java new file mode 100644 index 0000000..7b7de9e --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/util/DatabaseFileNameUtil.java @@ -0,0 +1,37 @@ +package com.njcn.gather.systemops.database.util; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * 数据库运维文件名工具。 + */ +public final class DatabaseFileNameUtil { + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + + private DatabaseFileNameUtil() { + } + + public static String appendTodayWithTask(String fileName, String taskNo) { + String datedName = appendDate(fileName, LocalDate.now()); + int dotIndex = datedName.lastIndexOf('.'); + if (dotIndex > 0) { + return datedName.substring(0, dotIndex) + "_" + taskNo + datedName.substring(dotIndex); + } + return datedName + "_" + taskNo; + } + + private static String appendDate(String fileName, LocalDate date) { + if (fileName == null || date == null) { + return fileName; + } + String dateText = DATE_FORMATTER.format(date); + int separatorIndex = Math.max(fileName.lastIndexOf('/'), fileName.lastIndexOf('\\')); + int dotIndex = fileName.lastIndexOf('.'); + if (dotIndex > separatorIndex) { + return fileName.substring(0, dotIndex) + "_" + dateText + fileName.substring(dotIndex); + } + return fileName + "_" + dateText; + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/util/DatabaseOpsIdUtil.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/util/DatabaseOpsIdUtil.java new file mode 100644 index 0000000..f479a58 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/util/DatabaseOpsIdUtil.java @@ -0,0 +1,24 @@ +package com.njcn.gather.systemops.database.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +/** + * 数据库运维编号工具。 + */ +public final class DatabaseOpsIdUtil { + + private static final DateTimeFormatter TASK_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"); + + private DatabaseOpsIdUtil() { + } + + public static String uuid() { + return UUID.randomUUID().toString().replace("-", ""); + } + + public static String taskNo(String prefix) { + return prefix + LocalDateTime.now().format(TASK_FORMATTER); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/util/DatabasePathUtil.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/util/DatabasePathUtil.java new file mode 100644 index 0000000..39fd5f7 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/util/DatabasePathUtil.java @@ -0,0 +1,31 @@ +package com.njcn.gather.systemops.database.util; + +import cn.hutool.core.util.StrUtil; + +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * 数据库运维文件路径工具。 + */ +public final class DatabasePathUtil { + + private DatabasePathUtil() { + } + + public static Path normalize(String filePath) { + if (StrUtil.isBlank(filePath)) { + return null; + } + return Paths.get(filePath).toAbsolutePath().normalize(); + } + + public static boolean isUnder(Path path, Path root) { + if (path == null || root == null) { + return false; + } + Path normalizedPath = path.toAbsolutePath().normalize(); + Path normalizedRoot = root.toAbsolutePath().normalize(); + return normalizedPath.startsWith(normalizedRoot); + } +} diff --git a/system-ops/dbms/src/main/resources/sql/system-ops/dbms-database-ops-init.sql b/system-ops/dbms/src/main/resources/sql/system-ops/dbms-database-ops-init.sql new file mode 100644 index 0000000..13d965b --- /dev/null +++ b/system-ops/dbms/src/main/resources/sql/system-ops/dbms-database-ops-init.sql @@ -0,0 +1,119 @@ +CREATE TABLE IF NOT EXISTS `dbms_connection` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键', + `connection_name` VARCHAR(100) NOT NULL COMMENT '连接名称', + `db_type` VARCHAR(32) NOT NULL DEFAULT 'ORACLE' COMMENT '数据库类型:ORACLE,后续可扩展 MYSQL、INFLUXDB', + `host` VARCHAR(255) NOT NULL COMMENT '数据库主机地址', + `port` INT NOT NULL COMMENT '数据库端口', + `connect_type` VARCHAR(32) NULL COMMENT '连接类型:SERVICE_NAME、SID,Oracle 使用', + `service_name` VARCHAR(128) NULL COMMENT '服务名,Oracle SERVICE_NAME 模式使用', + `sid` VARCHAR(128) NULL COMMENT 'SID,Oracle SID 模式使用', + `database_name` VARCHAR(128) NULL COMMENT '数据库名或实例名,预留给 MySQL 等数据库使用', + `schema_name` VARCHAR(128) NULL COMMENT '默认 Schema,Oracle 使用', + `username` VARCHAR(128) NOT NULL COMMENT '用户名', + `password_cipher` VARCHAR(1000) NULL COMMENT '保存的数据库密码;为空表示不保存密码,执行时临时输入', + `save_password` TINYINT NOT NULL DEFAULT 1 COMMENT '是否保存密码:0-否,1-是', + `directory_name` VARCHAR(128) NULL COMMENT '默认数据库目录对象名称,Oracle Data Pump 使用', + `directory_path` VARCHAR(500) NULL COMMENT '目录对象对应物理路径,仅用于展示和校验', + `extra_config_json` JSON NULL COMMENT '扩展配置 JSON,用于保存不同数据库的差异配置', + `remark` VARCHAR(500) NULL COMMENT '备注', + `last_test_status` VARCHAR(32) NULL COMMENT '最近连接测试状态:SUCCESS、FAIL', + `last_test_message` VARCHAR(1000) NULL COMMENT '最近连接测试结果说明', + `last_test_time` DATETIME NULL COMMENT '最近连接测试时间', + `state` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-删除,1-正常', + `create_by` VARCHAR(64) NULL COMMENT '创建人', + `create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) NULL COMMENT '更新人', + `update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_dbms_connection_state` (`state`), + KEY `idx_dbms_connection_db_type` (`db_type`), + KEY `idx_dbms_connection_name` (`connection_name`), + KEY `idx_dbms_connection_schema` (`schema_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库连接配置表'; + +CREATE TABLE IF NOT EXISTS `dbms_operation_task` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键', + `task_no` VARCHAR(64) NOT NULL COMMENT '任务编号', + `connection_id` VARCHAR(64) NOT NULL COMMENT '数据库连接配置 ID', + `db_type` VARCHAR(32) NOT NULL COMMENT '数据库类型:ORACLE,后续可扩展 MYSQL、INFLUXDB', + `operation_type` VARCHAR(32) NOT NULL COMMENT '操作类型:BACKUP、RESTORE、DELETE', + `backup_strategy` VARCHAR(32) NULL COMMENT '备份策略:DATA_PUMP、JDBC_EXPORT', + `task_status` VARCHAR(32) NOT NULL DEFAULT 'WAITING' COMMENT '任务状态:WAITING、RUNNING、SUCCESS、FAIL、CANCELLED', + `schema_name` VARCHAR(128) NULL COMMENT '操作 Schema', + `target_names_json` JSON NULL COMMENT '操作对象名称列表 JSON,例如表名列表', + `request_param_json` JSON NULL COMMENT '请求参数快照 JSON,不保存运行时密码', + `result_message` VARCHAR(2000) NULL COMMENT '执行结果或失败原因', + `progress_percent` DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT '任务进度百分比', + `started_at` DATETIME NULL COMMENT '任务开始时间', + `finished_at` DATETIME NULL COMMENT '任务结束时间', + `state` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-删除,1-正常', + `create_by` VARCHAR(64) NULL COMMENT '创建人', + `create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) NULL COMMENT '更新人', + `update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_dbms_operation_task_no` (`task_no`), + KEY `idx_dbms_operation_connection` (`connection_id`), + KEY `idx_dbms_operation_db_type` (`db_type`), + KEY `idx_dbms_operation_type_status` (`operation_type`, `task_status`), + KEY `idx_dbms_operation_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库运维任务表'; + +CREATE TABLE IF NOT EXISTS `dbms_backup_file` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键', + `task_id` VARCHAR(64) NOT NULL COMMENT '备份任务 ID', + `connection_id` VARCHAR(64) NOT NULL COMMENT '数据库连接配置 ID', + `db_type` VARCHAR(32) NOT NULL COMMENT '数据库类型:ORACLE,后续可扩展 MYSQL、INFLUXDB', + `backup_strategy` VARCHAR(32) NOT NULL COMMENT '备份策略:DATA_PUMP、JDBC_EXPORT', + `file_format` VARCHAR(32) NOT NULL COMMENT '文件格式:DMP、SQL、CSV', + `schema_name` VARCHAR(128) NULL COMMENT '备份 Schema', + `target_names_json` JSON NULL COMMENT '备份对象名称列表 JSON,例如表名列表', + `backup_mode` VARCHAR(32) NOT NULL DEFAULT 'FULL_TABLE' COMMENT '备份模式:FULL_TABLE、TIME_RANGE、SIZE_SPLIT', + `backup_start_time` DATETIME NULL COMMENT '按时间备份开始时间', + `backup_end_time` DATETIME NULL COMMENT '按时间备份结束时间', + `time_column` VARCHAR(128) NULL COMMENT '按时间备份使用的时间字段', + `directory_name` VARCHAR(128) NULL COMMENT '数据库目录对象名称,Oracle Data Pump 使用', + `dump_file_name` VARCHAR(255) NULL COMMENT 'Data Pump dump 文件名', + `log_file_name` VARCHAR(255) NULL COMMENT 'Data Pump log 文件名', + `file_name` VARCHAR(255) NOT NULL COMMENT '主备份文件名,需包含 _yyyyMMdd', + `file_path` VARCHAR(1000) NOT NULL COMMENT '服务端记录的备份文件路径或目录对象映射路径', + `log_file_path` VARCHAR(1000) NULL COMMENT '备份日志文件路径', + `metadata_file_path` VARCHAR(1000) NULL COMMENT 'JDBC_EXPORT 元数据文件路径', + `file_size` BIGINT NULL COMMENT '文件大小,单位字节', + `checksum` VARCHAR(128) NULL COMMENT '文件校验值', + `state` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-删除,1-正常', + `create_by` VARCHAR(64) NULL COMMENT '创建人', + `create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) NULL COMMENT '更新人', + `update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_dbms_backup_task` (`task_id`), + KEY `idx_dbms_backup_connection` (`connection_id`), + KEY `idx_dbms_backup_db_type` (`db_type`), + KEY `idx_dbms_backup_strategy` (`backup_strategy`), + KEY `idx_dbms_backup_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库备份文件记录表'; + +CREATE TABLE IF NOT EXISTS `dbms_restore_record` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键', + `task_id` VARCHAR(64) NOT NULL COMMENT '恢复任务 ID', + `backup_file_id` VARCHAR(64) NOT NULL COMMENT '备份文件 ID', + `connection_id` VARCHAR(64) NOT NULL COMMENT '目标数据库连接配置 ID', + `db_type` VARCHAR(32) NOT NULL COMMENT '数据库类型:ORACLE,后续可扩展 MYSQL、INFLUXDB', + `restore_mode` VARCHAR(32) NOT NULL DEFAULT 'SKIP' COMMENT '恢复模式:SKIP、APPEND、TRUNCATE、REPLACE', + `target_schema_name` VARCHAR(128) NULL COMMENT '目标 Schema', + `target_names_json` JSON NULL COMMENT '恢复对象名称列表 JSON,例如表名列表', + `table_exists_action` VARCHAR(32) NULL COMMENT 'Data Pump TABLE_EXISTS_ACTION:SKIP、APPEND、TRUNCATE、REPLACE', + `overwrite_confirmed` TINYINT NOT NULL DEFAULT 0 COMMENT '是否已确认覆盖类操作:0-否,1-是', + `result_message` VARCHAR(2000) NULL COMMENT '恢复结果说明', + `state` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-删除,1-正常', + `create_by` VARCHAR(64) NULL COMMENT '创建人', + `create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) NULL COMMENT '更新人', + `update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_dbms_restore_task` (`task_id`), + KEY `idx_dbms_restore_backup_file` (`backup_file_id`), + KEY `idx_dbms_restore_connection` (`connection_id`), + KEY `idx_dbms_restore_db_type` (`db_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库恢复记录表'; diff --git a/system-ops/dbms/src/main/resources/sql/system-ops/system-ops-init.sql b/system-ops/dbms/src/main/resources/sql/system-ops/system-ops-init.sql new file mode 100644 index 0000000..d57c983 --- /dev/null +++ b/system-ops/dbms/src/main/resources/sql/system-ops/system-ops-init.sql @@ -0,0 +1,14 @@ +INSERT INTO `cn_tool`.`sys_function` +(`Id`, `Pid`, `Pids`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `Type`, `Remark`, `State`, `Create_By`, `Create_Time`, `Update_By`, `Update_Time`) +VALUES +('9f3b2c7a6e8d4b91a5c0f2d7e6a3b841', '0', '0', '系统运维', 'systemOps', '/systemOps', '/systemOps/index', 'Aim', 50, 0, '系统运维', 1, 'f8516cc81d964cd8b4b771a3b3985cd4', '2026-05-20 10:00:00', 'f8516cc81d964cd8b4b771a3b3985cd4', '2026-05-20 10:00:00'); + +INSERT INTO `cn_tool`.`sys_function` +(`Id`, `Pid`, `Pids`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `Type`, `Remark`, `State`, `Create_By`, `Create_Time`, `Update_By`, `Update_Time`) +VALUES +('2a7e5d9c1f4b4386b0c9e6f3a8d21754', '9f3b2c7a6e8d4b91a5c0f2d7e6a3b841', '0,9f3b2c7a6e8d4b91a5c0f2d7e6a3b841', '数据库监控', 'database', '/systemOps/database', '/systemOps/database/index', 'Monitor', 100, 0, '数据库监控', 1, 'f8516cc81d964cd8b4b771a3b3985cd4', '2026-05-20 10:10:00', 'f8516cc81d964cd8b4b771a3b3985cd4', '2026-05-20 10:10:00'); + +INSERT INTO `cn_tool`.`sys_function` +(`Id`, `Pid`, `Pids`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `Type`, `Remark`, `State`, `Create_By`, `Create_Time`, `Update_By`, `Update_Time`) +VALUES +('7c6d4a1e9b2f43c8a5e0d3f6b9c21875', '9f3b2c7a6e8d4b91a5c0f2d7e6a3b841', '0,9f3b2c7a6e8d4b91a5c0f2d7e6a3b841', '系统部署', 'deploy', '/systemOps/deploy', '/systemOps/deploy/index', 'Upload', 110, 0, '系统部署', 1, 'f8516cc81d964cd8b4b771a3b3985cd4', '2026-05-20 10:20:00', 'f8516cc81d964cd8b4b771a3b3985cd4', '2026-05-20 10:20:00'); diff --git a/tools/add-data/src/main/java/com/njcn/gather/tool/adddata/component/AddDataValueGenerator.java b/tools/add-data/src/main/java/com/njcn/gather/tool/adddata/component/AddDataValueGenerator.java index e51324d..784e8ef 100644 --- a/tools/add-data/src/main/java/com/njcn/gather/tool/adddata/component/AddDataValueGenerator.java +++ b/tools/add-data/src/main/java/com/njcn/gather/tool/adddata/component/AddDataValueGenerator.java @@ -223,7 +223,8 @@ public class AddDataValueGenerator { if (baseValue == null) { throw new IllegalStateException("派生字段缺少主值:" + column); } - double factor = noise(state.sharedSeed + column.hashCode(), 0.01D, 0.05D); + String baseColumn = resolveDerivedBaseColumn(column, metricType); + double factor = noise(state.sharedSeed + baseColumn.hashCode(), 0.01D, 0.05D); double delta = Math.max(Math.abs(baseValue) * factor, 0.005D); double value; if (MetricType.MAX.equals(metricType)) { @@ -239,6 +240,16 @@ public class AddDataValueGenerator { return round(value, 4); } + private String resolveDerivedBaseColumn(String column, MetricType metricType) { + if (MetricType.MAX.equals(metricType)) { + return removeSuffix(column, SUFFIX_MAX); + } + if (MetricType.MIN.equals(metricType)) { + return removeSuffix(column, SUFFIX_MIN); + } + return removeSuffix(column, SUFFIX_CP95); + } + /** * 构建同源基础状态。 * diff --git a/tools/add-data/src/test/java/com/njcn/gather/tool/adddata/component/AddDataValueGeneratorTest.java b/tools/add-data/src/test/java/com/njcn/gather/tool/adddata/component/AddDataValueGeneratorTest.java index cab4ddd..99e5f5c 100644 --- a/tools/add-data/src/test/java/com/njcn/gather/tool/adddata/component/AddDataValueGeneratorTest.java +++ b/tools/add-data/src/test/java/com/njcn/gather/tool/adddata/component/AddDataValueGeneratorTest.java @@ -25,4 +25,24 @@ class AddDataValueGeneratorTest { Assertions.assertEquals(0, row.get(3)); } + + @Test + void shouldKeepStatValuesOrderedForSameTimeAndMetric() { + AddDataValueGenerator generator = new AddDataValueGenerator(); + AddDataTableDefinition definition = new AddDataTableDefinition("data_v", + Arrays.asList("TIMEID", "LINEID", "PHASIC_TYPE", "QUALITYFLAG", + "RMS", "RMS_MAX", "RMS_CP95", "RMS_MIN"), + Arrays.asList("A"), 100, AddDataTableDefinition.TimeAxisType.REQUEST_INTERVAL); + + List row = generator.generateRow(definition, "line-001", + LocalDateTime.of(2026, 5, 18, 10, 0, 0), "A"); + + double avg = ((Number) row.get(4)).doubleValue(); + double max = ((Number) row.get(5)).doubleValue(); + double cp95 = ((Number) row.get(6)).doubleValue(); + double min = ((Number) row.get(7)).doubleValue(); + Assertions.assertTrue(max >= cp95, "MAX should be greater than or equal to CP95"); + Assertions.assertTrue(cp95 >= avg, "CP95 should be greater than or equal to AVG"); + Assertions.assertTrue(avg >= min, "AVG should be greater than or equal to MIN"); + } } diff --git a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/controller/AddLedgerController.java b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/controller/AddLedgerController.java index 1ec5e3d..510d5c5 100644 --- a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/controller/AddLedgerController.java +++ b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/controller/AddLedgerController.java @@ -6,10 +6,13 @@ 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.tool.addledger.pojo.param.AddDeviceUnitSaveParam; import com.njcn.gather.tool.addledger.pojo.param.AddLedgerEngineeringSaveParam; import com.njcn.gather.tool.addledger.pojo.param.AddLedgerEquipmentSaveParam; import com.njcn.gather.tool.addledger.pojo.param.AddLedgerLineSaveParam; import com.njcn.gather.tool.addledger.pojo.param.AddLedgerProjectSaveParam; +import com.njcn.gather.tool.addledger.pojo.po.AddOverlimitPO; +import com.njcn.gather.tool.addledger.pojo.vo.AddDeviceUnitVO; import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerDetailVO; import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerTreeNodeVO; import com.njcn.gather.tool.addledger.service.AddLedgerService; @@ -93,6 +96,26 @@ public class AddLedgerController extends BaseController { return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); } + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询设备单位") + @GetMapping("/equipment/unit") + public HttpResult getDeviceUnit(@RequestParam("devId") String devId) { + String methodDescribe = getMethodDescribe("getDeviceUnit"); + LogUtil.njcnDebug(log, "{},开始查询设备单位,devId={}", methodDescribe, devId); + AddDeviceUnitVO result = addLedgerService.getDeviceUnit(devId); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE) + @ApiOperation("保存设备单位") + @PostMapping("/equipment/unit/save") + public HttpResult saveDeviceUnit(@RequestBody @Validated AddDeviceUnitSaveParam param) { + String methodDescribe = getMethodDescribe("saveDeviceUnit"); + LogUtil.njcnDebug(log, "{},开始保存设备单位,devId={}", methodDescribe, param.getDevId()); + AddDeviceUnitVO result = addLedgerService.saveDeviceUnit(param); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD) @ApiOperation("新增或保存测点") @PostMapping("/line/save") @@ -103,6 +126,16 @@ public class AddLedgerController extends BaseController { return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); } + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询监测点限值") + @GetMapping("/line/overlimit") + public HttpResult getLineOverlimit(@RequestParam("lineId") String lineId) { + String methodDescribe = getMethodDescribe("getLineOverlimit"); + LogUtil.njcnDebug(log, "{},开始查询监测点限值,lineId={}", methodDescribe, lineId); + AddOverlimitPO result = addLedgerService.getLineOverlimit(lineId); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + @OperateInfo(info = LogEnum.BUSINESS_COMMON) @ApiOperation("查询设备可用线路号") @GetMapping("/line/availableLineNos") diff --git a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/mapper/AddDeviceUnitMapper.java b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/mapper/AddDeviceUnitMapper.java new file mode 100644 index 0000000..bdc3f92 --- /dev/null +++ b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/mapper/AddDeviceUnitMapper.java @@ -0,0 +1,10 @@ +package com.njcn.gather.tool.addledger.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njcn.gather.tool.addledger.pojo.po.AddDeviceUnit; + +/** + * 设备单位 Mapper。 + */ +public interface AddDeviceUnitMapper extends BaseMapper { +} diff --git a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/mapper/AddOverlimitMapper.java b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/mapper/AddOverlimitMapper.java new file mode 100644 index 0000000..5f5a379 --- /dev/null +++ b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/mapper/AddOverlimitMapper.java @@ -0,0 +1,10 @@ +package com.njcn.gather.tool.addledger.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njcn.gather.tool.addledger.pojo.po.AddOverlimitPO; + +/** + * 监测点限值 Mapper。 + */ +public interface AddOverlimitMapper extends BaseMapper { +} diff --git a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/constant/AddLedgerConst.java b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/constant/AddLedgerConst.java index 6d3b017..92f1487 100644 --- a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/constant/AddLedgerConst.java +++ b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/constant/AddLedgerConst.java @@ -33,12 +33,19 @@ public final class AddLedgerConst { public static final int LINE_RUN_STATUS_RUNNING = 0; public static final int LINE_INTERVAL_DEFAULT = 1; public static final String LOG_LEVEL_WARN = "WARN"; + public static final int LINE_TYPE_MAIN = 0; + public static final int LINE_TYPE_DISTRIBUTION = 1; public static final int MIN_LINE_NO = 1; public static final int MAX_LINE_NO = 20; public static final Set CON_TYPES = new LinkedHashSet(Arrays.asList(0, 1, 2)); + public static final Set LINE_TYPES = new LinkedHashSet(Arrays.asList( + LINE_TYPE_MAIN, + LINE_TYPE_DISTRIBUTION + )); + public static final Set VOL_GRADES = new LinkedHashSet(Arrays.asList( new BigDecimal("0.38"), new BigDecimal("10"), diff --git a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/param/AddDeviceUnitSaveParam.java b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/param/AddDeviceUnitSaveParam.java new file mode 100644 index 0000000..8ff1782 --- /dev/null +++ b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/param/AddDeviceUnitSaveParam.java @@ -0,0 +1,91 @@ +package com.njcn.gather.tool.addledger.pojo.param; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +/** + * 设备单位保存参数。 + */ +@Data +@ApiModel("设备单位保存参数") +public class AddDeviceUnitSaveParam { + + @ApiModelProperty(value = "设备ID", required = true) + @NotBlank(message = "设备 ID 不能为空") + private String devId; + + @ApiModelProperty("频率") + private String unitFrequency; + + @ApiModelProperty("频率偏差") + private String unitFrequencyDev; + + @ApiModelProperty("相电压有效值") + private String phaseVoltage; + + @ApiModelProperty("线电压有效值") + private String lineVoltage; + + @ApiModelProperty("电压上偏差") + private String voltageDev; + + @ApiModelProperty("电压下偏差") + private String uvoltageDev; + + @ApiModelProperty("电流有效值") + private String ieffective; + + @ApiModelProperty("单相有功功率") + private String singleP; + + @ApiModelProperty("单相视在功率") + private String singleViewP; + + @ApiModelProperty("单相无功功率") + private String singleNoP; + + @ApiModelProperty("总有功功率") + private String totalActiveP; + + @ApiModelProperty("总视在功率") + private String totalViewP; + + @ApiModelProperty("总无功功率") + private String totalNoP; + + @ApiModelProperty("相线电压基波有效值") + private String vfundEffective; + + @ApiModelProperty("基波电流") + private String ifund; + + @ApiModelProperty("基波有功功率") + private String fundActiveP; + + @ApiModelProperty("基波无功功率") + private String fundNoP; + + @ApiModelProperty("电压总谐波畸变率") + private String vdistortion; + + @ApiModelProperty("2-50次谐波电压含有率") + private String vharmonicRate; + + @ApiModelProperty("2-50次谐波电流有效值") + private String iharmonic; + + @ApiModelProperty("2-50次谐波有功功率") + private String pharmonic; + + @ApiModelProperty("0.5-49.5次间谐波电流有效值") + private String iiharmonic; + + @ApiModelProperty("正序电压") + private String positiveV; + + @ApiModelProperty("零序负序电压") + private String noPositiveV; +} diff --git a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/param/AddLedgerLineSaveParam.java b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/param/AddLedgerLineSaveParam.java index 2617baa..1a22732 100644 --- a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/param/AddLedgerLineSaveParam.java +++ b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/param/AddLedgerLineSaveParam.java @@ -81,6 +81,9 @@ public class AddLedgerLineSaveParam { @DecimalMin(value = "0", inclusive = true, message = "protocol_capacity 不能为负数") private BigDecimal protocolCapacity; + @ApiModelProperty("线路类型:0 主网,1 配网") + private Integer lineType; + @ApiModelProperty("监测对象类型") private String monitorObj; diff --git a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/po/AddDeviceUnit.java b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/po/AddDeviceUnit.java new file mode 100644 index 0000000..ff97dec --- /dev/null +++ b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/po/AddDeviceUnit.java @@ -0,0 +1,118 @@ +package com.njcn.gather.tool.addledger.pojo.po; + + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * 数据单位管理表。 + */ +@Data +@TableName("cs_device_unit") +public class AddDeviceUnit { + + private static final long serialVersionUID = 1L; + + @TableId(value = "dev_id") + @ApiModelProperty(value = "终端id") + private String devId; + + @TableField("unit_frequency") + @ApiModelProperty(value = "频率") + private String unitFrequency = "Hz"; + + @TableField("unit_frequency_dev") + @ApiModelProperty(value = "频率偏差") + private String unitFrequencyDev = "Hz"; + + @TableField("phase_voltage") + @ApiModelProperty(value = "相电压有效值") + private String phaseVoltage = "kV"; + + @TableField("line_voltage") + @ApiModelProperty(value = "线电压有效值") + private String lineVoltage = "kV"; + + @TableField("voltage_dev") + @ApiModelProperty(value = "电压上偏差") + private String voltageDev = "%"; + + @TableField("uvoltage_dev") + @ApiModelProperty(value = "电压下偏差") + private String uvoltageDev = "%"; + + @TableField("i_effective") + @ApiModelProperty(value = "电流有效值") + private String ieffective = "A"; + + @TableField("single_p") + @ApiModelProperty(value = "单相有功功率") + private String singleP = "kW"; + + @TableField("single_view_p") + @ApiModelProperty(value = "单相视在功率") + private String singleViewP = "kVA"; + + @TableField("single_no_p") + @ApiModelProperty(value = "单相无功功率") + private String singleNoP = "kVar"; + + @TableField("total_active_p") + @ApiModelProperty(value = "总有功功率") + private String totalActiveP = "kW"; + + @TableField("total_view_p") + @ApiModelProperty(value = "总视在功率") + private String totalViewP = "kVA"; + + @TableField("total_no_p") + @ApiModelProperty(value = "总无功功率") + private String totalNoP = "kVar"; + + @TableField("v_fund_effective") + @ApiModelProperty(value = "相(线)电压基波有效值") + private String vfundEffective = "kV"; + + @TableField("i_fund") + @ApiModelProperty(value = "基波电流") + private String ifund = "A"; + + @TableField("fund_active_p") + @ApiModelProperty(value = "基波有功功率") + private String fundActiveP = "kW"; + + @TableField("fund_no_p") + @ApiModelProperty(value = "基波无功功率") + private String fundNoP = "kVar"; + + @TableField("v_distortion") + @ApiModelProperty(value = "电压总谐波畸变率") + private String vdistortion = "%"; + + @TableField("v_harmonic_rate") + @ApiModelProperty(value = "2~50次谐波电压含有率") + private String vharmonicRate = "%"; + + @TableField("i_harmonic") + @ApiModelProperty(value = "2~50次谐波电流有效值") + private String iharmonic = "A"; + + @TableField("p_harmonic") + @ApiModelProperty(value = "2~50次谐波有功功率") + private String pharmonic = "kW"; + + @TableField("i_iharmonic") + @ApiModelProperty(value = "0.5~49.5次间谐波电流有效值") + private String iiharmonic = "A"; + + @TableField("positive_v") + @ApiModelProperty(value = "正序电压") + private String positiveV = "kV"; + + @TableField("no_positive_v") + @ApiModelProperty(value = "零序负序电压") + private String noPositiveV = "V"; +} diff --git a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/po/AddLedgerLinePO.java b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/po/AddLedgerLinePO.java index b3e9a78..3454af6 100644 --- a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/po/AddLedgerLinePO.java +++ b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/po/AddLedgerLinePO.java @@ -64,6 +64,12 @@ public class AddLedgerLinePO extends BaseEntity { private BigDecimal protocolCapacity; + /** + * 线路类型:0 主网,1 配网。 + */ + @TableField(exist = false) + private Integer lineType; + private String monitorObj; private Integer isGovern; diff --git a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/po/AddOverlimitPO.java b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/po/AddOverlimitPO.java new file mode 100644 index 0000000..49a3a63 --- /dev/null +++ b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/po/AddOverlimitPO.java @@ -0,0 +1,869 @@ +package com.njcn.gather.tool.addledger.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 越限阈值配置表。 + */ +@Data +@TableName("cs_overlimit") +public class AddOverlimitPO { + + private static final long serialVersionUID = 1L; + + /** + * 监测点序号 + */ + @TableId("line_id") + private String id; + + /** + * 频率限值 + */ + private Float freqDev; + + /** + * 电压波动 + */ + private Float voltageFluctuation; + + /** + * 电压上偏差限值 + */ + private Float voltageDev; + + /** + * 电压下偏差限值 + */ + private Float uvoltageDev; + + /** + * 三相电压不平衡度限值 + */ + private Float ubalance; + + /** + * 短时电压不平衡度限值 + */ + private Float shortUbalance; + + /** + * 闪变限值 + */ + private Float flicker; + + /** + * 电压总谐波畸变率限值 + */ + private Float uaberrance; + + /** + * 负序电流限值 + */ + private Float iNeg; + + /** + * 2次谐波电压限值 + */ + @TableField("uharm_2") + private Float uharm2; + + /** + * 3次谐波电压限值 + */ + @TableField("uharm_3") + private Float uharm3; + + /** + * 4次谐波电压限值 + */ + @TableField("uharm_4") + private Float uharm4; + + /** + * 5次谐波电压限值 + */ + @TableField("uharm_5") + private Float uharm5; + + /** + * 6次谐波电压限值 + */ + @TableField("uharm_6") + private Float uharm6; + + /** + * 7次谐波电压限值 + */ + @TableField("uharm_7") + private Float uharm7; + + /** + * 8次谐波电压限值 + */ + @TableField("uharm_8") + private Float uharm8; + + /** + * 9次谐波电压限值 + */ + @TableField("uharm_9") + private Float uharm9; + + /** + * 10次谐波电压限值 + */ + @TableField("uharm_10") + private Float uharm10; + + /** + * 11次谐波电压限值 + */ + @TableField("uharm_11") + private Float uharm11; + + /** + * 12次谐波电压限值 + */ + @TableField("uharm_12") + private Float uharm12; + + /** + * 13次谐波电压限值 + */ + @TableField("uharm_13") + private Float uharm13; + + /** + * 14次谐波电压限值 + */ + @TableField("uharm_14") + private Float uharm14; + + /** + * 15次谐波电压限值 + */ + @TableField("uharm_15") + private Float uharm15; + + /** + * 16次谐波电压限值 + */ + @TableField("uharm_16") + private Float uharm16; + + /** + * 17次谐波电压限值 + */ + @TableField("uharm_17") + private Float uharm17; + + /** + * 18次谐波电压限值 + */ + @TableField("uharm_18") + private Float uharm18; + + /** + * 19次谐波电压限值 + */ + @TableField("uharm_19") + private Float uharm19; + + /** + * 20次谐波电压限值 + */ + @TableField("uharm_20") + private Float uharm20; + + /** + * 21次谐波电压限值 + */ + @TableField("uharm_21") + private Float uharm21; + + /** + * 22次谐波电压限值 + */ + @TableField("uharm_22") + private Float uharm22; + + /** + * 23次谐波电压限值 + */ + @TableField("uharm_23") + private Float uharm23; + + /** + * 24次谐波电压限值 + */ + @TableField("uharm_24") + private Float uharm24; + + /** + * 25次谐波电压限值 + */ + @TableField("uharm_25") + private Float uharm25; + + /** + * 2次谐波电压限值 + */ + @TableField("uharm_26") + private Float uharm26; + + /** + * 3次谐波电压限值 + */ + @TableField("uharm_27") + private Float uharm27; + + /** + * 4次谐波电压限值 + */ + @TableField("uharm_28") + private Float uharm28; + + /** + * 5次谐波电压限值 + */ + @TableField("uharm_29") + private Float uharm29; + + /** + * 6次谐波电压限值 + */ + @TableField("uharm_30") + private Float uharm30; + + /** + * 7次谐波电压限值 + */ + @TableField("uharm_31") + private Float uharm31; + + /** + * 8次谐波电压限值 + */ + @TableField("uharm_32") + private Float uharm32; + + /** + * 9次谐波电压限值 + */ + @TableField("uharm_33") + private Float uharm33; + + /** + * 10次谐波电压限值 + */ + @TableField("uharm_34") + private Float uharm34; + + /** + * 11次谐波电压限值 + */ + @TableField("uharm_35") + private Float uharm35; + + /** + * 12次谐波电压限值 + */ + @TableField("uharm_36") + private Float uharm36; + + /** + * 13次谐波电压限值 + */ + @TableField("uharm_37") + private Float uharm37; + + /** + * 14次谐波电压限值 + */ + @TableField("uharm_38") + private Float uharm38; + + /** + * 15次谐波电压限值 + */ + @TableField("uharm_39") + private Float uharm39; + + /** + * 16次谐波电压限值 + */ + @TableField("uharm_40") + private Float uharm40; + + /** + * 17次谐波电压限值 + */ + @TableField("uharm_41") + private Float uharm41; + + /** + * 18次谐波电压限值 + */ + @TableField("uharm_42") + private Float uharm42; + + /** + * 19次谐波电压限值 + */ + @TableField("uharm_43") + private Float uharm43; + + /** + * 20次谐波电压限值 + */ + @TableField("uharm_44") + private Float uharm44; + + /** + * 21次谐波电压限值 + */ + @TableField("uharm_45") + private Float uharm45; + + /** + * 22次谐波电压限值 + */ + @TableField("uharm_46") + private Float uharm46; + + /** + * 23次谐波电压限值 + */ + @TableField("uharm_47") + private Float uharm47; + + /** + * 24次谐波电压限值 + */ + @TableField("uharm_48") + private Float uharm48; + + /** + * 25次谐波电压限值 + */ + @TableField("uharm_49") + private Float uharm49; + + /** + * 50次谐波电压限值 + */ + @TableField("uharm_50") + private Float uharm50; + + + + /** + * 2次谐波电流限值 + */ + @TableField("iharm_2") + private Float iharm2; + + /** + * 3次谐波电流限值 + */ + @TableField("iharm_3") + private Float iharm3; + + /** + * 4次谐波电流限值 + */ + @TableField("iharm_4") + private Float iharm4; + + /** + * 5次谐波电流限值 + */ + @TableField("iharm_5") + private Float iharm5; + + /** + * 6次谐波电流限值 + */ + @TableField("iharm_6") + private Float iharm6; + + /** + * 7次谐波电流限值 + */ + @TableField("iharm_7") + private Float iharm7; + + /** + * 8次谐波电流限值 + */ + @TableField("iharm_8") + private Float iharm8; + + /** + * 9次谐波电流限值 + */ + @TableField("iharm_9") + private Float iharm9; + + /** + * 10次谐波电流限值 + */ + @TableField("iharm_10") + private Float iharm10; + + /** + * 11次谐波电流限值 + */ + @TableField("iharm_11") + private Float iharm11; + + /** + * 12次谐波电流限值 + */ + @TableField("iharm_12") + private Float iharm12; + + /** + * 13次谐波电流限值 + */ + @TableField("iharm_13") + private Float iharm13; + + /** + * 14次谐波电流限值 + */ + @TableField("iharm_14") + private Float iharm14; + + /** + * 15次谐波电流限值 + */ + @TableField("iharm_15") + private Float iharm15; + + /** + * 16次谐波电流限值 + */ + @TableField("iharm_16") + private Float iharm16; + + /** + * 17次谐波电流限值 + */ + @TableField("iharm_17") + private Float iharm17; + + /** + * 18次谐波电流限值 + */ + @TableField("iharm_18") + private Float iharm18; + + /** + * 19次谐波电流限值 + */ + @TableField("iharm_19") + private Float iharm19; + + /** + * 20次谐波电流限值 + */ + @TableField("iharm_20") + private Float iharm20; + + /** + * 21次谐波电流限值 + */ + @TableField("iharm_21") + private Float iharm21; + + /** + * 22次谐波电流限值 + */ + @TableField("iharm_22") + private Float iharm22; + + /** + * 23次谐波电流限值 + */ + @TableField("iharm_23") + private Float iharm23; + + /** + * 24次谐波电流限值 + */ + @TableField("iharm_24") + private Float iharm24; + + /** + * 25次谐波电流限值 + */ + @TableField("iharm_25") + private Float iharm25; + + /** + * 2次谐波电压限值 + */ + @TableField("iharm_26") + private Float iharm26; + + /** + * 3次谐波电压限值 + */ + @TableField("iharm_27") + private Float iharm27; + + /** + * 4次谐波电压限值 + */ + @TableField("iharm_28") + private Float iharm28; + + /** + * 5次谐波电压限值 + */ + @TableField("iharm_29") + private Float iharm29; + + /** + * 6次谐波电压限值 + */ + @TableField("iharm_30") + private Float iharm30; + + /** + * 7次谐波电压限值 + */ + @TableField("iharm_31") + private Float iharm31; + + /** + * 8次谐波电压限值 + */ + @TableField("iharm_32") + private Float iharm32; + + /** + * 9次谐波电压限值 + */ + @TableField("iharm_33") + private Float iharm33; + + /** + * 10次谐波电压限值 + */ + @TableField("iharm_34") + private Float iharm34; + + /** + * 11次谐波电压限值 + */ + @TableField("iharm_35") + private Float iharm35; + + /** + * 12次谐波电压限值 + */ + @TableField("iharm_36") + private Float iharm36; + + /** + * 13次谐波电压限值 + */ + @TableField("iharm_37") + private Float iharm37; + + /** + * 14次谐波电压限值 + */ + @TableField("iharm_38") + private Float iharm38; + + /** + * 15次谐波电压限值 + */ + @TableField("iharm_39") + private Float iharm39; + + /** + * 16次谐波电压限值 + */ + @TableField("iharm_40") + private Float iharm40; + + /** + * 17次谐波电压限值 + */ + @TableField("iharm_41") + private Float iharm41; + + /** + * 18次谐波电压限值 + */ + @TableField("iharm_42") + private Float iharm42; + + /** + * 19次谐波电压限值 + */ + @TableField("iharm_43") + private Float iharm43; + + /** + * 20次谐波电压限值 + */ + @TableField("iharm_44") + private Float iharm44; + + /** + * 21次谐波电压限值 + */ + @TableField("iharm_45") + private Float iharm45; + + /** + * 22次谐波电压限值 + */ + @TableField("iharm_46") + private Float iharm46; + + /** + * 23次谐波电压限值 + */ + @TableField("iharm_47") + private Float iharm47; + + /** + * 24次谐波电压限值 + */ + @TableField("iharm_48") + private Float iharm48; + + /** + * 25次谐波电压限值 + */ + @TableField("iharm_49") + private Float iharm49; + + /** + * 50次谐波电压限值 + */ + @TableField("iharm_50") + private Float iharm50; + + + + /** + * 0.5次间谐波电压限值 + */ + @TableField("inuharm_1") + private Float inuharm1; + + /** + * 1.5次间谐波电压限值 + */ + @TableField("inuharm_2") + private Float inuharm2; + + /** + * 2.5次间谐波电压限值 + */ + @TableField("inuharm_3") + private Float inuharm3; + + /** + * 3.5次间谐波电压限值 + */ + @TableField("inuharm_4") + private Float inuharm4; + + /** + * 4.5次间谐波电压限值 + */ + @TableField("inuharm_5") + private Float inuharm5; + + /** + * 5.5次间谐波电压限值 + */ + @TableField("inuharm_6") + private Float inuharm6; + + /** + * 6.5次间谐波电压限值 + */ + @TableField("inuharm_7") + private Float inuharm7; + + /** + * 7.5次间谐波电压限值 + */ + @TableField("inuharm_8") + private Float inuharm8; + + /** + * 8.5次间谐波电压限值 + */ + @TableField("inuharm_9") + private Float inuharm9; + + /** + * 9.5次间谐波电压限值 + */ + @TableField("inuharm_10") + private Float inuharm10; + + /** + * 10.5次间谐波电压限值 + */ + @TableField("inuharm_11") + private Float inuharm11; + + /** + * 11.5次间谐波电压限值 + */ + @TableField("inuharm_12") + private Float inuharm12; + + /** + * 12.5次间谐波电压限值 + */ + @TableField("inuharm_13") + private Float inuharm13; + + /** + * 13.5次间谐波电压限值 + */ + @TableField("inuharm_14") + private Float inuharm14; + + /** + * 14.5次间谐波电压限值 + */ + @TableField("inuharm_15") + private Float inuharm15; + + /** + * 15.5次间谐波电压限值 + */ + @TableField("inuharm_16") + private Float inuharm16; + + public AddOverlimitPO(){} + + + + public void buildIHarm(Float[] iHarmTem){ + this.iharm2= iHarmTem[0]; + this.iharm4= iHarmTem[2]; + this.iharm6= iHarmTem[4]; + this.iharm8= iHarmTem[6]; + this.iharm10= iHarmTem[8]; + this.iharm12= iHarmTem[10]; + this.iharm14= iHarmTem[12]; + this.iharm16= iHarmTem[14]; + this.iharm18= iHarmTem[16]; + this.iharm20= iHarmTem[18]; + this.iharm22= iHarmTem[20]; + this.iharm24= iHarmTem[22]; + this.iharm26= iHarmTem[24]; + this.iharm28= iHarmTem[26]; + this.iharm30= iHarmTem[28]; + this.iharm32= iHarmTem[30]; + this.iharm34= iHarmTem[32]; + this.iharm36= iHarmTem[34]; + this.iharm38= iHarmTem[36]; + this.iharm40= iHarmTem[38]; + this.iharm42= iHarmTem[40]; + this.iharm44= iHarmTem[42]; + this.iharm46= iHarmTem[44]; + this.iharm48= iHarmTem[46]; + this.iharm50= iHarmTem[48]; + + + + this.iharm3= iHarmTem[1]; + this.iharm5= iHarmTem[3]; + this.iharm7= iHarmTem[5]; + this.iharm9= iHarmTem[7]; + this.iharm11= iHarmTem[9]; + this.iharm13= iHarmTem[11]; + this.iharm15= iHarmTem[13]; + this.iharm17= iHarmTem[15]; + this.iharm19= iHarmTem[17]; + this.iharm21= iHarmTem[19]; + this.iharm23= iHarmTem[21]; + this.iharm25= iHarmTem[23]; + this.iharm27= iHarmTem[25]; + this.iharm29= iHarmTem[27]; + this.iharm31= iHarmTem[29]; + this.iharm33= iHarmTem[31]; + this.iharm35= iHarmTem[33]; + this.iharm37= iHarmTem[35]; + this.iharm39= iHarmTem[37]; + this.iharm41= iHarmTem[39]; + this.iharm43= iHarmTem[41]; + this.iharm45= iHarmTem[43]; + this.iharm47= iHarmTem[45]; + this.iharm49= iHarmTem[47]; + } + + public void buildUharm(Float resultEven,Float resultOdd){ + this.uharm2=resultEven; + this.uharm4=resultEven; + this.uharm6=resultEven; + this.uharm8=resultEven; + this.uharm10=resultEven; + this.uharm12=resultEven; + this.uharm14=resultEven; + this.uharm16=resultEven; + this.uharm18=resultEven; + this.uharm20=resultEven; + this.uharm22=resultEven; + this.uharm24=resultEven; + this.uharm26=resultEven; + this.uharm28=resultEven; + this.uharm30=resultEven; + this.uharm32=resultEven; + this.uharm34=resultEven; + this.uharm36=resultEven; + this.uharm38=resultEven; + this.uharm40=resultEven; + this.uharm42=resultEven; + this.uharm44=resultEven; + this.uharm46=resultEven; + this.uharm48=resultEven; + this.uharm50=resultEven; + + + this.uharm3=resultOdd; + this.uharm5=resultOdd; + this.uharm7=resultOdd; + this.uharm9=resultOdd; + this.uharm11=resultOdd; + this.uharm13=resultOdd; + this.uharm15=resultOdd; + this.uharm17=resultOdd; + this.uharm19=resultOdd; + this.uharm21=resultOdd; + this.uharm23=resultOdd; + this.uharm25=resultOdd; + this.uharm27=resultOdd; + this.uharm29=resultOdd; + this.uharm31=resultOdd; + this.uharm33=resultOdd; + this.uharm35=resultOdd; + this.uharm37=resultOdd; + this.uharm39=resultOdd; + this.uharm41=resultOdd; + this.uharm43=resultOdd; + this.uharm45=resultOdd; + this.uharm47=resultOdd; + this.uharm49=resultOdd; + } + +} diff --git a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/vo/AddDeviceUnitVO.java b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/vo/AddDeviceUnitVO.java new file mode 100644 index 0000000..a4ba429 --- /dev/null +++ b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/vo/AddDeviceUnitVO.java @@ -0,0 +1,60 @@ +package com.njcn.gather.tool.addledger.pojo.vo; + +import lombok.Data; + +/** + * 设备单位配置。 + */ +@Data +public class AddDeviceUnitVO { + + private String devId; + + private String unitFrequency; + + private String unitFrequencyDev; + + private String phaseVoltage; + + private String lineVoltage; + + private String voltageDev; + + private String uvoltageDev; + + private String ieffective; + + private String singleP; + + private String singleViewP; + + private String singleNoP; + + private String totalActiveP; + + private String totalViewP; + + private String totalNoP; + + private String vfundEffective; + + private String ifund; + + private String fundActiveP; + + private String fundNoP; + + private String vdistortion; + + private String vharmonicRate; + + private String iharmonic; + + private String pharmonic; + + private String iiharmonic; + + private String positiveV; + + private String noPositiveV; +} diff --git a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/vo/AddLedgerDetailVO.java b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/vo/AddLedgerDetailVO.java index b1611f2..f6cf799 100644 --- a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/vo/AddLedgerDetailVO.java +++ b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/pojo/vo/AddLedgerDetailVO.java @@ -1,5 +1,6 @@ package com.njcn.gather.tool.addledger.pojo.vo; +import com.njcn.gather.tool.addledger.pojo.po.AddOverlimitPO; import lombok.Data; import java.math.BigDecimal; @@ -70,6 +71,10 @@ public class AddLedgerDetailVO { private BigDecimal protocolCapacity; + private Integer lineType; + + private AddOverlimitPO overlimit; + private String monitorObj; private Integer isGovern; diff --git a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/service/AddLedgerService.java b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/service/AddLedgerService.java index e3ceb3c..71437cb 100644 --- a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/service/AddLedgerService.java +++ b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/service/AddLedgerService.java @@ -1,10 +1,13 @@ package com.njcn.gather.tool.addledger.service; +import com.njcn.gather.tool.addledger.pojo.param.AddDeviceUnitSaveParam; import com.njcn.gather.tool.addledger.pojo.param.AddLedgerEngineeringSaveParam; import com.njcn.gather.tool.addledger.pojo.param.AddLedgerEquipmentSaveParam; import com.njcn.gather.tool.addledger.pojo.param.AddLedgerLinePathQueryParam; import com.njcn.gather.tool.addledger.pojo.param.AddLedgerLineSaveParam; import com.njcn.gather.tool.addledger.pojo.param.AddLedgerProjectSaveParam; +import com.njcn.gather.tool.addledger.pojo.po.AddOverlimitPO; +import com.njcn.gather.tool.addledger.pojo.vo.AddDeviceUnitVO; import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerDetailVO; import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO; import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerTreeNodeVO; @@ -27,8 +30,14 @@ public interface AddLedgerService { AddLedgerDetailVO saveEquipment(AddLedgerEquipmentSaveParam param); + AddDeviceUnitVO getDeviceUnit(String devId); + + AddDeviceUnitVO saveDeviceUnit(AddDeviceUnitSaveParam param); + AddLedgerDetailVO saveLine(AddLedgerLineSaveParam param); + AddOverlimitPO getLineOverlimit(String lineId); + List availableLineNos(String deviceId, String lineId); Map listLinePathByLineIds(List lineIds); diff --git a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/service/impl/AddLedgerServiceImpl.java b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/service/impl/AddLedgerServiceImpl.java index 3ef72b2..cb3cd65 100644 --- a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/service/impl/AddLedgerServiceImpl.java +++ b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/service/impl/AddLedgerServiceImpl.java @@ -7,23 +7,30 @@ import com.njcn.gather.tool.addledger.mapper.AddLedgerEquipmentMapper; import com.njcn.gather.tool.addledger.mapper.AddLedgerLedgerMapper; import com.njcn.gather.tool.addledger.mapper.AddLedgerLineMapper; import com.njcn.gather.tool.addledger.mapper.AddLedgerProjectMapper; +import com.njcn.gather.tool.addledger.mapper.AddDeviceUnitMapper; +import com.njcn.gather.tool.addledger.mapper.AddOverlimitMapper; import com.njcn.gather.tool.addledger.pojo.constant.AddLedgerConst; +import com.njcn.gather.tool.addledger.pojo.param.AddDeviceUnitSaveParam; import com.njcn.gather.tool.addledger.pojo.param.AddLedgerEngineeringSaveParam; import com.njcn.gather.tool.addledger.pojo.param.AddLedgerEquipmentSaveParam; import com.njcn.gather.tool.addledger.pojo.param.AddLedgerLinePathQueryParam; import com.njcn.gather.tool.addledger.pojo.param.AddLedgerLineSaveParam; import com.njcn.gather.tool.addledger.pojo.param.AddLedgerProjectSaveParam; +import com.njcn.gather.tool.addledger.pojo.po.AddDeviceUnit; import com.njcn.gather.tool.addledger.pojo.po.AddLedgerEngineeringPO; import com.njcn.gather.tool.addledger.pojo.po.AddLedgerEquipmentPO; import com.njcn.gather.tool.addledger.pojo.po.AddLedgerLedgerPO; import com.njcn.gather.tool.addledger.pojo.po.AddLedgerLinePO; import com.njcn.gather.tool.addledger.pojo.po.AddLedgerProjectPO; +import com.njcn.gather.tool.addledger.pojo.po.AddOverlimitPO; +import com.njcn.gather.tool.addledger.pojo.vo.AddDeviceUnitVO; import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerDetailVO; import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO; import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerTreeNodeVO; import com.njcn.gather.tool.addledger.service.AddLedgerService; import com.njcn.gather.tool.addledger.util.AddLedgerIdUtil; import com.njcn.gather.tool.addledger.util.AddLedgerLineNoUtil; +import com.njcn.gather.tool.addledger.util.COverlimitUtil; import com.njcn.web.utils.RequestUtil; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -51,6 +58,8 @@ public class AddLedgerServiceImpl implements AddLedgerService { private final AddLedgerEquipmentMapper equipmentMapper; private final AddLedgerLineMapper lineMapper; private final AddLedgerLedgerMapper ledgerMapper; + private final AddDeviceUnitMapper deviceUnitMapper; + private final AddOverlimitMapper overlimitMapper; private final AddLedgerTreeBuilder treeBuilder; @Override @@ -167,6 +176,7 @@ public class AddLedgerServiceImpl implements AddLedgerService { equipment.setUsageStatus(AddLedgerConst.ENABLE); equipment.setSort(0); equipmentMapper.insert(equipment); + saveDefaultDeviceUnit(id); saveLedger(id, projectLedger.getId(), buildChildPids(projectLedger), equipment.getName(), AddLedgerConst.LEVEL_EQUIPMENT); } else { equipmentMapper.updateById(equipment); @@ -175,6 +185,39 @@ public class AddLedgerServiceImpl implements AddLedgerService { return detail(id, AddLedgerConst.LEVEL_EQUIPMENT); } + @Override + public AddDeviceUnitVO getDeviceUnit(String devId) { + String deviceId = requireText(devId, "设备 ID 不能为空"); + requireEquipment(deviceId); + AddDeviceUnit unit = deviceUnitMapper.selectById(deviceId); + if (unit == null) { + unit = buildDefaultDeviceUnit(deviceId); + } + return buildDeviceUnitVO(unit); + } + + @Override + @Transactional + public AddDeviceUnitVO saveDeviceUnit(AddDeviceUnitSaveParam param) { + if (param == null) { + throw new IllegalArgumentException("设备单位参数不能为空"); + } + String deviceId = requireText(param.getDevId(), "设备 ID 不能为空"); + requireEquipment(deviceId); + AddDeviceUnit unit = deviceUnitMapper.selectById(deviceId); + boolean create = unit == null; + if (create) { + unit = buildDefaultDeviceUnit(deviceId); + } + applyDeviceUnitParam(unit, param); + if (create) { + deviceUnitMapper.insert(unit); + } else { + deviceUnitMapper.updateById(unit); + } + return buildDeviceUnitVO(unit); + } + @Override @Transactional public AddLedgerDetailVO saveLine(AddLedgerLineSaveParam param) { @@ -206,6 +249,7 @@ public class AddLedgerServiceImpl implements AddLedgerService { line.setDevCapacity(param.getDevCapacity()); line.setBasicCapacity(param.getBasicCapacity()); line.setProtocolCapacity(param.getProtocolCapacity()); + line.setLineType(param.getLineType() == null ? AddLedgerConst.LINE_TYPE_MAIN : param.getLineType()); line.setMonitorObj(trimToNull(param.getMonitorObj())); line.setIsGovern(param.getIsGovern() == null ? AddLedgerConst.DISABLE : param.getIsGovern()); line.setMonitorUser(trimToNull(param.getMonitorUser())); @@ -221,6 +265,7 @@ public class AddLedgerServiceImpl implements AddLedgerService { lineMapper.updateById(line); updateLedgerName(id, AddLedgerConst.LEVEL_LINE, line.getName()); } + saveOrUpdateOverlimit(line); return detail(id, AddLedgerConst.LEVEL_LINE); } @@ -231,6 +276,12 @@ public class AddLedgerServiceImpl implements AddLedgerService { return AddLedgerLineNoUtil.resolveAvailableLineNos(usedLineNos, null); } + @Override + public AddOverlimitPO getLineOverlimit(String lineId) { + AddLedgerLinePO line = requireLine(lineId); + return overlimitMapper.selectById(line.getLineId()); + } + @Override public Map listLinePathByLineIds(List lineIds) { List normalizedLineIds = normalizeIds(lineIds); @@ -296,6 +347,7 @@ public class AddLedgerServiceImpl implements AddLedgerService { String updateBy = currentUserId(); if (!lineIds.isEmpty()) { lineMapper.softDeleteByIds(lineIds, updateBy); + overlimitMapper.deleteBatchIds(lineIds); } if (!equipmentIds.isEmpty()) { equipmentMapper.softDeleteByIds(equipmentIds, updateBy); @@ -344,6 +396,9 @@ public class AddLedgerServiceImpl implements AddLedgerService { requireNonNegativeIfPresent(param.getDevCapacity(), "dev_capacity 不能为负数"); requireNonNegativeIfPresent(param.getBasicCapacity(), "basic_capacity 不能为负数"); requireNonNegativeIfPresent(param.getProtocolCapacity(), "protocol_capacity 不能为负数"); + if (param.getLineType() != null && !AddLedgerConst.LINE_TYPES.contains(param.getLineType())) { + throw new IllegalArgumentException("lineType 只能是 0 或 1"); + } } private void assertLineNoUnique(String deviceId, Integer lineNo, String lineId) { @@ -437,6 +492,16 @@ public class AddLedgerServiceImpl implements AddLedgerService { ledgerMapper.insert(ledger); } + private void saveDefaultDeviceUnit(String devId) { + deviceUnitMapper.insert(buildDefaultDeviceUnit(devId)); + } + + private AddDeviceUnit buildDefaultDeviceUnit(String devId) { + AddDeviceUnit unit = new AddDeviceUnit(); + unit.setDevId(devId); + return unit; + } + private void updateLedgerName(String id, Integer level, String name) { AddLedgerLedgerPO ledger = requireLedger(id, level, levelName(level) + "节点"); ledger.setName(name); @@ -493,6 +558,8 @@ public class AddLedgerServiceImpl implements AddLedgerService { detail.setDevCapacity(line.getDevCapacity()); detail.setBasicCapacity(line.getBasicCapacity()); detail.setProtocolCapacity(line.getProtocolCapacity()); + detail.setLineType(AddLedgerConst.LINE_TYPE_MAIN); + detail.setOverlimit(overlimitMapper.selectById(line.getLineId())); detail.setMonitorObj(line.getMonitorObj()); detail.setIsGovern(line.getIsGovern()); detail.setMonitorUser(line.getMonitorUser()); @@ -500,6 +567,83 @@ public class AddLedgerServiceImpl implements AddLedgerService { return detail; } + private void saveOrUpdateOverlimit(AddLedgerLinePO line) { + AddOverlimitPO overlimit = COverlimitUtil.globalAssemble( + toFloat(line.getVolGrade()), + toFloat(line.getProtocolCapacity()), + toFloat(line.getDevCapacity()), + toFloat(line.getShortCircuitCapacity()), + null, + line.getLineType() == null ? AddLedgerConst.LINE_TYPE_MAIN : line.getLineType()); + overlimit.setId(line.getLineId()); + if (overlimitMapper.selectById(line.getLineId()) == null) { + overlimitMapper.insert(overlimit); + } else { + overlimitMapper.updateById(overlimit); + } + } + + private Float toFloat(BigDecimal value) { + return value == null ? null : value.floatValue(); + } + + private void applyDeviceUnitParam(AddDeviceUnit unit, AddDeviceUnitSaveParam param) { + unit.setUnitFrequency(defaultIfBlank(param.getUnitFrequency(), unit.getUnitFrequency())); + unit.setUnitFrequencyDev(defaultIfBlank(param.getUnitFrequencyDev(), unit.getUnitFrequencyDev())); + unit.setPhaseVoltage(defaultIfBlank(param.getPhaseVoltage(), unit.getPhaseVoltage())); + unit.setLineVoltage(defaultIfBlank(param.getLineVoltage(), unit.getLineVoltage())); + unit.setVoltageDev(defaultIfBlank(param.getVoltageDev(), unit.getVoltageDev())); + unit.setUvoltageDev(defaultIfBlank(param.getUvoltageDev(), unit.getUvoltageDev())); + unit.setIeffective(defaultIfBlank(param.getIeffective(), unit.getIeffective())); + unit.setSingleP(defaultIfBlank(param.getSingleP(), unit.getSingleP())); + unit.setSingleViewP(defaultIfBlank(param.getSingleViewP(), unit.getSingleViewP())); + unit.setSingleNoP(defaultIfBlank(param.getSingleNoP(), unit.getSingleNoP())); + unit.setTotalActiveP(defaultIfBlank(param.getTotalActiveP(), unit.getTotalActiveP())); + unit.setTotalViewP(defaultIfBlank(param.getTotalViewP(), unit.getTotalViewP())); + unit.setTotalNoP(defaultIfBlank(param.getTotalNoP(), unit.getTotalNoP())); + unit.setVfundEffective(defaultIfBlank(param.getVfundEffective(), unit.getVfundEffective())); + unit.setIfund(defaultIfBlank(param.getIfund(), unit.getIfund())); + unit.setFundActiveP(defaultIfBlank(param.getFundActiveP(), unit.getFundActiveP())); + unit.setFundNoP(defaultIfBlank(param.getFundNoP(), unit.getFundNoP())); + unit.setVdistortion(defaultIfBlank(param.getVdistortion(), unit.getVdistortion())); + unit.setVharmonicRate(defaultIfBlank(param.getVharmonicRate(), unit.getVharmonicRate())); + unit.setIharmonic(defaultIfBlank(param.getIharmonic(), unit.getIharmonic())); + unit.setPharmonic(defaultIfBlank(param.getPharmonic(), unit.getPharmonic())); + unit.setIiharmonic(defaultIfBlank(param.getIiharmonic(), unit.getIiharmonic())); + unit.setPositiveV(defaultIfBlank(param.getPositiveV(), unit.getPositiveV())); + unit.setNoPositiveV(defaultIfBlank(param.getNoPositiveV(), unit.getNoPositiveV())); + } + + private AddDeviceUnitVO buildDeviceUnitVO(AddDeviceUnit unit) { + AddDeviceUnitVO vo = new AddDeviceUnitVO(); + vo.setDevId(unit.getDevId()); + vo.setUnitFrequency(unit.getUnitFrequency()); + vo.setUnitFrequencyDev(unit.getUnitFrequencyDev()); + vo.setPhaseVoltage(unit.getPhaseVoltage()); + vo.setLineVoltage(unit.getLineVoltage()); + vo.setVoltageDev(unit.getVoltageDev()); + vo.setUvoltageDev(unit.getUvoltageDev()); + vo.setIeffective(unit.getIeffective()); + vo.setSingleP(unit.getSingleP()); + vo.setSingleViewP(unit.getSingleViewP()); + vo.setSingleNoP(unit.getSingleNoP()); + vo.setTotalActiveP(unit.getTotalActiveP()); + vo.setTotalViewP(unit.getTotalViewP()); + vo.setTotalNoP(unit.getTotalNoP()); + vo.setVfundEffective(unit.getVfundEffective()); + vo.setIfund(unit.getIfund()); + vo.setFundActiveP(unit.getFundActiveP()); + vo.setFundNoP(unit.getFundNoP()); + vo.setVdistortion(unit.getVdistortion()); + vo.setVharmonicRate(unit.getVharmonicRate()); + vo.setIharmonic(unit.getIharmonic()); + vo.setPharmonic(unit.getPharmonic()); + vo.setIiharmonic(unit.getIiharmonic()); + vo.setPositiveV(unit.getPositiveV()); + vo.setNoPositiveV(unit.getNoPositiveV()); + return vo; + } + private AddLedgerDetailVO buildBaseDetail(AddLedgerLedgerPO ledger) { AddLedgerDetailVO detail = new AddLedgerDetailVO(); detail.setId(ledger.getId()); @@ -551,6 +695,11 @@ public class AddLedgerServiceImpl implements AddLedgerService { return trimmed.isEmpty() ? null : trimmed; } + private String defaultIfBlank(String value, String defaultValue) { + String text = trimToNull(value); + return text == null ? defaultValue : text; + } + private boolean isBlank(String value) { return trimToNull(value) == null; } diff --git a/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/util/COverlimitUtil.java b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/util/COverlimitUtil.java new file mode 100644 index 0000000..b680793 --- /dev/null +++ b/tools/add-ledger/src/main/java/com/njcn/gather/tool/addledger/util/COverlimitUtil.java @@ -0,0 +1,414 @@ +package com.njcn.gather.tool.addledger.util; + +import com.njcn.gather.tool.addledger.pojo.po.AddOverlimitPO; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Arrays; + + +/** + * pqs + * 限值计算工具类 + * + * @author cdf + * @date 2023/5/15 + */ +public class COverlimitUtil { + + private static final float DEFAULT_LIMIT = 3.14159f; + private static final float DEFAULT_CURRENT_LIMIT = -3.14159f; + private static final int LINE_TYPE_DISTRIBUTION = 1; + + private static final float KV_0_22 = 0.22f; + private static final float KV_0_6 = 0.6f; + private static final float KV_1 = 1.0f; + private static final float KV_6 = 6.0f; + private static final float KV_10 = 10.0f; + private static final float KV_20 = 20.0f; + private static final float KV_35 = 35.0f; + private static final float KV_66 = 66.0f; + private static final float KV_110 = 110.0f; + private static final float KV_220 = 220.0f; + private static final float KV_330 = 330.0f; + private static final float KV_500 = 500.0f; + private static final float KV_750 = 750.0f; + private static final float KV_1000 = 1000.0f; + + + /** + * 谐波电流系数 + */ + private static final double[][] ARR = { + {78, 62, 39, 62, 26, 44, 19, 21, 16, 28, 13, 24, 11, 12, 9.7, 18, 8.6, 16, 7.8, 8.9, 7.1, 14, 6.5, 12, 6.0, 6.9, 5.6, 11, 5.2, 10, 4.9, 5.6, 4.6, 8.9, 4.3, 8.4, 4.1, 4.8, 3.9, 7.6, 3.7, 7.2, 3.5, 4.1, 3.4, 6.6, 3.3, 6.3, 3.1}, + {43, 34, 21, 34, 14, 24, 11, 11, 8.5, 16, 7.1, 13, 6.1, 6.8, 5.3, 10, 4.7, 9, 4.3, 4.9, 3.9, 7.4, 3.6, 6.8, 3.3, 3.8, 3.1, 5.9, 2.9, 5.5, 2.7, 3.1, 2.5, 4.9, 2.4, 4.6, 2.3, 2.6, 2.2, 4.1, 2.0, 4.0, 2.0, 2.3, 1.9, 3.6, 1.8, 3.5, 1.7}, + {26, 20, 13, 20, 8.5, 15, 6.4, 6.8, 5.1, 9.3, 4.3, 7.9, 3.7, 4.1, 3.2, 6, 2.8, 5.4, 2.6, 2.9, 2.3, 4.5, 2.1, 4.1, 2.0, 2.2, 1.9, 3.4, 1.7, 3.2, 1.6, 1.8, 1.5, 2.9, 1.4, 2.7, 1.4, 1.5, 1.3, 2.4, 1.2, 2.3, 1.2, 1.3, 1.1, 2.1, 1.1, 2.0, 1.0}, + {15, 12, 7.7, 12, 5.1, 8.8, 3.8, 4.1, 3.1, 5.6, 2.6, 4.7, 2.2, 2.5, 1.9, 3.6, 1.7, 3.2, 1.5, 1.8, 1.4, 2.7, 1.3, 2.5, 1.2, 1.3, 1.1, 2.1, 1.0, 1.9, 0.9, 1.1, 0.9, 1.7, 0.8, 1.6, 0.8, 0.9, 0.8, 1.5, 0.7, 1.4, 0.7, 0.8, 0.7, 1.3, 0.6, 1.2, 0.6}, + {16, 13, 8.1, 13, 5.4, 9.3, 4.1, 4.3, 3.3, 5.9, 2.7, 5, 2.3, 2.6, 2, 3.8, 1.8, 3.4, 1.6, 1.9, 1.5, 2.8, 1.4, 2.6, 1.2, 1.4, 1.1, 2.2, 1.1, 2.1, 1.0, 1.2, 0.9, 1.9, 0.9, 1.8, 0.8, 1.0, 0.8, 1.6, 0.8, 1.5, 0.7, 0.9, 0.7, 1.4, 0.7, 1.3, 0.6}, + {12, 9.6, 6, 9.6, 4, 6.8, 3, 3.2, 2.4, 4.3, 2, 3.7, 1.7, 1.9, 1.5, 2.8, 1.3, 2.5, 1.2, 1.4, 1.1, 2.1, 1, 1.9, 0.9, 1.1, 0.9, 1.7, 0.8, 1.5, 0.8, 0.9, 0.7, 1.4, 0.7, 1.3, 0.6, 0.7, 0.6, 1.2, 0.6, 1.1, 0.5, 0.6, 0.5, 1.0, 0.5, 1.0, 0.5} + }; + + + /** + * 计算监测点限值 + * @param voltageLevel 电压等级(10kV = 10 220kV = 220 ) + * @param protocolCapacity 协议容量 + * @param devCapacity 设备容量 + * @param shortCapacity 短路容量 + * @param powerFlag 0.用户侧 1.电网侧 + * @param lineType 0.主网 1.配网 需要注意配网目前没有四种容量,谐波电流幅值限值,负序电流限值无法计算默认-3.14159 + */ + public static AddOverlimitPO globalAssemble(Float voltageLevel, Float protocolCapacity, Float devCapacity, + Float shortCapacity, Integer powerFlag, Integer lineType) { + if (voltageLevel == null) { + throw new IllegalArgumentException("电压等级不能为空"); + } + AddOverlimitPO overlimit = new AddOverlimitPO(); + voltageDeviation(overlimit,voltageLevel); + frequency(overlimit); + voltageFluctuation(overlimit,voltageLevel); + voltageFlicker(overlimit,voltageLevel); + totalHarmonicDistortion(overlimit,voltageLevel); + uHarm(overlimit,voltageLevel); + threeVoltageUnbalance(overlimit); + interharmonicCurrent(overlimit,voltageLevel); + + if(isDistributionLine(lineType)) { + //配网 + Float[] iHarmTem = new Float[49]; + for (int i = 0; i <= 48; i++) { + //目前只处理了配网II类测点,III类测点暂未处理,III类测点参考主网 + iHarmTem[i] = getHarmTag(i+2,voltageLevel).floatValue(); + } + overlimit.buildIHarm(iHarmTem); + overlimit.setINeg(DEFAULT_CURRENT_LIMIT); + }else if (hasMainNetworkCapacity(protocolCapacity, devCapacity, shortCapacity)) { + //主网 + iHarm(overlimit, voltageLevel, protocolCapacity, devCapacity, shortCapacity); + negativeSequenceCurrent(overlimit, voltageLevel, shortCapacity); + } else { + setDefaultCurrentLimit(overlimit); + } + return overlimit; + } + + private static boolean isDistributionLine(Integer lineType) { + return lineType != null && lineType == LINE_TYPE_DISTRIBUTION; + } + + private static boolean hasMainNetworkCapacity(Float protocolCapacity, Float devCapacity, Float shortCapacity) { + return protocolCapacity != null && devCapacity != null && shortCapacity != null + && devCapacity > 0 && shortCapacity > 0; + } + + private static void setDefaultCurrentLimit(AddOverlimitPO overlimit) { + Float[] iHarmTem = new Float[49]; + Arrays.fill(iHarmTem, DEFAULT_CURRENT_LIMIT); + overlimit.buildIHarm(iHarmTem); + overlimit.setINeg(DEFAULT_CURRENT_LIMIT); + } + + + /** + * 电压偏差限值 + * + */ + public static void voltageDeviation(AddOverlimitPO overlimit,Float voltageLevel) { + float voltageDev = DEFAULT_LIMIT,uvoltageDev = DEFAULT_LIMIT; + if(voltageLevel <= KV_0_22){ + voltageDev = 7.0f; + uvoltageDev=-10.0f; + }else if(voltageLevel>KV_0_22&&voltageLevel=KV_20&&voltageLevel=KV_35&&voltageLevel=KV_66&&voltageLevel<=KV_110){ + voltageDev = 7.0f; + uvoltageDev=-3.0f; + }else if(voltageLevel>KV_110){ + voltageDev = 10.0f; + uvoltageDev=-10.0f; + } + overlimit.setVoltageDev(voltageDev); + overlimit.setUvoltageDev(uvoltageDev); + } + + + /** + * 频率偏差 + * 默认限值:±0.2Hz(即:-0.2 Hz≤限值≤0.2 Hz) + */ + public static void frequency(AddOverlimitPO overlimit) { + overlimit.setFreqDev(0.2f); + } + + + /** + * 电压波动 + * 对LV、MV:0≤限值≤3%;对HV:0≤限值≤2.5%。 + * LV、MV、HV的定义: + * 低压(LV) UN≤1kV + * 中压(MV) 1kV<UN≤35kV + * 高压(HV) 35kV<UN≤220kV + * 超高压(EHV),220kV<UN,参照HV执行 + */ + public static void voltageFluctuation(AddOverlimitPO overlimit, Float voltageLevel) { + if (voltageLevel < KV_35) { + overlimit.setVoltageFluctuation(3.0f); + } else { + overlimit.setVoltageFluctuation(2.5f); + } + } + + + + /** + * 电压闪变 + * ≤110kV 1 + * >110kV 0.8 + */ + public static void voltageFlicker(AddOverlimitPO overlimit, Float voltageLevel) { + if (voltageLevel <= KV_110) { + overlimit.setFlicker(1.0f); + } else { + overlimit.setFlicker(0.8f); + } + } + + + /** + * 总谐波电压畸变率 + * + * + */ + public static void totalHarmonicDistortion(AddOverlimitPO overlimit, Float voltageLevel) { + float result = DEFAULT_LIMIT; + if (voltageLevel < KV_6) { + result = 5.0f; + } else if(voltageLevel >= KV_6 && voltageLevel <= KV_20){ + result = 4.0f; + } else if(voltageLevel >= KV_35 && voltageLevel <= KV_66){ + result = 3.0f; + } else if(voltageLevel >= KV_110 && voltageLevel <= KV_1000){ + result = 2.0f; + } + overlimit.setUaberrance(result); + } + + + + /** + * 谐波电压含有率 + */ + public static void uHarm(AddOverlimitPO overlimit, Float voltageLevel) { + float resultOdd = DEFAULT_LIMIT,resultEven = DEFAULT_LIMIT; + if (voltageLevel < KV_6) { + resultOdd = 4.0f; + resultEven = 2.0f; + } else if(voltageLevel >= KV_6 && voltageLevel <= KV_20){ + resultOdd = 3.2f; + resultEven = 1.6f; + } else if(voltageLevel >= KV_35 && voltageLevel <= KV_66){ + resultOdd = 2.4f; + resultEven = 1.2f; + } else if(voltageLevel >= KV_110 && voltageLevel <= KV_1000){ + resultOdd = 1.6f; + resultEven = 0.8f; + } + overlimit.buildUharm(resultEven,resultOdd); + } + + + /** + * 负序电压不平衡(三相电压不平衡度) + * + */ + public static void threeVoltageUnbalance(AddOverlimitPO overlimit) { + overlimit.setUbalance(2.0f); + overlimit.setShortUbalance(4.0f); + } + + + /*---------------------------------谐波电流限值start-----------------------------------*/ + + /** + * 谐波电流限值 + */ + public static void iHarm(AddOverlimitPO overlimit, Float voltageLevel,Float protocolCapacity,Float devCapacity,Float shortCapacity) { + float calCap = shortCapacity/getDlCapByVoltageLevel(voltageLevel); + //24谐波电流幅值 + Float[] iHarmTem = new Float[49]; + for (int i = 0; i <= 48; i++) { + float inHarm = iHarmCalculate(i+2,voltageLevel,protocolCapacity,devCapacity,calCap); + iHarmTem[i] = inHarm; + } + overlimit.buildIHarm(iHarmTem); + } + /** + * @Description: iHarmCalculate + * @Param: protocolCapacity 协议容量 devCapacity设备容量 calCap 短路容量 + * @return: float + * @Author: clam + * @Date: 2024/2/4 + */ + private static float iHarmCalculate(int nHarm, Float voltageLevel,float protocolCapacity, float devCapacity,float calCap) { + double tag = calCap*getHarmTag(nHarm,voltageLevel); + Double limit = getHarmonicLimit(nHarm,tag,new BigDecimal(String.valueOf(devCapacity)).doubleValue(),new BigDecimal(String.valueOf(protocolCapacity)).doubleValue()); + BigDecimal bigDecimal = BigDecimal.valueOf(limit).setScale(4,RoundingMode.HALF_UP); + return bigDecimal.floatValue(); + } + + + /** + * 电流谐波限值 + */ + private static Double getHarmTag(Integer iCount, Float voltageLevel) { + int x, y; + if (voltageLevel < KV_6) { + x = 0; + } else if (voltageLevel buildLedger(invocation.getArgument(0), "project-001", + AddLedgerConst.ROOT_PARENT_ID + ",engineering-001,project-001", AddLedgerConst.LEVEL_EQUIPMENT)); + when(equipmentMapper.selectOne(any())).thenReturn(buildActiveEquipment("device-001")); + + service.saveEquipment(param); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AddDeviceUnit.class); + verify(deviceUnitMapper).insert(captor.capture()); + Assertions.assertNotNull(captor.getValue().getDevId()); + Assertions.assertEquals("Hz", captor.getValue().getUnitFrequency()); + Assertions.assertEquals("kV", captor.getValue().getPhaseVoltage()); + } + + @Test + void saveDeviceUnitShouldUpdateExistingDeviceUnit() { + AddDeviceUnit existing = new AddDeviceUnit(); + existing.setDevId("device-001"); + when(deviceUnitMapper.selectById(eq("device-001"))).thenReturn(existing); + when(equipmentMapper.selectOne(any())).thenReturn(buildActiveEquipment("device-001")); + + AddDeviceUnitSaveParam param = new AddDeviceUnitSaveParam(); + param.setDevId("device-001"); + param.setUnitFrequency("MHz"); + param.setPhaseVoltage("V"); + + AddDeviceUnitVO result = service.saveDeviceUnit(param); + + verify(deviceUnitMapper).updateById(any(AddDeviceUnit.class)); + Assertions.assertEquals("device-001", result.getDevId()); + Assertions.assertEquals("MHz", result.getUnitFrequency()); + Assertions.assertEquals("V", result.getPhaseVoltage()); + } + + @Test + void getLineOverlimitShouldQueryExistingLineOverlimit() { + AddLedgerLinePO line = new AddLedgerLinePO(); + line.setLineId("line-001"); + AddOverlimitPO overlimit = new AddOverlimitPO(); + overlimit.setId("line-001"); + + when(lineMapper.selectOne(any())).thenReturn(line); + when(overlimitMapper.selectById(eq("line-001"))).thenReturn(overlimit); + + AddOverlimitPO result = service.getLineOverlimit("line-001"); + + Assertions.assertSame(overlimit, result); + } + private AddLedgerLineSaveParam buildValidLineParam() { AddLedgerLineSaveParam param = new AddLedgerLineSaveParam(); param.setDeviceId("device-001"); @@ -63,4 +139,33 @@ class AddLedgerServiceImplTest { param.setPt2Ratio(BigDecimal.ONE); return param; } + + private AddLedgerEquipmentSaveParam buildValidEquipmentParam() { + AddLedgerEquipmentSaveParam param = new AddLedgerEquipmentSaveParam(); + param.setProjectId("project-001"); + param.setName("设备A"); + param.setNdid("ndid-001"); + param.setMac("00:11:22:33:44:55"); + param.setDevModel("model-001"); + return param; + } + + private AddLedgerLedgerPO buildLedger(String id, String pid, String pids, Integer level) { + AddLedgerLedgerPO ledger = new AddLedgerLedgerPO(); + ledger.setId(id); + ledger.setPid(pid); + ledger.setPids(pids); + ledger.setLevel(level); + ledger.setState(AddLedgerConst.STATE_NORMAL); + ledger.setName(id); + return ledger; + } + + private AddLedgerEquipmentPO buildActiveEquipment(String id) { + AddLedgerEquipmentPO equipment = new AddLedgerEquipmentPO(); + equipment.setId(id); + equipment.setName("设备A"); + equipment.setRunStatus(AddLedgerConst.EQUIPMENT_RUN_STATUS_OFFLINE); + return equipment; + } }