From 36962221f575243557cac0a07703fcad61a54c53 Mon Sep 17 00:00:00 2001 From: yexb <553699424@qq.com> Date: Tue, 9 Jun 2026 13:14:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(dbms):=20=E5=A2=9E=E5=8A=A0=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E5=A4=87=E4=BB=BD=E4=BB=BB=E5=8A=A1=E5=81=9C?= =?UTF-8?q?=E6=AD=A2=E9=87=8D=E5=90=AF=E5=8A=9F=E8=83=BD=E5=92=8CMySQL?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加了备份任务停止和重启接口及实现 - 实现了对MySQL数据库的支持,包括数据库名配置 - 重构了数据库连接和备份操作的SPI架构 - 优化了备份文件删除逻辑,支持目录递归删除 - 增加了连接名称唯一性校验 - 完善了备份任务状态管理和错误处理机制 - 更新了数据库连接参数验证逻辑 --- .gitignore | 2 + ...SteadyChecksquareInfluxQueryComponent.java | 55 ++ ...adyChecksquareValueOrderRuleComponent.java | 143 +++++ .../bo/SteadyChecksquareValuePointBO.java | 22 + .../pojo/vo/SteadyChecksquareItemVO.java | 9 + .../SteadyChecksquareValueOrderDetailVO.java | 36 ++ .../vo/SteadyChecksquareValueOrderRuleVO.java | 22 + .../impl/SteadyChecksquareServiceImpl.java | 14 + .../steady-data-view-index.sql | 41 ++ .../steady-menu-icon-update_20260525.sql | 42 ++ ...dyChecksquareInfluxQueryComponentTest.java | 21 + ...hecksquareValueOrderRuleComponentTest.java | 155 +++++ .../SteadyChecksquareServiceImplTest.java | 64 ++- system-ops/dbms/README.md | 32 +- system-ops/dbms/pom.xml | 5 + .../component/DatabasePasswordComponent.java | 22 +- .../component/JdbcExportComponent.java | 541 ++++++++++++++++++ .../component/OracleJdbcComponent.java | 89 +++ .../database/config/DbmsProperties.java | 1 + .../database/constant/DatabaseOpsConst.java | 1 + .../controller/DatabaseBackupController.java | 16 + .../pojo/enums/OperationTypeEnum.java | 10 + .../database/pojo/enums/RestoreModeEnum.java | 11 + .../pojo/param/DatabaseBackupParam.java | 18 + .../pojo/param/DatabaseConnectionParam.java | 15 +- .../pojo/vo/DatabaseConnectionVO.java | 1 + .../database/pojo/vo/DatabaseTableVO.java | 8 + .../service/DatabaseOperationTaskService.java | 4 + .../impl/DatabaseBackupFileServiceImpl.java | 63 +- .../impl/DatabaseConnectionServiceImpl.java | 119 +++- .../DatabaseOperationTaskServiceImpl.java | 234 ++++---- .../impl/DatabaseRestoreServiceImpl.java | 47 +- .../mysql/MysqlConnectionOperator.java | 102 ++++ .../mysql/MysqlJdbcExportBackupOperator.java | 146 +++++ .../mysql/MysqlJdbcExportRestoreOperator.java | 48 ++ .../support/mysql/MysqlJdbcUrlUtil.java | 18 + .../oracle/OracleConnectionOperator.java | 37 ++ .../oracle/OracleDataPumpBackupOperator.java | 122 ++++ .../oracle/OracleDataPumpRestoreOperator.java | 47 ++ .../OracleJdbcExportBackupOperator.java | 116 ++++ .../OracleJdbcExportRestoreOperator.java | 51 ++ .../support/spi/DatabaseBackupOperator.java | 17 + .../spi/DatabaseConnectionOperator.java | 19 + .../support/spi/DatabaseOperatorRegistry.java | 43 ++ .../support/spi/DatabaseRestoreOperator.java | 18 + .../sql/system-ops/dbms-database-ops-init.sql | 119 ++++ .../sql/system-ops/system-ops-init.sql | 14 + 47 files changed, 2553 insertions(+), 227 deletions(-) create mode 100644 steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponent.java create mode 100644 steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/bo/SteadyChecksquareValuePointBO.java create mode 100644 steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareValueOrderDetailVO.java create mode 100644 steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareValueOrderRuleVO.java create mode 100644 steady/steady-DataView/src/main/resources/sql/steady-DataView/steady-data-view-index.sql create mode 100644 steady/steady-DataView/src/main/resources/sql/steady-DataView/steady-menu-icon-update_20260525.sql create mode 100644 steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponentTest.java create mode 100644 system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/JdbcExportComponent.java create mode 100644 system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/OracleJdbcComponent.java create mode 100644 system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/OperationTypeEnum.java create mode 100644 system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/RestoreModeEnum.java create mode 100644 system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlConnectionOperator.java create mode 100644 system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcExportBackupOperator.java create mode 100644 system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcExportRestoreOperator.java create mode 100644 system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcUrlUtil.java create mode 100644 system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleConnectionOperator.java create mode 100644 system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleDataPumpBackupOperator.java create mode 100644 system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleDataPumpRestoreOperator.java create mode 100644 system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleJdbcExportBackupOperator.java create mode 100644 system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleJdbcExportRestoreOperator.java create mode 100644 system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseBackupOperator.java create mode 100644 system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseConnectionOperator.java create mode 100644 system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseOperatorRegistry.java create mode 100644 system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseRestoreOperator.java create mode 100644 system-ops/dbms/src/main/resources/sql/system-ops/dbms-database-ops-init.sql create mode 100644 system-ops/dbms/src/main/resources/sql/system-ops/system-ops-init.sql diff --git a/.gitignore b/.gitignore index c6cb740..399ac3e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ target/ logs/ docs/ +.codex-tmp/ +.docs/ # Log file *.log diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponent.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponent.java index 721875e..b7a1d20 100644 --- a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponent.java +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponent.java @@ -4,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 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 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 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 result = new ArrayList(); + 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(); diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponent.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponent.java new file mode 100644 index 0000000..a70ccd6 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponent.java @@ -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 REQUIRED_STATS = Collections.unmodifiableList(Arrays.asList("MAX", "CP95", "AVG", "MIN")); + private static final int ABNORMAL_THRESHOLD = 1; + + private final SteadyChecksquareInfluxQueryComponent influxQueryComponent; + + public SteadyChecksquareValueOrderRuleVO check(String lineId, SteadyTrendIndicatorDefinitionBO indicator, + Integer harmonicOrder, LocalDateTime startTime, + LocalDateTime endTime, int intervalMinutes) { + SteadyChecksquareValueOrderRuleVO result = new SteadyChecksquareValueOrderRuleVO(); + if (!supportValueOrderRule(indicator)) { + return result; + } + for (String phase : indicator.getPhaseCodes()) { + Map> statValueMap = queryStatValueMap(lineId, indicator, + harmonicOrder, phase, startTime, endTime, intervalMinutes); + appendAbnormalDetails(result, phase, statValueMap); + } + result.setAbnormalPointCount(result.getAbnormalDetails().size()); + result.setAbnormal(result.getAbnormalPointCount() > ABNORMAL_THRESHOLD); + return result; + } + + private boolean supportValueOrderRule(SteadyTrendIndicatorDefinitionBO indicator) { + return indicator != null && indicator.getSupportStats() != null && indicator.getSupportStats().containsAll(REQUIRED_STATS); + } + + private Map> queryStatValueMap(String lineId, + SteadyTrendIndicatorDefinitionBO indicator, + Integer harmonicOrder, String phase, + LocalDateTime startTime, LocalDateTime endTime, + int intervalMinutes) { + Map> result = new LinkedHashMap>(); + for (String statType : REQUIRED_STATS) { + SteadyTrendResolvedFieldBO field = buildResolvedField(lineId, indicator, harmonicOrder, phase, statType); + result.put(statType, toValueMap(influxQueryComponent.queryValuePoints(field, startTime, endTime, intervalMinutes))); + } + return result; + } + + private void appendAbnormalDetails(SteadyChecksquareValueOrderRuleVO result, String phase, + Map> statValueMap) { + Map maxValues = statValueMap.get("MAX"); + Map cp95Values = statValueMap.get("CP95"); + Map avgValues = statValueMap.get("AVG"); + Map minValues = statValueMap.get("MIN"); + if (maxValues == null || cp95Values == null || avgValues == null || minValues == null) { + return; + } + for (Map.Entry entry : maxValues.entrySet()) { + LocalDateTime time = entry.getKey(); + BigDecimal maxValue = entry.getValue(); + BigDecimal cp95Value = cp95Values.get(time); + BigDecimal avgValue = avgValues.get(time); + BigDecimal minValue = minValues.get(time); + // 缺少任一统计值时由缺数校验负责,不重复计入大小关系异常。 + if (maxValue == null || cp95Value == null || avgValue == null || minValue == null) { + continue; + } + if (maxValue.compareTo(cp95Value) > 0 && cp95Value.compareTo(avgValue) > 0 && avgValue.compareTo(minValue) > 0) { + continue; + } + result.getAbnormalDetails().add(buildDetail(time, phase, 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 toValueMap(List points) { + Map result = new LinkedHashMap(); + if (points == null || points.isEmpty()) { + return result; + } + for (SteadyChecksquareValuePointBO point : points) { + if (point != null && point.getTime() != null && point.getValue() != null) { + result.put(point.getTime(), point.getValue()); + } + } + return result; + } + + private SteadyTrendResolvedFieldBO buildResolvedField(String lineId, SteadyTrendIndicatorDefinitionBO indicator, + Integer harmonicOrder, String phase, String statType) { + SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO(); + field.setMeasurement(indicator.getTableName()); + field.setField(resolveField(indicator, harmonicOrder)); + field.setLineId(lineId); + field.setIndicatorCode(indicator.getIndicatorCode()); + field.setIndicatorName(indicator.getName()); + field.setPhase(phase); + field.setStatType(statType); + field.setUnit(indicator.getUnit()); + return field; + } + + private String resolveField(SteadyTrendIndicatorDefinitionBO indicator, Integer harmonicOrder) { + if (Boolean.TRUE.equals(indicator.getHarmonic())) { + return indicator.getHarmonicFieldPrefix() + "_" + harmonicOrder; + } + List fields = indicator.getSeriesFields(); + if (fields == null || fields.isEmpty()) { + return ""; + } + return fields.get(0).getField(); + } +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/bo/SteadyChecksquareValuePointBO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/bo/SteadyChecksquareValuePointBO.java new file mode 100644 index 0000000..bf5df4a --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/bo/SteadyChecksquareValuePointBO.java @@ -0,0 +1,22 @@ +package com.njcn.gather.steady.checksquare.pojo.bo; + +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 数据校验指标值时间点。 + */ +@Data +public class SteadyChecksquareValuePointBO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 对齐后的统计时间。 */ + private LocalDateTime time; + + /** 指标值。 */ + private BigDecimal value; +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareItemVO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareItemVO.java index b31d994..1108794 100644 --- a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareItemVO.java +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareItemVO.java @@ -54,6 +54,15 @@ public class SteadyChecksquareItemVO implements Serializable { @ApiModelProperty("最大连续缺失时长,单位分钟") private Integer maxContinuousMissingMinutes; + @ApiModelProperty("指标值大小关系是否异常") + private Boolean abnormal; + + @ApiModelProperty("指标值大小关系异常累计值") + private Integer abnormalPointCount; + + @ApiModelProperty("指标值大小关系异常明细") + private List abnormalDetails = new ArrayList(); + @ApiModelProperty("统计类型摘要") private List statSummaries = new ArrayList(); diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareValueOrderDetailVO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareValueOrderDetailVO.java new file mode 100644 index 0000000..1a807ba --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareValueOrderDetailVO.java @@ -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; +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareValueOrderRuleVO.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareValueOrderRuleVO.java new file mode 100644 index 0000000..b0530b5 --- /dev/null +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareValueOrderRuleVO.java @@ -0,0 +1,22 @@ +package com.njcn.gather.steady.checksquare.pojo.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 数据校验指标值大小关系规则结果。 + */ +@Data +public class SteadyChecksquareValueOrderRuleVO implements Serializable { + + private static final long serialVersionUID = 1L; + + private Boolean abnormal = false; + + private Integer abnormalPointCount = 0; + + private List abnormalDetails = new ArrayList(); +} diff --git a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImpl.java b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImpl.java index d90a9c6..a4bceea 100644 --- a/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImpl.java +++ b/steady/steady-DataView/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImpl.java @@ -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 queryMergedActualSlots(String lineId, SteadyTrendIndicatorDefinitionBO indicator, Integer harmonicOrder, String statType, LocalDateTime startTime, LocalDateTime endTime, int intervalMinutes) { diff --git a/steady/steady-DataView/src/main/resources/sql/steady-DataView/steady-data-view-index.sql b/steady/steady-DataView/src/main/resources/sql/steady-DataView/steady-data-view-index.sql new file mode 100644 index 0000000..4880578 --- /dev/null +++ b/steady/steady-DataView/src/main/resources/sql/steady-DataView/steady-data-view-index.sql @@ -0,0 +1,41 @@ +-- 稳态数据查看建议索引。 +-- 本脚本不自动执行,请按数据库现状审阅后单独执行。 + +CREATE INDEX idx_data_v_time_line_phase +ON data_v (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_i_time_line_phase +ON data_i (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_flicker_time_line_phase +ON data_flicker (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_fluc_time_line_phase +ON data_fluc (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_harmphasic_i_time_line_phase +ON data_harmphasic_i (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_harmphasic_v_time_line_phase +ON data_harmphasic_v (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_harmpower_p_time_line_phase +ON data_harmpower_p (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_harmpower_q_time_line_phase +ON data_harmpower_q (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_harmpower_s_time_line_phase +ON data_harmpower_s (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_harmrate_i_time_line_phase +ON data_harmrate_i (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_harmrate_v_time_line_phase +ON data_harmrate_v (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_inharm_i_time_line_phase +ON data_inharm_i (TIMEID, LINEID, PHASIC_TYPE); + +CREATE INDEX idx_data_plt_time_line_phase +ON data_plt (TIMEID, LINEID, PHASIC_TYPE); diff --git a/steady/steady-DataView/src/main/resources/sql/steady-DataView/steady-menu-icon-update_20260525.sql b/steady/steady-DataView/src/main/resources/sql/steady-DataView/steady-menu-icon-update_20260525.sql new file mode 100644 index 0000000..d4751de --- /dev/null +++ b/steady/steady-DataView/src/main/resources/sql/steady-DataView/steady-menu-icon-update_20260525.sql @@ -0,0 +1,42 @@ +-- 稳态模块菜单图标修正脚本。 +-- 本脚本不自动执行,请按数据库现状审阅后单独执行。 + +UPDATE sys_function +SET Icon = 'DataAnalysis' +WHERE State = 1 + AND Type = 0 + AND ( + Name = '稳态模块' + OR Code IN ('steady', 'steadyModule', 'steadyDataView') + OR Path IN ('/steady', '/steadyDataView', '/steady/data-view') + ); + +UPDATE sys_function +SET Icon = 'DataBoard' +WHERE State = 1 + AND Type = 0 + AND ( + Name = '稳态数据' + OR Code IN ('steadyData', 'steadyDataDetail') + OR Path IN ('/steady/data', '/steady/data-view/detail', '/steadyDataView/index') + ); + +UPDATE sys_function +SET Icon = 'TrendCharts' +WHERE State = 1 + AND Type = 0 + AND ( + Name = '稳态趋势' + OR Code IN ('steadyTrend', 'steadyDataTrend') + OR Path IN ('/steady/trend', '/steady/data-view/trend', '/steadyTrend/index') + ); + +UPDATE sys_function +SET Icon = 'CircleCheck' +WHERE State = 1 + AND Type = 0 + AND ( + Name = '数据验证' + OR Code IN ('dataValidation', 'steadyDataValidation') + OR Path IN ('/steady/data-validation', '/steady/data-view/validation', '/dataValidation/index') + ); diff --git a/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponentTest.java b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponentTest.java index 83de0ee..473325f 100644 --- a/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponentTest.java +++ b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponentTest.java @@ -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")); + } } diff --git a/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponentTest.java b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponentTest.java new file mode 100644 index 0000000..e2c44fa --- /dev/null +++ b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponentTest.java @@ -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; + } +} diff --git a/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImplTest.java b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImplTest.java index 57a49d8..f51387e 100644 --- a/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImplTest.java +++ b/steady/steady-DataView/src/test/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImplTest.java @@ -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(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(); + } } diff --git a/system-ops/dbms/README.md b/system-ops/dbms/README.md index 7d41369..383359e 100644 --- a/system-ops/dbms/README.md +++ b/system-ops/dbms/README.md @@ -2,7 +2,7 @@ ## 模块定位 -`dbms` 是 `system-ops` 下的数据库运维模块,当前面向 Oracle 数据库提供连接配置、连接测试、表列表查询、备份、恢复、任务状态查询和删除接口。 +`dbms` 是 `system-ops` 下的数据库运维模块,当前支持 Oracle、MySQL 两类数据库运维能力,其中 Oracle 支持 `DATA_PUMP`、`JDBC_EXPORT`,MySQL 当前支持 `JDBC_EXPORT`。 ## 当前接口 @@ -53,6 +53,7 @@ dbms: backup: storage-path: D:/dbms-backup default-max-file-size-mb: 512 + mysql-fetch-size: 1000 tools: expdp-path: impdp-path: @@ -62,20 +63,31 @@ dbms: - `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`。 ## 当前行为 -- 一期仅支持 `ORACLE`。 -- 连接密码支持两种运行方式: - - 前端每次传 `temporaryPassword`。 - - 连接已保存密文,且公共 `Sm4Utils` 提供 `decryptData_ECB` 时由后端自动解密复用。 -- 新增连接前的测试接口允许只传 `temporaryPassword`,不强制把密码写进 `connection.password`。 +- 当前能力矩阵如下: + +| 数据库类型 | 连接测试 | 表列表 | JDBC_EXPORT | DATA_PUMP | +| --- | --- | --- | --- | --- | +| ORACLE | 支持 | 支持 | 支持 | 支持 | +| MYSQL | 支持 | 支持 | 支持 | 不支持 | +- 备份和恢复只允许基于已保存且连接可用的连接配置发起。 +- 新增连接前的测试接口仍可传 `temporaryPassword` 做临时连通性测试。 - 备份任务异步执行,只有实际文件生成成功后才会写入 `dbms_backup_file` 记录。 - `JDBC_EXPORT` 当前会生成两类文件: - - 主数据文件:`*.csv` - - 元数据文件:`*_metadata_yyyyMMdd_.json` +- MySQL `JDBC_EXPORT` 会按任务号创建独立备份目录,每张表独立 CSV,默认按 512MB 分片: + - 数据分片文件:`_part001_yyyyMMdd_.csv` + - 元数据文件:`mysql_jdbc_export_metadata_yyyyMMdd_.json` +- 备份任务支持停止和重新开始: + - `POST /database/backups/tasks/stop` + - `POST /database/backups/tasks/restart` - `JDBC_EXPORT` 恢复依赖元数据文件,不再允许缺少元数据直接发起恢复。 - 删除备份文件时,会校验目标路径必须位于受管备份目录下,避免误删非备份文件。 @@ -83,7 +95,7 @@ dbms: - `DATA_PUMP` 仍依赖部署机器可执行 `expdp`、`impdp`,并且 Oracle 侧已准备好 `directory` 对象和权限。 - 当前代码要求 `DATA_PUMP` 连接配置里补齐可管理的 `directoryPath`,否则虽然 Oracle 端可能已导出成功,后端无法安全管理文件记录与删除。 -- `JDBC_EXPORT` 恢复当前仅覆盖表数据,不承诺恢复索引、约束、触发器、序列、存储过程、权限等 Oracle 对象。 +- `JDBC_EXPORT` 恢复当前仅覆盖表数据,不承诺恢复索引、约束、触发器、序列、存储过程、权限等数据库对象。 - `TIME_RANGE` 模式当前只在 `JDBC_EXPORT` 场景真正参与查询过滤;`DATA_PUMP` 尚未接入 Oracle `QUERY` 参数。 -- `SIZE_SPLIT` 参数目前已做入参校验,但尚未实现真正的导出分片。 +- MySQL `JDBC_EXPORT` 已实现按大小分片;Oracle `JDBC_EXPORT` 仍沿用原单文件导出路径。 - 本轮仅完成代码路径和文档收口,未执行 `mvn` 编译、测试或真实库联调。 diff --git a/system-ops/dbms/pom.xml b/system-ops/dbms/pom.xml index 46f43a7..776b0c3 100644 --- a/system-ops/dbms/pom.xml +++ b/system-ops/dbms/pom.xml @@ -35,5 +35,10 @@ org.springframework spring-tx + + com.oracle + ojdbc6 + 11.2.0.3 + diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/DatabasePasswordComponent.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/DatabasePasswordComponent.java index 079abbd..22823af 100644 --- a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/DatabasePasswordComponent.java +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/DatabasePasswordComponent.java @@ -1,11 +1,8 @@ package com.njcn.gather.systemops.database.component; import cn.hutool.core.util.StrUtil; -import com.njcn.common.utils.sm.Sm4Utils; import org.springframework.stereotype.Component; -import java.lang.reflect.Method; - /** * 数据库连接密码处理组件。 */ @@ -16,29 +13,16 @@ public class DatabasePasswordComponent { if (StrUtil.isBlank(plainText)) { return null; } - return new Sm4Utils(Sm4Utils.globalSecretKey).encryptData_ECB(plainText); + return plainText; } /** - * 优先使用本次请求传入的临时密码;如果公共 SM4 工具存在解密能力,则复用已保存密文。 + * 优先使用本次请求传入的临时密码;否则复用已保存的数据库密码。 */ public String resolveRuntimePassword(String passwordCipher, String temporaryPassword) { if (StrUtil.isNotBlank(temporaryPassword)) { return temporaryPassword; } - if (StrUtil.isBlank(passwordCipher)) { - return null; - } - try { - Sm4Utils sm4Utils = new Sm4Utils(Sm4Utils.globalSecretKey); - Method decryptMethod = Sm4Utils.class.getMethod("decryptData_ECB", String.class); - Object plainText = decryptMethod.invoke(sm4Utils, passwordCipher); - if (plainText instanceof String && StrUtil.isNotBlank((String) plainText)) { - return (String) plainText; - } - } catch (Exception ignored) { - // 兼容公共工具不同版本,未找到解密方法时继续走统一失败提示。 - } - throw new IllegalArgumentException("当前环境未确认密码解密方法,请传入临时密码执行本次操作"); + return StrUtil.isBlank(passwordCipher) ? null : passwordCipher; } } diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/JdbcExportComponent.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/JdbcExportComponent.java new file mode 100644 index 0000000..f39a270 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/JdbcExportComponent.java @@ -0,0 +1,541 @@ +package com.njcn.gather.systemops.database.component; + +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.njcn.gather.systemops.database.constant.DatabaseOpsConst; +import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam; +import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.Statement; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.function.BooleanSupplier; + +/** + * JDBC 表数据导出与恢复组件。 + */ +@Component +@RequiredArgsConstructor +public class JdbcExportComponent { + + private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("^[A-Za-z][A-Za-z0-9_#$]*$"); + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private final ObjectMapper objectMapper; + + public void exportCsv(Connection jdbcConnection, String ownerName, DatabaseBackupParam.CreateParam param, + Path dataFilePath, Path metadataFilePath) throws Exception { + Files.createDirectories(dataFilePath.getParent()); + if (metadataFilePath.getParent() != null) { + Files.createDirectories(metadataFilePath.getParent()); + } + List metadataList = new ArrayList<>(); + try (BufferedWriter writer = Files.newBufferedWriter(dataFilePath, StandardCharsets.UTF_8)) { + for (String tableName : param.getTargetNames()) { + metadataList.add(exportTable(jdbcConnection, ownerName, tableName, param, writer)); + } + } + try (BufferedWriter metadataWriter = Files.newBufferedWriter(metadataFilePath, StandardCharsets.UTF_8)) { + objectMapper.writeValue(metadataWriter, metadataList); + } + } + + public void importCsv(Connection jdbcConnection, Path dataFilePath, Path metadataFilePath, String dbType, + String restoreMode, String targetOwnerName) throws Exception { + String metadataText = new String(Files.readAllBytes(metadataFilePath), StandardCharsets.UTF_8); + if (metadataText.trim().startsWith("{")) { + importCsvV2(jdbcConnection, metadataFilePath, dbType, restoreMode, targetOwnerName); + return; + } + List metadataList = Arrays.asList(objectMapper.readValue(metadataFilePath.toFile(), TableMetadata[].class)); + Map metadataMap = new LinkedHashMap<>(); + for (TableMetadata metadata : metadataList) { + metadataMap.put(metadata.getFullTableName(), metadata); + } + jdbcConnection.setAutoCommit(false); + try (BufferedReader reader = Files.newBufferedReader(dataFilePath, StandardCharsets.UTF_8)) { + try { + String line; + TableMetadata currentMetadata = null; + List currentColumns = null; + while ((line = reader.readLine()) != null) { + if (line.startsWith("-- TABLE ")) { + currentMetadata = metadataMap.get(line.substring("-- TABLE ".length()).trim()); + if (currentMetadata == null) { + throw new IllegalArgumentException("未找到表元数据:" + line); + } + currentColumns = null; + prepareTargetTable(jdbcConnection, currentMetadata, dbType, restoreMode, targetOwnerName); + continue; + } + if (currentMetadata == null) { + continue; + } + if (currentColumns == null) { + currentColumns = parseCsvLine(line); + continue; + } + List values = parseCsvLine(line); + insertRow(jdbcConnection, currentMetadata, currentColumns, values, dbType, restoreMode, targetOwnerName); + } + jdbcConnection.commit(); + } catch (Exception exception) { + jdbcConnection.rollback(); + throw exception; + } + } + } + + public ExportManifest exportMysqlCsvV2(Connection jdbcConnection, String databaseName, String taskNo, + DatabaseBackupParam.CreateParam param, Path backupDirectory, + Path metadataFilePath, int fetchSize, long maxPartBytes, + BooleanSupplier cancelled) throws Exception { + Files.createDirectories(backupDirectory); + ExportManifest manifest = new ExportManifest(); + manifest.setVersion(2); + manifest.setDbType("MYSQL"); + manifest.setBackupStrategy("JDBC_EXPORT"); + manifest.setTaskNo(taskNo); + manifest.setDatabaseName(databaseName); + List tableMetadataList = new ArrayList<>(); + manifest.setTables(tableMetadataList); + for (String tableName : param.getTargetNames()) { + checkCancelled(cancelled, backupDirectory); + tableMetadataList.add(exportMysqlTableV2(jdbcConnection, tableName, param, backupDirectory, taskNo, + fetchSize, maxPartBytes, cancelled)); + } + try (BufferedWriter metadataWriter = Files.newBufferedWriter(metadataFilePath, StandardCharsets.UTF_8)) { + objectMapper.writeValue(metadataWriter, manifest); + } + return manifest; + } + + private TableMetadata exportTable(Connection connection, String ownerName, String tableName, + DatabaseBackupParam.CreateParam param, BufferedWriter writer) throws Exception { + String normalizedOwner = normalizeOwner(ownerName); + String normalizedTable = normalizeMysqlIdentifier(tableName); + String fullTableName = buildFullTableName(normalizedOwner, normalizedTable); + String querySql = buildQuerySql(fullTableName, param); + TableMetadata metadata = new TableMetadata(); + metadata.setOwnerName(normalizedOwner); + metadata.setTableName(normalizedTable); + metadata.setFullTableName(fullTableName); + metadata.setTimeColumn(StrUtil.isBlank(param.getTimeColumn()) ? null : normalizeMysqlIdentifier(param.getTimeColumn())); + metadata.setStartTime(param.getStartTime() == null ? null : param.getStartTime().format(DATE_TIME_FORMATTER)); + metadata.setEndTime(param.getEndTime() == null ? null : param.getEndTime().format(DATE_TIME_FORMATTER)); + writer.write("-- TABLE " + fullTableName); + writer.newLine(); + try (PreparedStatement statement = connection.prepareStatement(querySql)) { + fillQueryParams(statement, param); + try (ResultSet resultSet = statement.executeQuery()) { + ResultSetMetaData resultSetMetaData = resultSet.getMetaData(); + int columnCount = resultSetMetaData.getColumnCount(); + List columnNames = new ArrayList<>(); + List columnTypes = new ArrayList<>(); + for (int i = 1; i <= columnCount; i++) { + String columnName = resultSetMetaData.getColumnName(i); + columnNames.add(normalizeMysqlIdentifier(columnName)); + columnTypes.add(resultSetMetaData.getColumnTypeName(i)); + if (i > 1) { + writer.write(","); + } + writer.write(escape(columnName)); + } + writer.newLine(); + long rowCount = 0L; + while (resultSet.next()) { + for (int i = 1; i <= columnCount; i++) { + if (i > 1) { + writer.write(","); + } + writer.write(escape(resultSet.getString(i))); + } + writer.newLine(); + rowCount++; + } + metadata.setColumnNames(columnNames); + metadata.setColumnTypes(columnTypes); + metadata.setRowCount(rowCount); + return metadata; + } + } + } + + private TableExportMetadata exportMysqlTableV2(Connection connection, String tableName, + DatabaseBackupParam.CreateParam param, Path backupDirectory, + String taskNo, int fetchSize, long maxPartBytes, + BooleanSupplier cancelled) throws Exception { + String normalizedTable = normalizeIdentifier(tableName); + String querySql = buildQuerySql(normalizedTable, param); + TableExportMetadata metadata = new TableExportMetadata(); + metadata.setTableName(normalizedTable); + metadata.setFullTableName(normalizedTable); + metadata.setTimeColumn(StrUtil.isBlank(param.getTimeColumn()) ? null : normalizeIdentifier(param.getTimeColumn())); + metadata.setStartTime(param.getStartTime() == null ? null : param.getStartTime().format(DATE_TIME_FORMATTER)); + metadata.setEndTime(param.getEndTime() == null ? null : param.getEndTime().format(DATE_TIME_FORMATTER)); + metadata.setColumns(new ArrayList<>()); + metadata.setParts(new ArrayList<>()); + try (PreparedStatement statement = connection.prepareStatement(querySql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) { + statement.setFetchSize(fetchSize); + fillQueryParams(statement, param); + try (ResultSet resultSet = statement.executeQuery()) { + ResultSetMetaData resultSetMetaData = resultSet.getMetaData(); + int columnCount = resultSetMetaData.getColumnCount(); + List columnNames = new ArrayList<>(); + for (int i = 1; i <= columnCount; i++) { + String columnName = resultSetMetaData.getColumnName(i); + columnNames.add(columnName); + ColumnMetadata columnMetadata = new ColumnMetadata(); + columnMetadata.setName(columnName); + columnMetadata.setType(resultSetMetaData.getColumnTypeName(i)); + metadata.getColumns().add(columnMetadata); + } + PartWriter partWriter = openPartWriter(backupDirectory, normalizedTable, taskNo, + metadata.getParts().size() + 1, columnNames); + metadata.getParts().add(partWriter.getPart()); + long totalRows = 0L; + try { + while (resultSet.next()) { + checkCancelled(cancelled, backupDirectory); + if (partWriter.shouldRotate(maxPartBytes)) { + partWriter.close(); + partWriter = openPartWriter(backupDirectory, normalizedTable, taskNo, + metadata.getParts().size() + 1, columnNames); + metadata.getParts().add(partWriter.getPart()); + } + partWriter.writeRow(resultSet, columnCount); + totalRows++; + } + } finally { + partWriter.close(); + } + metadata.setRowCount(totalRows); + return metadata; + } + } + } + + private String buildQuerySql(String fullTableName, DatabaseBackupParam.CreateParam param) { + StringBuilder sql = new StringBuilder("SELECT * FROM ").append(fullTableName); + if (param.getStartTime() != null && param.getEndTime() != null && StrUtil.isNotBlank(param.getTimeColumn())) { + sql.append(" WHERE ").append(normalizeIdentifier(param.getTimeColumn())).append(" BETWEEN ? AND ?"); + } + return sql.toString(); + } + + private void fillQueryParams(PreparedStatement statement, DatabaseBackupParam.CreateParam param) throws Exception { + if (param.getStartTime() != null && param.getEndTime() != null && StrUtil.isNotBlank(param.getTimeColumn())) { + statement.setString(1, param.getStartTime().format(DATE_TIME_FORMATTER)); + statement.setString(2, param.getEndTime().format(DATE_TIME_FORMATTER)); + } + } + + private void prepareTargetTable(Connection connection, TableMetadata metadata, String dbType, String restoreMode, + String targetOwnerName) throws Exception { + if (!"TRUNCATE".equalsIgnoreCase(restoreMode) + && !("REPLACE".equalsIgnoreCase(restoreMode) && !isMysql(dbType))) { + return; + } + String fullTargetName = buildTargetTableName(metadata, targetOwnerName); + try (Statement statement = connection.createStatement()) { + statement.execute("TRUNCATE TABLE " + fullTargetName); + } + } + + private void insertRow(Connection connection, TableMetadata metadata, List columns, + List values, String dbType, String restoreMode, String targetOwnerName) throws Exception { + String fullTargetName = buildTargetTableName(metadata, targetOwnerName); + StringBuilder placeholders = new StringBuilder(); + for (int i = 0; i < columns.size(); i++) { + if (i > 0) { + placeholders.append(","); + } + placeholders.append("?"); + } + String sql = buildInsertSql(dbType, restoreMode, fullTargetName, columns, placeholders.toString()); + try (PreparedStatement statement = connection.prepareStatement(sql)) { + for (int i = 0; i < columns.size(); i++) { + statement.setString(i + 1, i < values.size() ? values.get(i) : null); + } + statement.executeUpdate(); + } + } + + private String buildInsertSql(String dbType, String restoreMode, String fullTargetName, List columns, + String placeholders) { + String command = "INSERT INTO"; + if (isMysql(dbType) && "SKIP".equalsIgnoreCase(restoreMode)) { + // MySQL 跳过重复主键行,避免普通恢复因历史数据重复而整体失败。 + command = "INSERT IGNORE INTO"; + } else if (isMysql(dbType) && "REPLACE".equalsIgnoreCase(restoreMode)) { + command = "REPLACE INTO"; + } + return command + " " + fullTargetName + " (" + String.join(",", columns) + ") VALUES (" + placeholders + ")"; + } + + private void importCsvV2(Connection jdbcConnection, Path metadataFilePath, String dbType, String restoreMode, + String targetOwnerName) throws Exception { + ExportManifest manifest = objectMapper.readValue(metadataFilePath.toFile(), ExportManifest.class); + jdbcConnection.setAutoCommit(false); + try { + for (TableExportMetadata tableMetadata : manifest.getTables()) { + prepareTargetTable(jdbcConnection, toLegacyMetadata(tableMetadata), dbType, restoreMode, targetOwnerName); + for (FilePartMetadata part : tableMetadata.getParts()) { + importPart(jdbcConnection, metadataFilePath.getParent(), tableMetadata, part, dbType, restoreMode, targetOwnerName); + } + } + jdbcConnection.commit(); + } catch (Exception exception) { + jdbcConnection.rollback(); + throw exception; + } + } + + private void importPart(Connection jdbcConnection, Path backupDirectory, TableExportMetadata tableMetadata, + FilePartMetadata part, String dbType, String restoreMode, String targetOwnerName) throws Exception { + Path partPath = backupDirectory.resolve(part.getFileName()).normalize(); + if (!partPath.startsWith(backupDirectory.normalize())) { + throw new IllegalArgumentException("备份分片路径不在元数据目录内:" + part.getFileName()); + } + try (BufferedReader reader = Files.newBufferedReader(partPath, StandardCharsets.UTF_8)) { + List columns = null; + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("-- TABLE ")) { + continue; + } + if (columns == null) { + columns = parseCsvLine(line); + continue; + } + List values = parseCsvLine(line); + insertRow(jdbcConnection, toLegacyMetadata(tableMetadata), columns, values, dbType, restoreMode, targetOwnerName); + } + } + } + + private boolean isMysql(String dbType) { + return DatabaseOpsConst.DB_TYPE_MYSQL.equalsIgnoreCase(dbType); + } + + private PartWriter openPartWriter(Path backupDirectory, String tableName, String taskNo, int partIndex, + List columnNames) throws IOException { + String rawName = tableName.toLowerCase(Locale.ROOT) + "_part" + String.format("%03d", partIndex) + ".csv"; + String fileName = DatabaseFileNameUtil.appendTodayWithTask(rawName, taskNo); + Path filePath = backupDirectory.resolve(fileName).normalize(); + BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8); + writer.write("-- TABLE " + tableName); + writer.newLine(); + for (int i = 0; i < columnNames.size(); i++) { + if (i > 0) { + writer.write(","); + } + writer.write(escape(columnNames.get(i))); + } + writer.newLine(); + FilePartMetadata part = new FilePartMetadata(); + part.setFileName(fileName); + part.setFilePath(filePath.toString()); + part.setRowCount(0L); + part.setFileSize(0L); + return new PartWriter(writer, filePath, part); + } + + private TableMetadata toLegacyMetadata(TableExportMetadata metadata) { + TableMetadata legacy = new TableMetadata(); + legacy.setOwnerName(null); + legacy.setTableName(metadata.getTableName()); + legacy.setFullTableName(metadata.getFullTableName()); + legacy.setTimeColumn(metadata.getTimeColumn()); + legacy.setStartTime(metadata.getStartTime()); + legacy.setEndTime(metadata.getEndTime()); + legacy.setRowCount(metadata.getRowCount()); + List columnNames = new ArrayList<>(); + List columnTypes = new ArrayList<>(); + for (ColumnMetadata column : metadata.getColumns()) { + columnNames.add(column.getName()); + columnTypes.add(column.getType()); + } + legacy.setColumnNames(columnNames); + legacy.setColumnTypes(columnTypes); + return legacy; + } + + private void checkCancelled(BooleanSupplier cancelled, Path backupDirectory) { + if (cancelled != null && cancelled.getAsBoolean()) { + throw new IllegalStateException("备份任务已停止,已生成文件保留在:" + backupDirectory); + } + } + + private String buildTargetTableName(TableMetadata metadata, String targetOwnerName) { + String owner = normalizeOwner(StrUtil.blankToDefault(targetOwnerName, metadata.getOwnerName())); + return buildFullTableName(owner, metadata.getTableName()); + } + + private String buildFullTableName(String ownerName, String tableName) { + if (StrUtil.isBlank(ownerName)) { + return tableName; + } + return ownerName + "." + tableName; + } + + private String normalizeOwner(String ownerName) { + if (StrUtil.isBlank(ownerName)) { + return null; + } + return normalizeIdentifier(ownerName); + } + + private List parseCsvLine(String line) { + List result = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean quoted = false; + for (int i = 0; i < line.length(); i++) { + char currentChar = line.charAt(i); + if (currentChar == '"') { + if (quoted && i + 1 < line.length() && line.charAt(i + 1) == '"') { + current.append('"'); + i++; + } else { + quoted = !quoted; + } + continue; + } + if (currentChar == ',' && !quoted) { + result.add(current.toString()); + current.setLength(0); + continue; + } + current.append(currentChar); + } + result.add(current.toString()); + return result; + } + + private String escape(String value) { + if (value == null) { + return ""; + } + return "\"" + value.replace("\"", "\"\"") + "\""; + } + + private String normalizeIdentifier(String value) { + if (value == null || !IDENTIFIER_PATTERN.matcher(value).matches()) { + throw new IllegalArgumentException("数据库对象名称格式不正确:" + value); + } + return value.trim().toUpperCase(Locale.ROOT); + } + + private String normalizeMysqlIdentifier(String value) { + if (value == null || !IDENTIFIER_PATTERN.matcher(value).matches()) { + throw new IllegalArgumentException("数据库对象名称格式不正确:" + value); + } + return value.trim(); + } + + @Data + public static class TableMetadata { + private String ownerName; + private String tableName; + private String fullTableName; + private List columnNames; + private List columnTypes; + private String timeColumn; + private String startTime; + private String endTime; + private Long rowCount; + } + + @Data + public static class ExportManifest { + private Integer version; + private String dbType; + private String backupStrategy; + private String taskNo; + private String databaseName; + private List tables; + } + + @Data + public static class TableExportMetadata { + private String tableName; + private String fullTableName; + private String timeColumn; + private String startTime; + private String endTime; + private List columns; + private Long rowCount; + private List parts; + } + + @Data + public static class ColumnMetadata { + private String name; + private String type; + } + + @Data + public static class FilePartMetadata { + private String fileName; + private String filePath; + private Long rowCount; + private Long fileSize; + } + + private class PartWriter { + private final BufferedWriter writer; + private final Path filePath; + private final FilePartMetadata part; + + private PartWriter(BufferedWriter writer, Path filePath, FilePartMetadata part) { + this.writer = writer; + this.filePath = filePath; + this.part = part; + } + + private FilePartMetadata getPart() { + return part; + } + + private boolean shouldRotate(long maxPartBytes) throws IOException { + writer.flush(); + return part.getRowCount() > 0 && Files.size(filePath) >= maxPartBytes; + } + + private void writeRow(ResultSet resultSet, int columnCount) throws Exception { + for (int i = 1; i <= columnCount; i++) { + if (i > 1) { + writer.write(","); + } + writer.write(escape(resultSet.getString(i))); + } + writer.newLine(); + part.setRowCount(part.getRowCount() + 1); + } + + private void close() throws IOException { + writer.close(); + part.setFileSize(Files.exists(filePath) ? Files.size(filePath) : 0L); + } + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/OracleJdbcComponent.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/OracleJdbcComponent.java new file mode 100644 index 0000000..c5cb533 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/component/OracleJdbcComponent.java @@ -0,0 +1,89 @@ +package com.njcn.gather.systemops.database.component; + +import cn.hutool.core.util.StrUtil; +import com.njcn.gather.systemops.database.constant.DatabaseOpsConst; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTableVO; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTestResultVO; +import org.springframework.stereotype.Component; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Oracle JDBC 连接与元数据探测组件。 + */ +@Component +public class OracleJdbcComponent { + + public DatabaseTestResultVO test(DatabaseConnection connection, String password) { + DatabaseTestResultVO result = new DatabaseTestResultVO(); + try (Connection ignored = openConnection(connection, password)) { + result.setSuccess(true); + result.setMessage("连接成功"); + } catch (Exception exception) { + result.setSuccess(false); + result.setMessage(exception.getMessage()); + } + return result; + } + + public List listTables(DatabaseConnection connection, String password, String schemaName) throws Exception { + String owner = StrUtil.blankToDefault(schemaName, connection.getSchemaName()); + if (StrUtil.isBlank(owner)) { + owner = connection.getUsername(); + } + owner = owner.trim().toUpperCase(Locale.ROOT); + String sql = "SELECT t.owner, t.table_name, t.num_rows, o.last_ddl_time, c.comments " + + "FROM all_tables t " + + "LEFT JOIN all_tab_comments c " + + "ON t.owner = c.owner AND t.table_name = c.table_name " + + "LEFT JOIN all_objects o " + + "ON t.owner = o.owner AND t.table_name = o.object_name AND o.object_type = 'TABLE' " + + "WHERE t.owner = ? ORDER BY t.table_name"; + try (Connection jdbcConnection = openConnection(connection, password); + PreparedStatement statement = jdbcConnection.prepareStatement(sql)) { + statement.setString(1, owner); + try (ResultSet resultSet = statement.executeQuery()) { + List result = new ArrayList<>(); + while (resultSet.next()) { + DatabaseTableVO table = new DatabaseTableVO(); + table.setOwner(resultSet.getString("owner")); + table.setTableName(resultSet.getString("table_name")); + table.setEngine(DatabaseOpsConst.DB_TYPE_ORACLE); + table.setTableRows(getLongValue(resultSet, "num_rows")); + Timestamp updateTime = resultSet.getTimestamp("last_ddl_time"); + table.setUpdateTime(updateTime == null ? null : updateTime.toLocalDateTime()); + table.setComments(resultSet.getString("comments")); + result.add(table); + } + return result; + } + } + } + + private Long getLongValue(ResultSet resultSet, String columnName) throws Exception { + long value = resultSet.getLong(columnName); + return resultSet.wasNull() ? null : value; + } + + public Connection openConnection(DatabaseConnection connection, String password) throws Exception { + if (StrUtil.isBlank(password)) { + throw new IllegalArgumentException("数据库密码不能为空"); + } + return DriverManager.getConnection(buildJdbcUrl(connection), connection.getUsername(), password); + } + + public String buildJdbcUrl(DatabaseConnection connection) { + if (DatabaseOpsConst.CONNECT_TYPE_SID.equalsIgnoreCase(connection.getConnectType())) { + return "jdbc:oracle:thin:@" + connection.getHost() + ":" + connection.getPort() + ":" + connection.getSid(); + } + return "jdbc:oracle:thin:@//" + connection.getHost() + ":" + connection.getPort() + "/" + connection.getServiceName(); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/config/DbmsProperties.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/config/DbmsProperties.java index a442266..59324d0 100644 --- a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/config/DbmsProperties.java +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/config/DbmsProperties.java @@ -19,6 +19,7 @@ public class DbmsProperties { public static class Backup { private String storagePath = "D:/dbms-backup"; private Integer defaultMaxFileSizeMb = 512; + private Integer mysqlFetchSize = 1000; } @Data diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/constant/DatabaseOpsConst.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/constant/DatabaseOpsConst.java index 6a4fbb1..41322ce 100644 --- a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/constant/DatabaseOpsConst.java +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/constant/DatabaseOpsConst.java @@ -6,6 +6,7 @@ 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 = "确认删除"; diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/controller/DatabaseBackupController.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/controller/DatabaseBackupController.java index 47f5104..092c59a 100644 --- a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/controller/DatabaseBackupController.java +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/controller/DatabaseBackupController.java @@ -61,6 +61,22 @@ public class DatabaseBackupController extends BaseController { return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseOperationTaskService.getStatus(taskId), methodDescribe); } + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE) + @ApiOperation("停止备份任务") + @PostMapping("/tasks/stop") + public HttpResult stop(@RequestBody @Validated DatabaseBackupParam.StopParam param) { + String methodDescribe = getMethodDescribe("stop"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseOperationTaskService.stopBackupTask(param), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD) + @ApiOperation("重新开始备份任务") + @PostMapping("/tasks/restart") + public HttpResult restart(@RequestBody @Validated DatabaseBackupParam.RestartParam param) { + String methodDescribe = getMethodDescribe("restart"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseOperationTaskService.restartBackupTask(param), methodDescribe); + } + @OperateInfo(info = LogEnum.BUSINESS_COMMON) @ApiOperation("查询备份文件") @PostMapping("/files/list") diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/OperationTypeEnum.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/OperationTypeEnum.java new file mode 100644 index 0000000..74a02c8 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/OperationTypeEnum.java @@ -0,0 +1,10 @@ +package com.njcn.gather.systemops.database.pojo.enums; + +/** + * 数据库运维操作类型。 + */ +public enum OperationTypeEnum { + BACKUP, + RESTORE, + DELETE +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/RestoreModeEnum.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/RestoreModeEnum.java new file mode 100644 index 0000000..65c3c82 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/enums/RestoreModeEnum.java @@ -0,0 +1,11 @@ +package com.njcn.gather.systemops.database.pojo.enums; + +/** + * 恢复模式。 + */ +public enum RestoreModeEnum { + SKIP, + APPEND, + TRUNCATE, + REPLACE +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseBackupParam.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseBackupParam.java index 1e110bf..00e6eb9 100644 --- a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseBackupParam.java +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseBackupParam.java @@ -64,4 +64,22 @@ public class DatabaseBackupParam { @ApiModelProperty("备份策略") private String backupStrategy; } + + @Data + @ApiModel("停止备份任务参数") + public static class StopParam { + @ApiModelProperty("备份任务 ID") + @NotBlank(message = "备份任务 ID 不能为空") + private String taskId; + } + + @Data + @ApiModel("重新开始备份任务参数") + public static class RestartParam { + @ApiModelProperty("备份任务 ID") + @NotBlank(message = "备份任务 ID 不能为空") + private String taskId; + @ApiModelProperty("临时密码,原连接未保存密码时传入") + private String temporaryPassword; + } } diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseConnectionParam.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseConnectionParam.java index 409d4ae..b640370 100644 --- a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseConnectionParam.java +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/param/DatabaseConnectionParam.java @@ -1,5 +1,6 @@ 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; @@ -20,7 +21,7 @@ public class DatabaseConnectionParam { @NotBlank(message = "连接名称不能为空") private String connectionName; - @ApiModelProperty("数据库类型,一期固定 ORACLE") + @ApiModelProperty("数据库类型:ORACLE、MYSQL") private String dbType; @ApiModelProperty("数据库主机地址") @@ -40,6 +41,9 @@ public class DatabaseConnectionParam { @ApiModelProperty("SID") private String sid; + @ApiModelProperty("数据库名,MySQL 使用") + private String databaseName; + @ApiModelProperty("Schema") private String schemaName; @@ -113,7 +117,14 @@ public class DatabaseConnectionParam { private String connectionId; @ApiModelProperty("临时密码,不保存密码时传入") private String temporaryPassword; - @ApiModelProperty("Schema,不传则使用连接默认 Schema") + @ApiModelProperty("兼容前端传入的运行时密码;为空时复用数据库 password_cipher") + private String password; + @JsonAlias("password_cipher") + @ApiModelProperty("兼容前端传入的已保存密码") + private String passwordCipher; + @ApiModelProperty("兼容前端传入的临时连接参数") + private DatabaseConnectionParam connection; + @ApiModelProperty("Schema 或数据库名,不传则使用连接默认值") private String schemaName; } } diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseConnectionVO.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseConnectionVO.java index 537e0b5..a7a6f8d 100644 --- a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseConnectionVO.java +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseConnectionVO.java @@ -17,6 +17,7 @@ public class DatabaseConnectionVO { private String connectType; private String serviceName; private String sid; + private String databaseName; private String schemaName; private String username; private Integer savePassword; diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseTableVO.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseTableVO.java index 996eeef..c4cb609 100644 --- a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseTableVO.java +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/pojo/vo/DatabaseTableVO.java @@ -2,6 +2,8 @@ package com.njcn.gather.systemops.database.pojo.vo; import lombok.Data; +import java.time.LocalDateTime; + /** * 数据库表信息。 */ @@ -9,5 +11,11 @@ import lombok.Data; public class DatabaseTableVO { private String owner; private String tableName; + private Long autoIncrementValue = 0L; + private Long autoIncrement = 0L; + private LocalDateTime updateTime; + private Long dataLength; + private String engine; + private Long tableRows; private String comments; } diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/DatabaseOperationTaskService.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/DatabaseOperationTaskService.java index 64f623b..ef12b43 100644 --- a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/DatabaseOperationTaskService.java +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/service/DatabaseOperationTaskService.java @@ -18,6 +18,10 @@ public interface DatabaseOperationTaskService extends IService paths = Files.walk(path)) { + paths.sorted(Comparator.reverseOrder()).forEach(this::deleteSinglePath); + } + } else { Files.delete(path); } } catch (BusinessException exception) { @@ -123,7 +146,16 @@ public class DatabaseBackupFileServiceImpl extends ServiceImpl implements DatabaseConnectionService { private final DatabasePasswordComponent databasePasswordComponent; - private final OracleJdbcComponent oracleJdbcComponent; + private final DatabaseOperatorRegistry databaseOperatorRegistry; private final ObjectProvider databaseOperationTaskServiceProvider; @Override @@ -61,6 +62,7 @@ public class DatabaseConnectionServiceImpl extends ServiceImpl listTables(DatabaseConnectionParam.TablesParam param) { DatabaseConnection connection = requireEnabled(param.getConnectionId()); try { - return oracleJdbcComponent.listTables(connection, resolvePassword(connection, param.getTemporaryPassword()), param.getSchemaName()); + 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()); } @@ -141,7 +144,18 @@ public class DatabaseConnectionServiceImpl extends ServiceImpl 0) { + throw new BusinessException(CommonResponseEnum.FAIL, "连接名称已存在"); } } @@ -200,6 +260,21 @@ public class DatabaseConnectionServiceImpl extends ServiceImpl listTables(DatabaseConnection connection, String password, String schemaOrDatabaseName) throws Exception { + String databaseName = StrUtil.blankToDefault(schemaOrDatabaseName, connection.getDatabaseName()); + String sql = "SELECT t.table_schema, t.table_name, t.auto_increment, t.update_time, " + + "t.data_length, t.engine, t.table_rows, t.table_comment, " + + "MAX(CASE WHEN c.extra LIKE '%auto_increment%' THEN 1 ELSE 0 END) AS has_auto_increment " + + "FROM information_schema.tables t " + + "LEFT JOIN information_schema.columns c " + + "ON t.table_schema = c.table_schema AND t.table_name = c.table_name " + + "WHERE t.table_schema = ? AND t.table_type = 'BASE TABLE' " + + "GROUP BY t.table_schema, t.table_name, t.auto_increment, t.update_time, " + + "t.data_length, t.engine, t.table_rows, t.table_comment " + + "ORDER BY t.table_name"; + try (Connection jdbcConnection = openConnection(connection, password); + PreparedStatement statement = jdbcConnection.prepareStatement(sql)) { + statement.setString(1, databaseName); + try (ResultSet resultSet = statement.executeQuery()) { + List result = new ArrayList<>(); + while (resultSet.next()) { + DatabaseTableVO table = new DatabaseTableVO(); + table.setOwner(resultSet.getString("table_schema").toUpperCase(Locale.ROOT)); + table.setTableName(resultSet.getString("table_name")); + if (resultSet.getInt("has_auto_increment") == 1) { + fillAutoIncrement(table, defaultZero(getLongValue(resultSet, "auto_increment"))); + } + Timestamp updateTime = resultSet.getTimestamp("update_time"); + table.setUpdateTime(updateTime == null ? null : updateTime.toLocalDateTime()); + table.setDataLength(getLongValue(resultSet, "data_length")); + table.setEngine(resultSet.getString("engine")); + table.setTableRows(getLongValue(resultSet, "table_rows")); + table.setComments(resultSet.getString("table_comment")); + result.add(table); + } + return result; + } + } + } + + private Long getLongValue(ResultSet resultSet, String columnName) throws Exception { + long value = resultSet.getLong(columnName); + return resultSet.wasNull() ? null : value; + } + + private Long defaultZero(Long value) { + return value == null ? 0L : value; + } + + private void fillAutoIncrement(DatabaseTableVO table, Long autoIncrement) { + table.setAutoIncrementValue(autoIncrement); + table.setAutoIncrement(autoIncrement); + } + + private Connection openConnection(DatabaseConnection connection, String password) throws Exception { + if (StrUtil.isBlank(password)) { + throw new IllegalArgumentException("数据库密码不能为空"); + } + return DriverManager.getConnection(MysqlJdbcUrlUtil.build(connection), connection.getUsername(), password); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcExportBackupOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcExportBackupOperator.java new file mode 100644 index 0000000..961ce2a --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcExportBackupOperator.java @@ -0,0 +1,146 @@ +package com.njcn.gather.systemops.database.support.mysql; + +import cn.hutool.core.util.StrUtil; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.systemops.database.component.JdbcExportComponent; +import com.njcn.gather.systemops.database.config.DbmsProperties; +import com.njcn.gather.systemops.database.mapper.DatabaseOperationTaskMapper; +import com.njcn.gather.systemops.database.pojo.enums.BackupModeEnum; +import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum; +import com.njcn.gather.systemops.database.pojo.enums.FileFormatEnum; +import com.njcn.gather.systemops.database.pojo.enums.TaskStatusEnum; +import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; +import com.njcn.gather.systemops.database.support.spi.DatabaseBackupOperator; +import com.njcn.gather.systemops.database.util.DatabaseChecksumUtil; +import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil; +import com.njcn.gather.systemops.database.util.DatabaseOpsIdUtil; +import com.njcn.gather.systemops.database.util.DatabasePathUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.time.LocalDateTime; +import java.util.Locale; +import java.util.stream.Stream; + +/** + * MySQL JDBC_EXPORT 大数据量备份实现。 + */ +@Component +@RequiredArgsConstructor +public class MysqlJdbcExportBackupOperator implements DatabaseBackupOperator { + + private final JdbcExportComponent jdbcExportComponent; + private final DbmsProperties dbmsProperties; + private final DatabaseOperationTaskMapper databaseOperationTaskMapper; + + @Override + public boolean support(String dbType, String backupStrategy) { + return "MYSQL".equalsIgnoreCase(dbType) && BackupStrategyEnum.JDBC_EXPORT.name().equals(backupStrategy); + } + + @Override + public DatabaseBackupFile executeBackup(DatabaseOperationTask task, DatabaseConnection connection, String password, + DatabaseBackupParam.CreateParam param) throws Exception { + Path backupDirectory = buildManagedPath(dbmsProperties.getBackup().getStoragePath(), task.getTaskNo()); + String metadataFileName = DatabaseFileNameUtil.appendTodayWithTask("mysql_jdbc_export_metadata.json", task.getTaskNo()); + Path metadataFilePath = backupDirectory.resolve(metadataFileName).normalize(); + int fetchSize = positiveOrDefault(dbmsProperties.getBackup().getMysqlFetchSize(), 1000); + long maxPartBytes = resolveMaxPartBytes(param); + try (Connection jdbcConnection = DriverManager.getConnection(MysqlJdbcUrlUtil.build(connection), connection.getUsername(), password)) { + jdbcExportComponent.exportMysqlCsvV2(jdbcConnection, connection.getDatabaseName(), task.getTaskNo(), param, + backupDirectory, metadataFilePath, fetchSize, maxPartBytes, () -> isTaskCancelled(task.getId())); + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.FAIL, exception.getMessage() + ",导出目录:" + backupDirectory); + } + return buildBackupFile(task, connection, param, backupDirectory, metadataFilePath); + } + + private boolean isTaskCancelled(String taskId) { + DatabaseOperationTask task = databaseOperationTaskMapper.selectById(taskId); + return task != null && TaskStatusEnum.CANCELLED.name().equals(task.getTaskStatus()); + } + + private DatabaseBackupFile buildBackupFile(DatabaseOperationTask task, DatabaseConnection connection, + DatabaseBackupParam.CreateParam param, Path backupDirectory, + Path metadataFilePath) throws Exception { + if (!Files.exists(metadataFilePath)) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份元数据文件未生成"); + } + DatabaseBackupFile file = new DatabaseBackupFile(); + file.setId(DatabaseOpsIdUtil.uuid()); + file.setTaskId(task.getId()); + file.setConnectionId(connection.getId()); + file.setDbType(connection.getDbType()); + file.setBackupStrategy(task.getBackupStrategy()); + file.setFileFormat(FileFormatEnum.CSV.name()); + file.setSchemaName(task.getSchemaName()); + file.setTargetNamesJson(task.getTargetNamesJson()); + file.setBackupMode(StrUtil.blankToDefault(param.getBackupMode(), BackupModeEnum.FULL_TABLE.name()).toUpperCase(Locale.ROOT)); + file.setBackupStartTime(param.getStartTime()); + file.setBackupEndTime(param.getEndTime()); + file.setTimeColumn(param.getTimeColumn()); + file.setDirectoryName(null); + file.setDumpFileName(null); + file.setLogFileName(null); + file.setFileName(backupDirectory.getFileName().toString()); + file.setFilePath(backupDirectory.toString()); + file.setLogFilePath(null); + file.setMetadataFilePath(metadataFilePath.toString()); + file.setFileSize(readDirectoryFileSize(backupDirectory)); + file.setChecksum(DatabaseChecksumUtil.sha256(metadataFilePath)); + file.setState(1); + file.setCreateTime(LocalDateTime.now()); + file.setUpdateTime(LocalDateTime.now()); + return file; + } + + private long resolveMaxPartBytes(DatabaseBackupParam.CreateParam param) { + Integer maxFileSizeMb = param.getMaxFileSizeMb(); + if (maxFileSizeMb == null || maxFileSizeMb <= 0) { + maxFileSizeMb = positiveOrDefault(dbmsProperties.getBackup().getDefaultMaxFileSizeMb(), 512); + } + return maxFileSizeMb.longValue() * 1024L * 1024L; + } + + private int positiveOrDefault(Integer value, int defaultValue) { + return value == null || value <= 0 ? defaultValue : value; + } + + private Long readDirectoryFileSize(Path directory) { + try { + if (directory != null && Files.exists(directory) && Files.isDirectory(directory)) { + final long[] total = new long[]{0L}; + try (Stream paths = Files.walk(directory)) { + paths.filter(path -> Files.exists(path) && !Files.isDirectory(path)) + .forEach(path -> { + try { + total[0] += Files.size(path); + } catch (Exception ignored) { + // 忽略单个文件大小读取失败,避免影响备份记录生成。 + } + }); + } + return total[0]; + } + } catch (Exception ignored) { + return null; + } + return null; + } + + private Path buildManagedPath(String rootPath, String directoryName) { + Path root = DatabasePathUtil.normalize(rootPath); + if (root == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份目录未配置"); + } + return root.resolve(directoryName).normalize(); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcExportRestoreOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcExportRestoreOperator.java new file mode 100644 index 0000000..ecf68d5 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcExportRestoreOperator.java @@ -0,0 +1,48 @@ +package com.njcn.gather.systemops.database.support.mysql; + +import com.njcn.gather.systemops.database.component.JdbcExportComponent; +import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum; +import com.njcn.gather.systemops.database.pojo.param.DatabaseRestoreParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; +import com.njcn.gather.systemops.database.pojo.po.DatabaseRestoreRecord; +import com.njcn.gather.systemops.database.service.DatabaseBackupFileService; +import com.njcn.gather.systemops.database.support.spi.DatabaseRestoreOperator; +import org.springframework.stereotype.Component; + +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; + +/** + * MySQL JDBC_EXPORT 恢复实现。 + */ +@Component +public class MysqlJdbcExportRestoreOperator implements DatabaseRestoreOperator { + + private final JdbcExportComponent jdbcExportComponent; + private final DatabaseBackupFileService databaseBackupFileService; + + public MysqlJdbcExportRestoreOperator(JdbcExportComponent jdbcExportComponent, + DatabaseBackupFileService databaseBackupFileService) { + this.jdbcExportComponent = jdbcExportComponent; + this.databaseBackupFileService = databaseBackupFileService; + } + + @Override + public boolean support(String dbType, String backupStrategy) { + return "MYSQL".equalsIgnoreCase(dbType) && BackupStrategyEnum.JDBC_EXPORT.name().equals(backupStrategy); + } + + @Override + public void executeRestore(DatabaseOperationTask task, DatabaseRestoreRecord record, DatabaseBackupFile backupFile, + DatabaseConnection connection, String password, DatabaseRestoreParam.CreateParam param) throws Exception { + Path dataFilePath = databaseBackupFileService.resolveManagedPath(backupFile, backupFile.getFilePath()); + Path metadataFilePath = databaseBackupFileService.resolveManagedPath(backupFile, backupFile.getMetadataFilePath()); + try (Connection jdbcConnection = DriverManager.getConnection(MysqlJdbcUrlUtil.build(connection), connection.getUsername(), password)) { + jdbcExportComponent.importCsv(jdbcConnection, dataFilePath, metadataFilePath, + connection.getDbType(), record.getRestoreMode(), null); + } + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcUrlUtil.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcUrlUtil.java new file mode 100644 index 0000000..8dd8eb7 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/mysql/MysqlJdbcUrlUtil.java @@ -0,0 +1,18 @@ +package com.njcn.gather.systemops.database.support.mysql; + +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; + +/** + * MySQL JDBC URL 构造工具。 + */ +public final class MysqlJdbcUrlUtil { + + private MysqlJdbcUrlUtil() { + } + + public static String build(DatabaseConnection connection) { + return "jdbc:mysql://" + connection.getHost() + ":" + connection.getPort() + "/" + connection.getDatabaseName() + + "?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai" + + "&useCursorFetch=true&connectTimeout=5000&socketTimeout=30000"; + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleConnectionOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleConnectionOperator.java new file mode 100644 index 0000000..db21bca --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleConnectionOperator.java @@ -0,0 +1,37 @@ +package com.njcn.gather.systemops.database.support.oracle; + +import com.njcn.gather.systemops.database.component.OracleJdbcComponent; +import com.njcn.gather.systemops.database.constant.DatabaseOpsConst; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTableVO; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTestResultVO; +import com.njcn.gather.systemops.database.support.spi.DatabaseConnectionOperator; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * Oracle 连接能力实现。 + */ +@Component +@RequiredArgsConstructor +public class OracleConnectionOperator implements DatabaseConnectionOperator { + + private final OracleJdbcComponent oracleJdbcComponent; + + @Override + public boolean support(String dbType) { + return DatabaseOpsConst.DB_TYPE_ORACLE.equalsIgnoreCase(dbType); + } + + @Override + public DatabaseTestResultVO test(DatabaseConnection connection, String password) { + return oracleJdbcComponent.test(connection, password); + } + + @Override + public List listTables(DatabaseConnection connection, String password, String schemaOrDatabaseName) throws Exception { + return oracleJdbcComponent.listTables(connection, password, schemaOrDatabaseName); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleDataPumpBackupOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleDataPumpBackupOperator.java new file mode 100644 index 0000000..daa3ca0 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleDataPumpBackupOperator.java @@ -0,0 +1,122 @@ +package com.njcn.gather.systemops.database.support.oracle; + +import cn.hutool.core.util.StrUtil; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.systemops.database.component.DataPumpCommandExecutor; +import com.njcn.gather.systemops.database.pojo.enums.BackupModeEnum; +import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum; +import com.njcn.gather.systemops.database.pojo.enums.FileFormatEnum; +import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; +import com.njcn.gather.systemops.database.service.DatabaseBackupFileService; +import com.njcn.gather.systemops.database.support.spi.DatabaseBackupOperator; +import com.njcn.gather.systemops.database.util.DatabaseChecksumUtil; +import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil; +import com.njcn.gather.systemops.database.util.DatabaseOpsIdUtil; +import com.njcn.gather.systemops.database.util.DatabasePathUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.Locale; + +/** + * Oracle DATA_PUMP 备份实现。 + */ +@Component +@RequiredArgsConstructor +public class OracleDataPumpBackupOperator implements DatabaseBackupOperator { + + private final DataPumpCommandExecutor dataPumpCommandExecutor; + private final DatabaseBackupFileService databaseBackupFileService; + + @Override + public boolean support(String dbType, String backupStrategy) { + return "ORACLE".equalsIgnoreCase(dbType) && BackupStrategyEnum.DATA_PUMP.name().equals(backupStrategy); + } + + @Override + public DatabaseBackupFile executeBackup(DatabaseOperationTask task, DatabaseConnection connection, String password, + DatabaseBackupParam.CreateParam param) { + String directoryName = StrUtil.blankToDefault(param.getDirectoryName(), connection.getDirectoryName()); + if (StrUtil.isBlank(directoryName)) { + throw new BusinessException(CommonResponseEnum.FAIL, "DATA_PUMP 备份需要 Oracle Directory 名称"); + } + String baseName = buildBaseFileName(connection, task); + String dumpFileName = DatabaseFileNameUtil.appendTodayWithTask(baseName + ".dmp", task.getTaskNo()); + String logFileName = DatabaseFileNameUtil.appendTodayWithTask(baseName + ".log", task.getTaskNo()); + DataPumpCommandExecutor.CommandResult commandResult = dataPumpCommandExecutor.expdp(connection, password, + directoryName, dumpFileName, logFileName, param.getTargetNames()); + if (!Boolean.TRUE.equals(commandResult.getSuccess())) { + throw new BusinessException(CommonResponseEnum.FAIL, "Data Pump 执行失败:" + commandResult.getOutput()); + } + if (StrUtil.isBlank(connection.getDirectoryPath())) { + throw new BusinessException(CommonResponseEnum.FAIL, "Data Pump 备份需要配置可管理的 directoryPath"); + } + Path dumpPath = buildManagedPath(connection.getDirectoryPath(), dumpFileName); + Path logPath = buildManagedPath(connection.getDirectoryPath(), logFileName); + return buildBackupFile(task, connection, param, FileFormatEnum.DMP.name(), dumpFileName, dumpPath, logFileName, logPath, null); + } + + private DatabaseBackupFile buildBackupFile(DatabaseOperationTask task, DatabaseConnection connection, + DatabaseBackupParam.CreateParam param, String fileFormat, String fileName, + Path filePath, String logFileName, Path logFilePath, Path metadataFilePath) { + if (filePath == null || !Files.exists(filePath)) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份文件未生成"); + } + DatabaseBackupFile file = new DatabaseBackupFile(); + file.setId(DatabaseOpsIdUtil.uuid()); + file.setTaskId(task.getId()); + file.setConnectionId(connection.getId()); + file.setDbType(connection.getDbType()); + file.setBackupStrategy(task.getBackupStrategy()); + file.setFileFormat(fileFormat); + file.setSchemaName(task.getSchemaName()); + file.setTargetNamesJson(task.getTargetNamesJson()); + file.setBackupMode(StrUtil.blankToDefault(param.getBackupMode(), BackupModeEnum.FULL_TABLE.name()).toUpperCase(Locale.ROOT)); + file.setBackupStartTime(param.getStartTime()); + file.setBackupEndTime(param.getEndTime()); + file.setTimeColumn(param.getTimeColumn()); + file.setDirectoryName(StrUtil.blankToDefault(param.getDirectoryName(), connection.getDirectoryName())); + file.setDumpFileName(FileFormatEnum.DMP.name().equals(fileFormat) ? fileName : null); + file.setLogFileName(logFileName); + file.setFileName(fileName); + file.setFilePath(filePath.toString()); + file.setLogFilePath(logFilePath == null ? null : logFilePath.toString()); + file.setMetadataFilePath(metadataFilePath == null ? null : metadataFilePath.toString()); + file.setFileSize(readFileSize(filePath)); + file.setChecksum(DatabaseChecksumUtil.sha256(filePath)); + file.setState(1); + file.setCreateTime(LocalDateTime.now()); + file.setUpdateTime(LocalDateTime.now()); + return file; + } + + private Long readFileSize(Path filePath) { + try { + if (filePath != null && Files.exists(filePath) && !Files.isDirectory(filePath)) { + return Files.size(filePath); + } + } catch (Exception ignored) { + return null; + } + return null; + } + + private Path buildManagedPath(String rootPath, String fileName) { + Path root = DatabasePathUtil.normalize(rootPath); + if (root == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份目录未配置"); + } + return root.resolve(fileName).normalize(); + } + + private String buildBaseFileName(DatabaseConnection connection, DatabaseOperationTask task) { + return connection.getSchemaName() + "_" + task.getBackupStrategy().toLowerCase(Locale.ROOT); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleDataPumpRestoreOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleDataPumpRestoreOperator.java new file mode 100644 index 0000000..e23096d --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleDataPumpRestoreOperator.java @@ -0,0 +1,47 @@ +package com.njcn.gather.systemops.database.support.oracle; + +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.systemops.database.component.DataPumpCommandExecutor; +import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum; +import com.njcn.gather.systemops.database.pojo.param.DatabaseRestoreParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; +import com.njcn.gather.systemops.database.pojo.po.DatabaseRestoreRecord; +import com.njcn.gather.systemops.database.support.spi.DatabaseRestoreOperator; +import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil; +import org.springframework.stereotype.Component; + +/** + * Oracle DATA_PUMP 恢复实现。 + */ +@Component +public class OracleDataPumpRestoreOperator implements DatabaseRestoreOperator { + + private final DataPumpCommandExecutor dataPumpCommandExecutor; + + public OracleDataPumpRestoreOperator(DataPumpCommandExecutor dataPumpCommandExecutor) { + this.dataPumpCommandExecutor = dataPumpCommandExecutor; + } + + @Override + public boolean support(String dbType, String backupStrategy) { + return "ORACLE".equalsIgnoreCase(dbType) && BackupStrategyEnum.DATA_PUMP.name().equals(backupStrategy); + } + + @Override + public void executeRestore(DatabaseOperationTask task, DatabaseRestoreRecord record, DatabaseBackupFile backupFile, + DatabaseConnection connection, String password, DatabaseRestoreParam.CreateParam param) { + DataPumpCommandExecutor.CommandResult result = dataPumpCommandExecutor.impdp(connection, password, + backupFile.getDirectoryName(), backupFile.getDumpFileName(), buildRestoreLogName(task), + record.getTableExistsAction()); + if (!Boolean.TRUE.equals(result.getSuccess())) { + throw new BusinessException(CommonResponseEnum.FAIL, "Data Pump 恢复失败:" + result.getOutput()); + } + } + + private String buildRestoreLogName(DatabaseOperationTask task) { + return DatabaseFileNameUtil.appendTodayWithTask(task.getTaskNo() + "_restore.log", task.getTaskNo()); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleJdbcExportBackupOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleJdbcExportBackupOperator.java new file mode 100644 index 0000000..d4f73b5 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleJdbcExportBackupOperator.java @@ -0,0 +1,116 @@ +package com.njcn.gather.systemops.database.support.oracle; + +import cn.hutool.core.util.StrUtil; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.systemops.database.component.JdbcExportComponent; +import com.njcn.gather.systemops.database.component.OracleJdbcComponent; +import com.njcn.gather.systemops.database.config.DbmsProperties; +import com.njcn.gather.systemops.database.pojo.enums.BackupModeEnum; +import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum; +import com.njcn.gather.systemops.database.pojo.enums.FileFormatEnum; +import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; +import com.njcn.gather.systemops.database.support.spi.DatabaseBackupOperator; +import com.njcn.gather.systemops.database.util.DatabaseChecksumUtil; +import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil; +import com.njcn.gather.systemops.database.util.DatabaseOpsIdUtil; +import com.njcn.gather.systemops.database.util.DatabasePathUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.time.LocalDateTime; +import java.util.Locale; + +/** + * Oracle JDBC_EXPORT 备份实现。 + */ +@Component +@RequiredArgsConstructor +public class OracleJdbcExportBackupOperator implements DatabaseBackupOperator { + + private final JdbcExportComponent jdbcExportComponent; + private final OracleJdbcComponent oracleJdbcComponent; + private final DbmsProperties dbmsProperties; + + @Override + public boolean support(String dbType, String backupStrategy) { + return "ORACLE".equalsIgnoreCase(dbType) && BackupStrategyEnum.JDBC_EXPORT.name().equals(backupStrategy); + } + + @Override + public DatabaseBackupFile executeBackup(DatabaseOperationTask task, DatabaseConnection connection, String password, + DatabaseBackupParam.CreateParam param) throws Exception { + String baseName = buildBaseFileName(connection, task); + String fileName = DatabaseFileNameUtil.appendTodayWithTask(baseName + ".csv", task.getTaskNo()); + String metadataFileName = DatabaseFileNameUtil.appendTodayWithTask(baseName + "_metadata.json", task.getTaskNo()); + Path dataFilePath = buildManagedPath(dbmsProperties.getBackup().getStoragePath(), fileName); + Path metadataFilePath = buildManagedPath(dbmsProperties.getBackup().getStoragePath(), metadataFileName); + try (Connection jdbcConnection = oracleJdbcComponent.openConnection(connection, password)) { + jdbcExportComponent.exportCsv(jdbcConnection, connection.getSchemaName(), param, dataFilePath, metadataFilePath); + } + return buildBackupFile(task, connection, param, fileName, dataFilePath, metadataFilePath); + } + + private DatabaseBackupFile buildBackupFile(DatabaseOperationTask task, DatabaseConnection connection, + DatabaseBackupParam.CreateParam param, String fileName, Path filePath, + Path metadataFilePath) { + if (filePath == null || !Files.exists(filePath)) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份文件未生成"); + } + DatabaseBackupFile file = new DatabaseBackupFile(); + file.setId(DatabaseOpsIdUtil.uuid()); + file.setTaskId(task.getId()); + file.setConnectionId(connection.getId()); + file.setDbType(connection.getDbType()); + file.setBackupStrategy(task.getBackupStrategy()); + file.setFileFormat(FileFormatEnum.CSV.name()); + file.setSchemaName(task.getSchemaName()); + file.setTargetNamesJson(task.getTargetNamesJson()); + file.setBackupMode(StrUtil.blankToDefault(param.getBackupMode(), BackupModeEnum.FULL_TABLE.name()).toUpperCase(Locale.ROOT)); + file.setBackupStartTime(param.getStartTime()); + file.setBackupEndTime(param.getEndTime()); + file.setTimeColumn(param.getTimeColumn()); + file.setDirectoryName(null); + file.setDumpFileName(null); + file.setLogFileName(null); + file.setFileName(fileName); + file.setFilePath(filePath.toString()); + file.setLogFilePath(null); + file.setMetadataFilePath(metadataFilePath.toString()); + file.setFileSize(readFileSize(filePath)); + file.setChecksum(DatabaseChecksumUtil.sha256(filePath)); + file.setState(1); + file.setCreateTime(LocalDateTime.now()); + file.setUpdateTime(LocalDateTime.now()); + return file; + } + + private Long readFileSize(Path filePath) { + try { + if (filePath != null && Files.exists(filePath) && !Files.isDirectory(filePath)) { + return Files.size(filePath); + } + } catch (Exception ignored) { + return null; + } + return null; + } + + private Path buildManagedPath(String rootPath, String fileName) { + Path root = DatabasePathUtil.normalize(rootPath); + if (root == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "备份目录未配置"); + } + return root.resolve(fileName).normalize(); + } + + private String buildBaseFileName(DatabaseConnection connection, DatabaseOperationTask task) { + return connection.getSchemaName() + "_" + task.getBackupStrategy().toLowerCase(Locale.ROOT); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleJdbcExportRestoreOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleJdbcExportRestoreOperator.java new file mode 100644 index 0000000..bafa664 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/oracle/OracleJdbcExportRestoreOperator.java @@ -0,0 +1,51 @@ +package com.njcn.gather.systemops.database.support.oracle; + +import com.njcn.gather.systemops.database.component.JdbcExportComponent; +import com.njcn.gather.systemops.database.component.OracleJdbcComponent; +import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum; +import com.njcn.gather.systemops.database.pojo.param.DatabaseRestoreParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; +import com.njcn.gather.systemops.database.pojo.po.DatabaseRestoreRecord; +import com.njcn.gather.systemops.database.service.DatabaseBackupFileService; +import com.njcn.gather.systemops.database.support.spi.DatabaseRestoreOperator; +import org.springframework.stereotype.Component; + +import java.nio.file.Path; +import java.sql.Connection; + +/** + * Oracle JDBC_EXPORT 恢复实现。 + */ +@Component +public class OracleJdbcExportRestoreOperator implements DatabaseRestoreOperator { + + private final JdbcExportComponent jdbcExportComponent; + private final OracleJdbcComponent oracleJdbcComponent; + private final DatabaseBackupFileService databaseBackupFileService; + + public OracleJdbcExportRestoreOperator(JdbcExportComponent jdbcExportComponent, + OracleJdbcComponent oracleJdbcComponent, + DatabaseBackupFileService databaseBackupFileService) { + this.jdbcExportComponent = jdbcExportComponent; + this.oracleJdbcComponent = oracleJdbcComponent; + this.databaseBackupFileService = databaseBackupFileService; + } + + @Override + public boolean support(String dbType, String backupStrategy) { + return "ORACLE".equalsIgnoreCase(dbType) && BackupStrategyEnum.JDBC_EXPORT.name().equals(backupStrategy); + } + + @Override + public void executeRestore(DatabaseOperationTask task, DatabaseRestoreRecord record, DatabaseBackupFile backupFile, + DatabaseConnection connection, String password, DatabaseRestoreParam.CreateParam param) throws Exception { + Path dataFilePath = databaseBackupFileService.resolveManagedPath(backupFile, backupFile.getFilePath()); + Path metadataFilePath = databaseBackupFileService.resolveManagedPath(backupFile, backupFile.getMetadataFilePath()); + try (Connection jdbcConnection = oracleJdbcComponent.openConnection(connection, password)) { + jdbcExportComponent.importCsv(jdbcConnection, dataFilePath, metadataFilePath, + connection.getDbType(), record.getRestoreMode(), record.getTargetSchemaName()); + } + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseBackupOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseBackupOperator.java new file mode 100644 index 0000000..2b77599 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseBackupOperator.java @@ -0,0 +1,17 @@ +package com.njcn.gather.systemops.database.support.spi; + +import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; + +/** + * 按数据库类型与备份策略隔离备份执行能力。 + */ +public interface DatabaseBackupOperator { + + boolean support(String dbType, String backupStrategy); + + DatabaseBackupFile executeBackup(DatabaseOperationTask task, DatabaseConnection connection, String password, + DatabaseBackupParam.CreateParam param) throws Exception; +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseConnectionOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseConnectionOperator.java new file mode 100644 index 0000000..dd2075c --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseConnectionOperator.java @@ -0,0 +1,19 @@ +package com.njcn.gather.systemops.database.support.spi; + +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTableVO; +import com.njcn.gather.systemops.database.pojo.vo.DatabaseTestResultVO; + +import java.util.List; + +/** + * 按数据库类型隔离连接测试与表查询能力。 + */ +public interface DatabaseConnectionOperator { + + boolean support(String dbType); + + DatabaseTestResultVO test(DatabaseConnection connection, String password); + + List listTables(DatabaseConnection connection, String password, String schemaOrDatabaseName) throws Exception; +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseOperatorRegistry.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseOperatorRegistry.java new file mode 100644 index 0000000..dc68664 --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseOperatorRegistry.java @@ -0,0 +1,43 @@ +package com.njcn.gather.systemops.database.support.spi; + +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 数据库能力路由注册器。 + */ +@Component +@RequiredArgsConstructor +public class DatabaseOperatorRegistry { + + private final List connectionOperators; + private final List backupOperators; + private final List restoreOperators; + + public DatabaseConnectionOperator getConnectionOperator(String dbType) { + return connectionOperators.stream() + .filter(operator -> operator.support(dbType)) + .findFirst() + .orElseThrow(() -> new BusinessException(CommonResponseEnum.FAIL, "暂不支持的数据库类型:" + dbType)); + } + + public DatabaseBackupOperator getBackupOperator(String dbType, String backupStrategy) { + return backupOperators.stream() + .filter(operator -> operator.support(dbType, backupStrategy)) + .findFirst() + .orElseThrow(() -> new BusinessException(CommonResponseEnum.FAIL, + "暂不支持的备份能力:" + dbType + "/" + backupStrategy)); + } + + public DatabaseRestoreOperator getRestoreOperator(String dbType, String backupStrategy) { + return restoreOperators.stream() + .filter(operator -> operator.support(dbType, backupStrategy)) + .findFirst() + .orElseThrow(() -> new BusinessException(CommonResponseEnum.FAIL, + "暂不支持的恢复能力:" + dbType + "/" + backupStrategy)); + } +} diff --git a/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseRestoreOperator.java b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseRestoreOperator.java new file mode 100644 index 0000000..ed433ba --- /dev/null +++ b/system-ops/dbms/src/main/java/com/njcn/gather/systemops/database/support/spi/DatabaseRestoreOperator.java @@ -0,0 +1,18 @@ +package com.njcn.gather.systemops.database.support.spi; + +import com.njcn.gather.systemops.database.pojo.param.DatabaseRestoreParam; +import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile; +import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection; +import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask; +import com.njcn.gather.systemops.database.pojo.po.DatabaseRestoreRecord; + +/** + * 按数据库类型与备份策略隔离恢复执行能力。 + */ +public interface DatabaseRestoreOperator { + + boolean support(String dbType, String backupStrategy); + + void executeRestore(DatabaseOperationTask task, DatabaseRestoreRecord record, DatabaseBackupFile backupFile, + DatabaseConnection connection, String password, DatabaseRestoreParam.CreateParam param) throws Exception; +} diff --git a/system-ops/dbms/src/main/resources/sql/system-ops/dbms-database-ops-init.sql b/system-ops/dbms/src/main/resources/sql/system-ops/dbms-database-ops-init.sql new file mode 100644 index 0000000..13d965b --- /dev/null +++ b/system-ops/dbms/src/main/resources/sql/system-ops/dbms-database-ops-init.sql @@ -0,0 +1,119 @@ +CREATE TABLE IF NOT EXISTS `dbms_connection` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键', + `connection_name` VARCHAR(100) NOT NULL COMMENT '连接名称', + `db_type` VARCHAR(32) NOT NULL DEFAULT 'ORACLE' COMMENT '数据库类型:ORACLE,后续可扩展 MYSQL、INFLUXDB', + `host` VARCHAR(255) NOT NULL COMMENT '数据库主机地址', + `port` INT NOT NULL COMMENT '数据库端口', + `connect_type` VARCHAR(32) NULL COMMENT '连接类型:SERVICE_NAME、SID,Oracle 使用', + `service_name` VARCHAR(128) NULL COMMENT '服务名,Oracle SERVICE_NAME 模式使用', + `sid` VARCHAR(128) NULL COMMENT 'SID,Oracle SID 模式使用', + `database_name` VARCHAR(128) NULL COMMENT '数据库名或实例名,预留给 MySQL 等数据库使用', + `schema_name` VARCHAR(128) NULL COMMENT '默认 Schema,Oracle 使用', + `username` VARCHAR(128) NOT NULL COMMENT '用户名', + `password_cipher` VARCHAR(1000) NULL COMMENT '保存的数据库密码;为空表示不保存密码,执行时临时输入', + `save_password` TINYINT NOT NULL DEFAULT 1 COMMENT '是否保存密码:0-否,1-是', + `directory_name` VARCHAR(128) NULL COMMENT '默认数据库目录对象名称,Oracle Data Pump 使用', + `directory_path` VARCHAR(500) NULL COMMENT '目录对象对应物理路径,仅用于展示和校验', + `extra_config_json` JSON NULL COMMENT '扩展配置 JSON,用于保存不同数据库的差异配置', + `remark` VARCHAR(500) NULL COMMENT '备注', + `last_test_status` VARCHAR(32) NULL COMMENT '最近连接测试状态:SUCCESS、FAIL', + `last_test_message` VARCHAR(1000) NULL COMMENT '最近连接测试结果说明', + `last_test_time` DATETIME NULL COMMENT '最近连接测试时间', + `state` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-删除,1-正常', + `create_by` VARCHAR(64) NULL COMMENT '创建人', + `create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) NULL COMMENT '更新人', + `update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_dbms_connection_state` (`state`), + KEY `idx_dbms_connection_db_type` (`db_type`), + KEY `idx_dbms_connection_name` (`connection_name`), + KEY `idx_dbms_connection_schema` (`schema_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库连接配置表'; + +CREATE TABLE IF NOT EXISTS `dbms_operation_task` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键', + `task_no` VARCHAR(64) NOT NULL COMMENT '任务编号', + `connection_id` VARCHAR(64) NOT NULL COMMENT '数据库连接配置 ID', + `db_type` VARCHAR(32) NOT NULL COMMENT '数据库类型:ORACLE,后续可扩展 MYSQL、INFLUXDB', + `operation_type` VARCHAR(32) NOT NULL COMMENT '操作类型:BACKUP、RESTORE、DELETE', + `backup_strategy` VARCHAR(32) NULL COMMENT '备份策略:DATA_PUMP、JDBC_EXPORT', + `task_status` VARCHAR(32) NOT NULL DEFAULT 'WAITING' COMMENT '任务状态:WAITING、RUNNING、SUCCESS、FAIL、CANCELLED', + `schema_name` VARCHAR(128) NULL COMMENT '操作 Schema', + `target_names_json` JSON NULL COMMENT '操作对象名称列表 JSON,例如表名列表', + `request_param_json` JSON NULL COMMENT '请求参数快照 JSON,不保存运行时密码', + `result_message` VARCHAR(2000) NULL COMMENT '执行结果或失败原因', + `progress_percent` DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT '任务进度百分比', + `started_at` DATETIME NULL COMMENT '任务开始时间', + `finished_at` DATETIME NULL COMMENT '任务结束时间', + `state` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-删除,1-正常', + `create_by` VARCHAR(64) NULL COMMENT '创建人', + `create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) NULL COMMENT '更新人', + `update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_dbms_operation_task_no` (`task_no`), + KEY `idx_dbms_operation_connection` (`connection_id`), + KEY `idx_dbms_operation_db_type` (`db_type`), + KEY `idx_dbms_operation_type_status` (`operation_type`, `task_status`), + KEY `idx_dbms_operation_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库运维任务表'; + +CREATE TABLE IF NOT EXISTS `dbms_backup_file` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键', + `task_id` VARCHAR(64) NOT NULL COMMENT '备份任务 ID', + `connection_id` VARCHAR(64) NOT NULL COMMENT '数据库连接配置 ID', + `db_type` VARCHAR(32) NOT NULL COMMENT '数据库类型:ORACLE,后续可扩展 MYSQL、INFLUXDB', + `backup_strategy` VARCHAR(32) NOT NULL COMMENT '备份策略:DATA_PUMP、JDBC_EXPORT', + `file_format` VARCHAR(32) NOT NULL COMMENT '文件格式:DMP、SQL、CSV', + `schema_name` VARCHAR(128) NULL COMMENT '备份 Schema', + `target_names_json` JSON NULL COMMENT '备份对象名称列表 JSON,例如表名列表', + `backup_mode` VARCHAR(32) NOT NULL DEFAULT 'FULL_TABLE' COMMENT '备份模式:FULL_TABLE、TIME_RANGE、SIZE_SPLIT', + `backup_start_time` DATETIME NULL COMMENT '按时间备份开始时间', + `backup_end_time` DATETIME NULL COMMENT '按时间备份结束时间', + `time_column` VARCHAR(128) NULL COMMENT '按时间备份使用的时间字段', + `directory_name` VARCHAR(128) NULL COMMENT '数据库目录对象名称,Oracle Data Pump 使用', + `dump_file_name` VARCHAR(255) NULL COMMENT 'Data Pump dump 文件名', + `log_file_name` VARCHAR(255) NULL COMMENT 'Data Pump log 文件名', + `file_name` VARCHAR(255) NOT NULL COMMENT '主备份文件名,需包含 _yyyyMMdd', + `file_path` VARCHAR(1000) NOT NULL COMMENT '服务端记录的备份文件路径或目录对象映射路径', + `log_file_path` VARCHAR(1000) NULL COMMENT '备份日志文件路径', + `metadata_file_path` VARCHAR(1000) NULL COMMENT 'JDBC_EXPORT 元数据文件路径', + `file_size` BIGINT NULL COMMENT '文件大小,单位字节', + `checksum` VARCHAR(128) NULL COMMENT '文件校验值', + `state` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-删除,1-正常', + `create_by` VARCHAR(64) NULL COMMENT '创建人', + `create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) NULL COMMENT '更新人', + `update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_dbms_backup_task` (`task_id`), + KEY `idx_dbms_backup_connection` (`connection_id`), + KEY `idx_dbms_backup_db_type` (`db_type`), + KEY `idx_dbms_backup_strategy` (`backup_strategy`), + KEY `idx_dbms_backup_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库备份文件记录表'; + +CREATE TABLE IF NOT EXISTS `dbms_restore_record` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键', + `task_id` VARCHAR(64) NOT NULL COMMENT '恢复任务 ID', + `backup_file_id` VARCHAR(64) NOT NULL COMMENT '备份文件 ID', + `connection_id` VARCHAR(64) NOT NULL COMMENT '目标数据库连接配置 ID', + `db_type` VARCHAR(32) NOT NULL COMMENT '数据库类型:ORACLE,后续可扩展 MYSQL、INFLUXDB', + `restore_mode` VARCHAR(32) NOT NULL DEFAULT 'SKIP' COMMENT '恢复模式:SKIP、APPEND、TRUNCATE、REPLACE', + `target_schema_name` VARCHAR(128) NULL COMMENT '目标 Schema', + `target_names_json` JSON NULL COMMENT '恢复对象名称列表 JSON,例如表名列表', + `table_exists_action` VARCHAR(32) NULL COMMENT 'Data Pump TABLE_EXISTS_ACTION:SKIP、APPEND、TRUNCATE、REPLACE', + `overwrite_confirmed` TINYINT NOT NULL DEFAULT 0 COMMENT '是否已确认覆盖类操作:0-否,1-是', + `result_message` VARCHAR(2000) NULL COMMENT '恢复结果说明', + `state` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-删除,1-正常', + `create_by` VARCHAR(64) NULL COMMENT '创建人', + `create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) NULL COMMENT '更新人', + `update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_dbms_restore_task` (`task_id`), + KEY `idx_dbms_restore_backup_file` (`backup_file_id`), + KEY `idx_dbms_restore_connection` (`connection_id`), + KEY `idx_dbms_restore_db_type` (`db_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库恢复记录表'; diff --git a/system-ops/dbms/src/main/resources/sql/system-ops/system-ops-init.sql b/system-ops/dbms/src/main/resources/sql/system-ops/system-ops-init.sql new file mode 100644 index 0000000..d57c983 --- /dev/null +++ b/system-ops/dbms/src/main/resources/sql/system-ops/system-ops-init.sql @@ -0,0 +1,14 @@ +INSERT INTO `cn_tool`.`sys_function` +(`Id`, `Pid`, `Pids`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `Type`, `Remark`, `State`, `Create_By`, `Create_Time`, `Update_By`, `Update_Time`) +VALUES +('9f3b2c7a6e8d4b91a5c0f2d7e6a3b841', '0', '0', '系统运维', 'systemOps', '/systemOps', '/systemOps/index', 'Aim', 50, 0, '系统运维', 1, 'f8516cc81d964cd8b4b771a3b3985cd4', '2026-05-20 10:00:00', 'f8516cc81d964cd8b4b771a3b3985cd4', '2026-05-20 10:00:00'); + +INSERT INTO `cn_tool`.`sys_function` +(`Id`, `Pid`, `Pids`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `Type`, `Remark`, `State`, `Create_By`, `Create_Time`, `Update_By`, `Update_Time`) +VALUES +('2a7e5d9c1f4b4386b0c9e6f3a8d21754', '9f3b2c7a6e8d4b91a5c0f2d7e6a3b841', '0,9f3b2c7a6e8d4b91a5c0f2d7e6a3b841', '数据库监控', 'database', '/systemOps/database', '/systemOps/database/index', 'Monitor', 100, 0, '数据库监控', 1, 'f8516cc81d964cd8b4b771a3b3985cd4', '2026-05-20 10:10:00', 'f8516cc81d964cd8b4b771a3b3985cd4', '2026-05-20 10:10:00'); + +INSERT INTO `cn_tool`.`sys_function` +(`Id`, `Pid`, `Pids`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `Type`, `Remark`, `State`, `Create_By`, `Create_Time`, `Update_By`, `Update_Time`) +VALUES +('7c6d4a1e9b2f43c8a5e0d3f6b9c21875', '9f3b2c7a6e8d4b91a5c0f2d7e6a3b841', '0,9f3b2c7a6e8d4b91a5c0f2d7e6a3b841', '系统部署', 'deploy', '/systemOps/deploy', '/systemOps/deploy/index', 'Upload', 110, 0, '系统部署', 1, 'f8516cc81d964cd8b4b771a3b3985cd4', '2026-05-20 10:20:00', 'f8516cc81d964cd8b4b771a3b3985cd4', '2026-05-20 10:20:00');