Compare commits

2 Commits

Author SHA1 Message Date
36962221f5 feat(dbms): 增加数据库备份任务停止重启功能和MySQL支持
- 添加了备份任务停止和重启接口及实现
- 实现了对MySQL数据库的支持,包括数据库名配置
- 重构了数据库连接和备份操作的SPI架构
- 优化了备份文件删除逻辑,支持目录递归删除
- 增加了连接名称唯一性校验
- 完善了备份任务状态管理和错误处理机制
- 更新了数据库连接参数验证逻辑
2026-06-09 13:14:43 +08:00
5f6c10b9cb feat(add-ledger): 新增线路类型和设备单位管理功能
- 添加线路类型常量定义(主网0,配网1)及验证逻辑
- 新增设备单位查询和保存接口及实现
- 新增监测点限值查询接口
- 扩展AddLedgerDetailVO和AddLedgerLinePO实体类以支持线路类型字段
- 在测点保存时自动计算并保存限值信息
- 添加设备单位默认配置初始化逻辑
- 新增COverlimitUtil工具类用于限值计算
- 完善相关单元测试用例
2026-05-29 15:15:22 +08:00
92 changed files with 6689 additions and 6 deletions

2
.gitignore vendored
View File

@@ -5,6 +5,8 @@
target/
logs/
docs/
.codex-tmp/
.docs/
# Log file
*.log

View File

@@ -4,12 +4,14 @@ 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;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
@@ -22,7 +24,9 @@ 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.List;
import java.util.Set;
/**
@@ -56,7 +60,29 @@ public class SteadyChecksquareInfluxQueryComponent {
}
}
public List<SteadyChecksquareValuePointBO> queryValuePoints(SteadyTrendResolvedFieldBO field, LocalDateTime startTime,
LocalDateTime endTime, int intervalMinutes) {
validateConfig();
String query = buildValuePointQuery(field, startTime, endTime);
long startMillis = System.currentTimeMillis();
log.info("数据校验指标值 InfluxDB 查询开始measurement={}field={}lineId={}phase={}statType={}query={}",
field.getMeasurement(), field.getField(), field.getLineId(), field.getPhase(), field.getStatType(), query);
try {
String body = executeQuery(query);
List<SteadyChecksquareValuePointBO> points = parseValuePoints(body, intervalMinutes);
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());
throw ex;
}
}
public String buildChecksquareQuery(SteadyTrendResolvedFieldBO field, LocalDateTime startTime, LocalDateTime endTime) {
return buildValuePointQuery(field, startTime, endTime);
}
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("\"");
@@ -94,6 +120,35 @@ public class SteadyChecksquareInfluxQueryComponent {
}
}
private List<SteadyChecksquareValuePointBO> 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");
List<SteadyChecksquareValuePointBO> result = new ArrayList<SteadyChecksquareValuePointBO>();
if (!values.isArray()) {
return result;
}
for (JsonNode value : values) {
if (value.size() < 2 || value.get(1).isNull()) {
continue;
}
LocalDateTime time = parseInfluxTime(value.get(0).asText());
if (time == null) {
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 LocalDateTime alignToPreviousSlot(LocalDateTime time, int intervalMinutes) {
LocalDateTime minuteFloor = time.withSecond(0).withNano(0);
int minuteOfDay = minuteFloor.getHour() * 60 + minuteFloor.getMinute();

View File

@@ -0,0 +1,143 @@
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<String> 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<String, Map<LocalDateTime, BigDecimal>> statValueMap = queryStatValueMap(lineId, indicator,
harmonicOrder, phase, startTime, endTime, intervalMinutes);
appendAbnormalDetails(result, phase, 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<String, Map<LocalDateTime, BigDecimal>> queryStatValueMap(String lineId,
SteadyTrendIndicatorDefinitionBO indicator,
Integer harmonicOrder, String phase,
LocalDateTime startTime, LocalDateTime endTime,
int intervalMinutes) {
Map<String, Map<LocalDateTime, BigDecimal>> result = new LinkedHashMap<String, Map<LocalDateTime, BigDecimal>>();
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,
Map<String, Map<LocalDateTime, BigDecimal>> statValueMap) {
Map<LocalDateTime, BigDecimal> maxValues = statValueMap.get("MAX");
Map<LocalDateTime, BigDecimal> cp95Values = statValueMap.get("CP95");
Map<LocalDateTime, BigDecimal> avgValues = statValueMap.get("AVG");
Map<LocalDateTime, BigDecimal> minValues = statValueMap.get("MIN");
if (maxValues == null || cp95Values == null || avgValues == null || minValues == null) {
return;
}
for (Map.Entry<LocalDateTime, BigDecimal> 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, maxValue, minValue, avgValue, cp95Value));
}
}
private SteadyChecksquareValueOrderDetailVO buildDetail(LocalDateTime time, String phase, BigDecimal maxValue,
BigDecimal minValue, BigDecimal avgValue, BigDecimal cp95Value) {
SteadyChecksquareValueOrderDetailVO detail = new SteadyChecksquareValueOrderDetailVO();
detail.setTime(OUTPUT_TIME_FORMATTER.format(time));
detail.setPhase(phase);
detail.setMaxValue(maxValue);
detail.setMinValue(minValue);
detail.setAvgValue(avgValue);
detail.setCp95Value(cp95Value);
return detail;
}
private Map<LocalDateTime, BigDecimal> toValueMap(List<SteadyChecksquareValuePointBO> points) {
Map<LocalDateTime, BigDecimal> result = new LinkedHashMap<LocalDateTime, BigDecimal>();
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<SteadyTrendSeriesFieldBO> fields = indicator.getSeriesFields();
if (fields == null || fields.isEmpty()) {
return "";
}
return fields.get(0).getField();
}
}

View File

@@ -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;
}

View File

@@ -54,6 +54,15 @@ public class SteadyChecksquareItemVO implements Serializable {
@ApiModelProperty("最大连续缺失时长,单位分钟")
private Integer maxContinuousMissingMinutes;
@ApiModelProperty("指标值大小关系是否异常")
private Boolean abnormal;
@ApiModelProperty("指标值大小关系异常累计值")
private Integer abnormalPointCount;
@ApiModelProperty("指标值大小关系异常明细")
private List<SteadyChecksquareValueOrderDetailVO> abnormalDetails = new ArrayList<SteadyChecksquareValueOrderDetailVO>();
@ApiModelProperty("统计类型摘要")
private List<SteadyChecksquareStatSummaryVO> statSummaries = new ArrayList<SteadyChecksquareStatSummaryVO>();

View File

@@ -0,0 +1,36 @@
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 BigDecimal maxValue;
@ApiModelProperty("最小值")
private BigDecimal minValue;
@ApiModelProperty("平均值")
private BigDecimal avgValue;
@ApiModelProperty("CP95 值")
private BigDecimal cp95Value;
}

View File

@@ -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<SteadyChecksquareValueOrderDetailVO> abnormalDetails = new ArrayList<SteadyChecksquareValueOrderDetailVO>();
}

View File

@@ -4,12 +4,14 @@ import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.steady.checksquare.component.SteadyChecksquareCalculator;
import com.njcn.gather.steady.checksquare.component.SteadyChecksquareInfluxQueryComponent;
import com.njcn.gather.steady.checksquare.component.SteadyChecksquareValueOrderRuleComponent;
import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareQueryVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareSegmentVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareStatDetailVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareStatSummaryVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderRuleVO;
import com.njcn.gather.steady.checksquare.service.SteadyChecksquareService;
import com.njcn.gather.steady.datavie.component.SteadyTrendIndicatorCatalog;
import com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendIndicatorDefinitionBO;
@@ -52,6 +54,7 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService {
private final SteadyTrendIndicatorCatalog indicatorCatalog;
private final SteadyChecksquareInfluxQueryComponent influxQueryComponent;
private final SteadyChecksquareCalculator calculator;
private final SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent;
private final AddDataTimeSlotCalculator timeSlotCalculator;
private final AddLedgerService addLedgerService;
@@ -138,9 +141,20 @@ 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 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<LocalDateTime> queryMergedActualSlots(String lineId, SteadyTrendIndicatorDefinitionBO indicator, Integer harmonicOrder,
String statType, LocalDateTime startTime, LocalDateTime endTime,
int intervalMinutes) {

View File

@@ -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);

View File

@@ -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')
);

View File

@@ -33,4 +33,25 @@ 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"));
}
}

View File

@@ -0,0 +1,155 @@
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, "8"), 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, "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, 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("8"), result.getAbnormalDetails().get(0).getCp95Value());
}
@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, "10"));
}
if ("CP95".equals(statType)) {
return Collections.singletonList(point(time, "8"));
}
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 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;
}
}

View File

@@ -2,9 +2,12 @@ package com.njcn.gather.steady.checksquare.service.impl;
import com.njcn.gather.steady.checksquare.component.SteadyChecksquareCalculator;
import com.njcn.gather.steady.checksquare.component.SteadyChecksquareInfluxQueryComponent;
import com.njcn.gather.steady.checksquare.component.SteadyChecksquareValueOrderRuleComponent;
import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareQueryVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderDetailVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderRuleVO;
import com.njcn.gather.steady.datavie.component.SteadyTrendIndicatorCatalog;
import com.njcn.gather.tool.adddata.component.AddDataTimeSlotCalculator;
import com.njcn.gather.tool.addledger.pojo.vo.AddLedgerLinePathVO;
@@ -19,6 +22,7 @@ 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.when;
@@ -31,9 +35,12 @@ class SteadyChecksquareServiceImplTest {
@Test
void shouldUseFixedFlickerIntervalsPerIndicator() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class);
AddLedgerService addLedgerService = mock(AddLedgerService.class);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
influxQueryComponent, new SteadyChecksquareCalculator(), new AddDataTimeSlotCalculator(), addLedgerService);
influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, new AddDataTimeSlotCalculator(), addLedgerService);
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("进线一");
@@ -66,9 +73,12 @@ class SteadyChecksquareServiceImplTest {
@Test
void shouldOnlyQueryRequestedHarmonicOrders() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class);
AddLedgerService addLedgerService = mock(AddLedgerService.class);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
influxQueryComponent, new SteadyChecksquareCalculator(), new AddDataTimeSlotCalculator(), addLedgerService);
influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, new AddDataTimeSlotCalculator(), addLedgerService);
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("进线一");
@@ -95,9 +105,12 @@ class SteadyChecksquareServiceImplTest {
@Test
void shouldKeepRequestedHarmonicOrdersDistinctAndOrdered() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class);
AddLedgerService addLedgerService = mock(AddLedgerService.class);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
influxQueryComponent, new SteadyChecksquareCalculator(), new AddDataTimeSlotCalculator(), addLedgerService);
influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, new AddDataTimeSlotCalculator(), addLedgerService);
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("进线一");
@@ -122,9 +135,54 @@ class SteadyChecksquareServiceImplTest {
Assertions.assertEquals(Integer.valueOf(5), items.get(1).getHarmonicOrder());
}
@Test
void shouldAssembleValueOrderRuleResultIntoItem() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class);
AddLedgerService addLedgerService = mock(AddLedgerService.class);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, new AddDataTimeSlotCalculator(), addLedgerService);
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("进线一");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1)))
.thenReturn(new HashSet<LocalDateTime>(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 = service.query(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());
}
private void assertItemInterval(SteadyChecksquareItemVO item, String indicatorCode, int intervalMinutes, int expectedPointCount) {
Assertions.assertEquals(indicatorCode, item.getIndicatorCode());
Assertions.assertEquals(Integer.valueOf(intervalMinutes), item.getIntervalMinutes());
Assertions.assertEquals(Integer.valueOf(expectedPointCount), item.getExpectedPointCount());
}
private SteadyChecksquareValueOrderRuleVO emptyRuleResult() {
return new SteadyChecksquareValueOrderRuleVO();
}
}

View File

@@ -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 分片:
- 数据分片文件:`<table>_part001_yyyyMMdd_<taskNo>.csv`
- 元数据文件:`mysql_jdbc_export_metadata_yyyyMMdd_<taskNo>.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` 编译、测试或真实库联调。

View File

@@ -26,5 +26,19 @@
<artifactId>spingboot2.3.12</artifactId>
<version>2.3.12</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>mybatis-plus</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0.3</version>
</dependency>
</dependencies>
</project>

View File

@@ -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<String> tableNames) {
List<String> 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<String> 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<String> 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<String> 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<String> command) {
if (command.size() < 2) {
return String.join(" ", command);
}
List<String> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<TableMetadata> 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<TableMetadata> metadataList = Arrays.asList(objectMapper.readValue(metadataFilePath.toFile(), TableMetadata[].class));
Map<String, TableMetadata> 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<String> 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<String> 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<TableExportMetadata> 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<String> columnNames = new ArrayList<>();
List<String> 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<String> 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<String> columns,
List<String> 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<String> 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<String> columns = null;
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("-- TABLE ")) {
continue;
}
if (columns == null) {
columns = parseCsvLine(line);
continue;
}
List<String> 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<String> 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<String> columnNames = new ArrayList<>();
List<String> 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<String> parseCsvLine(String line) {
List<String> 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<String> columnNames;
private List<String> 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<TableExportMetadata> tables;
}
@Data
public static class TableExportMetadata {
private String tableName;
private String fullTableName;
private String timeColumn;
private String startTime;
private String endTime;
private List<ColumnMetadata> columns;
private Long rowCount;
private List<FilePartMetadata> 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);
}
}
}

View File

@@ -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<DatabaseTableVO> 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<DatabaseTableVO> 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();
}
}

View File

@@ -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<Runnable>(8),
runnable -> {
Thread thread = new Thread(runnable);
thread.setName("dbms-task-" + threadIndex.getAndIncrement());
return thread;
},
(runnable, executor) -> log.warn("数据库运维任务线程池已满,拒绝新的任务")
);
}
}

View File

@@ -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;
}
}

View File

@@ -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() {
}
}

View File

@@ -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<DatabaseTaskCreateVO> 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<Page<DatabaseTaskVO>> 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<DatabaseTaskVO> 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<Boolean> 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<DatabaseTaskCreateVO> 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<Page<DatabaseBackupFileVO>> listFiles(@RequestBody @Validated DatabaseBackupParam.FileQueryParam param) {
String methodDescribe = getMethodDescribe("listFiles");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseBackupFileService.listFiles(param), methodDescribe);
}
}

View File

@@ -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<Page<DatabaseConnectionVO>> 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<Boolean> 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<Boolean> 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<Boolean> 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<DatabaseTestResultVO> 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<List<DatabaseTableVO>> tables(@RequestBody @Validated DatabaseConnectionParam.TablesParam param) {
String methodDescribe = getMethodDescribe("tables");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseConnectionService.listTables(param), methodDescribe);
}
}

View File

@@ -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<Boolean> 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<Boolean> 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);
}
}

View File

@@ -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<DatabaseTaskCreateVO> 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<DatabaseTaskVO> status(@RequestParam("taskId") String taskId) {
String methodDescribe = getMethodDescribe("status");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseOperationTaskService.getStatus(taskId), methodDescribe);
}
}

View File

@@ -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<DatabaseBackupFile> {
}

View File

@@ -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<DatabaseConnection> {
}

View File

@@ -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<DatabaseOperationTask> {
}

View File

@@ -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<DatabaseRestoreRecord> {
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.systemops.database.pojo.enums;
/**
* 备份模式。
*/
public enum BackupModeEnum {
FULL_TABLE,
TIME_RANGE,
SIZE_SPLIT
}

View File

@@ -0,0 +1,9 @@
package com.njcn.gather.systemops.database.pojo.enums;
/**
* 备份策略。
*/
public enum BackupStrategyEnum {
DATA_PUMP,
JDBC_EXPORT
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.systemops.database.pojo.enums;
/**
* 备份文件格式。
*/
public enum FileFormatEnum {
DMP,
SQL,
CSV
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.systemops.database.pojo.enums;
/**
* 数据库运维操作类型。
*/
public enum OperationTypeEnum {
BACKUP,
RESTORE,
DELETE
}

View File

@@ -0,0 +1,11 @@
package com.njcn.gather.systemops.database.pojo.enums;
/**
* 恢复模式。
*/
public enum RestoreModeEnum {
SKIP,
APPEND,
TRUNCATE,
REPLACE
}

View File

@@ -0,0 +1,12 @@
package com.njcn.gather.systemops.database.pojo.enums;
/**
* 运维任务状态。
*/
public enum TaskStatusEnum {
WAITING,
RUNNING,
SUCCESS,
FAIL,
CANCELLED
}

View File

@@ -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<String> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<DatabaseBackupFile> {
Page<DatabaseBackupFileVO> listFiles(DatabaseBackupParam.FileQueryParam param);
boolean deleteBackupFile(String backupFileId, String confirmText);
void validateBackupFileReadable(DatabaseBackupFile backupFile);
Path resolveManagedPath(DatabaseBackupFile backupFile, String filePath);
}

View File

@@ -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<DatabaseConnection> {
Page<DatabaseConnectionVO> listConnections(DatabaseConnectionParam.QueryParam queryParam);
boolean addConnection(DatabaseConnectionParam param);
boolean updateConnection(DatabaseConnectionParam.UpdateParam param);
boolean deleteConnection(DatabaseConnectionParam.DeleteParam param);
DatabaseTestResultVO testConnection(DatabaseConnectionParam.TestParam param);
List<DatabaseTableVO> listTables(DatabaseConnectionParam.TablesParam param);
DatabaseConnection requireEnabled(String connectionId);
String resolvePassword(DatabaseConnection connection, String temporaryPassword);
}

View File

@@ -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<DatabaseOperationTask> {
DatabaseTaskCreateVO createBackupTask(DatabaseBackupParam.CreateParam param);
Page<DatabaseTaskVO> 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);
}

View File

@@ -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<DatabaseRestoreRecord> {
DatabaseTaskCreateVO createRestoreTask(DatabaseRestoreParam.CreateParam param);
}

View File

@@ -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<DatabaseBackupFileMapper, DatabaseBackupFile> implements DatabaseBackupFileService {
private final DbmsProperties dbmsProperties;
@Override
public Page<DatabaseBackupFileVO> listFiles(DatabaseBackupParam.FileQueryParam param) {
DatabaseBackupParam.FileQueryParam query = param == null ? new DatabaseBackupParam.FileQueryParam() : param;
LambdaQueryWrapper<DatabaseBackupFile> 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<DatabaseBackupFile> page = this.page(new Page<>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), wrapper);
Page<DatabaseBackupFileVO> 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<Path> 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;
}
}

View File

@@ -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<DatabaseConnectionMapper, DatabaseConnection> implements DatabaseConnectionService {
private final DatabasePasswordComponent databasePasswordComponent;
private final DatabaseOperatorRegistry databaseOperatorRegistry;
private final ObjectProvider<DatabaseOperationTaskService> databaseOperationTaskServiceProvider;
@Override
public Page<DatabaseConnectionVO> listConnections(DatabaseConnectionParam.QueryParam queryParam) {
DatabaseConnectionParam.QueryParam query = queryParam == null ? new DatabaseConnectionParam.QueryParam() : queryParam;
LambdaQueryWrapper<DatabaseConnection> 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<DatabaseConnection> page = this.page(new Page<>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), wrapper);
Page<DatabaseConnectionVO> 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<DatabaseTableVO> 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;
}
}

View File

@@ -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<DatabaseOperationTaskMapper, DatabaseOperationTask> 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<DatabaseTaskVO> listBackupTasks(DatabaseBackupParam.TaskQueryParam param) {
DatabaseBackupParam.TaskQueryParam query = param == null ? new DatabaseBackupParam.TaskQueryParam() : param;
LambdaQueryWrapper<DatabaseOperationTask> 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<DatabaseOperationTask> page = this.page(new Page<>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), wrapper);
Page<DatabaseTaskVO> 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;
}
}

View File

@@ -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<DatabaseRestoreRecordMapper, DatabaseRestoreRecord> 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());
}
}
}

View File

@@ -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<DatabaseTableVO> 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<DatabaseTableVO> 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);
}
}

View File

@@ -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<Path> 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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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";
}
}

View File

@@ -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<DatabaseTableVO> listTables(DatabaseConnection connection, String password, String schemaOrDatabaseName) throws Exception {
return oracleJdbcComponent.listTables(connection, password, schemaOrDatabaseName);
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}
}

View File

@@ -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;
}

View File

@@ -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<DatabaseTableVO> listTables(DatabaseConnection connection, String password, String schemaOrDatabaseName) throws Exception;
}

View File

@@ -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<DatabaseConnectionOperator> connectionOperators;
private final List<DatabaseBackupOperator> backupOperators;
private final List<DatabaseRestoreOperator> 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));
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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、SIDOracle 使用',
`service_name` VARCHAR(128) NULL COMMENT '服务名Oracle SERVICE_NAME 模式使用',
`sid` VARCHAR(128) NULL COMMENT 'SIDOracle SID 模式使用',
`database_name` VARCHAR(128) NULL COMMENT '数据库名或实例名,预留给 MySQL 等数据库使用',
`schema_name` VARCHAR(128) NULL COMMENT '默认 SchemaOracle 使用',
`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_ACTIONSKIP、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='数据库恢复记录表';

View File

@@ -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');

View File

@@ -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<AddDeviceUnitVO> 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<AddDeviceUnitVO> 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<AddOverlimitPO> 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")

View File

@@ -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<AddDeviceUnit> {
}

View File

@@ -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<AddOverlimitPO> {
}

View File

@@ -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<Integer> CON_TYPES = new LinkedHashSet<Integer>(Arrays.asList(0, 1, 2));
public static final Set<Integer> LINE_TYPES = new LinkedHashSet<Integer>(Arrays.asList(
LINE_TYPE_MAIN,
LINE_TYPE_DISTRIBUTION
));
public static final Set<BigDecimal> VOL_GRADES = new LinkedHashSet<BigDecimal>(Arrays.asList(
new BigDecimal("0.38"),
new BigDecimal("10"),

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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 = "250次谐波电压含有率")
private String vharmonicRate = "%";
@TableField("i_harmonic")
@ApiModelProperty(value = "250次谐波电流有效值")
private String iharmonic = "A";
@TableField("p_harmonic")
@ApiModelProperty(value = "250次谐波有功功率")
private String pharmonic = "kW";
@TableField("i_iharmonic")
@ApiModelProperty(value = "0.549.5次间谐波电流有效值")
private String iiharmonic = "A";
@TableField("positive_v")
@ApiModelProperty(value = "正序电压")
private String positiveV = "kV";
@TableField("no_positive_v")
@ApiModelProperty(value = "零序负序电压")
private String noPositiveV = "V";
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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<Integer> availableLineNos(String deviceId, String lineId);
Map<String, AddLedgerLinePathVO> listLinePathByLineIds(List<String> lineIds);

View File

@@ -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<String, AddLedgerLinePathVO> listLinePathByLineIds(List<String> lineIds) {
List<String> 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;
}

View File

@@ -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){
voltageDev = 7.0f;
uvoltageDev=-7.0f;
}else if(voltageLevel>=KV_20&&voltageLevel<KV_35){
voltageDev = 7.0f;
uvoltageDev=-7.0f;
}else if(voltageLevel>=KV_35&&voltageLevel<KV_66){
voltageDev = 10.0f;
uvoltageDev=-10.0f;
}else if(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、MV0≤限值≤3%对HV0≤限值≤2.5%。
* LV、MV、HV的定义
* 低压LV UN≤1kV
* 中压MV 1kVUN≤35kV
* 高压HV 35kVUN≤220kV
* 超高压EHV220kVUN参照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<KV_10) {
x = 1;
} else if (voltageLevel<KV_35) {
x = 2;
} else if (voltageLevel<KV_66) {
x = 3;
} else if (voltageLevel<KV_110) {
x = 4;
} else {
x = 5;
}
y = iCount - 2;
return ARR[x][y];
}
/**
* 相位叠加系数的取值
*/
public static Double getHarmonicLimit(Integer times, double iTag, double supply, double user) {
if (supply == 0) {
return 0.0;
}
double coefficient = 2.0;
if (times == 3) {
coefficient = 1.1;
} else if (times == 5) {
coefficient = 1.2;
} else if (times == 7) {
coefficient = 1.4;
} else if (times == 11) {
coefficient = 1.8;
} else if (times == 13) {
coefficient = 1.9;
}
BigDecimal bd = new BigDecimal(iTag * Math.pow((user / supply), (1 / coefficient)));
bd = bd.setScale(6, RoundingMode.HALF_UP);
return Double.parseDouble(bd.toString());
}
/**
* 根据电压等级获取基准短路容量
*/
public static float getDlCapByVoltageLevel(Float voltageLevel){
float capValue;
if(voltageLevel< KV_0_6){
capValue = 10;
}else if(voltageLevel<KV_20){
capValue = 100;
}else if(voltageLevel<KV_35){
capValue = 200;
}else if(voltageLevel<KV_66){
capValue = 250;
}else if(voltageLevel<KV_110){
capValue = 500;
}else if(voltageLevel<KV_220){
capValue = 750;
}else if(voltageLevel<KV_330){
capValue = 2000;
}else if(voltageLevel<KV_500){
capValue = 3000;
}else if(voltageLevel<KV_750){
capValue = 4500;
}else if(voltageLevel<KV_1000){
capValue = 7000;
}else {
capValue = 9000;
}
return capValue;
}
/*---------------------------------谐波电流限值end-----------------------------------*/
/**
* 间谐波电压含有率
*/
public static void interharmonicCurrent(AddOverlimitPO overlimit,Float voltageLevel){
float aValue,bValue;
if(voltageLevel <= KV_1){
aValue = 0.2f;bValue = 0.5f;
}else {
aValue = 0.16f;bValue = 0.4f;
}
overlimit.setInuharm1(aValue);
overlimit.setInuharm2(aValue);
overlimit.setInuharm3(bValue);
overlimit.setInuharm4(bValue);
overlimit.setInuharm5(bValue);
overlimit.setInuharm6(bValue);
overlimit.setInuharm7(bValue);
overlimit.setInuharm8(bValue);
overlimit.setInuharm9(bValue);
overlimit.setInuharm10(bValue);
overlimit.setInuharm11(bValue);
overlimit.setInuharm12(bValue);
overlimit.setInuharm13(bValue);
overlimit.setInuharm14(bValue);
overlimit.setInuharm15(bValue);
overlimit.setInuharm16(bValue);
}
/**
* 负序电流限值
*/
public static void negativeSequenceCurrent(AddOverlimitPO overlimit,Float voltageLevel,Float shortCapacity){
double v = (0.013*shortCapacity*1000)/(getUl(voltageLevel)*Math.sqrt(3));
overlimit.setINeg((float) v);
}
/**
* 获取额定线电压
*/
private static float getUl(Float voltageLevel){
float value;
if(voltageLevel<KV_6){
value = 0.4f;
}else if(voltageLevel<KV_10){
value = 6.3f;
}else if(voltageLevel<KV_20){
value = 10.5f;
}else if(voltageLevel<KV_35){
value = 21.0f;
}else if(voltageLevel<KV_66){
value = 36.5f;
}else if(voltageLevel<KV_110){
value = 69.0f;
}else if(voltageLevel<KV_220){
value = 115.5f;
}else if(voltageLevel<KV_330){
value = 230.0f;
}else {
value = 345.0f;
}
return value;
}
}

View File

@@ -6,15 +6,28 @@ 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.AddLedgerEquipmentSaveParam;
import com.njcn.gather.tool.addledger.pojo.param.AddLedgerLineSaveParam;
import com.njcn.gather.tool.addledger.pojo.po.AddDeviceUnit;
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.AddOverlimitPO;
import com.njcn.gather.tool.addledger.pojo.vo.AddDeviceUnitVO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.math.BigDecimal;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class AddLedgerServiceImplTest {
@@ -24,6 +37,8 @@ class AddLedgerServiceImplTest {
private final AddLedgerEquipmentMapper equipmentMapper = mock(AddLedgerEquipmentMapper.class);
private final AddLedgerLineMapper lineMapper = mock(AddLedgerLineMapper.class);
private final AddLedgerLedgerMapper ledgerMapper = mock(AddLedgerLedgerMapper.class);
private final AddDeviceUnitMapper deviceUnitMapper = mock(AddDeviceUnitMapper.class);
private final AddOverlimitMapper overlimitMapper = mock(AddOverlimitMapper.class);
private final AddLedgerTreeBuilder treeBuilder = mock(AddLedgerTreeBuilder.class);
private final AddLedgerServiceImpl service = new AddLedgerServiceImpl(
@@ -32,6 +47,8 @@ class AddLedgerServiceImplTest {
equipmentMapper,
lineMapper,
ledgerMapper,
deviceUnitMapper,
overlimitMapper,
treeBuilder);
@Test
@@ -50,6 +67,65 @@ class AddLedgerServiceImplTest {
+ "。如果是新增监测点,请不要传 lineId如果是编辑监测点请传已存在的测点 ID", exception.getMessage());
}
@Test
void saveEquipmentShouldCreateDefaultDeviceUnitWhenCreatingEquipment() {
AddLedgerEquipmentSaveParam param = buildValidEquipmentParam();
AddLedgerLedgerPO projectLedger = buildLedger("project-001", "engineering-001",
AddLedgerConst.ROOT_PARENT_ID + ",engineering-001", AddLedgerConst.LEVEL_PROJECT);
AddLedgerLedgerPO engineeringLedger = buildLedger("engineering-001", AddLedgerConst.ROOT_PARENT_ID,
AddLedgerConst.ROOT_PARENT_ID, AddLedgerConst.LEVEL_ENGINEERING);
when(ledgerMapper.selectActiveNode(eq("project-001"), eq(AddLedgerConst.LEVEL_PROJECT))).thenReturn(projectLedger);
when(ledgerMapper.selectActiveNode(eq("engineering-001"), eq(AddLedgerConst.LEVEL_ENGINEERING))).thenReturn(engineeringLedger);
when(ledgerMapper.selectActiveNode(any(String.class), eq(AddLedgerConst.LEVEL_EQUIPMENT)))
.thenAnswer(invocation -> 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<AddDeviceUnit> 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;
}
}